使用 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<DSPComplex>
以传递给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<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))
的东西优化掉,其中标量是 1.0 浮点数【参考方案2】:
4:您已在音频总线上安装了回调处理程序。这很可能以实时线程优先级和频繁运行。您不应该做任何有可能阻塞的事情(这可能会导致优先级反转和音频故障):
分配内存(realp
、imagp
- [Float](.....)
是 Array[float]
的简写 - 并且可能在堆上分配`。预先分配这些
调用冗长的操作,例如vDSP_create_fftsetup()
- 它还分配内存并对其进行初始化。同样,您可以在函数之外分配一次。
【讨论】:
CoreAudio 团队在今年的 WWDC 上对音频代码的 swift 问题相当冷淡。他们推荐了 C++ 或 C 的传统方法。以上是关于使用 Swift 中的 Accelerate 框架来自 AVAudioPCMBuffer 的频谱图的主要内容,如果未能解决你的问题,请参考以下文章
来自 UIImage 或来自文件的 vImage - Swift 和 Accelerate 框架
如何使用 vDSP / Accelerate in swift for iOS 计算向量元素的平方根