【问题标题】:How to schedule MIDI events at "sample accurate" times?如何在“样本准确”时间安排 MIDI 事件?
【发布时间】:2020-04-16 04:13:47
【问题描述】:

我正在尝试在 iOS 上构建一个音序器应用程序。 Apple Developer 网站上有一个示例,可以让音频单元播放重复音阶,这里:

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

在示例代码中,有一个文件“SimplePlayEngine.swift”,其中有一个类“InstrumentPlayer”,用于处理将 MIDI 事件发送到选定的音频单元。它生成一个带有循环的线程,该循环遍历比例。它通过调用音频单元的 AUScheduleMIDIEventBlock 发送一个 MIDI Note On 消息,使线程休眠一小段时间,发送一个 Note 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)
        ...
    }
    ...
}

这对于演示来说效果很好,但它不强制执行严格的时间安排,因为只要线程唤醒,就会安排事件。

我怎样才能修改它以在特定的节奏和采样精确的时间播放音阶?

我的假设是,在每次渲染之前,我需要一种方法让合成器音频单元在我的代码中调用回调,其中包含即将渲染的帧数。然后我可以每隔“x”帧安排一个 MIDI 事件。您可以为scheduleMIDIEventBlock 的第一个参数添加一个偏移量,最大为缓冲区的大小,这样我就可以使用它在给定的渲染周期中将事件安排在正确的帧上。

我尝试使用audioUnit.token(byAddingRenderObserver: AURenderObserver),但我给它的回调从未被调用,即使应用程序正在发出声音。该方法听起来像是 AudioUnitAddRenderNotify 的 Swift 版本,从我在这里读到的内容来看,这听起来像是我需要做的 - https://stackoverflow.com/a/46869149/11924045。怎么不叫呢?甚至有可能使用 Swift 使这个“样本准确”,还是我需要为此使用 C?

我在正确的轨道上吗?感谢您的帮助!

【问题讨论】:

    标签: ios avfoundation audiounit audiotoolbox auv3


    【解决方案1】:

    你在正确的轨道上。 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()
    )
    

    使用了AURenderCallbackscheduleMIDIEventBlock。您可以分别换入 AURenderObserverMusicDeviceMIDIEvent,获得类似的样本准确结果:

    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 音序的示例。您仍然应该遵循rules of rendering 来可靠地实现这些模式。

    【讨论】:

      【解决方案2】:

      采样准确定时通常需要使用 RemoteIO 音频单元,并使用 C 代码在每个音频回调块中的所需采样位置手动插入采样。

      (几年前关于核心音频的 WWDC 会议建议不要在音频实时上下文中使用 Swift。不确定是否有任何改变了该建议。)

      或者,对于 MIDI,在每个连续的 scheduleMIDIEventBlock 调用中使用精确递增的时间值,而不是 AUEventSampleTimeImmediate,并稍微提前设置这些调用。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-04-16
        • 1970-01-01
        • 1970-01-01
        • 2020-10-26
        • 2012-05-28
        相关资源
        最近更新 更多