使用Apple FFT和Accelerate框架


71

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


23
呐喊 该文档是可恶的。
P I

@Pi特别是有关特殊数据排序的部分-实际上在许多情况下并不适用。
marko

Answers:


137

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

  • 创建一个新项目
  • 删除除main.m和xxx_info.plist以外的所有文件
  • 进入项目设置并搜索pch并阻止其尝试加载.pch(请参阅我们刚刚删除的内容)
  • 将代码示例复制粘贴到main.m中的所有内容上
  • 删除#include的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被覆盖。这就是所有这种特殊打包Malarkey的原因-这样我们就可以将返回值压缩为与发送值相同的空间。

为了提供一些见解(例如,如下所示:为什么我们首先要使用此功能?),假设我们要对麦克风输入执行音高检测,并且我们对其进行了设置,以便每次都触发一些回调麦克风进入1024个浮动状态。假设麦克风的采样率为44.1kHz,则约为44帧/秒。

因此,我们的时间窗口是1024个样本的持续时间,即1/44 s。

因此,我们将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 Offset”。碰巧Bin [0]和Bin [n / 2]将始终具有复数分量0,因此A [0] .realp用于存储Bin [0],而A [0] .imagp用于存储Bin [ n / 2]

每个复数的大小就是在该频率附近振动的能量。

因此,正如您所看到的,它不是一个非常好的音高检测器,因为它没有足够精细的粒度。有一个狡猾的技巧,即 利用帧之间的相位变化从FFT Bin中提取精确频率,以获得给定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个复数#:

// 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)

也就是在复杂平面上单位圆上的一点。

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

  • 选择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 ^(任何)= 1,这是直流偏移

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

但是我听到你问“将信号投射到一个类固醇意味着什么?”

您可以这样想:针绕着类固醇旋转,因此在样本k处,针指向k.theta方向,长度为signal [k]。恰好与类固醇频率匹配的信号会在某个方向上凸出结果形状。因此,如果将所有贡献加起来,就会得到一个很强的合成向量。如果频率接近匹配,则凸出部分将变小,并会在圆周围缓慢移动。对于与频率不匹配的信号,这些贡献将相互抵消。

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

但是要点是:如果我们选择将1024个样本投影到{z0,...,z512}上,我们将通过z512到z0进行预计算,这就是该预计算步骤


请注意,如果您使用真实代码执行此操作,则可能希望在应用程序加载时执行一次,并在退出时调用互补释放函数。不要做很多次-价格昂贵。

// 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偏移,Nyquist#)。文档概述对此包装进行了说明。它非常整洁-基本上,它可以将转换的(复杂)结果打包为与(实际但奇怪打包的)输入相同的内存占用量。

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);

它不是44它的43!这在较高的垃圾箱中是如此重要!22050/512 = 43!
Curnelious

1
深入解释。您可以发布此链接所指向的苹果链接吗?我进行了搜索,但它使我得到了多个样本,我真的很想通过您的解释来理解它。谢谢!
尼拉夫·巴特

1
这是一个很棒的帖子。是否有一个github项目可用于逐步执行代码?
迈克尔

1
你好 我们可以在某个地方看到完整的代码吗?我找不到此处引用的Apple示例。谢谢
Andrei Filip

26

这是一个真实的示例:使用Accelerate的vDSP fft例程对Remote IO音频单元的输入进行自相关的c ++代码段。利用这个框架是相当复杂的,但文件是不是糟糕。

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);
    }
}

1
很好的例子,但是您能为我指出这两个函数的实现方向吗:getMaxFramesPerSlice()和quadInterpolate()?
CJ Hanson

抱歉,还有一个问题...由于我的音频是16位lpcm,我在缓冲区中获取了整数数据,如何有效地将其更改为浮点数以便与fft代码一起使用?
CJ Hanson

1
@CJ:好像getMaxFramesPerSlice()正在检索每次触发回调时发送的帧数。我认为,这同样可能是#define。
P I

3
@Ohmu,它是一种使用输入信号的自相关的简单音高检测算法。在这种情况下getMaxFramesPerSlice()无法进行设置#define,因为每次运行都会有所不同。该方法实际上是相应音频单元属性访问器的包装。该代码将输入置零,因为将相同的缓冲区传递到设备的输出-将其置零可防止反馈环路。
Art Gillespie

1
我认为vDSP_zvmags不应将其应用于元素0,因为它的虚部实际上是Nyquist存储桶的实部。您不应该只是A.realp[0]A.imagp[0],而不是bzero A.imagp[0]吗?
2012年

14

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

我不知道有什么帮助,但是我从调谐器应用程序(musicianskit.com/developer.php)上传了我的Pitch Detector对象。还有一个示例xCode 4项目可供下载(因此,您可以看到实现的工作原理)。

我正在上载FFT实施示例-请保持关注,一旦发生,我将对其进行更新。

祝您编码愉快!


感谢您的分享,但是您的示例没有编译时出现以下错误:1)。错误:“ interp”的类型冲突[3]。2)。自动关联/自动关联/AudioController.m:92:32:错误:使用未声明的标识符'recordingCallback'[3]
Meir 2012年

2
github.com/kevmdev/PitchDetectorExample抱歉,我很懒...但是有项目。它应该可以正确编译(至少在几周前我上一次尝试过),但是今晚我会再次检查!
Kpmurphy91 2013年

4

这是另一个真实的示例:https : //github.com/krafter/DetectingAudioFrequency


krafter-我知道它很旧,但是您的回购真棒!只是想知道是否有办法找到最高频率而不是最高频率?
艾伦·斯卡帕

谢谢!要回答您的问题-是的,您可以。在输出数组中,您将索引作为频率,将值作为幅度。因此,第一个元素是最低频率,最后一个元素是最高频率(反之亦然)。
krafter

但是实际出现的最高频率并不能告诉您太多,现实世界的声音始终包含整个频谱,但是有些频率只是微弱的而有些则是突出的。想一想。另请注意,您只能检测有限范围的频率。这是奈奎斯特定理。在这里查看我的答案以获取详细信息:stackoverflow.com/a/19966776/468812
krafter 2015年

好,很好。我仍然只想看看是否可以检测到18000hz之类的高频率,而同时又出现了其他更突出的噪声。不确定是否可行?在ViewController.mm的此函数中,maxIndex是否代表频谱中找到的最高频率?静态Float32 strongestFrequencyHZ(Float32 * buffer,FFTHelperRef * fftHelper,UInt32 frameSize,Float32 * freqValue)
艾伦·斯卡帕

仅仅使用我的示例而没有做任何修改,我今天就能够在iPhone 4上检测到18000hz的声音,使用Audacity产生音调和SVEN小扬声器也没有问题。从理论上讲,如果使用44100采样率,则可以检测到22050Hz。我今天也检测到19000Hz甚至20000Hz。还检测到我的头部有些疼痛:))
krafter 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.