5

问题 1 我的第一个问题是关于 MIDI使用 anAVAudioPlayerNode和 an时的播放同步。AVAudioSequencer基本上我正在尝试通过 MIDI 播放一些东西,但它们需要完全同步。

我知道AVAudioPlayerNodes 有同步方法,但音序器似乎没有类似的东西。

目前我已经尝试在单独的线程上使用CAMediaTime() + delayand usleep,但它们似乎效果不佳。

问题 2我使用点击engine.inputNode来获取录音,与音乐播放分开。但是,似乎录制开始得更早。当我将录制的数据与原始回放进行比较时,差异约为 300 毫秒。我可以在 300 毫秒后开始录制,但即便如此,这也不能保证精确同步,并且可能取决于机器。

所以我的问题是,什么是确保录制在播放开始的那一刻准确开始的好方法?

4

2 回答 2

7

对于同步音频 io,通常最好创建一个参考时间,然后将此时间用于所有与时间相关的计算。

AVAudioPlayerNode.play(at:)是播放器所需要的。对于水龙头,您需要使用关闭中提供的时间手动过滤掉(部分)缓冲区。不幸的是,AVAudioSequencer 没有在特定时间开始的功能,但是您可以使用hostTime(forBeats)获得与已经播放音序器的节拍相关的参考时间。如果我没记错的话,您不能将音序器设置为负位置,所以这并不理想。

这是一个 hacky 解决方法,应该会产生非常准确的结果:

AVAudioSequencer必须在获得参考时间之前启动,将所有midi数据偏移1,启动音序器,然后立即获得与节拍1相关的参考时间,然后将播放器的开始同步到这个时间,并使用它过滤掉水龙头捕获的不需要的音频。

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)
    }
}
于 2018-10-24T01:43:30.493 回答
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对象,song2AVAudioPlayerNode播放常规歌曲的对象。

奇迹般有效!

于 2018-10-26T06:11:36.107 回答