在 .NET 中使用 Windows MIDI SYSEX API





5.00/5 (4投票s)
本文描述了在托管环境中使用 Windows MIDI API 函数。
引言
此代码是为了向 GT-8 吉他处理器发送和接收 MIDI 系统独占消息而创建的。MIDI 发送和接收函数自 NT 以来就存在于 Windows 中。目前,.NET 没有可执行相同功能的函数。可以使用标准的 PInvoke
方法将 API 函数与 .NET 互联。但是,这些函数是异步工作的,因此需要回调函数来向调用代码发出数据传输完成的信号。大多数描述如何使用此 API 的文章都使用 Windows 消息选项作为回调。虽然这可以创建功能完善的程序,但它不允许封装,因为必须使用窗口来接收回调消息。它还需要重写窗口过程来处理 MIDI 消息。本文描述了使用回调函数选项,以便所有 MIDI 功能都可以包含在单个类中。
背景
PInvoke
允许 .NET 托管代码调用非托管 API 函数。典型的 API 函数声明如下所示:
[DllImport("winmm.dll", SetLastError=true)]
public static extern uint midiOutGetNumDevs();
midiOutGetNumDevs
函数位于 winmm.dll 中。该函数必须标记为 extern
,因为它没有函数体(它在 DLL 中实现)。
设置 SetLastError
属性,以便如果在 API 调用期间发生任何错误,可以使用 Marshal.GetLastWin32Error
函数检索这些错误。
如果 API 函数具有非简单变量类型的引用参数,则必须将这些参数作为指向非托管内存的指针发送。可以使用 Marsal.AllocHGlobal
函数创建非托管内存块。Marshal
类中也有用于在托管内存和非托管内存之间复制数据的函数。
非托管内存不会被垃圾回收(只有指针会被回收)。因此,务必使用 Marshal.Release
函数正确释放使用的任何非托管内存。
标准做法是将应用程序的所有 API 声明存储在单个类内的 static
函数中。
Using the Code
附加的源代码包含一个用于从 BOSS GT-8 吉他处理器发送和接收数据的应用程序。这意味着部分代码特定于该应用程序。但是,其中有用于发送和接收 MIDI 数据的类,可以在其他应用程序中重用,以与任何需要短消息或长消息的 MIDI 设备进行通信。
所有 API 声明都包含在 CExternals
类中。通过将此类中的代码与 API 文档进行比较,可以将其用于任何 API 函数。
CMIDIOutDevice
CMIDIOutDevice
类用于通过 PC 的 MIDI 端口发送短消息或长 MIDI 消息。首先,调用 ListDevices
方法返回所有可用 MIDI 输出端口的列表。重要的是要注意列表中设备的索引,因为该索引必须用于访问所需的端口。要打开端口,请调用 OpenPort
方法。该方法接受一个参数,即要打开的端口的索引。
端口打开后,可以使用 SendShortMessage
发送短消息。这需要三个参数:MIDI 状态、参数 1 和参数 2(有关详细信息,请参阅 MIDI 文档)。
要发送长消息(或系统独占消息),请使用 SendLongMessage
方法。这需要一个字节数组,其中包含要通过打开的 MIDI 端口发送的所有数据。数组中的所有字节都将从 0
开始发送。如果 MIDI 设备需要任何标头或尾部,则必须将其包含在数组中。
OpenPort
、ClosePort
、SendShortMessage
和 SendLongMessage
函数是异步的。当所需函数完成后,将引发 MessageRecieved
事件以指示函数已完成。
CMIDIInDevice
CMIDIInDevice
的工作原理与 CMIDIOutDevice
相同。同样使用 ListDevices
函数列出可用设备。请注意,并非所有设备都可以接收和发送消息。
当使用 OpenPort
方法打开所需端口后,该类将监听所选端口上的短消息和长消息。如果收到短消息,将引发 ShortMessage
事件,其中包含 MIDI 消息的状态、parameter1
和 parameter2
。
对于长消息,该类将等待接收缓冲区满或端口关闭。然后它将返回一个 LongMessage
事件。该事件包含一个字节数组,其中包含接收到的数据。接收数据缓冲区的长度在实例化该类时设置。
通过 MessageReceived
事件发送从 MIDI 端口接收到的消息。
如果在处理接收到的数据时发生任何错误,将引发 ReceiveError
事件。
关注点
处理非托管指针
程序真正的兴趣在于处理长(系统独占)消息的发送和接收。
要发送长消息,请使用 midiOutLongMsg()
API 函数。该函数可在 MSDN 上找到文档。第一个和最后一个参数是简单值。但是,lpMidiOutHdr
参数需要指向 MIDIHDR
结构的指针。将结构发送到 API 函数通常不会造成任何问题。但是,在这种情况下,结构的一个成员是指向包含长消息的数据缓冲区的指针。由于 API 函数会操作缓冲区中的数据,因此结构指针和其中的数据缓冲区指针都必须指向非托管内存。但是,它们还需要来自程序托管部分的数据。
首先,创建结构 MIDIHDR
的一个实例(typMsgHeader
)。由于这是托管的,可以在其字段中正常输入数据。将 lpData
字段(指向非托管内存缓冲区的指针)分配给非托管数据指针。此指针的大小与托管字节数组(messageBuffer
)相同。
typMsgHeader.lpData = Marshal.AllocHGlobal(messageBuffer.Count());
然后可以使用 Marshall.Copy
函数将托管数组中的数据复制到此指针的数据中。
Marshal.Copy(messageBuffer, 0, typMsgHeader.lpData, messageBuffer.Count());
然后创建第二个非托管数据指针。这次的大小是结构的(使用 Marshall.SizeOf()
函数)。
DataBufferPointer = Marshal.AllocHGlobal(Marshal.SizeOf(typMsgHeader));
然后使用 Marshall.StructureToPtr()
将托管内存中的数据复制到非托管内存。
Marshal.StructureToPtr(typMsgHeader, DataBufferPointer, true);
最后,可以使用指向结构的非托管数据指针调用 API 函数。
lngReturn = (uint)CExternals.midiOutLongMsg(mMIDIOutHandle, DataBufferPointer,
(uint)Marshal.SizeOf(typMsgHeader));
需要注意的是,在发送之前,仍然需要调用相应的 API 函数来准备标头。在 CMIDIOutDevice
的 SendLongMessage
函数中可以找到操作标头并使用 API 发送数据的示例。
调用 MIDI 接收 API 时,数据缓冲区结构的创建是相似的。这可以在 CMIDIInDevice
的 StartRecording
函数中找到。但是,在这种情况下,必须将缓冲区传输回托管内存才能读取接收到的数据。这在 CMIDIInDevice
的 LongMessageReceived
函数中完成。首先,将结构指针复制到 MIDIHDR
结构的一个实例中;
InHeader = (CExternals.MIDIHDR) Marshal.PtrToStructure(DataBufferPointer,
typeof(CExternals.MIDIHDR));
然后可以访问数据缓冲区指针并将其复制到字节数组中。
Marshal.Copy(InHeader.lpData, MIDIInBuffer, 0, mInBufferLength);
需要注意的是,在将数据从缓冲区读出之前,不能销毁结构指针。另外,由于结构和数据指针指向非托管区域,因此使用的内存不会被垃圾回收。因此,务必确保正确释放这些数据以防止内存泄漏。要释放非托管内存,请使用 Marshal.Release
函数。
API 回调函数
需要回调函数的 API 函数处理起来出奇地简单。步骤如下:
- 为回调函数创建一个委托,该委托具有与 API 文档相同的参数和返回值。
- 声明需要回调函数的 API,并将回调参数声明为上面创建的委托类型。
- 创建一个函数来处理具有与委托匹配的签名的回调消息。
- 调用 API 函数时,创建委托的实例并将其作为回调函数中的相应参数进行分配。
线程问题
由于 MIDI API 调用是异步的,因此当事件发生时,回调函数将位于与调用函数不同的线程上。这会导致问题,尤其是当您想在窗体中显示数据时,因为回调线程无法访问运行在主 UI 线程上的控件。可以在窗体中纠正此问题。但是,这意味着使用 MIDI 类的任何人都需要意识到这一点,以防止出现错误。另一种方法是使用极其有用(但经常被忽视)的 SynchronisationContext
类。该类可用于在线程之间发布消息。
因此,通过在实例化类时记录调用线程,可以使用 SychronisationContext
实例将回调线程中的数据安全地传输到主线程。