Ios 如何安排MIDI活动在;“样品准确”;时代?

Ios 如何安排MIDI活动在;“样品准确”;时代?,ios,avfoundation,audiounit,audiotoolbox,auv3,Ios,Avfoundation,Audiounit,Audiotoolbox,Auv3,我正在尝试在iOS上构建一个sequencer应用程序。苹果开发者网站上有一个示例,可以让音频单元播放重复音阶,如下所示: 在示例代码中,有一个文件“SimplePlayEngine.swift”,其中有一个类“InstrumentPlayer”,用于将MIDI事件发送到选定的音频单元。它生成了一个线程,其中包含一个循环,该循环在比例中进行迭代。它通过调用音频单元的AUScheduleMIDIEventBlock来发送消息上的MIDI注释,短时间睡眠线程,发送注释,然后重复 以下是一个节略版本

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

在示例代码中,有一个文件“SimplePlayEngine.swift”,其中有一个类“InstrumentPlayer”,用于将MIDI事件发送到选定的音频单元。它生成了一个线程,其中包含一个循环,该循环在比例中进行迭代。它通过调用音频单元的AUScheduleMIDIEventBlock来发送消息上的MIDI注释,短时间睡眠线程,发送注释,然后重复

以下是一个节略版本:

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(通过添加renderobserver:AURenderObserver),但是我给它的回调从未被调用,即使应用程序发出声音。这种方法听起来像是AudioUnitAddRenderNotify的Swift版本,从我在这里读到的内容来看,这听起来像是我需要做的事情-。为什么不叫它?是否有可能使用Swift使这个“样本准确”,或者我需要使用C来实现这一点


我走对了吗?谢谢你的帮助

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

(几年前,一次关于core audio的WWDC会议建议不要在音频实时环境中使用Swift。不确定该建议是否有任何改变。)


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

您的思路是正确的。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()
)
请注意,这些只是示例,说明了如何在飞行中进行样本精确的MIDI排序。您仍然应该遵循以可靠地实现这些模式

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)