对于同步音频 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)
}
}