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;
class MyReceiver : public CMIDIReceiver
{
public:
void ReceiveMsg(DWORD Msg, DWORD TimeStamp);
void ReceiveMsg(LPSTR Msg, DWORD BytesRecorded, DWORD TimeStamp)
{}
void OnError(DWORD Msg, DWORD TimeStamp)
{ std::cout << "Invalid short message received!\n"; }
void OnError(LPSTR Msg, DWORD BytesRecorded, DWORD TimeStamp) {}
};
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
{
if(CMIDIInDevice::GetNumDevs() > 0)
{
MyReceiver Receiver;
CMIDIInDevice InDevice(Receiver);
InDevice.Open(0);
InDevice.StartRecording();
::Sleep(10000);
InDevice.StopRecording();
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;
const unsigned char CHANNEL = 0;
const unsigned char NOTE_ID = 64;
const unsigned char VELOCITY = 127;
int main(void)
{
try
{
if(CMIDIOutDevice::GetNumDevs() > 0)
{
CMIDIOutDevice OutDevice;
OutDevice.Open(0);
CShortMsg NoteOnMsg(midi::NOTE_ON, CHANNEL, NOTE_ID,
VELOCITY, 0);
NoteOnMsg.SendMsg(OutDevice);
::Sleep(5000);
CShortMsg NoteOffMsg(midi::NOTE_OFF, CHANNEL, NOTE_ID, 0, 0);
NoteOffMsg.SendMsg(OutDevice);
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