我正在尝试将录制的音频(来自AVAudioEngine
的inputNode
)与录制过程中播放的音频文件同步。结果应该像多轨录音一样,每个后续新轨道都与录制时播放的先前轨道同步。
由于AVAudioEngine
的输出和输入节点之间的sampleTime
不同,我使用hostTime
来确定原始音频和输入缓冲区的偏移量。
在iOS上,我认为我必须使用AVAudioSession
的各种延迟属性(inputLatency
、outputLatency
、ioBufferDuration
)来协调轨道以及主机时间偏移,但我还没有找到使它们正常工作的魔法组合。对于各种AVAudioEngine
和Node
属性(如latency
和presentationLatency
),情况也是如此。
在macOS上,AVAudioSession
不存在(除了Catalyst之外),这意味着我无法访问那些数字。同时,AVAudioNodes
上的latency
/presentationLatency
属性在大多数情况下报告0.0
。在macOS上,我确实可以访问AudioObjectGetPropertyData
,并询问系统有关kAudioDevicePropertyLatency
、kAudioDevicePropertyBufferSize
、kAudioDevicePropertySafetyOffset
等的信息,但我再次对如何协调所有这些的公式感到困惑。
我在https://github.com/jnpdx/AudioEngineLoopbackLatencyTest上有一个示例项目,可运行简单的回送测试(在macOS、iOS或Mac Catalyst上)并显示结果。在我的Mac上,轨道之间的偏移量约为720个样本。在其他人的Mac上,我看到了多达1500个样本的偏移量。
在我的iPhone上,通过使用AVAudioSession
的outputLatency
+inputLatency
,我可以接近完美的样本。但是,同样的公式会导致iPad上的不对齐。
每个平台上同步输入和输出时间戳的魔法公式是什么?我知道它们可能在每个平台上都不同,这很好,而且我知道我不会获得100%的准确性,但在经过自己的校准过程之前,我希望尽可能接近
这是我目前的代码示例(完整的同步逻辑可以在https://github.com/jnpdx/AudioEngineLoopbackLatencyTest/blob/main/AudioEngineLoopbackLatencyTest/AudioManager.swift找到):
//Schedule playback of original audio during initial playback
let delay = 0.33 * state.secondsToTicks
let audioTime = AVAudioTime(hostTime: mach_absolute_time() + UInt64(delay))
state.audioBuffersScheduledAtHost = audioTime.hostTime
...
//in the inputNode's inputTap, store the first timestamp
audioEngine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (pcmBuffer, timestamp) in
if self.state.inputNodeTapBeganAtHost == 0 {
self.state.inputNodeTapBeganAtHost = timestamp.hostTime
}
}
...
//after playback, attempt to reconcile/sync the timestamps recorded above
let timestampToSyncTo = state.audioBuffersScheduledAtHost
let inputNodeHostTimeDiff = Int64(state.inputNodeTapBeganAtHost) - Int64(timestampToSyncTo)
let inputNodeDiffInSamples = Double(inputNodeHostTimeDiff) / state.secondsToTicks * inputFileBuffer.format.sampleRate //secondsToTicks is calculated using mach_timebase_info
//play the original metronome audio at sample position 0 and try to sync everything else up to it
let originalAudioTime = AVAudioTime(sampleTime: 0, atRate: renderingEngine.mainMixerNode.outputFormat(forBus: 0).sampleRate)
originalAudioPlayerNode.scheduleBuffer(metronomeFileBuffer, at: originalAudioTime, options: []) {
print("Played original audio")
}
//play the tap of the input node at its determined sync time -- this _does not_ appear to line up in the result file
let inputAudioTime = AVAudioTime(sampleTime: AVAudioFramePosition(inputNodeDiffInSamples), atRate: renderingEngine.mainMixerNode.outputFormat(forBus: 0).sampleRate)
recordedInputNodePlayer.scheduleBuffer(inputFileBuffer, at: inputAudioTime, options: []) {
print("Input buffer played")
}
运行样例应用程序后,这是我得到的结果:
AVAudioSinkNode
,所以我会试一试。 - jnpdx