This code was created to send and receive MIDI system exclusive messages to a GT-8 guitar processor. The MIDI send and receive functions have been available in Windows since NT. At this time, there are no .NET functions available to perform the same functions. The API functions can be interfaced with .NET using the standard
PInvoke methods. However, the functions work asynchronously and so require callback functions to signal to the calling code when data transfer is complete. Most of the articles available which describe how to use this API use the Windows message option as a callback. While this creates a perfectly functional program, it does not allow encapsulation as a Window must be used to receive the callback messages. It also requires the Window Procedure to be overridden to handle the MIDI messages. This article describes using the callback function option so that all MIDI functionality can be contained within a single class.
PInvoke allows .NET managed code call unmanaged API functions. A typical API function declaration would be as shown here:
public static extern uint midiOutGetNumDevs();
midiOutGetNumDevs function is held in the winmm.dll. The function must be marked as
extern as there will be no function body (it is implemented in the DLL).
SetLastError attribute is set so that if any errors occur during the API call, these can be retrieved using the
If the API functions have reference parameters that are not simple variable types, these parameters must be sent as a pointer to unmanaged memory. A block of unmanaged memory can be created using the
Marsal.AllocHGlobal function. There are also functions within the
Marshal class to copy data between managed and unmanaged memory.
Unmanaged memory is not garbage collected (only the pointers are). So, it is important to make sure that any unmanaged memory used is released correctly using the
The standard practice is to store all the API declarations for the application as
static functions within a single class.
Using the Code
The attached source code contains an application for sending a receiving data from a BOSS GT-8 guitar processor. This means that some of the code is specific to the application. However there are classes for sending and receiving MIDI data that can be reused in other applications to communicate with any MIDI devices which require short or long MIDI messages.
All API declarations are contained within the
CExternals class. By comparing the code in this class with the API documentation, the methods used can be applied to any API function.
CMIDIOutDevice class is used to send short or long MIDI messages via the PCs MIDI port. First, the
ListDevices method is called to return a list of all available MIDI output ports. It is important to note the index of the devices in the list as this index must be used to access the desired port. To open a port, call the
OpenPort method. This takes a single parameter which is the index of the port to open.
With the port open, a short message can be sent using the
SendShortMessage. This requires three parameters for the MIDI status, parameter 1 and parameter 2 (see MIDI documentation for further details).
To send a long (or System Exclusive) message, the
SendLongMessage method is used. This requires a byte array containing all the data to send via the open MIDI port. All bytes in the array will be sent starting at
0. If there are any headers or footers required by the MIDI device, then these must also be included in the array.
SendLongMessage functions are asynchronous. When the required function completes, a
MessageRecieved event will be raised to indicate that the function has completed.
CMIDIInDevice operates on the same principles as the
CMIDIOutDevice. The available devices are again listed using the
ListDevices function. Note that not all devices can receive as well as send messages.
When the desired port is opened with the
OpenPort method, the class will listen for short and long messages on the selected port. If a short message is received, the
ShortMessage event will be raised containing the status,
parameter2 of the MIDI message.
For a long message, the class will wait until the receive buffer is full or the port is closed. It will then return a
LongMessage event. This contains a byte array containing the received data. The size of the receive data buffer is set when the class is instantiated.
Messages received from the MIDI port will be sent via the
If any errors occur when handling the received data, these will be raised as
Points of Interest
Handling Unmanaged Pointers
The real interest from the program comes from handling the sending and receiving of the Long (System Exclusive) messages.
To send a long message, the
midiOutLongMsg() API function is used. The documentation for this function can be found on MSDN. The first and last parameters are simple values. However, the
lpMidiOutHdr parameter requires a pointer to a
MIDIHDR structure. Sending a structure to an API function normally does not cause any problems. However, in this case, one of the members of the structure is a pointer to the data buffer containing the long message. Because the API function manipulates the data in the buffer, both the structure pointer and the data buffer pointer within it must be to unmanaged memory. But, they also require data from the managed parts of the program.
First of all, an instance of the structure
MIDIHDR is created (
typMsgHeader). As this is managed, data can be entered into its fields in the normal way. The
lpData field (the pointer to the unmanaged memory buffer) is assigned to an unmanaged data pointer. This is the same size as the managed byte array (
typMsgHeader.lpData = Marshal.AllocHGlobal(messageBuffer.Count());
The data from the managed array can then be copied into the data of this pointer using the
Marshal.Copy(messageBuffer, 0, typMsgHeader.lpData, messageBuffer.Count());
A second unmanaged data pointer is then created. This time with the size of the structure (using the
DataBufferPointer = Marshal.AllocHGlobal(Marshal.SizeOf(typMsgHeader));
The data from the managed memory is then copied to unmanaged memory using the
Marshal.StructureToPtr(typMsgHeader, DataBufferPointer, true);
Finally, the API function can be called using the unmanaged data pointer to the structure:
lngReturn = (uint)CExternals.midiOutLongMsg(mMIDIOutHandle, DataBufferPointer,
It should be noted that the header still needs to be prepared by calling the appropriate API function before it can be sent. An example of manipulating the header and sending the data using the APIs can be found in the
SendLongMessage function of
The creation of the data buffer structure is similar when calling the MIDI receive API. This can be found in the
StartRecording function of
CMIDIInDevice. However, in this case the buffer must be transferred back in to managed memory to read the received data. This is done in the
LongMessageReceived function of
CMIDIInDevice. Firstly, the structure pointer is copied to an instance of the
InHeader = (CExternals.MIDIHDR) Marshal.PtrToStructure(DataBufferPointer,
The data buffer pointer can then be accessed and copied to a byte array:
Marshal.Copy(InHeader.lpData, MIDIInBuffer, 0, mInBufferLength);
It should be noted that the structure pointer must not be destroyed before the data is read out of the buffer. Also, as the structure and data pointers are to unmanaged areas, the memory used will not be garbage collected. So, it is important to ensure that this data is released correctly to prevent memory leaks. To release unmanaged memory, use the
API Callback Functions
API functions that require callback functions are surprisingly simple to handle. The steps are as follows:
- Create a delegate for the callback function with the same parameter and return values as the API documentation.
- Declare the API requiring the callback function with the callback parameter declared with a type of the delegate created above.
- Create a function to handle the callback messages with a signature that matches the delegate.
- When calling the API function , create an instance of the delegate and assign this as the appropriate parameter in the callback function.
Because the MIDI API calls act asynchronously, the callback functions when events occur will be on a different thread to the calling function. This causes problems, especially if you want to display the data in a form as the callback thread cannot access the controls running on the main UI thread. It is possible to correct this issue in the form. However this means that whoever is using the MIDI classes needs to be aware of this to prevent errors. An alternative is to use the extremely useful (but often overlooked)
SynchronisationContext class. This can be used to essentially post messages between threads.
So, by recording the calling thread when the class is instantiated, data from the callback threads can be safely transferred to the main thread using the