👋

vDSP基礎解説 - 信号処理の隠れた英雄

に公開

難易度: ★★☆☆☆(初級〜中級)
想定読者: オーディオ/信号処理をする人、リアルタイム処理が必要な人

TL;DR

  • vDSPはAccelerateフレームワーク内のデジタル信号処理ライブラリ
  • FFT、畳み込み、ベクトル演算などが超高速
  • 音声処理、オーディオ分析、科学計算で威力を発揮
  • GPU転送オーバーヘッドがないため、リアルタイム処理に最適

GPUだけじゃない:CPUの最適化も重要

ここまでGPU関連の技術を見てきた。でも、全てをGPUで処理すればいいというわけではない。

  • データ転送のオーバーヘッド
  • レイテンシ要件
  • 省電力性
  • 小規模データ

こういった場面では、CPUで処理した方が効率的なことも多い。そんなとき頼りになるのがvDSPだ。


vDSPとは?

vDSPはAccelerateフレームワークの一部で、デジタル信号処理に特化したライブラリ。

Apple公式のプログラミングガイドによると:

"The vDSP API provides mathematical functions for applications such as speech, sound, audio, and video processing, diagnostic medical imaging, radar signal processing, seismic analysis, and scientific data processing."

(vDSP APIは、音声、サウンド、オーディオ、ビデオ処理、診断医療画像、レーダー信号処理、地震分析、科学データ処理などのアプリケーション向けの数学関数を提供します)

出典:Apple Developer - vDSP Programming Guide

名前の由来は "vector Digital Signal Processing"。ベクトル演算を使った高速デジタル信号処理、ということ。


Accelerateフレームワーク全体像

vDSPを理解するには、まずAccelerateフレームワークの全体像を把握しておこう:

"The Accelerate framework provides high-performance, energy-efficient computation on the CPU by leveraging its vector-processing capability."

(Accelerateフレームワークは、CPUのベクトル処理能力を活用して、高性能でエネルギー効率の高い計算を提供します)

出典:Apple Developer - Accelerate Overview

Accelerateのコンポーネント

Accelerate Framework
├── vDSP      - デジタル信号処理(FFT、畳み込み等)
├── vImage    - 画像処理
├── BLAS      - 基本線形代数(ベクトル・行列演算)
├── LAPACK    - 高度な線形代数(固有値、連立方程式等)
├── BNNS      - CPU上のニューラルネットワーク
├── vForce    - ベクトル化された数学関数
└── Sparse    - スパース行列演算

これらすべてが、Apple Siliconのベクトル演算ユニット(ARM NEON)を活用して高速化されている。


vDSPでできること

vDSPの機能は多岐にわたる:

"The vDSP functions operate on real and complex data types. The functions include data type conversions, fast Fourier transforms (FFTs), and vector-to-vector and vector-to-scalar operations."

(vDSP関数は実数と複素数データ型に対して動作します。関数にはデータ型変換、高速フーリエ変換(FFT)、ベクトル対ベクトルおよびベクトル対スカラー演算が含まれます)

出典:Apple Developer - About the vDSP API

主な機能カテゴリ

カテゴリ 機能 用途例
フーリエ変換 FFT、DFT、DCT スペクトル分析、圧縮
畳み込み 1D/2D畳み込み、相関 フィルタリング、特徴検出
ベクトル演算 加減乗除、内積、ノルム 数値計算全般
Biquadフィルタ IIRフィルタ オーディオイコライザ
データ変換 Float32⇔Float64、Int⇔Float データ前処理

実践:FFTでオーディオ周波数分析

オーディオ信号の周波数成分を分析するFFTは、vDSPの代表的なユースケースだ。

C言語スタイル(低レベル)

import Accelerate

let n = 1024  // サンプル数
let log2n = vDSP_Length(log2(Float(n)))

// FFTセットアップを作成(事前計算、再利用可能)
guard let fftSetup = vDSP_create_fftsetup(log2n, FFT_RADIX2) else {
    fatalError("FFT setup failed")
}
defer { vDSP_destroy_fftsetup(fftSetup) }

// 入力信号(実数)
var signal: [Float] = Array(repeating: 0, count: n)
// サイン波を生成(例:440Hz)
for i in 0..<n {
    signal[i] = sin(2.0 * .pi * 440.0 * Float(i) / 44100.0)
}

// Split Complex形式に変換
var realPart = [Float](repeating: 0, count: n/2)
var imagPart = [Float](repeating: 0, count: n/2)

realPart.withUnsafeMutableBufferPointer { realBP in
    imagPart.withUnsafeMutableBufferPointer { imagBP in
        var splitComplex = DSPSplitComplex(
            realp: realBP.baseAddress!,
            imagp: imagBP.baseAddress!
        )
        
        signal.withUnsafeBytes { signalBytes in
            let signalPtr = signalBytes.bindMemory(to: DSPComplex.self).baseAddress!
            vDSP_ctoz(signalPtr, 2, &splitComplex, 1, vDSP_Length(n/2))
        }
        
        // FFT実行
        vDSP_fft_zrip(fftSetup, &splitComplex, 1, log2n, FFTDirection(kFFTDirection_Forward))
        
        // マグニチュード計算
        var magnitudes = [Float](repeating: 0, count: n/2)
        vDSP_zvabs(&splitComplex, 1, &magnitudes, 1, vDSP_Length(n/2))
        
        // 最大値のインデックス(=支配的な周波数)
        var maxMagnitude: Float = 0
        var maxIndex: vDSP_Length = 0
        vDSP_maxvi(magnitudes, 1, &maxMagnitude, &maxIndex, vDSP_Length(n/2))
        
        let dominantFrequency = Float(maxIndex) * 44100.0 / Float(n)
        print("Dominant frequency: \(dominantFrequency) Hz")
    }
}

Swift Overlayスタイル(モダン)

最近のvDSPはSwiftフレンドリーなAPIも提供している:

import Accelerate

let signal: [Float] = ... // 入力信号
let n = signal.count

// FFTを作成
let fft = vDSP.FFT(
    log2n: vDSP_Length(log2(Float(n))),
    radix: .radix2,
    ofType: DSPSplitComplex.self
)!

// フォワードFFT
var realPart = [Float](repeating: 0, count: n/2)
var imagPart = [Float](repeating: 0, count: n/2)

signal.withUnsafeBufferPointer { signalPtr in
    var splitComplex = DSPSplitComplex(realp: &realPart, imagp: &imagPart)
    fft.forward(input: signalPtr.baseAddress!, output: &splitComplex)
}

// マグニチュード計算(Swift風)
let magnitudes = zip(realPart, imagPart).map { sqrt($0*$0 + $1*$1) }

Biquadフィルタ:オーディオイコライザの実装

Logic ProやGarageBandのようなオーディオアプリでイコライザを実装するなら、Biquadフィルタがある:

import Accelerate

// Biquadフィルタの係数(ローパスフィルタの例)
// これらの係数は目的の周波数特性に応じて計算
let b0: Float = 0.292893218813
let b1: Float = 0.585786437627
let b2: Float = 0.292893218813
let a1: Float = 0.0
let a2: Float = 0.171572875254

// フィルタ係数配列
var coefficients: [Float] = [b0, b1, b2, a1, a2]

// 遅延要素(フィルタの状態、ステレオなら2チャンネル分)
var delays = [Float](repeating: 0, count: 2)

// 入力オーディオ
var inputSignal: [Float] = ... 
var outputSignal = [Float](repeating: 0, count: inputSignal.count)

// フィルタ処理
vDSP_biquad(
    &coefficients,
    &delays,
    &inputSignal, 1,
    &outputSignal, 1,
    vDSP_Length(inputSignal.count)
)

リアルタイムオーディオ処理で、この手の計算が毎フレーム必要になる。vDSPなしでは考えられない。

マルチバンドイコライザ

複数のBiquadフィルタを連結することで、マルチバンドイコライザを実装できる:

// 5バンドイコライザの係数(各バンドごとに5係数)
var allCoefficients: [Float] = [
    // バンド1: 60Hz
    b0_1, b1_1, b2_1, a1_1, a2_1,
    // バンド2: 230Hz
    b0_2, b1_2, b2_2, a1_2, a2_2,
    // ... 続く
]

var delays = [Float](repeating: 0, count: 2 * 5)  // 5セクション × 2遅延

// マルチセクションBiquad
vDSP_biquadm(
    &allCoefficients,
    &delays,
    &inputSignal, 1,
    &outputSignal, 1,
    vDSP_Length(inputSignal.count),
    5  // セクション数
)

ベクトル演算:高速な配列処理

単純な配列演算もvDSPなら超高速:

import Accelerate

let a: [Float] = [1, 2, 3, 4, 5]
let b: [Float] = [5, 4, 3, 2, 1]
var result = [Float](repeating: 0, count: 5)

// 要素ごとの加算
vDSP_vadd(a, 1, b, 1, &result, 1, vDSP_Length(5))
// result = [6, 6, 6, 6, 6]

// 要素ごとの乗算
vDSP_vmul(a, 1, b, 1, &result, 1, vDSP_Length(5))
// result = [5, 8, 9, 8, 5]

// スカラー乗算
var scalar: Float = 2.0
vDSP_vsmul(a, 1, &scalar, &result, 1, vDSP_Length(5))
// result = [2, 4, 6, 8, 10]

// 内積
var dotProduct: Float = 0
vDSP_dotpr(a, 1, b, 1, &dotProduct, vDSP_Length(5))
// dotProduct = 35

// 総和
var sum: Float = 0
vDSP_sve(a, 1, &sum, vDSP_Length(5))
// sum = 15

// 平均
var mean: Float = 0
vDSP_meanv(a, 1, &mean, vDSP_Length(5))
// mean = 3.0

「forループで書けばいいじゃん」と思うかもしれないが、vDSPはSIMD命令(NEON)を使って並列処理する。特に大きな配列では圧倒的な差が出る。


vDSP vs Neural Engine / GPU

「でも、GPUやNeural Engineの方が速いんじゃないの?」という疑問があるだろう。

Apple Developer Forumsでの回答:

"vDSP and veclib DONT use ANE."

(vDSPとveclibはANE(Neural Engine)を使用しません)

出典:Apple Developer Forums

これは制限ではなく、むしろ設計上の選択だ。

vDSPの強み

特徴 vDSP (CPU) GPU Neural Engine
レイテンシ 最小 転送オーバーヘッドあり 転送オーバーヘッドあり
性能予測 安定 バッチサイズ依存 モデル依存
省電力 小規模データで有利 大規模で有利 ML特化
可用性 常に利用可能 デバイス依存 A11以降

使い分けガイド

小さなデータ(< 数千要素)
  → vDSP / Accelerate

リアルタイム処理(オーディオ等)
  → vDSP(レイテンシ最小)

大規模バッチ処理
  → GPU / MPS

ML推論
  → Core ML / Neural Engine

機械学習との関係:前処理・後処理

機械学習パイプラインでもvDSPは活躍する。

音声認識の前処理例

// 音声認識モデルの入力を作る流れ

// 1. 音声波形を取得
let waveform: [Float] = ... // 44100Hzのオーディオ

// 2. プリエンファシス(vDSP)
var emphasized = [Float](repeating: 0, count: waveform.count)
let alpha: Float = 0.97
for i in 1..<waveform.count {
    emphasized[i] = waveform[i] - alpha * waveform[i-1]
}

// 3. フレーム分割とウィンドウ関数適用(vDSP)
let frameSize = 400
let hopSize = 160
var window = [Float](repeating: 0, count: frameSize)
vDSP_hann_window(&window, vDSP_Length(frameSize), Int32(vDSP_HANN_NORM))

// 4. 各フレームにFFT(vDSP)
for frameStart in stride(from: 0, to: emphasized.count - frameSize, by: hopSize) {
    var frame = Array(emphasized[frameStart..<frameStart+frameSize])
    var windowedFrame = [Float](repeating: 0, count: frameSize)
    vDSP_vmul(frame, 1, window, 1, &windowedFrame, 1, vDSP_Length(frameSize))
    
    // FFT実行...
}

// 5. メルフィルタバンク適用(vDSP行列演算)
// ...

// 6. Core MLモデルに入力
let mlInput = ...
let prediction = try model.prediction(input: mlInput)

前処理をvDSPで高速化し、推論をCore ML/Neural Engineで実行。これがApple流の最適パイプラインだ。


パフォーマンス Tips

1. セットアップを再利用

// ❌ 毎回作成(遅い)
for _ in 0..<1000 {
    let fftSetup = vDSP_create_fftsetup(log2n, FFT_RADIX2)
    // 使用...
    vDSP_destroy_fftsetup(fftSetup)
}

// ✅ 一度作成して再利用
let fftSetup = vDSP_create_fftsetup(log2n, FFT_RADIX2)!
for _ in 0..<1000 {
    // fftSetupを使用...
}
vDSP_destroy_fftsetup(fftSetup)

2. ストライドを活用

インターリーブデータも効率的に処理:

// ステレオオーディオ(LRLRLR...)から左チャンネルだけ処理
let stereo: [Float] = ...
var leftMagnitudes = [Float](repeating: 0, count: stereo.count / 2)

// ストライド2で左チャンネルのみアクセス
vDSP_vabs(stereo, 2, &leftMagnitudes, 1, vDSP_Length(stereo.count / 2))

3. インプレース演算

可能な場合は同じバッファに結果を書き込む:

var data: [Float] = ...

// インプレース(メモリ節約)
vDSP_vabs(data, 1, &data, 1, vDSP_Length(data.count))

まとめ:GPUだけが全てじゃない

機械学習の文脈でCPU演算が軽視されがちだが、vDSPのような高度に最適化されたライブラリは依然として重要だ。

特に:

  • リアルタイムオーディオ処理
  • 信号の前処理・後処理
  • レイテンシクリティカルな処理
  • 小〜中規模のベクトル演算

こういった場面では、vDSPが最適解になる。Accelerateフレームワーク全体を含めて、Apple Siliconの機械学習スタックの重要な一角を担っている。


参考文献

Discussion