72

有没有人使用过Apple FFTiPhone 应用程序,或者知道我在哪里可以找到关于如何使用它的示例应用程序?我知道 Apple 发布了一些示例代码,但我不确定如何将其实施到实际项目中。

4

4 回答 4

139

我刚刚获得了适用于 iPhone 项目的 FFT 代码:

  • 创建一个新项目
  • 删除除 main.m 和 xxx_info.plist 之外的所有文件
  • 转到项目设置并搜索 pch 并阻止它尝试加载 .pch (看到我们刚刚删除了它)
  • 将代码示例复制粘贴到 main.m 中的任何内容上
  • 删除#include's Carbon 的行。Carbon 用于 OSX。
  • 删除所有框架,添加加速框架

您可能还需要从 info.plist 中删除一个条目,该条目告诉项目加载 xib,但我 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 -- 即我们可以可靠检测到的最低频率是该窗口内的一个完整波,即 44Hz 波。

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

  • 等等

  • bin[512].idealFreq = 512 * 44Hz -- 我们可以检测到的最高频率(称为奈奎斯特频率)是每对点代表一个波,即窗口内的 512 个完整波,即 512 * 44Hz,或: n/2 * bin[1].idealFreq

  • 实际上还有一个额外的 Bin,Bin[0],通常被称为“DC 偏移”。碰巧 Bin[0] 和 Bin[n/2] 总是有复分量 0,所以 A[0].realp 用于存储 Bin[0] 而 A[0].imagp 用于存储 Bin[ n/2]

每个复数的大小是围绕该频率振动的能量。

所以,正如你所看到的,它不会是一个非常好的音高检测器,因为它没有足够精细的粒度。有一个巧妙的技巧是 使用帧之间的相位变化从 FFT Bins 中提取精确频率,以获得给定 bin 的精确频率。

好的,现在进入代码:

注意 vDSP_fft_zrip 中的“ip”,=“就地”,即输出覆盖 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);

接下来我们将它们打包到 A 中作为 n/2 复数 #s:

// 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 课程: 傅立叶理论需要很长时间才能理解(我已经断断续续地研究了好几年了)

一个cisoid是:

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

即复平面单位圆上的一点。

当您将复数相乘时,角度会相加。所以 z^k 会一直围绕单位圆跳跃;z^k 可以在角度 k.theta 处找到

  • 选择 z1 = 0+1i,即从实轴转四分之一圈,并注意 z1^2 z1^3 z1^4 各转四分之一圈,因此 z1^4 = 1

  • 选择z2 = -1,即半圈。z2^4 = 1 但此时 z2 已完成 2 个周期(z2^2 也是 = 1)。因此,您可以将 z1 视为基频,将 z2 视为一次谐波

  • 类似地,z3 =“四分之三转”点,即 -i 恰好完成 3 个周期,但实际上每次前进 3/4 与每次后退 1/4 相同

即 z3 只是 z1 但方向相反——它被称为混叠

z2 是最高有意义的频率,因为我们选择了 4 个样本来保持全波。

  • z0 = 1+0i, z0^(anything)=1,这是直流偏移

您可以将任何 4 点信号表示为 z0 z1 和 z2 的线性组合, 即您将其投影到这些基向量上

但我听到你问“将信号投射到 cisoid 上意味着什么?”

你可以这样想:针绕着cisoid旋转,所以在样本k处,针指向k.theta方向,长度为signal[k]。与 cisoid 的频率完全匹配的信号将使结果形状在某个方向上凸出。所以如果你把所有的贡献加起来,你会得到一个强大的合成向量。如果频率几乎匹配,则凸起会更小,并且会绕着圆圈缓慢移动。对于与频率不匹配的信号,这些贡献将相互抵消。

http://complextoreal.com/tutorials/tutorial-4-fourier-analysis-made-easy-part-1/ 将帮助您获得直观的理解。

但要点是;如果我们选择将 1024 个样本投影到 {z0,...,z512},我们将预先计算 z0 到 z512,这就是这个预先计算步骤


请注意,如果您在实际代码中执行此操作,您可能希望在应用程序加载时执行此操作一次,并在应用程序退出时调用一次补充释放函数。不要做很多次——它很贵。

// 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 函数中。因此(除非您想要最终的内存优化)只需为您需要的最高分辨率创建一组,并将其用于所有内容。

现在是实际的转换,利用我们刚刚预先计算的东西:

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

此时 A 将包含 n/2 个复数,只有第一个实际上是两个伪装成复数的实数(DC 偏移量,奈奎斯特 #)。文档概述解释了这种包装。它非常简洁——基本上它允许将转换的(复杂)结果打包到与(真实但奇怪打包的)输入相同的内存占用中。

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

然后再回来......我们仍然需要从 A 中解压我们的原始数组。然后我们比较只是为了检查我们是否已经回到了我们开始时的状态,释放我们预先计算的线轴并完成!

可是等等!在你打开包装之前,还有最后一件事需要做:

// 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);
于 2010-11-04T07:41:27.783 回答
26

这是一个真实的示例:一段 c++ 片段,它使用 Accelerate 的 vDSP fft 例程对远程 IO 音频单元的输入进行自相关。使用这个框架非常复杂,但文档还不错

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);
    }
}
于 2010-08-20T21:09:34.633 回答
14

虽然我会说 Apple 的 FFT 框架很快......您需要知道 FFT 如何工作才能获得准确的音高检测(即计算每个连续 FFT 的相位差以找到准确的音高,而不是音高最占主导地位的 bin)。

我不知道它是否有任何帮助,但我从我的调谐器应用程序 (musicianskit.com/developer.php) 上传了我的音高检测器对象。还有一个示例 xCode 4 项目可供下载(因此您可以了解实现的工作原理)。

我正在上传一个示例 FFT 实现——敬请期待,一旦发生,我会更新它。

快乐编码!

于 2012-06-14T23:05:03.860 回答
4

这是另一个现实世界的例子: https ://github.com/krafter/DetectingAudioFrequency

于 2014-04-28T10:42:38.657 回答