AVAudioEngine如何在macOS/iOS上协调/同步输入/输出时间戳

13

我正在尝试将录制的音频(来自AVAudioEngineinputNode)与录制过程中播放的音频文件同步。结果应该像多轨录音一样,每个后续新轨道都与录制时播放的先前轨道同步。

由于AVAudioEngine的输出和输入节点之间的sampleTime不同,我使用hostTime来确定原始音频和输入缓冲区的偏移量。

在iOS上,我认为我必须使用AVAudioSession的各种延迟属性(inputLatencyoutputLatencyioBufferDuration)来协调轨道以及主机时间偏移,但我还没有找到使它们正常工作的魔法组合。对于各种AVAudioEngineNode属性(如latencypresentationLatency),情况也是如此。

在macOS上,AVAudioSession不存在(除了Catalyst之外),这意味着我无法访问那些数字。同时,AVAudioNodes上的latency/presentationLatency属性在大多数情况下报告0.0。在macOS上,我确实可以访问AudioObjectGetPropertyData,并询问系统有关kAudioDevicePropertyLatencykAudioDevicePropertyBufferSizekAudioDevicePropertySafetyOffset等的信息,但我再次对如何协调所有这些的公式感到困惑。

我在https://github.com/jnpdx/AudioEngineLoopbackLatencyTest上有一个示例项目,可运行简单的回送测试(在macOS、iOS或Mac Catalyst上)并显示结果。在我的Mac上,轨道之间的偏移量约为720个样本。在其他人的Mac上,我看到了多达1500个样本的偏移量。

在我的iPhone上,通过使用AVAudioSessionoutputLatency+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")
}


运行样例应用程序后,这是我得到的结果:

同步测试结果


只是出于好奇,你最终对iOS的结论是什么? - Arshia
根据我的经验,使用 Taps 不能保证时间精度!我会使用 AVAudioSinkNode 进行录制,这相当于好老的 RenderCallback 并且能够保证采样精确。 - Arshia
1
@Arshia,与苹果工程师交谈后,我的结论是(并且注意到即使 Logic 也会出现这种“错误”),如果不经过手动校准过程,就无法从输入相对于输出获得精确的时间信息。话虽如此,我还没有像你建议的那样尝试使用 AVAudioSinkNode,所以我会试一试。 - jnpdx
感谢分享。 - Arshia
顺便提一下:在实时音频线程中写入时,您可能希望避免使用AVAudioFile,因为它似乎是同步的,并且您不希望进行任何磁盘访问...另一个选择是使用ExtAudioFileWriteAsync(C-API)。 - Arshia
是的——这里的目标是尽可能保持代码简短。 - jnpdx
2个回答

7

本答案仅适用于原生macOS系统

常规延迟确定

输出

在通常情况下,设备上的流的输出延迟由以下属性之和确定:

  1. kAudioDevicePropertySafetyOffset
  2. kAudioStreamPropertyLatency
  3. kAudioDevicePropertyLatency
  4. kAudioDevicePropertyBufferFrameSize

应检索 kAudioObjectPropertyScopeOutput 的设备安全偏移量、流和设备延迟值。

在我的Mac上,对于44.1 kHz的音频设备 MacBook Pro Speakers ,这相当于71 + 424 + 11 + 512 = 1018帧。

输入

同样,输入延迟由以下属性之和确定:

  1. kAudioDevicePropertySafetyOffset
  2. kAudioStreamPropertyLatency
  3. kAudioDevicePropertyLatency
  4. kAudioDevicePropertyBufferFrameSize

应检索 kAudioObjectPropertyScopeInput 的设备安全偏移量、流和设备延迟值。

在我的Mac上,对于44.1 kHz的音频设备 MacBook Pro Microphone ,这相当于114 + 2404 + 40 + 512 = 3070帧。

AVAudioEngine

如何将上述信息与 AVAudioEngine 相关联并不立即清楚。在内部, AVAudioEngine 创建一个私有聚合设备,Core Audio基本上自动处理聚合设备的延迟补偿。

在此答案的实验过程中,我发现一些(大多数?)音频设备没有正确报告延迟。至少看起来是这样,这使得准确的延迟确定几乎不可能。

我能够使用我的Mac内置音频进行相当精确的同步,使用以下调整:

// Some non-zero value to get AVAudioEngine running
let startDelay = 0.1

// The original audio file start time
let originalStartingFrame: AVAudioFramePosition = AVAudioFramePosition(playerNode.outputFormat(forBus: 0).sampleRate * startDelay)

// The output tap's first sample is delivered to the device after the buffer is filled once
// A number of zero samples equal to the buffer size is produced initially
let outputStartingFrame: AVAudioFramePosition = Int64(state.outputBufferSizeFrames)

// The first output sample makes it way back into the input tap after accounting for all the latencies
let inputStartingFrame: AVAudioFramePosition = outputStartingFrame - Int64(state.outputLatency + state.outputStreamLatency + state.outputSafetyOffset + state.inputSafetyOffset + state.inputLatency + state.inputStreamLatency)

在我的Mac上,AVAudioEngine聚合设备报告的值为:

// Output:
// kAudioDevicePropertySafetyOffset:    144
// kAudioDevicePropertyLatency:          11
// kAudioStreamPropertyLatency:         424
// kAudioDevicePropertyBufferFrameSize: 512

// Input:
// kAudioDevicePropertySafetyOffset:     154
// kAudioDevicePropertyLatency:            0
// kAudioStreamPropertyLatency:         2404
// kAudioDevicePropertyBufferFrameSize:  512

即相当于以下偏移量:

originalStartingFrame =  4410
outputStartingFrame   =   512
inputStartingFrame    = -2625

顺便说一下,我已经更新了我的存储库,将这些数字合并到了 feature/printLowLevelLatencies 分支中(https://github.com/jnpdx/AudioEngineLoopbackLatencyTest)。 - jnpdx
我的测试机器的数字与你的相似(1596 输出,150 输入),都是在 MBA 上进行的。在他的机器上,这似乎导致偏移更大,约为 500 个样本。你知道为什么流延迟和缓冲帧大小应该在输出端考虑,而不是输入端吗? - jnpdx
我读了几遍,但我认为我明白你的意思了。我的Mac报告的数字与你的相似(-70调整输入对66 kAudioDevicePropertySafetyOffset,以及1112调整输出对1117,用于inBuffer + outBuffer + out安全)。我错过的部分并且从您的帖子中不清楚的是这些数字是否可以在某种程度上用于对麦克风回路音频进行校准 - 我的测试(未考虑延迟)显示大约~750帧。我似乎无法将这些数字转换为该数字。你认为可能吗? 你设法对齐音频了吗? - jnpdx
https://chat.stackoverflow.com/rooms/227464/room-for-jn-pdx-and-sbooth - jnpdx
@jn_pdx 请查看编辑并查看该方法是否更适合您。我对之前使用的设备提供的延迟值失去了信心,并使用了我的Mac内置音频,结果似乎更好。 - sbooth
显示剩余3条评论

1

我可能无法回答你的问题,但我相信你的问题中没有提到的一种属性可以报告额外的延迟信息。

我只在HAL/AUHAL层工作过(从未使用AVAudioEngine),但在讨论计算总延迟时,会涉及一些音频设备/流属性:kAudioDevicePropertyLatencykAudioStreamPropertyLatency

稍微查了一下,我发现这些属性在AVAudioIONodepresentationLatency属性文档中提到(https://developer.apple.com/documentation/avfoundation/avaudioionode/1385631-presentationlatency)。我预计驱动程序报告的硬件延迟将在那里。(我怀疑标准的latency属性报告输入样本出现在“正常”节点输出的延迟时间,而IO情况是特殊的)

虽然这不是关于AVAudioEngine的内容,但是这里有一封来自CoreAudio邮件列表的信息,讨论了一些使用低级属性的背景知识:https://lists.apple.com/archives/coreaudio-api/2017/Jul/msg00035.html


presentationLatency 在 Catalyst 中对于输入和输出节点均为 0.0。在 Mac 上,它报告与 AVAudioSession.sharedInstance().outputLatency(以及 mainMixerNode.outputPresentationLatency)相同的 399 个样本。因此,知道这些属性是对齐的很有用。常规的 latency 属性都报告为 0.0(这让我想知道它们一开始为什么存在)。因此,在我的机器上还有大约 300+ 个样本需要解释...现在正在查看邮件列表链接... - jnpdx
你的链接最终指向了2020年1月的一个帖子,人们在iOS上讨论了这些问题。普遍共识是用户必须校准他们的系统才能接近样本完美。这似乎令人惊讶,因为多轨录音软件总是需要这样做。https://lists.apple.com/archives/coreaudio-api/2020/Jan/index.html - jnpdx

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