Как запланировать MIDI-события в «точное время выборки»? - PullRequest
0 голосов
/ 16 апреля 2020

Я пытаюсь создать приложение секвенсора на iOS. На сайте Apple Developer есть образец, который заставляет аудиоустройство воспроизводить повторяющуюся шкалу, здесь:

https://developer.apple.com/documentation/audiotoolbox/incorporating_audio_effects_and_instruments

В примере кода есть файл " SimplePlayEngine.swift ", с классом" InstrumentPlayer ", который обрабатывает отправку MIDI-событий на выбранный аудиоустройство. Он порождает поток с al oop, который перебирает масштаб. Он отправляет сообщение MIDI Note On, вызывая AUScheduleMIDIEventBlock аудиоустройства, на короткое время спит нить, отправляет ноту Off и повторяет.

Вот сокращенная версия:

DispatchQueue.global(qos: .default).async {
    ...
    while self.isPlaying {
        // cbytes is set to MIDI Note On message
        ...
        self.audioUnit.scheduleMIDIEventBlock!(AUEventSampleTimeImmediate, 0, 3, cbytes)
        usleep(useconds_t(0.2 * 1e6))
        ...
        // cbytes is now MIDI Note Off message
        self.noteBlock(AUEventSampleTimeImmediate, 0, 3, cbytes)
        ...
    }
    ...
}

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

Как я могу изменить его, чтобы воспроизводить шкалу в определенном темпе с образцом -точный тайминг?

Я предполагаю, что мне нужен способ заставить аудиоустройство синтезатора вызывать обратный вызов в моем коде перед каждым рендерингом с количеством кадров, которые должны быть визуализированы. Затем я могу запланировать MIDI-событие на каждое количество «х» кадров. Вы можете добавить смещение, вплоть до размера буфера, к первому параметру scheduleMIDIEventBlock, чтобы я мог использовать его для планирования события точно в нужном кадре в данном цикле рендеринга.

I попытался использовать audioUnit.token(byAddingRenderObserver: AURenderObserver), но ответный звонок, который я ему дал, никогда не вызывался, хотя приложение издавало звук. Этот метод звучит так, как будто это Swift-версия AudioUnitAddRenderNotify, и из того, что я здесь прочитал, звучит как то, что мне нужно сделать - { ссылка }. Почему это не будет называться? Можно ли даже сделать этот «точный образец» с помощью Swift или мне нужно использовать C для этого?

Я на правильном пути? Спасибо за вашу помощь!

Ответы [ 2 ]

0 голосов
/ 21 апреля 2020

Вы на правильном пути. События MIDI могут быть запланированы с точностью сэмпла в обратном вызове рендеринга:

let sampler = AVAudioUnitSampler()

...

let renderCallback: AURenderCallback = {
    (inRefCon: UnsafeMutableRawPointer,
    ioActionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>,
    inTimeStamp: UnsafePointer<AudioTimeStamp>,
    inBusNumber: UInt32,
    inNumberFrames: UInt32,
    ioData: UnsafeMutablePointer<AudioBufferList>?) -> OSStatus in

    if ioActionFlags.pointee == AudioUnitRenderActionFlags.unitRenderAction_PreRender {
        let sampler = Unmanaged<AVAudioUnitSampler>.fromOpaque(inRefCon).takeUnretainedValue()

        let bpm = 960.0
        let samples = UInt64(44000 * 60.0 / bpm)
        let sampleTime = UInt64(inTimeStamp.pointee.mSampleTime)
        let cbytes = UnsafeMutablePointer<UInt8>.allocate(capacity: 3)
        cbytes[0] = 0x90
        cbytes[1] = 64
        cbytes[2] = 127
        for i:UInt64 in 0..<UInt64(inNumberFrames) {
            if (((sampleTime + i) % (samples)) == 0) {
                sampler.auAudioUnit.scheduleMIDIEventBlock!(Int64(i), 0, 3, cbytes)
            }
        }
    }

    return noErr
}

AudioUnitAddRenderNotify(sampler.audioUnit,
                         renderCallback,
                         Unmanaged.passUnretained(sampler).toOpaque()
)

При этом используются AURenderCallback и scheduleMIDIEventBlock. Вы можете поменять значения AURenderObserver и MusicDeviceMIDIEvent, соответственно, с похожими результатами с точностью до сэмпла:

let audioUnit = sampler.audioUnit

let renderObserver: AURenderObserver = {
    (actionFlags: AudioUnitRenderActionFlags,
    timestamp: UnsafePointer<AudioTimeStamp>,
    frameCount: AUAudioFrameCount,
    outputBusNumber: Int) -> Void in

    if (actionFlags.contains(.unitRenderAction_PreRender)) {
        let bpm = 240.0
        let samples = UInt64(44000 * 60.0 / bpm)
        let sampleTime = UInt64(timestamp.pointee.mSampleTime)

        for i:UInt64 in 0..<UInt64(frameCount) {
            if (((sampleTime + i) % (samples)) == 0) {
                MusicDeviceMIDIEvent(audioUnit, 144, 64, 127, UInt32(i))
            }
        }
    }
}

let _ = sampler.auAudioUnit.token(byAddingRenderObserver: renderObserver)

Обратите внимание, что это всего лишь примеры того, как можно выполнить MIDI-секвенирование с точностью до сэмпла на лету , Вы все равно должны следовать правилам рендеринга , чтобы надежно реализовать эти шаблоны.

0 голосов
/ 21 апреля 2020

Точная синхронизация сэмплов обычно требует использования аудиоустройства RemoteIO и ручной вставки сэмплов в нужную позицию сэмплов в каждый блок обратного аудиоклипа с использованием кода C.

(Сессия WWD C по базовому аудио несколько лет назад рекомендовала не использовать Swift в контексте аудио в реальном времени. Не уверен, что что-то изменило эту рекомендацию.)

Или, для MIDI используйте точно увеличенное значение времени в каждом последующем вызове scheduleMIDIEventBlock вместо AUEventSampleTimeImmediate и установите эти вызовы немного раньше времени.

...