Синхронизация AVAudioEngine для воспроизведения и записи MIDI - PullRequest
0 голосов
/ 20 октября 2018

Вопрос 1 Мой первый вопрос касается синхронизации воспроизведения при использовании AVAudioPlayerNode и AVAudioSequencer для MIDI.В основном я пытаюсь воспроизвести что-то через MIDI, но они должны быть идеально синхронизированы.

Я знаю, что есть методы синхронизации для AVAudioPlayerNode s, но секвенсор, похоже, не имеет ничего подобного.

В настоящее время я пытался использовать CAMediaTime() + delay и usleep в отдельных потоках, но, похоже, они не очень хорошо работают.

Вопрос 2 IЯ использую кран на engine.inputNode, чтобы получить запись, отдельно от воспроизведения музыки.Однако, похоже, что запись начинается раньше.Когда я сравниваю записанные данные с исходным воспроизведением, разница составляет около 300 мс.Я мог бы начать запись через 300 мс позже, но даже тогда, это не гарантирует точную синхронизацию и, скорее всего, будет зависеть от машины.

Поэтому мой вопрос в том, что было бы хорошим способом обеспечить точное начало записив данный момент начинается воспроизведение?

Ответы [ 2 ]

0 голосов
/ 26 октября 2018

ответ 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)

Здесь midi - это объект AVAudioSequencer, а song2 - это * 1027.* играет обычную песню.

Работает как шарм!

0 голосов
/ 24 октября 2018

Для синхронизации звука io часто лучше создать эталонное время, а затем использовать это время для всех расчетов, связанных с синхронизацией.

AVAudioPlayerNode.play (at:) - это то, что вам нужно для игрока.Для касания вам необходимо отфильтровать (частичные) буферы вручную, используя время, указанное в закрытии.К сожалению, AVAudioSequencer не имеет возможности запуска в определенное время, но вы можете получить эталонное время, соотнесенное с ударом, с помощью уже воспроизводимого секвенсора, используя hostTime (forBeats) .Если я правильно помню, вы не можете установить секвенсор в отрицательное положение, так что это не идеально.

Вот хакерский обходной путь, который должен дать очень точные результаты:

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)
    }
}
...