使用Swift中的Accelerate框架从AVAudioPCMBuffer生成频谱图。

13

我正在尝试使用Swift从一个AVAudioPCMBuffer生成频谱图。我在AVAudioMixerNode上安装了一个tap,然后通过音频缓冲回调接收信号。我想将缓冲区中的信号转换为[Float: Float]字典,其中键表示频率,值表示相应频率上的音频幅度。

我尝试使用苹果的Accelerate框架,但是得到的结果似乎有问题。我确定这只是我转换信号的方式不对。

我查看了此博客文章等参考资料。

以下是我的代码:

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

我的问题是

  1. 如何将 buffer.floatChannelData 转换为 UnsafePointer<DSPComplex>,以便传递给 vDSP_ctoz 函数?也许还有一种不同/更好的方法可以绕过 vDSP_ctoz 吗?
  2. 如果缓冲区包含来自多个通道的音频,是否会有所不同?当缓冲区音频通道数据交错和非交错时有什么不同?
  3. 如何将 fft 数组中的索引转换为 Hz 中的频率?
  4. 我可能还做错了什么吗?

更新

感谢大家的建议。最终我按照被接受的答案所建议的方式填充了复数数组。当我在音叉上播放一个 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]))
    }
}
< p > 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)
}

3
嗨,我刚发现了你在Stack Overflow上的问题,必须说一声:谢谢!毫无疑问,你为我节省了大量的研究时间。我仍在逐渐理解这个答案的工作原理,但我想表达我的感激之情,因为它似乎还没有被广泛关注(或者可能只是对大多数人不相关)。 - sova
这个问题很久了,但第二部分的“out”变量是什么,你是怎么得到它的? - Logan
@Logan:out 变量是 DSPSplitComplex 的一个实例。它保存了一个复数,其中实部和虚部分别存储在不同的数组中。它由 FFT 函数填充。 - Jakub
@Jakub 谢谢,我成功解决了这个问题。你节省了我大量的时间!这是一个赞! - Logan
2个回答

3
  1. 简单粗暴的方法:您可以直接将浮点数组强制转换。其中实部和虚部是依次排列的。
  2. 这取决于音频是否交错。如果它是交错的(大多数情况下),左右声道将以STRIDE 2的方式在数组中呈现。
  3. 您的情况中最低频率是1024个样本周期的频率。在44100kHz的情况下,它约为23ms,谱中的最低频率将为1 /(1024/44100)(~ 43Hz)。下一个频率将是两倍于此的频率(~ 86Hz),以此类推。

谢谢@user1232690。用这种方式填充复杂数组似乎很有效。为了让其他人受益,我将在原帖中发布解决方案。 - Jakub
顺便提一下,for var i=0; i<bufferSize/2; ++i 可以通过以下方式进行优化:vDSP_vsmul(realp, 1, &scalar, &(complexValues) + 0, 2, (UInt)(bufferSize/2))vDSP_vsmul(imagp, 1, &scalar, &(complexValues) + 1, 2, (UInt)(bufferSize/2)),其中 scalar 是浮点数 1.0。 - user1232690

1

4:您在音频总线上安装了一个回调处理程序,这可能以实时线程优先级运行并且频繁执行。您不应该执行任何有潜在阻止的操作(它很可能导致优先级倒置和崩溃的音频):

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

  2. 调用长时间运行的操作,例如 vDSP_create_fftsetup() - 这也会分配内存并初始化它。同样,您可以在函数外部分配一次。


CoreAudio团队在今年的WWDC上对于Swift用于音频代码的问题态度相当冷淡。他们推荐传统的C++或C方法。 - marko

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