如何在Swift中使用苹果的Accelerate框架来计算实数信号的FFT?

3

如何在iOS的Swift中使用Accelerate框架计算实信号的FFT?

可用的网上示例

苹果的Accelerate框架似乎提供了高效计算信号FFT的函数。

不幸的是,互联网上大多数可用的示例,如Swift-FFT-ExampleTempiFFT,在测试时会崩溃并调用Objective C API。

Apple文档回答了很多问题,但也引发了一些其他问题(这篇文章是否强制执行?为什么需要进行此转换调用?)。

Stack Overflow讨论线程

有少数主题涉及FFT和具体示例。特别是Swift中使用Accelerate进行FFT, Swift中的DFT结果与MATLAB不同Swift中FFT计算不正确。但是,它们都没有直接回答“从0开始应该如何正确地做到这一点”的问题?

我花了一天时间才弄清楚如何正确地做到这一点,因此我希望本主题可以清楚地解释如何使用苹果的FFT,展示需要避免的陷阱,并帮助开发人员节省宝贵的时间。


什么会导致崩溃?我在iOS应用程序中使用vDSP进行FFT已经十多年了,从未见过与FFT相关的崩溃。对于实际信号,您不需要进行转换,因为您的严格实际数据已经是分裂复数的一半(另一半开始作为零缓冲区)。 - hotpaw2
如果您每秒执行数百次FFT,则使用相同的DSPSplitComplex作为输入和输出将无法正常工作。未正确确保指针在计算期间保持有效也会导致偶发性崩溃。这并不是vDSP.FFT对象本身存在任何问题,而更多地与缺乏文档相关。 - Jeremy Cochoy
1个回答

12

TL;DR:如果您需要一个可用的实现来复制粘贴,请单击此处是gist

FFT是什么?

快速傅里叶变换(FFT)是一种算法,它将时间域中的信号(在通常很小的时间间隔内进行的一系列测量)转换为相位域中表示的信号(一组频率)。通过变换,我们失去了沿时间轴的信号,但我们获得了区分信号所包含的频率的能力。这通常用于在各种硬件和YouTube视频上显示您正在听的音乐的频谱图。

FFT 与 复数 相关。如果您不知道它们是什么,我们可以假装它是半径和角度的组合。在二维平面上每个点都有一个复数。实数(通常的浮点数)可以看作是线上的位置(左侧为负,右侧为正)。

Nb:FFT(FFT(FFT(FFT(X))))= X(取决于您的 FFT 实现的常数)。

如何计算实信号的 FFT。

通常情况下,您希望计算音频信号的小窗口的 FFT。为了举例说明,我们将采用一个小的 1024 个样本的窗口。您还应该使用 2 的幂次方,否则会变得更加困难。

var signal: [Float] // Array of length 1024

首先,你需要为计算初始化一些常量。
// The length of the input
length = vDSP_Length(signal.count)
// The power of two of two times the length of the input.
// Do not forget this factor 2.
log2n = vDSP_Length(ceil(log2(Float(length * 2))))
// Create the instance of the FFT class which allow computing FFT of complex vector with length
// up to `length`.
fftSetup = vDSP.FFT(log2n: log2n, radix: .radix2, ofType: DSPSplitComplex.self)!

根据苹果的文档,我们首先需要创建一个复杂数组作为输入。 不要被教程误导。通常你想要的是将信号复制为输入的实部,并保持复数部分为空。
// Input / Output arrays

var forwardInputReal = [Float](signal) // Copy the signal here
var forwardInputImag = [Float](repeating: 0, count: Int(length))
var forwardOutputReal = [Float](repeating: 0, count: Int(length))
var forwardOutputImag = [Float](repeating: 0, count: Int(length))

注意,FFT函数不允许同时使用相同的splitComplex作为输入和输出。如果你遇到崩溃,这可能是原因。这就是为什么我们定义了一个输入和一个输出。

现在,我们必须小心并“锁定”指向这四个数组的指针,如文档示例所示。如果你只是将&forwardInputReal作为DSPSplitComplex的参数使用,指针可能会在下一行失效,你很可能会经历应用程序的间歇性崩溃。

    forwardInputReal.withUnsafeMutableBufferPointer { forwardInputRealPtr in
      forwardInputImag.withUnsafeMutableBufferPointer { forwardInputImagPtr in
        forwardOutputReal.withUnsafeMutableBufferPointer { forwardOutputRealPtr in
          forwardOutputImag.withUnsafeMutableBufferPointer { forwardOutputImagPtr in
            // Input
            let forwardInput = DSPSplitComplex(realp: forwardInputRealPtr.baseAddress!, imagp: forwardInputImagPtr.baseAddress!)
            // Output
            var forwardOutput = DSPSplitComplex(realp: forwardOutputRealPtr.baseAddress!, imagp: forwardOutputImagPtr.baseAddress!)

            // FFT call goes here
          }
        }
      }
    }

现在,最后一行:调用您的fft函数:
fftSetup.forward(input: forwardInput, output: &forwardOutput)

你的FFT结果现在可以在forwardOutputRealforwardOutputImag中使用。

如果你只想要每个频率的振幅,而不关心实部和虚部,你可以在输入和输出旁边声明一个额外的数组:

var magnitudes = [Float](repeating: 0, count: Int(length))

在你的FFT之后添加以下内容来计算每个“bin”的振幅:
vDSP.absolute(forwardOutput, result: &magnitudes)

你能详细说明如何做吗?我对这个结果很感兴趣,因为在 iPhone X 和模拟器上使用相同的 DSPSplitComplex 作为输入和输出不起作用。 :) - Jeremy Cochoy
如果您有实现实际快速傅里叶变换(例如numpy rfft)的兴趣,我也很感兴趣。该实现可以利用对称性减少计算数量。 - Jeremy Cochoy
2
你应该使用vDSP的实数到复数FFT,而不是带有显式零值的复数到复数FFT。虽然这个接口并不是很好(需要使用ztoc来准备数据),但它会更快,占用更少的内存。 - Eric Postpischil
1
执行FFT不会丢失时间概念。时间信息存在于复数的相位中。(一个简单的证明是,除了算术舍入误差之外,FFT是完全可逆的,没有任何信息丢失。) - Eric Postpischil
我也想知道重新排序vDSP的iRFFT输出到单个数组中的成本有多高(根据文档,输出在实部和虚部中交错)。 - Jeremy Cochoy
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接