如何检测 MidiInProc 中丢失的 SysEx 数据?

Posted

技术标签:

【中文标题】如何检测 MidiInProc 中丢失的 SysEx 数据?【英文标题】:How can I detect missed SysEx data in MidiInProc? 【发布时间】:2018-02-07 10:26:43 【问题描述】:

我正在 Windows 10 下用 C# 编写一个程序来从 MIDI 设备捕获 SysEx 消息。我正在使用如下回调函数:

    private delegate void MidiInProc(
        int handle,
        uint msg,
        int instance,
        int param1,
        int param2);

    [DllImport("winmm.dll")]
    private static extern int midiInOpen(
        out int handle,
        int deviceID,
        MidiInProc proc,
        int instance,
        int flags);

    private int hHandle;

    public MidiInput(int deviceID)
    
        MidiInProc midiInProc = MidiInProcess;

        int hResult = midiInOpen(
            out hHandle,
            deviceID,
            midiInProc,
            0,
            CALLBACK_FUNCTION | MIDI_IO_STATUS);

        for (int i = 0; i < NUM_MIDIHDRS; ++i)
        
            InitMidiHdr(i);
        
    

    private void MidiInProcess(int hMidiIn, uint uMsg, int dwInstance, int dwParam1, int dwParam2)
    
        switch (uMsg)
        
            case MIM_OPEN:

                break;

            case MIM_CLOSE:

                break;

            case MIM_DATA:

                QueueData(dwParam1);

                break;

            case MIM_MOREDATA:

                QueueData(dwParam1);

                break;

            case MIM_LONGDATA:

                QueueLongData((IntPtr)dwParam1);

                break;

            case MIM_ERROR:

                throw new ApplicationException(string.Format("Invalid MIDI message: 0", dwParam1));

            case MIM_LONGERROR:

                throw new ApplicationException("Invalid SysEx message.");

            default:

                throw new ApplicationException(string.Format("unexpected message: 0", uMsg));
        
    

InitMidiHdr 方法在堆上创建NUM_MIDIHDRS 标头和缓冲区,并通过调用MidiInPrepareHeaderMidiInAddBuffer 将它们的地址传递给驱动程序。当接收到 SysEx 数据时,回调切换到 MIM_LONGDATA 情况并将缓冲区地址排队到另一个线程,该线程将其出列,处理它,然后通过另一个调用将其传递回驱动程序以进一步使用 MidiInAddBuffer

现在,一些程序不使用单独的线程,而是在回调的线程上处理 SysEx 数据。根据我的阅读,这在大多数情况下都有效,但并非总是如此,并且违反了MSDN's advice:“应用程序不应从回调函数内部调用任何多媒体函数[。]”这可能不是当代的问题驱动程序,但过去有些用户在直接从回调中调用 MidiInAddBuffer 时报告了死锁。

但是……

当我使用工作线程调用MidiInAddBuffer 时,我想不出一种方法来保证它能够跟上对回调的调用。当然,如果工人所做的唯一 事情是返回缓冲区,它可能会保持领先于回调,但依赖一个线程保持领先于另一个没有显式同步是一种不好的做法。 (让回调等待来自它已返回缓冲区的工作人员的信号不起作用,因为 MidiInAddBuffer 在从另一个线程调用时会阻塞,直到回调返回,如果您将其返回设置为返回,则会导致死锁来自工作线程中的MidiInAddBuffer。)因此,在这种情况下,驱动程序可能会在 MIDI 设备仍在发送 SysEx 数据时耗尽缓冲区。 (事实上​​,即使在回调自己完成所有处理的情况下,它也可能落后,驱动程序在设备停止发送 SysEx 数据之前用完所有缓冲区。)

我发现几个开源项目使用工作人员调用MidiInAddBuffer,但似乎都依赖工作人员来保持领先于回调的调用。事实上,在一种情况下,我向工作线程添加了一个 50 毫秒的睡眠调用,结果是它远远落后于回调线程,这最终意味着只有大约一半的 SysEx 消息到达回调。显然,驱动程序不会在内部缓冲 SysEx 数据,当它没有可用的缓冲区时,会丢弃 SysEx 数据,直到它获得另一个缓冲区。

现在,内存很便宜等等,一个“解决方案”是拥有 很多 个缓冲区。然而,我的 Behringer BCF2000 在单个转储中发送几乎 500 条不同的 SysEx 消息,每条消息都在自己的缓冲区中。这不是一个不可能提供的数字,但它需要猜测有多少真的足够(而且,鉴于人们使用 SysEx 消息做的一些奇特的事情,比如传递音频样本,这种方法可能会变得笨拙)。

唉,当我的测试显示我的代码跟不上时,MIM_LONGERROR 案例永远不会被调用,所以这没有任何帮助。

所以,这是我的问题:如果我的代码无法跟上 MIDI 设备驱动程序消耗 SysEx 缓冲区的速度并且我最终丢失了一些 SysEx 数据,有没有办法至少检测到我错过了那些数据?

【问题讨论】:

【参考方案1】:

没有针对丢失的 SysEx 缓冲区的报告机制。

在 3125 字节/秒的 MIDI 速度下,50 毫秒的延迟对应于 156 字节的缓冲区;如果实际消息较短,那么您肯定会落后。 但真正的代码不会有如此一致的延迟,所以这不是一个现实的测试。您的线程可能会出现随机调度延迟,但只要您有足够的缓冲区排队,这没有问题。 (如果其他一些具有更高优先级的代码根本无法执行您的代码,那么您无论如何也无能为力。)

【讨论】:

遗漏 SysEx 缓冲区的报告机制是什么? 糟糕,打错字了。 啊,谢谢。这些 MIDI 速度适用于使用实际 MIDI 硬件时。我正在使用的设备是 USB,它的运行速度要快得多。但是,如果 SysEx 数据是通过网络发送的,那么 50 毫秒的延迟也不会是罕见的。或者,更简单地说,如果工作线程阻塞了用户输入。无论如何,我担心的不是延迟,而是工人无法知道它是否错过了一条消息。一种方法是跟踪缓冲区计数,每次调用 worker 时递减,每次返回时递增。如果计数达到零,您可能会在此之后丢失消息。

以上是关于如何检测 MidiInProc 中丢失的 SysEx 数据?的主要内容,如果未能解决你的问题,请参考以下文章

如何检测丢失的字体字符

如何检测 JMS 主题连接丢失

如何检测蓝牙设备是不是超出范围,或者我们丢失了它?

如何检测套接字连接何时丢失?

如何检测到 TadoConnection 丢失了与服务器的通信?

我可以使用 Python3.6 Sanic 在 websockets 中检测到“连接丢失”吗?