使用 Swift 中的 Accelerate 框架来自 AVAudioPCMBuffer 的频谱图

Posted

技术标签:

【中文标题】使用 Swift 中的 Accelerate 框架来自 AVAudioPCMBuffer 的频谱图【英文标题】:Spectrogram from AVAudioPCMBuffer using Accelerate framework in Swift 【发布时间】:2015-12-29 17:02:49 【问题描述】:

我正在尝试从 Swift 中的 AVAudioPCMBuffer 生成频谱图。我在AVAudioMixerNode 上安装了一个水龙头,并接收到带有音频缓冲区的回调。我想将缓冲区中的信号转换为[Float:Float] 字典,其中键代表频率,值代表相应频率上的音频幅度。

我尝试使用 Apple 的 Accelerate 框架,但我得到的结果似乎令人怀疑。我确定这只是我转换信号的方式。

我查看了this blog post 以供参考。

这是我所拥有的:

self.audioEngine.mainMixerNode.installTapOnBus(0, bufferSize: 1024, format: nil, block:  buffer, when in
    let bufferSize: Int = Int(buffer.frameLength)

    // Set up the transform
    let log2n = UInt(round(log2(Double(bufferSize))))
    let fftSetup = vDSP_create_fftsetup(log2n, Int32(kFFTRadix2))

    // Create the complex split value to hold the output of the transform
    var realp = [Float](count: bufferSize/2, repeatedValue: 0)
    var imagp = [Float](count: bufferSize/2, repeatedValue: 0)
    var output = DSPSplitComplex(realp: &realp, imagp: &imagp)

    // Now I need to convert the signal from the buffer to complex value, this is what I'm struggling to grasp.
    // The complexValue should be UnsafePointer<DSPComplex>. How do I generate it from the buffer's floatChannelData?
    vDSP_ctoz(complexValue, 2, &output, 1, UInt(bufferSize / 2))

    // Do the fast Fournier forward transform
    vDSP_fft_zrip(fftSetup, &output, 1, log2n, Int32(FFT_FORWARD))

    // Convert the complex output to magnitude
    var fft = [Float](count:Int(bufferSize / 2), repeatedValue:0.0)
    vDSP_zvmags(&output, 1, &fft, 1, vDSP_length(bufferSize / 2))

    // Release the setup
    vDSP_destroy_fftsetup(fftsetup)

    // TODO: Convert fft to [Float:Float] dictionary of frequency vs magnitude. How?
)

我的问题是

    如何将buffer.floatChannelData 转换为UnsafePointer&lt;DSPComplex&gt; 以传递给vDSP_ctoz 函数?有没有不同/更好的方法可以绕过vDSP_ctoz? 如果缓冲区包含来自多个通道的音频,这会有所不同吗?缓冲音频通道数据交错或不交错有何不同? 如何将fft 数组中的索引转换为以Hz 为单位的频率? 还有什么我做错了吗?

更新

感谢大家的建议。我最终按照接受的答案中的建议填充了复杂的数组。当我绘制值并在音叉上播放 440 Hz 音调时,它会准确记录它应该在哪里。

这是填充数组的代码:

var channelSamples: [[DSPComplex]] = []
for var i=0; i<channelCount; ++i 
    channelSamples.append([])
    let firstSample = buffer.format.interleaved ? i : i*bufferSize
    for var j=firstSample; j<bufferSize; j+=buffer.stride*2 
        channelSamples[i].append(DSPComplex(real: buffer.floatChannelData.memory[j], imag: buffer.floatChannelData.memory[j+buffer.stride]))
    

channelSamples 数组为每个通道保存单独的样本数组。

为了计算大小,我使用了这个:

var spectrum = [Float]()
for var i=0; i<bufferSize/2; ++i 
    let imag = out.imagp[i]
    let real = out.realp[i]
    let magnitude = sqrt(pow(real,2)+pow(imag,2))
    spectrum.append(magnitude)

【问题讨论】:

嘿,刚刚发现你的堆栈溢出问题,我得说:谢谢!你无疑为我节省了大量的研究时间。我仍然对这个答案的工作原理很感兴趣,但我想表达一些赞赏,因为它似乎还没有被发现(或者可能与大多数人无关) 这个问题已经很老了,但是第二部分的“out”变量是什么?你是怎么得到的? @Logan:out 变量是DSPSplitComplex 的一个实例。它包含一个复数,其中实部和虚部存储在单独的数组中。它由 FFT 函数填充。 @Jakub 谢谢,我知道如何让它工作了。你为我节省了大量时间!这是一个赞成票! 【参考方案1】:
    hacky 方法:你可以只转换一个浮点数组。 reals 和 imag 值一个接一个地变化。 这取决于音频是否交错。如果它是交错的(大多数情况下)左右通道都在 STRIDE 2 的数组中 在您的情况下,最低频率是 1024 个样本周期的频率。如果是 44100kHz,它是 ~23ms,频谱的最低频率将是 1/(1024/44100) (~43Hz)。下一个频率将是这个频率的两倍(~86Hz)等等。

【讨论】:

谢谢@user1232690。以这种方式填充复杂数组似乎效果很好。为了其他人的利益,我将在原帖中发布解决方案。 顺便说一句 for var i=0; i&lt;bufferSize/2; ++i 可以用类似 vDSP_vsmul(realp, 1, &amp;scalar, &amp;(complexValues) + 0, 2, (UInt)(bufferSize/2))vDSP_vsmul(imagp, 1, &amp;scalar, &amp;(complexValues) + 1, 2, (UInt)(bufferSize/2)) 的东西优化掉,其中标量是 1.0 浮点数【参考方案2】:

4:您已在音频总线上安装了回调处理程序。这很可能以实时线程优先级和频繁运行。您不应该做任何有可能阻塞的事情(这可能会导致优先级反转和音频故障):

    分配内存(realpimagp - [Float](.....)Array[float] 的简写 - 并且可能在堆上分配`。预先分配这些

    调用冗长的操作,例如vDSP_create_fftsetup() - 它还分配内存并对其进行初始化。同样,您可以在函数之外分配一次。

【讨论】:

CoreAudio 团队在今年的 WWDC 上对音频代码的 swift 问题相当冷淡。他们推荐了 C++ 或 C 的传统方法。

以上是关于使用 Swift 中的 Accelerate 框架来自 AVAudioPCMBuffer 的频谱图的主要内容,如果未能解决你的问题,请参考以下文章

来自 UIImage 或来自文件的 vImage - Swift 和 Accelerate 框架

为啥有时 Apple Accelerate 框架很慢?

为什么Apple Accelerate框架有时会很慢?

如何使用 vDSP / Accelerate in swift for iOS 计算向量元素的平方根

在 Swift 中使用 Accelerate Framework 复数支持

为啥 Swift 中的 FFT 与 Python 中的不同?