Синхронизация AVAudioEngine для воспроизведения и записи MIDI

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

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

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

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

Итак, мой вопрос: что было бы хорошим способом гарантировать, что запись начнется именно в момент начала воспроизведения?


person funct7    schedule 20.10.2018    source источник


Ответы (2)


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

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

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

AVAudioSequencer должен быть запущен до получения эталонного времени, сместить все ваши миди-данные на 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)
    }
}
person dave234    schedule 24.10.2018
comment
Спасибо. К сожалению, это не сработало для меня, потому что hostTime(forBeats:error:) продолжало вызывать сбой, но это определенно поставило меня на правильный путь. Я сошлюсь на ответ в своем собственном ответе и опубликую, что я сделал. - person funct7; 26.10.2018
comment
Там я исправил это, дождавшись завершения первого цикла рендеринга перед вызовом hostTime(forBeats:) - person dave234; 26.10.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 — это AVAudioPlayerNode, который воспроизводит обычную песню.

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

person funct7    schedule 26.10.2018
comment
Это довольно неточный метод здесь. Если точность выборки не требуется, это может быть хорошо, но метод, который я разместил ниже, должен дать вам очень точные результаты. - person dave234; 26.10.2018
comment
@ dave234 Интересно, откуда у тебя понятие неточности. Насколько я могу судить, и AVAudioSequencer, и AVAudioPlayerNode запланированы на будущий момент времени, который считается по mach_absolute_time(), также известному как время хоста. Единственная неточность может быть связана со слишком большим размером буфера записи, который также есть в вашем коде. Я лично тестировал свой код. Вы свои тестировали? - person funct7; 27.10.2018
comment
Вы делаете предположение, что машинное время при вызове start() будет коррелировать с фактическим временем запуска секвенсора, но это не так. Фактическое время начала последовательности после вызова start() недокументировано, но эмпирически это начало следующего цикла рендеринга звука - если вы вызывали prepareToPlay() до этого. Что касается тестирования, сравните delta с hostTime(forBeats: 0), и вы увидите расхождение, используя этот метод. - person dave234; 28.10.2018
comment
Да, пока люди слышат одновременно сыгранные ноты, они будут счастливы. Они не станут говорить, что один игрок играет на 10 тактов процессора быстрее, так что я закончил. - person funct7; 28.10.2018
comment
Это больше похоже на 0,02 секунды, но если звучит хорошо, то звучит хорошо :) - person dave234; 28.10.2018