TL;DR:如果您需要一个可用的实现来复制粘贴,请单击此处是gist。
FFT是什么?
快速傅里叶变换(FFT)是一种算法,它将时间域中的信号(在通常很小的时间间隔内进行的一系列测量)转换为相位域中表示的信号(一组频率)。通过变换,我们失去了沿时间轴的信号,但我们获得了区分信号所包含的频率的能力。这通常用于在各种硬件和YouTube视频上显示您正在听的音乐的频谱图。
FFT 与 复数 相关。如果您不知道它们是什么,我们可以假装它是半径和角度的组合。在二维平面上每个点都有一个复数。实数(通常的浮点数)可以看作是线上的位置(左侧为负,右侧为正)。
Nb:FFT(FFT(FFT(FFT(X))))= X(取决于您的 FFT 实现的常数)。
如何计算实信号的 FFT。
通常情况下,您希望计算音频信号的小窗口的 FFT。为了举例说明,我们将采用一个小的 1024 个样本的窗口。您还应该使用 2 的幂次方,否则会变得更加困难。
var signal: [Float]
首先,你需要为计算初始化一些常量。
length = vDSP_Length(signal.count)
log2n = vDSP_Length(ceil(log2(Float(length * 2))))
fftSetup = vDSP.FFT(log2n: log2n, radix: .radix2, ofType: DSPSplitComplex.self)!
根据苹果的文档,我们首先需要创建一个复杂数组作为输入。
不要被教程误导。通常你想要的是将信号复制为输入的实部,并保持复数部分为空。
var forwardInputReal = [Float](signal)
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
let forwardInput = DSPSplitComplex(realp: forwardInputRealPtr.baseAddress!, imagp: forwardInputImagPtr.baseAddress!)
var forwardOutput = DSPSplitComplex(realp: forwardOutputRealPtr.baseAddress!, imagp: forwardOutputImagPtr.baseAddress!)
}
}
}
}
现在,最后一行:调用您的fft函数:
fftSetup.forward(input: forwardInput, output: &forwardOutput)
你的FFT结果现在可以在forwardOutputReal
和forwardOutputImag
中使用。
如果你只想要每个频率的振幅,而不关心实部和虚部,你可以在输入和输出旁边声明一个额外的数组:
var magnitudes = [Float](repeating: 0, count: Int(length))
在你的FFT之后添加以下内容来计算每个“bin”的振幅:
vDSP.absolute(forwardOutput, result: &magnitudes)