Introduction
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.
Background
PInvoke
allows .NET managed code call unmanaged API functions. A typical API function declaration would be as shown here:
[DllImport("winmm.dll", SetLastError=true)]
public static extern uint midiOutGetNumDevs();
The 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).
The SetLastError
attribute is set so that if any errors occur during the API call, these can be retrieved using the Marshal.GetLastWin32Error
function.
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 Marshal.Release
function.
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
The 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.
The OpenPort
, ClosePort
, SendShortMessage
and SendLongMessage
functions are asynchronous. When the required function completes, a MessageRecieved
event will be raised to indicate that the function has completed.
CMIDIInDevice
The 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, parameter1
and 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 MessageReceived
events.
If any errors occur when handling the received data, these will be raised as ReceiveError
events.
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 (messageBuffer
):
typMsgHeader.lpData = Marshal.AllocHGlobal(messageBuffer.Count());
The data from the managed array can then be copied into the data of this pointer using the Marshall.Copy
function:
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 Marshall.SizeOf()
function:
DataBufferPointer = Marshal.AllocHGlobal(Marshal.SizeOf(typMsgHeader));
The data from the managed memory is then copied to unmanaged memory using the Marshall.StructureToPtr()
:
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,
(uint)Marshal.SizeOf(typMsgHeader));
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 CMIDIOutDevice
.
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 MIDIHDR
structure;
InHeader = (CExternals.MIDIHDR) Marshal.PtrToStructure(DataBufferPointer,
typeof(CExternals.MIDIHDR));
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 Marshal.Release
function.
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.
Threading Issues
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 SychronisationContext
instance.