swift - 如何使用 swift 同步核心音频的输入和播放
问题描述
我创建了一个用于进行声学测量的应用程序。该应用程序生成一个对数正弦扫描刺激,当用户按下“开始”时,应用程序同时播放刺激声音,并记录麦克风输入。
所有相当标准的东西。我正在使用核心音频,因为我想真正深入研究不同的功能,并可能使用多个接口,所以必须从某个地方开始学习。
这适用于 iOS,所以我正在创建一个带有 remoteIO 音频单元的 AUGraph 用于输入和输出。我已经声明了音频格式,它们是正确的,因为没有显示错误,并且 AUGraph 初始化、启动、播放声音和记录。
我在输入范围上有一个渲染回调来输入我的混音器的 1。(即,每次需要更多音频时,都会调用渲染回调,这会将一些样本从我的浮点刺激数组中读取到缓冲区中)。
let genContext = Unmanaged.passRetained(self).toOpaque()
var genCallbackStruct = AURenderCallbackStruct(inputProc: genCallback,
inputProcRefCon: genContext)
AudioUnitSetProperty(mixerUnit!, kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Input, 1, &genCallbackStruct,
UInt32(MemoryLayout<AURenderCallbackStruct>.size))
然后,我有一个输入回调,每次在 remoteIO 输入的输出范围内缓冲区已满时都会调用该回调。此回调将样本保存到数组中。
var inputCallbackStruct = AURenderCallbackStruct(inputProc: recordingCallback,
inputProcRefCon: context)
AudioUnitSetProperty(remoteIOUnit!, kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Global, 0, &inputCallbackStruct,
UInt32(MemoryLayout<AURenderCallbackStruct>.size))
一旦刺激到达最后一个样本,AUGraph 就会停止,然后我将刺激和记录的数组写入单独的 WAV 文件,以便检查我的数据。我发现目前在记录的输入和刺激之间存在大约 3000 个样本延迟。
虽然很难看到波形的开始(扬声器和麦克风都可能检测不到那么低),但刺激的结束(底部 WAV)和录音应该大致对齐。
音频会有传播时间,我意识到这一点,但在 44100Hz 采样率下,即 68ms。核心音频旨在降低延迟。
所以我的问题是,任何人都可以解释这个看起来相当高的额外延迟吗
我的 inputCallback 如下:
let recordingCallback: AURenderCallback = { (
inRefCon,
ioActionFlags,
inTimeStamp,
inBusNumber,
frameCount,
ioData ) -> OSStatus in
let audioObject = unsafeBitCast(inRefCon, to: AudioEngine.self)
var err: OSStatus = noErr
var bufferList = AudioBufferList(
mNumberBuffers: 1,
mBuffers: AudioBuffer(
mNumberChannels: UInt32(1),
mDataByteSize: 512,
mData: nil))
if let au: AudioUnit = audioObject.remoteIOUnit! {
err = AudioUnitRender(au,
ioActionFlags,
inTimeStamp,
inBusNumber,
frameCount,
&bufferList)
}
let data = Data(bytes: bufferList.mBuffers.mData!, count: Int(bufferList.mBuffers.mDataByteSize))
let samples = data.withUnsafeBytes {
UnsafeBufferPointer<Int16>(start: $0, count: data.count / MemoryLayout<Int16>.size)
}
let factor = Float(Int16.max)
var floats: [Float] = Array(repeating: 0.0, count: samples.count)
for i in 0..<samples.count {
floats[i] = (Float(samples[i]) / factor)
}
var j = audioObject.in1BufIndex
let m = audioObject.in1BufSize
for i in 0..<(floats.count) {
audioObject.in1Buf[j] = Float(floats[I])
j += 1 ; if j >= m { j = 0 }
}
audioObject.in1BufIndex = j
audioObject.inputCallbackFrameSize = Int(frameCount)
audioObject.callbackcount += 1
var WindowSize = totalRecordSize / Int(frameCount)
if audioObject.callbackcount == WindowSize {
audioObject.running = false
}
return 0
}
所以从引擎启动开始,应该在从remoteIO收集到第一组数据后调用这个回调。512 个样本,因为这是默认分配的缓冲区大小。它所做的只是将有符号整数转换为浮点数,然后保存到缓冲区。in1BufIndex 的值是对写入数组中最后一个索引的引用,每次回调都会引用和写入该索引,以确保数组中的数据对齐。
目前,在听到捕获的扫描之前,记录的阵列中似乎有大约 3000 个无声样本。通过在 Xcode 中调试检查记录的数组,所有样本都有值(是的,前 3000 个非常安静),但不知何故这并没有加起来。
下面是用于播放我的刺激的生成器回调
let genCallback: AURenderCallback = { (
inRefCon,
ioActionFlags,
inTimeStamp,
inBusNumber,
frameCount,
ioData) -> OSStatus in
let audioObject = unsafeBitCast(inRefCon, to: AudioEngine.self)
for buffer in UnsafeMutableAudioBufferListPointer(ioData!) {
var frames = buffer.mData!.assumingMemoryBound(to: Float.self)
var j = 0
if audioObject.stimulusReadIndex < (audioObject.Stimulus.count - Int(frameCount)){
for i in stride(from: 0, to: Int(frameCount), by: 1) {
frames[i] = Float((audioObject.Stimulus[j + audioObject.stimulusReadIndex]))
j += 1
audioObject.in2Buf[j + audioObject.stimulusReadIndex] = Float((audioObject.Stimulus[j + audioObject.stimulusReadIndex]))
}
audioObject.stimulusReadIndex += Int(frameCount)
}
}
return noErr;
}
解决方案
可能至少有 4 件事会导致往返延迟。
512 个样本或 11 毫秒是在 remoteIO 可以调用您的回调之前收集足够样本所需的时间。
声音以每毫秒约 1 英尺的速度传播,是往返的两倍。
DAC 具有输出延迟。
多个 ADC(您的 iOS 设备上有超过 1 个麦克风)需要时间来采样和后处理音频(用于 sigma-delta、波束形成、均衡等)。后处理可能以块的形式完成,因此会产生延迟以收集足够的样本(未记录的数字)用于一个块。
在 ADC 和系统内存之间移动数据(某些未知块大小的硬件 DMA?)以及驱动程序和操作系统上下文切换开销可能还会增加开销延迟。
还有一个启动延迟来启动音频硬件子系统(放大器等),因此最好在输出声音(频率扫描)之前开始播放和录制音频。
推荐阅读
- javascript - 标签动态设置 onclick 属性触发功能,即使我没有点击
- regex - BigQuery 正则表达式后向/前瞻解决方法?
- data-structures - 完整图中的后边和前边
- python - eli5.permutation_importance get_score_importances 使 Google Colab 会话崩溃
- node.js - 删除子文档时如何从父文档中删除引用 ObjectId?
- python - 如何添加新列并插入与 dask 中的另一列有关系的值?
- node.js - 用户卸载应用程序时如何在云功能中获取firebase UID?
- powerbi - PowerBI 直接查询方法 - 需要帮助
- r - 栅格叠加应用于第一个栅格的子集
- javascript - 带有类的新音频(url)