AVAudioEngine同步MIDI播放和录制

5

问题1:

我的第一个问题涉及使用AVAudioPlayerNodeAVAudioSequencer进行MIDI播放时的回放同步。基本上,我正在尝试通过MIDI播放一些东西,但它们需要完美同步。

我知道AVAudioPlayerNode有同步方法,但序列器似乎没有这样的方法。

目前,我已经尝试在单独的线程上使用CAMediaTime() + delayusleep,但它们似乎并不起作用。

问题2:

我正在使用engine.inputNode上的tap来获取录音,与音乐播放分开。但是,录制似乎是提前开始的。当我将记录的数据与原始回放进行比较时,差异约为300毫秒。我可以晚些时候再开始录制300毫秒,但即使如此,也不能保证精准同步,并且可能会因机器而异。

所以我的问题是,如何确保录制在回放开始的那一刻准确开始?

2个回答

7
为了同步音频io,通常最好创建一个参考时间,然后将此时间用于所有与时间有关的计算。AVAudioPlayerNode.play(at:)是您需要的播放器。对于tap,您需要使用闭包中提供的时间手动过滤(部分)缓冲区。不幸的是,AVAudioSequencer没有启动特定时间的设施,但是您可以使用hostTime(forBeats)获取与已经播放的序列相关联的节拍的参考时间。如果我没记错的话,您不能将序列设置为负位置,因此这并不理想。以下是一个hacky解决方法,应该会产生非常精确的结果:在获取参考时间之前必须启动AVAudioSequencer,将所有midi数据偏移1,启动序列,然后立即获取与第1拍相关联的参考时间,然后将播放器的开始与此时间同步,并使用它来过滤掉tap捕获的不需要的音频。
func syncStart() throws {
    //setup
    sequencer.currentPositionInBeats = 0
    player.scheduleFile(myFile, at: nil)
    player.prepare(withFrameCount: 4096)

    // Start and get reference time of beat 1
    try sequencer.start()
    // Wait until first render cycle completes or hostTime(forBeats) will err - AVAudioSequencer is fragile :/
    while (self.sequencer.currentPositionInBeats <= 0) { usleep(UInt32(0.001 * 1000000.0)) }
    var nsError: NSError?
    let hostTime = sequencer.hostTime(forBeats: 1, error: &nsError)
    let referenceTime = AVAudioTime(hostTime: hostTime)

    // AVAudioPlayer is great for this.
    player.play(at: referenceTime)

    // This just rejects buffers that come too soon. To do this right you need to record partial buffers.
    engine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: nil) { (buffer, audioTime) in
        guard audioTime.hostTime >= referenceTime.hostTime else { return }
        self.recordBuffer(buffer: buffer)
    }
}

谢谢。不幸的是,这对我没有用,因为hostTime(forBeats:error:)一直导致崩溃,但它确实让我走上了正确的道路。我会在我的答案中引用这个答案,并发布我所做的事情。 - funct7
我通过等待第一个渲染周期完成后再调用hostTime(forBeats:)来解决了这个问题。 - dave234

0

dave234的回答对我不起作用,因为hostTime(forBeats:error :) 甚至在先启动序列器后仍然会崩溃。(异步调度一段时间后确实可行,但这会引起更多的复杂性)。但是,它提供了有关同步方法的有价值的见解,以下是我的做法:

var refTime: AVAudioTime

if isMIDIPlayer {
    sequencer!.tracks.forEach { $0.offsetTime = 1 }
    sequencer!.currentPositionInBeats = 0

    let sec = sequencer!.seconds(forBeats: 1)
    let delta = AVAudioTime.hostTime(forSeconds: sec) + mach_absolute_time()
    refTime = AVAudioTime(hostTime: delta)

    try sequencer!.start()
} else {
    player!.prepare(withFrameCount: 4096)

    let delta = AVAudioTime.hostTime(forSeconds: 0.5) + mach_absolute_time()
    refTime = AVAudioTime(hostTime: delta)

    player!.play(at: refTime)
}

mixer.installTap(
    onBus: 0,
    bufferSize: 8,
    format: mixer.outputFormat(forBus: 0)
) { [weak self] (buffer, time) in
    guard let strongSelf = self else { return }
    guard time.hostTime >= refTime.hostTime else { print("NOPE"); return }

    do {
        try strongSelf.recordFile!.write(from: buffer)
    } catch {
        // TODO: Handle error
        print(error)
    }
}

关于代码片段的一些解释:

  • 我制作了一个通用的AudioPlayer,可以播放MIDI和其他歌曲文件,代码来自AudioPlayer内部的一个方法。
  • sequencer用于MIDI播放。
  • player用于其他歌曲文件。

MIDI播放同步使用类似的方法:

midi!.sequencer!.tracks.forEach { $0.offsetTime = 1 }

let sec = midi!.sequencer!.seconds(forBeats: 1)
let delta = AVAudioTime.hostTime(forSeconds: sec) + mach_absolute_time()
let refTime = AVAudioTime(hostTime: delta)

do {
    try midi!.play()
} catch {
    // TODO: Add error handler
    print(error)
}

song2.playAtTime(refTime)

在这里,midiAVAudioSequencer 对象,而 song2 是播放常规歌曲的 AVAudioPlayerNode

运行得非常好!


这种方法不太准确。如果不需要样本精度,也许可以接受,但是我下面发布的方法应该能给你带来非常准确的结果。 - dave234
我不知道你是从哪里得出了不准确的概念,就我所知,AVAudioSequencerAVAudioPlayerNode都被安排在将来的时间点播放,这取决于mach_absolute_time()(又称主机时间)。唯一的不准确之处可能源于写入缓冲区太大,这也是你的代码存在的问题。我已经亲自测试过我的代码。你测试过你的代码吗? - funct7
1
你做出了这样的假设,即在调用start()时机器时间将与序列器的实际启动时间相关,但事实并非如此。在调用start()后,序列的实际启动时间是未记录的,但经验上它是下一个音频渲染周期的开始 - 如果您之前调用了prepareToPlay()。就测试而言,将deltahostTime(forBeats: 0)进行比较,您将看到使用此方法的差异。 - dave234
只要人们听到同时播放的音符,他们就会感到满意。他们不会说某个玩家比另一个玩家快了10个CPU时钟周期,所以我就结束了。 - funct7
这更像是0.02秒,但如果听起来好,那就好 :) - dave234

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