Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Wrapper Library for Windows MIDI API

0.00/5 (No votes)
28 Jan 2008 24  
A small library encapsulating the Windows MIDI API
MIDIDeviceDemo Image

Introduction

Using the Windows low-level MIDI API is not necessarily easy. There are a lot of details to keep track of, and it can get a bit frustrating when you are trying to write a MIDI application. The goal of my small wrapper library is to elevate some of the grunt work that goes into using the Windows low-level MIDI API by encapsulating the functions into C++ classes. With these classes, most of the dirty work is handled behind the scenes. This leaves you free to concentrate on more important matters.

The Library

The MIDI library is encapsulated in the midi namespace. In order to use the library, you must link to the winmm.lib library. There are several parts to the library described in the following header files:

  • midi.h - Useful constants
  • MIDIInDevice.h - Interface for the CMIDIInDevice and CMIDIReceiver classes
  • MIDIOutDevice.h - Interface for the CMIDIOutDevice class
  • MIDIMsg.h - Abstract base class for all MIDI messages
  • ShortMsg.h - Interface for the CShortMsg class
  • LongMsg.h - Interface for the CLongMsg class

Let's look at each part of the library.

midi.h - Useful Constants

The midi.h header file contains many useful constants. For example, the midi::NOTE_OFF constant represents the value for turning off a MIDI note. These constants come in handy when creating and interpreting MIDI messages. You don't have to look up the values in a MIDI reference. Just plug in the constant you need, and you're all set.

MIDIInDevice.h - The CMIDIInDevice and CMIDIReceiver Classes

The CMIDIInDevice class represents MIDI input devices. A MIDI input device can be any hardware device installed on your computer, such as a soundcard, capable of receiving MIDI data. There can be several of these devices present, and the CMIDIInDevice class provides methods for determining the number and characteristics of these devices. The GetNumDevs() function returns the number of MIDI input devices on your system, and each number represents that device's identifier. The GetDevCaps() function returns information about a particular device by storing it in a MIDIINCAPS structure. This is a Windows structure that contains many attributes describing MIDI input devices, such as their names. A good first step before using the CMIDIInDevice class is to determine the number of MIDI input devices on your system and which one you want to use to receive MIDI data.

Before CMIDIInDevice objects can be used, they have to be opened. What do I mean by opened? Think of using the ifstream class from the standard library for reading the contents of a file. Before the file can be read, it has to be opened. The CMIDIInDevice class works in the same way - before it can "read" MIDI data, it has to be opened. You open CMIDIInDevice objects by passing a device identifier to either the constructor or the Open() function. The device identifier is a unique number representing a MIDI input device. You can retrieve this number by using the aforementioned GetNumDevs() and GetDevCaps() functions.

To begin receiving MIDI data, call the StartRecording() function after the MIDI input device has been opened. The device will continue receiving MIDI data until StopRecording() is called to stop the process. Where do the MIDI messages go once they are received? This leads us to the second major component in the input part of the library - the CMIDIReceiver class.

The CMIDIReceiver class is an abstract class representing objects that receive MIDI data from the CMIDIInDevice class. An object derived from the CMIDIReceiver class can register itself with an CMIDIInDevice object. The CMIDIInDevice will then pass on MIDI messages it receives by calling methods in the CMIDIReceiver derived object. The CMIDIReceiver has two methods for receiving short messages, and two methods for receiving system exclusive messages. In both cases, there is one method which is called when things have gone normally and a second method which is called if an error has occurred.

Short messages are represented by the DWORD data type. You don't have to do anything special to receive them. System exclusive messages, on the other hand, are represented by a LPSTR data buffer. In order to receive system exclusive messages, provide the CMIDIInDevice with buffers to store the messages. This is done with the AddSysExBuffer() function. Pass as many buffers as you like during the recording process. When the CMIDIInDevice receives a system exclusive message, it stores it in the buffer and passes it back through the CMIDIReceiver you registered.

Finally, once you are finished using the CMIDIInDevice, close the device simply by calling the Close() function.

To make short messages and system exclusive messages easier to work with, I've added three new classes: CMIDIMsg, CShortMsg, and CLongMsg. The CMIDIMsg class is the abstract base class for all MIDI messages. Both the CShortMsg and CLongMsg classes are derived from it. The CShortMsg class represents MIDI short messages such as note-on and note-off messages. The CLongMsg class represents system exclusive messages. It is essentially a buffer for the bytes that makes up an system exclusive message. It can be used for general purpose system exclusive messages, or you can derive a more specialized class from it.

A code snippet is worth a thousand words, so let's look at an example of using the CMIDIInDevice and CMIDIReceiver classes:

#include "StdAfx.h"
#include <iostream>
#include "MIDIInDevice.h"
#include "ShortMsg.h"

using midi::CMIDIInDevice;
using midi::CMIDIReceiver;

// My class for receiving MIDI messages
class MyReceiver : public CMIDIReceiver
{
public:
    // Receives short messages
    void ReceiveMsg(DWORD Msg, DWORD TimeStamp);

    // Receives long messages
    void ReceiveMsg(LPSTR Msg, DWORD BytesRecorded, DWORD TimeStamp)
    {}

    // Called when an invalid short message is received
    void OnError(DWORD Msg, DWORD TimeStamp)
    { std::cout << "Invalid short message received!\n"; }

    // Called when an invalid long message is received
    void OnError(LPSTR Msg, DWORD BytesRecorded, DWORD TimeStamp) {}
};

// Function called to receive short messages
void MyReceiver::ReceiveMsg(DWORD Msg, DWORD TimeStamp)
{
    midi::CShortMsg ShortMsg(Msg, TimeStamp);

    std::cout << "Command: " <<
static_cast<int>(ShortMsg.GetCommand());
    std::cout << "\nChannel: " <<
static_cast<int>(ShortMsg.GetChannel());
    std::cout << "\nDataByte1: " <<
static_cast<int>(ShortMsg.GetData1());
    std::cout << "\nDataByte2: " <<
static_cast<int>(ShortMsg.GetData2());
    std::cout << "\nTimeStamp: " <<
static_cast<int>(ShortMsg.GetTimeStamp());
    std::cout << "\n\n";
}

int main(void)
{
    try
    {
        // Make sure there is a MIDI input device present
        if(CMIDIInDevice::GetNumDevs() > 0)
        {
            MyReceiver Receiver;
            CMIDIInDevice InDevice(Receiver);

            // We'll use the first device - we're not picky here
            InDevice.Open(0);

            // Start recording
            InDevice.StartRecording();

            // Wait for some MIDI messages
            ::Sleep(10000);

            // Stop recording
            InDevice.StopRecording();

            // Close the device
            InDevice.Close();
        }
        else
        {
            std::cout << "No MIDI input devices present!\n";
        }
    }
    catch(const std::exception &Err)
    {
        std::cout << Err.what();
    }

    return 0;
}

Some notes about the code above:

  • All exceptions in the library are derived from std::exception, so catching this exception will catch any exception thrown.
  • Short messages are received as DWORD data types. They must be unpacked before you can do anything interesting with them. You can do this yourself, or you can allow the CShortMsg to take care of this for you. If you decide to pack and unpack short messages, you can use class methods in the CMIDIInDevice class and the CMIDIOutDevice class for unpacking and packing short messages respectively.
  • A time stamp value is associated with each message. The time stamp is measured in milliseconds since the recording process began.
  • The example above doesn't handle system exclusive messages. In order to do that, all you would have to do is pass the input device one or more buffers and implement the methods derived from CMIDIReceiver for receiving system exclusive messages.

MIDIOutDevice.h - The CMIDIOutDevice Class

The CMIDIOutDevice class represents MIDI output devices. Like the CMIDIInDevice class, it has methods for determining the number of devices present and for retrieving information about each device. Using an object of this class is straight forward. Simply open it, create a MIDI message, send the message, and close the device when you are done with it.

Here is another code example:

#include "StdAfx.h"
#include <iostream>
#include "midi.h"
#include "MIDIOutDevice.h"
#include "ShortMsg.h"

using midi::CMIDIOutDevice;
using midi::CShortMsg;

// Some useful constants
const unsigned char CHANNEL = 0;
const unsigned char NOTE_ID = 64;
const unsigned char VELOCITY = 127;

int main(void)
{
    try
    {
        // Make sure there is an output device present
        if(CMIDIOutDevice::GetNumDevs() > 0)
        {
            CMIDIOutDevice OutDevice;

            // Use the first device
            OutDevice.Open(0);

            // Create note-on message
            CShortMsg NoteOnMsg(midi::NOTE_ON, CHANNEL, NOTE_ID,
              VELOCITY, 0);

            // Turn note on
            NoteOnMsg.SendMsg(OutDevice);

            // Wait a bit
            ::Sleep(5000);

            // Create note-off message
            CShortMsg NoteOffMsg(midi::NOTE_OFF, CHANNEL, NOTE_ID, 0, 0);

            // Turn note off
            NoteOffMsg.SendMsg(OutDevice);

            // Close device
            OutDevice.Close();
        }
        else
        {
            std::cout << "No MIDI output devices present!\n";
        }
    }
    catch(const std::exception &Err)
    {
        std::cout << Err.what();
    }

    return 0;
}

Some comments:

  • The code above sends a note, waits for a few seconds, and then turns the note off.
  • To send a system exclusive message, go through the same process as above only prepare your message as a LPSTR buffer or create a CLongMsg object and initialize it with a valid system exclusive message.
  • CMIDIOutDevice doesn't pay any attention to time stamps. In order to send messages at a certain time, you'll have to create a timing scheme of some sort.

The MIDIDeviceDemo Demonstration App

The demo application uses the small MIDI library I've created for input and output. The piano keyboard is interactive. Clicking on a key with the mouse triggers a note. You can also play the piano from the computer keyboard. The keys from 'Z' to '/' correspond to the piano. To change the range of the computer keyboard, press keys 1-5. The '1' key represents the lowest range, and the '5' key represents the highest range.

If a MIDI keyboard is connected to the computer, playing keys on the keyboard will cause the corresponding notes on the keyboard display to change colors. Incoming channel messages are displayed as well. The application has very little practical value, but it does show how the library works.

Hopefully, you will find this small library as useful as I have. I would appreciate any comments or suggestions for future revisions. Take care!

History

  • 25 Aug 2002 - Updated source code
  • 22 Dec 2002 - Added GetDevID method to both the CMIDIInDevice class and the CMIDIOutDevice class. Added CMIDIMsg, CShortMsg, and CLongMsg classes to the library. Redid demo app from scratch. The keyboard display is derived from my new class CPianoCtrl. All source code is included in the demo project (no need to email any longer for the source!).
  • 9 Sept 2003 - Updated source code and fixed several bugs. The CShortMsg class can now send messages without the status byte. I also changed the way threads are created from using CreateThread to using AfxBeginThread and changed the way thread termination is handled so that it is done more gracefully. Several other changes to the library here and there as well as fixing some bugs in the demo application.
  • 23 Sep 2003 - Updated source code and demo
  • 29 Sep 2003 - Updated source code and demo
  • 25 Jan 2008 - Updated source code and demo
  • 28 Jan 2008 - Bug fix for last update

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here