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

标签 ios swift core-audio avaudioengine mac-catalyst

我正在尝试将录制的音频(从 AVAudioEngine inputNode )同步到录制过程中正在播放的音频文件。结果应该类似于多轨录制,其中每个后续的新轨道都与录制时正在播放的先前轨道同步。
因为sampleTime AVAudioEngine 之间的差异的输出和输入节点,我用hostTime确定原始音频和输入缓冲区的偏移量。
在 iOS 上,我假设我必须使用 AVAudioSession的各种延迟属性( inputLatencyoutputLatencyioBufferDuration )来协调轨道以及主机时间偏移,但我还没有找到让它们工作的神奇组合。各种 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")
}


运行示例应用程序时,我得到以下结果:
Result of sync test

最佳答案

此答案仅适用于 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/

    相关文章:

    ios - ExtAudioFileRead太慢。如何使其更快?

    ios - 错误 ITMS-90363 "Invalid Info.plist key. The key ' CFBundleIcons~iPad 中的捆绑...无效。”

    ios - 当我想使用“时,在 Swift 中 joinWithSeparator 的正确用法是什么?

    macos - [NSSearchField 对象] : unrecognized selector sent to instance

    class - 从协议(protocol)调用类方法作为参数

    ios - 应用间录音时点击

    iphone - 使用 OpenAL 将音频转换为 CAF 格式以便在 iPhone 上播放

    ios - 如何更改导航栏默认后退按钮

    ios - RW IOSFramework 教程使用 bundle 可能出现错误?

    swift - 有没有一种方法可以将多个按钮连接到一个 VC 中的多个 wkwebview,而不是 "creating a VC for each button and wkwebview"?