【问题标题】:How to properly use MIDIReadProc?如何正确使用 MIDIReadProc?
【发布时间】:2016-05-16 21:19:11
【问题描述】:

根据苹果的文档,它说:

因为您的 MIDIReadProc 回调是从单独的线程调用的, 使用提供的数据时要注意同步问题 这个回调。

这是否意味着,使用@synchronize 来进行线程阻塞以确保安全?

或者这是否意味着可能会发生同步时间问题?

我目前正在尝试读取 MIDI 文件,并使用 MIDIReadProc 来触发基于 MIDI 事件的软件合成器的音符开/音符关。我需要它非常可靠并且完全及时。现在,我注意到,当我使用这些 midi 事件并将音频写入缓冲区(全部从 MIDIReadProc 完成)时,时间非常草率,而且听起来根本不正确。所以我想知道,从 MIDIReadProc 消费 midi 事件的“正确”方式是什么?

另外,MIDIReadProc 是从 MIDI 文件中消费 MIDI 事件的唯一选项吗?

就设置可以由我的合成器直接使用的虚拟端点而言,还有其他选择吗?如果是这样,那具体是如何工作的?

【问题讨论】:

    标签: core-audio coremidi


    【解决方案1】:

    如果你假设这种格式的函数是midiReadProc

    void midiReadProc(const MIDIPacketList *packetList, 
                  void* readProcRefCon, 
                  void* srcConnRefCon)
    {
        MIDIPacket *packet = (MIDIPacket*)packetList->packet;
    
        int count = packetList->numPackets;
        for (int k=0; k<count; k++) {
            Byte midiStatus = packet->data[0];
            Byte midiChannel= midiStatus & 0x0F;
            Byte midiCommand = midiStatus >> 4;
    
        //parse MIDI messages, extract relevant information and pass it to the controller
        //controller must be visible from the midiReadProc
        }     
     packet = MIDIPacketNext(packet);
    }
    

    必须在 controller 中声明 MIDI 客户端,解释的 MIDI 事件从 MIDI 回调存储到 controller,并在每个音频渲染周期由 audioRenderCallback() 读取。通过这种方式,您可以最大限度地减少对 音频缓冲区的长度,您可以在设置 AudioUnit 期间协商使其尽可能短。

    控制器可以是您定义的@interface myMidiSynthController : NSViewController,包括一个 MIDI 通道矩阵和一个预先确定的每个通道的最大复音数,以及其他相关数据,例如接口元素、每个活动语音的相位累加器、 AudioComponentInstance 等...根据midiReadProc() 输入调整控制器的大小是错误的。现在 RAM 很便宜。

    我正在使用此类 MIDI 回调来处理来自 MIDI 设备的实时输入。关于 MIDI 文件的播放,如果您 想要处理任意复杂的流或文件,您也可能会遇到意外。 MIDI 标准本身 具有计时功能,与 MIDI 硬件允许的一样好。将整个文件读入内存后,您可以 将您的数据转换为您想要的任何内容,并使用您自己的代码来控制声音合成。
    请注意不要使用任何会阻塞音频渲染线程的代码(即在audioRenderCallback() 内部),或者会对其进行内存管理。

    【讨论】:

    • 但是你没有提到如何“将它传递给控制器​​”,以便它可以在这个特殊的 midiReadProc 高优先级线程之外进行处理。显然,我需要将这些数据包存储到主线程可以消费并转换为音频的数据结构中。如何正确地做到这一点?理想情况下,我想要一个多维数组,这样我就可以做 packet[midiChannel][currentChannelPacketIndex] = *packet ... 但我不知道如何判断给定通道上总共有多少数据包,以便我可以正确分配该存储的内存。
    • @patrick,您关于“主线程可以消耗某些东西并变成音频”的假设似乎有措辞问题,或者不太准确。只要音频制作/消费是在实时渲染线程上完成的,主线程只负责 GUI 更新,这是最有效的计时选项。关于将 MIDI 生成的数据传递给控制器​​,如果你只是这样传递它,你仍然需要将数据包解码为你的渲染线程可以理解的东西。您的问题相当广泛,我会尽快调整我的答案。
    • “关于 MIDI 文件的播放,如果你想处理任意复杂的流或文件,你也可能会遇到意外。” ——这是我的问题。我正在尝试流式传输 185bpm 的 midi 文件,其中最快的音符是 16 度,这听起来像是一场绝对的灾难..
    • 我担心的是 MidiReadProc 事件触发的时间是准确的。我做了一个实现,我将 midi 数据包写入我的合成器,并在一个单独的线程中,我的合成器轮询,等待midi 数据包到达,并执行注释事件,并将所有这些输出到缓冲区。播放完 midi 文件后,将播放缓冲区,听起来像火车残骸。我尝试了许多不同的实现/方法,但它们似乎都不起作用............
    • MidiReadProc 没有“发射”任何东西。您提供的信息不足以得出结论。请阅读此SO post。不要从任何 CoreMIDI I/O 上下文(例如此输入回调)调用阻塞函数,如 printf()、NSLog()、malloc() 或 Objective C 方法调用 - 你会导致优先级反转。请务必阅读documentation
    【解决方案2】:

    您可以使用AVAudioEngine.musicSequence 并准备您的音频单元图。然后使用MusicSequence API 加载您的 GM 文件。像这样你不需要自己做计时。请注意,到目前为止我自己还没有这样做,但我理解理论上它应该像这样工作。

    实例化合成器音频单元后,将其附加并连接到AVAudioEngine 图表。

    这是否意味着,使用@synchronize 来进行线程阻塞以确保安全?

    你所说的恰恰相反:你当然不应该锁定实时线程。如果资源已被锁定,@synchronized 指令将锁定。您可以考虑为实时线程使用无锁队列。另见Four common mistakes in audio development

    如果您必须使用 CoreMIDI 和 MIDIReadProc,您可以通过在回调中直接调用 MusicDeviceMIDIEvent 将 MIDI 命令发送到合成器音频单元。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-12-04
      • 2012-07-12
      • 2013-01-21
      • 2021-10-24
      • 2011-12-22
      • 2020-10-14
      • 2015-12-10
      • 2021-01-15
      相关资源
      最近更新 更多