ios - 如何安排 MIDI 事件在 "sample accurate"次?

标签 ios avfoundation audiounit audiotoolbox auv3

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

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 来实现?

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

最佳答案

你走在正确的道路上。可以在渲染回调中以采样精度安排 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可靠地实现这些模式。

关于ios - 如何安排 MIDI 事件在 "sample accurate"次?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61242565/

相关文章:

iphone - 如何录制/保存iPhone应用程序中正在播放的音频?

ios - UIRefreshController 遍历 UICollectionView

iphone - VBR 音频的 MPMusicPlayer currentPlaybackTime 不准确

swift - HLS 流使用 AVAssetResourceLoaderDelegate TS 片段请求缺少 Cookie header

ios - AVAssetWriter 忽略变换值

ios - AudioRingBuffer::GetTimeBounds 中的 EXC_BAD_ACCESS

ios - 如何限制信标的广告范围?

ios - 限制UIbutton的可触摸区域

ios - 申请被无故终止

macos - OSX 核心音频 : Getting inNumberFrames in advance - on initialization?