Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Using the Windows MIDI SYSEX API in .NET

5.00/5 (4 votes)
13 Apr 2016CPOL7 min read 20.5K   468  
This article describes using the Windows MIDI API functions in a managed environment.

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:

C#
[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):

C#
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:

C#
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:

C#
DataBufferPointer = Marshal.AllocHGlobal(Marshal.SizeOf(typMsgHeader));

The data from the managed memory is then copied to unmanaged memory using the Marshall.StructureToPtr():

C#
Marshal.StructureToPtr(typMsgHeader, DataBufferPointer, true);

Finally, the API function can be called using the unmanaged data pointer to the structure:

C#
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;

C#
InHeader = (CExternals.MIDIHDR) Marshal.PtrToStructure(DataBufferPointer, 
typeof(CExternals.MIDIHDR));

The data buffer pointer can then be accessed and copied to a byte array:

C#
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:

  1. Create a delegate for the callback function with the same parameter and return values as the API documentation.
  2. Declare the API requiring the callback function with the callback parameter declared with a type of the delegate created above.
  3. Create a function to handle the callback messages with a signature that matches the delegate.
  4. 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)