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

我正在尝试将录制的音频(从AVAudioEngine inputNode)同步到录制过程中正在播放的音频文件。结果应该类似于多轨录音,其中每个后续新音轨都与录制时正在播放的先前音轨同步。

因为sampleTime在不同之间AVAudioEngine的输出和输入节点,我使用hostTime确定偏移原始音频和输入缓冲器。

在 iOS 上,我会假设我必须使用AVAudioSession的各种延迟属性 ( inputLatency, outputLatency, ioBufferDuration) 来协调轨道以及主机时间偏移,但我还没有想出使它们工作的神奇组合。这同样适用于各种AVAudioEngineNode属性,如latencypresentationLatency.

在 macOS 上,AVAudioSession不存在(在 Catalyst 之外),这意味着我无法访问这些数字。同时,latency/presentationLatency属性在大多数情况下会AVAudioNodes报告0.0。在 macOS 上,我确实可以访问AudioObjectGetPropertyData并且可以向系统询问kAudioDevicePropertyLatency, kAudioDevicePropertyBufferSizekAudioDevicePropertySafetyOffset等,但是对于协调所有这些的公式又有点不知所措。

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

在我的 iPhone 上,我可以通过使用AVAudioSession's 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")
}


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

回答

此答案仅适用于本机 macOS

一般延迟确定

输出

在一般情况下,设备上流的输出延迟由以下属性的总和决定:

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

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

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

输入

同样,输入延迟由以下属性的总和决定:

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

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

在我的 Mac 上,音频设备MacBook Pro Microphone的 44.1 kHz 相当于 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


以上是AVAudioEngine在macOS/iOS上协调/同步输入/输出时间戳的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>