Playing MIDI Files in Windows (Part 2)

Jump to: Part 1, Part 2, Part 3, Part 4, Part 5

In the last article we demonstrated the Windows Multimedia APIs used to send MIDI commands to a MIDI capable output device. This article will build on that information and show how to parse and play an actual MIDI file.

Related source and example files can be downloaded here: mididemo.zip

The Windows Multimedia APIs do not provide a function that takes a pointer to a MIDI file stored directly in a memory buffer. That would be too easy. Instead, the MIDI file must be read into memory and parsed manually. This wiki article describes the MIDI file format in great detail. I’ll describe things here a little at a time building up to a (mostly) complete example.

A MIDI file begins with a MIDI header structure followed by one or more MIDI tracks. A MIDI header looks like this:

 | 31 - 24 | 23 - 16 | 15 - 8  |  7 - 0  |
 +---------------------------------------+
 |      Header Identifier (MThd)         |
 +---------------------------------------+
 |       Header size (always 6)          |
 +-------------------+-------------------+
 |       Format      |    Num Tracks     |
 +-------------------+-------------------+
 | Ticks per quarter |
 +-------------------+

It is important to note there is no padding between fields, therefore, if overlaying the memory buffer with a C structure you must be sure to use the pack pragma to prevent the compiler from adding padding to your structure.

#pragma pack(push, 1)

struct _mid_header {
	unsigned int	id;	// identifier "MThd"
	unsigned int	size;	// always 6 in big-endian format
	unsigned short	format;	// big-endian format
	unsigned short  tracks;	// number of tracks, big-endian
	unsigned short	ticks;	// number of ticks per quarter note, big-endian
};

#pragma pack(pop)

The first field in the header is the identifier marker. This is a four byte value containing the characters ‘M‘, ‘T‘, ‘h‘, and ‘d‘. The second field is the size field that specifies the size of the header structure, not including the identifier and size fields. In our case, this is always 6. Note that the numeric fields in a MIDI file are stored in big-endian format, not little-endian as is native on x86 processors. Before using these values, they must be converted to the correct representation for the platform.

The third field is the format specifier. This can be 0, 1, or 2. This specifier indicates whether or not the midi file contains a single track, multiple synchronous tracks or multiple asynchronous tracks respectively. We are only going to support format 0 and 1.

The fourth field is the number of tracks in the file. If the format field is 0, this field will always be 1.

The last field is the number of ticks per quarter note. This value is used to help determine the tempo for the MIDI file. More on this later.

Following the MIDI header is one or more MIDI tracks. Like the MIDI file itself, each track has a Track header structure. A Track header structure looks like this:

 | 31 - 24 | 23 - 16 | 15 - 8  |  7 - 0  |
 +---------------------------------------+
 |    Track Header Identifier (MTrk)     |
 +---------------------------------------+
 |              Track size               |
 +-------------------+-------------------+
#pragma pack(push, 1)

struct _mid_track {
	unsigned int	id;	// identifier "MTrk"
	unsigned int	length;	// track length, big-endian
};

#pragma pack(pop)

The first field in the track header is the identifier marker. This is a four byte value containing the characters ‘M‘, ‘T‘, ‘r‘, and ‘k‘. The second field is the length field that specifies the length of the track data, not including the identifier and length fields.

Following the Track header are delta-time values followed by a MIDI event. The delta-time is a variable length value specifying the number of PPQN ticks to wait before executing the MIDI event. A variable length value is defined as a 1 to 4 byte value, each byte containing 7 bits of the value in the lower 7 bits with the most significant bit used to indicate whether or not additional bytes follow. If the most significant bit is set, the previously read value should be multiplied by 128 and the next value added to the result. If the most significant bit is cleared, no more data bytes follow and you have the result.

The following code shows how to read a variable length value:

unsigned long read_var_long(unsigned char* buf, unsigned int* bytesread)
{
	unsigned long var = 0;
	unsigned char c;

	*bytesread = 0;

	do
	{
		c = buf[(*bytesread)++];
		var = (var << 7) + (c & 0x7f);
	}
	while(c & 0x80);

	return var;
}

Following the delta-time value is the MIDI event. The MIDI event consists of a status byte followed by one or more data bytes. The actual events are described in detail on the wiki page. For this example however, we are only concerned with the note-on and note-off events. It is important to note that the MIDI events specified in a MIDI file map one-to-one to the commands that are sent to the Windows API. We do not need to get into details about individual channels or any other specifics as described in the MIDI file format specification. Because the windows API takes the MIDI status byte as-is, there is no need to decode each individual event and do any additional processing other than determining how many data bytes must also be read. For this example, both note-on and note-off take two data bytes. The first data byte is the note number to operate on. The second data byte is the velocity (or volume) at which to operate. (e.g. how hard a key is pressed, string strummed, etc...)

For instance, a note-on event to play middle-C would look like this:

90, 3c, 7f 

This is read as note-on (90), middle-C (3c), velocity 127 (7f). For MIDI events the most significant bit is always set. (i.e. 90 = 10010000) For data bytes, the most significant bit on the data bytes are always cleared.

For this example, we are going to take a very simple MIDI file that plays the same scale as in example 5. Instead of the values hard coded, the MIDI file is going to be parsed and an array of delta-time value / MIDI event pairs will be generated.

 | 31 - 24 | 23 - 16 | 15 - 8  |  7 - 0  |
 +---------------------------------------+
 |            Delta-time ticks           |
 +---------------------------------------+
 |    00   |  Data 2 |  Data 1 | Status  |
 +-------------------+-------------------+
 |            Delta-time ticks           |
 +---------------------------------------+
 |    00   |  Data 2 |  Data 1 | Status  |
 +-------------------+-------------------+
 |                  ...                  |

Note that the MIDI event values are the same format as in the previous examples. These values will be passed as-is to midiOutShortMsg().

The delta-time ticks will be used by our algorithm for waiting the appropriate amount of time. Again this example will use the Sleep() function which is not necessarily precise enough for MIDI score timing but is sufficient for illustration.

The following function will parse our example MIDI file into the previously described array format. NOTE that this example is not sufficient to play any MIDI file and will really only work properly for the example file provided. In particular, this will only play very small single track MIDI files (format 0).

unsigned int get_buffer_ex6(unsigned char* buf, unsigned int len, unsigned int** out, unsigned int* outlen)
{
	unsigned char* tmp;
	unsigned int* streambuf;
	unsigned int streamlen = 0;
	unsigned int bytesread;

	unsigned int buflen = 64;

	tmp = buf;
	*out = NULL;
	*outlen = 0;

	streambuf = (unsigned int*)malloc(sizeof(unsigned int) * buflen);
	if(streambuf == NULL)
		return 0;

	memset(streambuf, 0, sizeof(unsigned int) * buflen);

	tmp += sizeof(struct _mid_header);
	tmp += sizeof(struct _mid_track);

	while(tmp < buf + len)
	{
		unsigned char cmd;
		unsigned int time = read_var_long(tmp, &bytesread);
		unsigned int msg = 0;

		tmp += bytesread;

		cmd = *tmp++;
		if((cmd & 0xf0) != 0xf0) // normal command
		{
			msg = ((unsigned long)cmd) |
				  ((unsigned long)*tmp++ << 8);

			if(!((cmd & 0xf0) == 0xc0 || (cmd & 0xf0) == 0xd0))
				msg |= ((unsigned long)*tmp++ << 16);

			streambuf[streamlen++] = time;
			streambuf[streamlen++] = msg;
		}
		else if(cmd == 0xff)
		{
			cmd = *tmp++; // cmd should be meta-event (0x2f for end of track)
			cmd = *tmp++; // cmd should be meta-event length
			tmp += cmd;
		}
	}

	*out = streambuf;
	*outlen = streamlen;

	return 0;
}

This function takes a buffer containing MIDI data, its length and returns an array of delta-time values and MIDI events to be sent to midiOutShortMsg(). The function begins by skipping over the MIDI header and the first Track header to find the beginning of the MIDI data. It then loops through reading first the delta-time, then formatting the MIDI event. An event will either be a normal command or the end-of-track Meta-event.

The command and first data byte are inserted into the "short message" variable msg. The commands 0xc0 (Patch change) and 0xd0 (Channel pressure) only take a single data byte. All the others take 2 data bytes. If the MIDI status is not one of those two commands, the second data byte is inserted into the msg variable. Finally, the time and msg variables are stuffed into the stream buffer to be returned.

When the command 0xff is encountered, in this example it can only be the end-of-track marker, the remaining three bytes are read reaching the end of input. The stream buffer is then returned to the caller.

The following function gets the stream buffer and loops through all the delta-time / MIDI event pairs passing each message to midiOutShortMsg():

unsigned int example6()
{
	unsigned int* streambuf = NULL;
	unsigned int streamlen = 0;

	unsigned char* midibuf = NULL;
	unsigned int midilen = 0;

	unsigned int err;
	HMIDIOUT out;
	const unsigned int PPQN_CLOCK = 5;
	unsigned int i;

	err = midiOutOpen(&out, 0, 0, 0, CALLBACK_NULL);
	if (err != MMSYSERR_NOERROR)
		printf("error opening default MIDI device: %d\n", err);
	else
		printf("successfully opened default MIDI device\n");

	midibuf = load_file((unsigned char*)"example6.mid", &midilen);
	if(midibuf == NULL)
	{
		printf("could not open example6.mid\n");
		return 0;
	}

	get_buffer_ex6(midibuf, midilen, &streambuf, &streamlen);

	i = 0;
	while(i < streamlen)
	{
		unsigned int time = streambuf[i++];

		Sleep(time * PPQN_CLOCK);

		err = midiOutShortMsg(out, streambuf[i++]);
		if(err != MMSYSERR_NOERROR)
			printf("error sending command: %d\n", err);
	}

	midiOutClose(out);
	free(streambuf);
	free(midibuf);

	return 0;
}

In this article we supported only very small single track MIDI files. We did however introduce the basic layout of the MIDI file and an approach to decoding and playing the individual MIDI events. In the next article we'll expand on this example and add support for multiple track MIDI files.

Related files:

Further reading:

Tags: , , , ,

Thursday, December 29th, 2011 C, Multimedia, Programming

Leave a Reply

 

Comments are moderated due to spammers. Your comments will not appear until I review and approve them. If you left a question I will answer as best I can so be sure to check back.