如何快速使用苹果的 Accelerate 框架来计算真实信号的 FFT?

Jer*_*hoy 3 fft ios accelerate-framework vdsp swift

我应该如何使用 Accelerate 框架在 iOS 上的 Swift 中计算真实信号的 FFT?

网络上的可用示例

Apple 的 Accelerate 框架似乎提供了有效计算信号 FFT 的功能。

不幸的是,Internet 上可用的大多数示例(如Swift-FFT-ExampleTempiFFT )在进行广泛测试并调用 Objective C API 时会崩溃。

苹果文档回答了许多问题,但也导致了一些人(这是一块强制?为什么我需要这个调用转换?)。

Stack Overflow 上的线程

很少有线程通过具体示例来解决 FFT 的各个方面。值得注意的是,在 Swift 中使用 Accelerate 的 FFT,Swift中的DFT 结果与 MATLABFFT 计算错误 - Swift 的结果不同。他们都没有直接解决“从 0 开始做这件事的正确方法是什么”的问题?

我花了一天时间才弄清楚如何正确地做到这一点,所以我希望这个帖子可以清楚地解释您应该如何使用 Apple 的 FFT,展示要避免的陷阱,并帮助开发人员节省宝贵的时间时间。

Jer*_*hoy 6

TL ; DR:如果您需要一个有效的实现来复制过去,这里是一个要点

什么是FFT?

快速傅立叶变换是一种算法,它采用时域中的信号——在规则的、通常很小的时间间隔内进行的测量集合——并将其转换为表示到相位域中的信号(频率的集合) )。能够表达由变换丢失的信号随时间的变化(变换是可逆的,这意味着通过计算 FFT 不会丢失任何信息,您可以应用 IFFT 来获取原始信号),但我们能够区分信号包含的频率。这通常用于显示您在各种硬件和 YouTube 视频上收听的音乐的频谱图。

FFT 适用于复数。如果您不知道它们是什么,让我们假设它是半径和角度的组合。二维平面上的每个点有一个复数。实数(通常的浮点数)可以看作是一条线上的位置(左边为负,右边为正)。

Nb:FFT(FFT(FFT(FFT(X))) = X(最多一个常数,取决于您的 FFT 实现)。

如何计算真实信号的 FFT。

通常,您要计算音频信号小窗口的 F​​FT。为了这个例子,我们将采用一个小的 1024 个样本窗口。您还希望使用 2 的幂,否则事情会变得更加困难。

var signal: [Float] // Array of length 1024
Run Code Online (Sandbox Code Playgroud)

首先,您需要为计算初始化一些常量。

// 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)!
Run Code Online (Sandbox Code Playgroud)

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

// 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))
Run Code Online (Sandbox Code Playgroud)

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

现在,我们必须小心并“锁定”指向这四个数组的指针,如文档示例中所示。如果您只是将其&forwardInputReal用作 的参数DSPSplitComplex,则指针可能会在下一行失效,并且您可能会遇到应用程序偶发性崩溃的情况。

    forwardInputReal.withUnsafeMutableBufferPointer { forwardInputRealPtr in
      forwardInputImag.withUnsafeMutableBufferPointer { forwardInputImagPtr in
        forwardOutputReal.withUnsafeMutableBufferPointer { forwardOutputRealPtr in
          fforwardOutputImag.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
          }
        }
      }
    }
Run Code Online (Sandbox Code Playgroud)

现在,压轴台词:呼叫您的 fft:

fftSetup.forward(input: forwardInput, output: &forwardOutput)
Run Code Online (Sandbox Code Playgroud)

您的 FFT 结果现在可以在forwardOutputReal和 中使用forwardOutputImag

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

var magnitudes = [Float](repeating: 0, count: Int(length))
Run Code Online (Sandbox Code Playgroud)

在您的 fft 计算每个“bin”的幅度之后立即添加:

vDSP.absolute(forwardOutput, result: &magnitudes)
Run Code Online (Sandbox Code Playgroud)

  • 您应该使用 vDSP 的实数到复数 FFT,而不是虚部具有显式零的复数到复数 FFT。虽然这个接口不是很好(并且需要使用 ztoc 来准备数据),但它会更快并且使用更少的内存。 (2认同)