我正在尝试将录制的音频(从 AVAudioEngine
inputNode
)同步到录制过程中正在播放的音频文件。结果应该类似于多轨录制,其中每个后续的新轨道都与录制时正在播放的先前轨道同步。
因为sampleTime
AVAudioEngine
之间的差异的输出和输入节点,我用hostTime
确定原始音频和输入缓冲区的偏移量。
在 iOS 上,我假设我必须使用 AVAudioSession
的各种延迟属性( inputLatency
, outputLatency
, ioBufferDuration
)来协调轨道以及主机时间偏移,但我还没有找到让它们工作的神奇组合。各种 AVAudioEngine
也是如此。和 Node
latency
等属性和 presentationLatency.
在 macOS 上,AVAudioSession
不存在(在 Catalyst 之外),这意味着我无权访问这些数字。同时,latency
/presentationLatency
AVAudioNodes
上的属性举报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")
}
运行示例应用程序时,我得到以下结果:最佳答案
此答案仅适用于 native macOS
一般延迟测定
输出
在一般情况下,设备上流的输出延迟由以下属性的总和确定:
kAudioDevicePropertySafetyOffset
kAudioStreamPropertyLatency
kAudioDevicePropertyLatency
kAudioDevicePropertyBufferFrameSize
应检索
kAudioObjectPropertyScopeOutput
的设备安全偏移量、流和设备延迟值.在我的 Mac 上用于音频设备
MacBook Pro Speakers
在 44.1 kHz 时,这相当于 71 + 424 + 11 + 512 = 1018 帧。输入
同样,输入延迟由以下属性的总和决定:
kAudioDevicePropertySafetyOffset
kAudioStreamPropertyLatency
kAudioDevicePropertyLatency
kAudioDevicePropertyBufferFrameSize
应检索
kAudioObjectPropertyScopeInput
的设备安全偏移量、流和设备延迟值.在我的 Mac 上用于音频设备
MacBook Pro Microphone
在 44.1 kHz 时,这相当于 114 + 2404 + 40 + 512 = 3070 帧。AVAudioEngine
上述信息与 AVAudioEngine
的关系目前还不清楚。内部 AVAudioEngine
创建一个私有(private)聚合设备,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
关于ios - AVAudioEngine 在 macOS/iOS 上协调/同步输入/输出时间戳,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65600996/