web-dev-qa-db-ja.com

Apple FFTとAccelerateフレームワークを使用する

誰かがiPhoneアプリにApple FFTを使用したことがありますか、それを使用する方法についてサンプルアプリケーションがどこにあるかを知っていますか? Appleにはいくつかのサンプルコードが投稿されていますが、実際のプロジェクトにそれを実装する方法がよくわかりません。

69
Ian Oswald

IPhoneプロジェクトでFFTコードが機能するようになりました。

  • 新しいプロジェクトを作成する
  • main.mおよびxxx_info.plistを除くすべてのファイルを削除します
  • 設定を投影し、pchを検索して、.pchをロードしようとするのを止めます(削除したばかりの状態を参照)
  • main.mにあるものの上にコード例をコピーして貼り付けます
  • #includeのCarbonの行を削除します。 CarbonはOSX用です。
  • すべてのフレームワークを削除し、加速フレームワークを追加します

また、プロジェクトにxibをロードするように指示するエントリをinfo.plistから削除する必要があるかもしれませんが、それを気にする必要がないことは90%確信しています。

注:コンソールへのプログラム出力、結果はエラーではなく0.000として出力されます。これは非常に高速です

このコードは本当に愚かなものです。寛大なコメントが付けられていますが、実際にはコメントは生活を楽にするものではありません。

基本的には次のとおりです。

vDSP_fft_zrip(setupReal, &A, stride, log2n, FFT_FORWARD);
vDSP_fft_zrip(setupReal, &A, stride, log2n, FFT_INVERSE);

N個の実数の浮動小数点数でFFTを実行し、次に反転して開始位置に戻ります。 ipはインプレースを表します。これは、&Aが上書きされることを意味します。これがこの特別なパッキングマラキーのすべての理由です。したがって、戻り値を送信値と同じスペースに押し込めます。

いくつかの視点を与えるために(たとえば、なぜこの関数を最初に使用するのでしょうか?)、マイク入力でピッチ検出を実行し、毎回コールバックがトリガーされるように設定したとしましょうマイクは1024フロートになります。マイクのサンプリングレートが44.1kHzであると仮定すると、これは最大で毎秒44フレームです。

したがって、タイムウィンドウは、1024サンプルの継続時間、つまり1/44秒です。

したがって、Aをマイクからの1024個のフロートでパックし、log2n = 10(2 ^ 10 = 1024)を設定し、いくつかのボビン(setupReal)を事前計算します。

vDSP_fft_zrip(setupReal, &A, stride, log2n, FFT_FORWARD);

これで、Aにはn/2個の複素数が含まれます。これらは、n/2個の周波数ビンを表します。

  • bin [1] .idealFreq = 44Hz-つまり、確実に検出できる最低周波数は、そのウィンドウ内の1つの完全な波、つまり44Hzの波です。

  • bin [2] .idealFreq = 2 * 44Hz

  • 等.

  • bin [512] .idealFreq = 512 * 44Hz-検出できる最高の周波数(ナイキスト周波数と呼ばれる)は、すべてのポイントのペアが波、つまりウィンドウ内の512の完全な波、つまり512 * 44Hzを表す場所、または: n/2 * bin [1] .idealFreq

  • 実際には、「DCオフセット」と呼ばれることが多い余分なBin、Bin [0]があります。そのため、Bin [0]とBin [n/2]は常に複素数成分0を持つため、A [0] .realpがBin [0]の格納に使用され、A [0] .imagpがBin [の格納に使用されますn/2]

そして、各複素数の大きさは、その周波数で振動するエネルギーの量です。

したがって、ご覧のとおり、十分に細かい粒度がないため、非常に優れたピッチ検出器ではありません。巧妙なトリックがあります フレーム間の位相変化を使用してFFTビンから正確な周波数を抽出する 指定されたビンの正確な周波数を取得します。

では、コードを見てみましょう。

VDSP_fft_zripの「ip」、「in place」、つまり出力がAを上書きすることに注意してください(「r」は実際の入力を意味します)

VDSP_fft_zripのドキュメントをご覧ください。

実データは分割複素数形式で格納され、奇数実数は分割複素数形式の虚数側に格納され、偶数実数は実数側に格納されます。

これはおそらく理解するのが最も難しいことです。プロセス全体を通して同じコンテナ(&A)を使用しています。そのため、最初はn個の実数で埋めたいと思います。 FFTの後、n/2個の複素数を保持します。次に、それを逆変換に入れ、元のn個の実数を取得することを望みます。

現在、Aの構造は複雑な値のためのセットアップです。そのため、vDSPは実数をパックする方法を標準化する必要があります。

最初にn個の実数を生成します:1、2、...、n

for (i = 0; i < n; i++)
    originalReal[i] = (float) (i + 1);

次に、それらをn/2複素数#としてAにパックします。

// 1. masquerades n real #s as n/2 complex #s = {1+2i, 3+4i, ...}
// 2. splits to 
//   A.realP = {1,3,...} (n/2 elts)
//   A.compP = {2,4,...} (n/2 elts)
//
vDSP_ctoz(
          (COMPLEX *) originalReal, 
          2,                            // stride 2, as each complex # is 2 floats
          &A, 
          1,                            // stride 1 in A.realP & .compP
          nOver2);                      // n/2 elts

あなたは本当にこれを得るためにAがどのように割り当てられているかを見る必要があるでしょう、おそらくドキュメントでCOMPLEX_SPLITを調べてください。

A.realp = (float *) malloc(nOver2 * sizeof(float));
A.imagp = (float *) malloc(nOver2 * sizeof(float));

次に、事前計算を行います。


数学ボードのクイックDSPクラス:フーリエ理論は頭を動かすのに長い時間がかかります(私はそれを見てきました)数年間オフになります)

シソイドは:

z = exp(i.theta) = cos(theta) + i.sin(theta)

i.e複素平面上の単位円上の点。

複素数を掛けると、角度が加算されます。したがって、z ^ kは単位円の周りをホッピングし続けます。 z ^ kは角度k.thetaで見つけることができます

  • z1 = 0 + 1i、つまり実軸から1/4回転を選択し、z1 ^ 2 z1 ^ 3 z1 ^ 4がそれぞれ別の1/4回転を与え、z1 ^ 4 = 1

  • z2 = -1、つまり半回転を選択します。また、z2 ^ 4 = 1ですが、z2はこの時点で2サイクルを完了しています(z2 ^ 2も= 1)。したがって、z1を基本周波数、z2を第1高調波と考えることができます

  • 同様に、z3 =「回転の4分の3」ポイント、つまり-iは正確に3サイクルを完了しますが、実際には毎回3/4進むことは毎回1/4戻ることと同じです

i.e z3はz1だけですが、反対方向にあります-それはエイリアシングと呼ばれます

z2は、最大の意味のある周波数です。フルウェーブを保持するために4つのサンプルを選択したためです。

  • z0 = 1 + 0i、z0 ^(anything)= 1、これはDC offset

任意の4ポイント信号をz0 z1とz2の線形結合として表現できますこれらの基底ベクトルに投影しています

しかし、「シソイドに信号を投影することはどういう意味ですか?」

このように考えることができます:針はシソイドの周りを回転するため、サンプルkで針は方向k.θを指しており、長さはsignal [k]です。シソイドの周波数に正確に一致する信号は、結果の形状をある方向に膨らませます。したがって、すべての寄与を合計すると、強力な結果ベクトルが得られます。周波数がほぼ一致する場合、バルジは小さくなり、円の周りをゆっくりと移動します。周波数と一致しない信号の場合、寄与は互いに相殺されます。

http://complextoreal.com/tutorials/tutorial-4-fourier-analysis-made-easy-part-1/は直感的な理解を得るのに役立ちます。

しかし、要点は; 1024個のサンプルを{z0、...、z512}に投影することを選択した場合、z0からz512までを事前計算することになり、これがこの事前計算ステップです


実際のコードでこれを実行している場合は、アプリの読み込み時に一度だけ実行し、終了時に補完的なリリース関数を1回呼び出すことに注意してください。何回もしないでください-それは高価です。

// let's say log2n = 8, so n=2^8=256 samples, or 'harmonics' or 'terms'
// if we pre-calculate the 256th roots of unity (of which there are 256) 
// that will save us time later.
//
// Note that this call creates an array which will need to be released 
// later to avoid leaking
setupReal = vDSP_create_fftsetup(log2n, FFT_RADIX2);

Log2nをたとえば8に設定すると、これらの事前計算された値を、解像度<= 2 ^ 8を使用する任意のfft関数にスローできることに注意してください。したがって(究極のメモリ最適化が必要な場合を除き)、必要な最高解像度用に1つのセットを作成し、それをすべてに使用します。

今、実際に変換し、先ほど計算したものを利用します:

vDSP_fft_zrip(setupReal, &A, stride, log2n, FFT_FORWARD);

この時点で、Aにはn/2個の複素数が含まれ、最初の1つのみが実際には複素数を装った2つの実数(DCオフセット、ナイキスト#)です。ドキュメントの概要では、この梱包について説明しています。基本的には、変換の(複雑な)結果を(実際の、しかし奇妙にパッケージ化された)入力と同じメモリフットプリントにパックすることができます。

vDSP_fft_zrip(setupReal, &A, stride, log2n, FFT_INVERSE);

そして再び...元の配列をAからアンパックする必要があります。それから、ちょうど最初に戻って、計算済みのボビンをリリースして完了したことを確認するために比較します。

ちょっと待って!開梱する前に、最後にやらなければならないことが1つあります。

// Need to see the documentation for this one...
// in order to optimise, different routines return values 
// that need to be scaled by different amounts in order to 
// be correct as per the math
// In this case...
scale = (float) 1.0 / (2 * n);

vDSP_vsmul(A.realp, 1, &scale, A.realp, 1, nOver2);
vDSP_vsmul(A.imagp, 1, &scale, A.imagp, 1, nOver2);
134
P i

以下は実際の例です。リモートのIOオーディオユニットの入力でAccelerateのvDSP fftルーチンを使用して自己相関を行うc ++のスニペット。このフレームワークの使用はかなり複雑ですが、ドキュメントはそうではありませんtoo悪い。

OSStatus DSPCore::initialize (double _sampleRate, uint16_t _bufferSize) {
    sampleRate = _sampleRate;
    bufferSize = _bufferSize;
    peakIndex = 0;
    frequency = 0.f;
    uint32_t maxFrames = getMaxFramesPerSlice();
    displayData = (float*)malloc(maxFrames*sizeof(float));
    bzero(displayData, maxFrames*sizeof(float));
    log2n = log2f(maxFrames);
    n = 1 << log2n;
    assert(n == maxFrames);
    nOver2 = maxFrames/2;
    A.realp = (float*)malloc(nOver2 * sizeof(float));
    A.imagp = (float*)malloc(nOver2 * sizeof(float));
    FFTSetup fftSetup = vDSP_create_fftsetup(log2n, FFT_RADIX2);

    return noErr;
}

void DSPCore::Render(uint32_t numFrames, AudioBufferList *ioData) {

    bufferSize = numFrames;
    float ln = log2f(numFrames);

    //vDSP autocorrelation

    //convert real input to even-odd
    vDSP_ctoz((COMPLEX*)ioData->mBuffers[0].mData, 2, &A, 1, numFrames/2);
    memset(ioData->mBuffers[0].mData, 0, ioData->mBuffers[0].mDataByteSize);
    //fft
    vDSP_fft_zrip(fftSetup, &A, 1, ln, FFT_FORWARD);

    // Absolute square (equivalent to mag^2)
    vDSP_zvmags(&A, 1, A.realp, 1, numFrames/2);
    bzero(A.imagp, (numFrames/2) * sizeof(float));    

    // Inverse FFT
    vDSP_fft_zrip(fftSetup, &A, 1, ln, FFT_INVERSE);

    //convert complex split to real
    vDSP_ztoc(&A, 1, (COMPLEX*)displayData, 2, numFrames/2);

    // Normalize
    float scale = 1.f/displayData[0];
    vDSP_vsmul(displayData, 1, &scale, displayData, 1, numFrames);

    // Naive peak-pick: find the first local maximum
    peakIndex = 0;
    for (size_t ii=1; ii < numFrames-1; ++ii) {
        if ((displayData[ii] > displayData[ii-1]) && (displayData[ii] > displayData[ii+1])) {
            peakIndex = ii;
            break;
        }
    }

    // Calculate frequency
    frequency = sampleRate / peakIndex + quadInterpolate(&displayData[peakIndex-1]);

    bufferSize = numFrames;

    for (int ii=0; ii<ioData->mNumberBuffers; ++ii) {
        bzero(ioData->mBuffers[ii].mData, ioData->mBuffers[ii].mDataByteSize);
    }
}
26
Art Gillespie

AppleのFFTフレームワークは速いと言いますが...正確なピッチ検出を得るにはFFTの仕組みを知る必要があります(つまり、連続する各FFTの位相差を計算して、ピッチではなく正確なピッチを見つける最も支配的なビン)。

助けになるかどうかはわかりませんが、チューナーアプリ(musicianskit.com/developer.php)からPitch Detectorオブジェクトをアップロードしました。ダウンロード用のxCode 4プロジェクトの例もあります(実装の仕組みを確認できます)。

サンプルFFT実装のアップロードに取り組んでいるので、ご期待ください。それが発生したら更新します。

ハッピーコーディング!

13
Kpmurphy91

もう1つの実際の例を次に示します。 https://github.com/krafter/DetectingAudioFrequency

4
krafter