Playing MIDI Files in Windows (Part 4)

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

In the previous article we developed an algorithm for combining multiple MIDI tracks into a single stream. In this article we will build on that example and add some more advanced MIDI concepts such as META events, running mode and a better timing mechanism.

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

META Events

The first code example below expands on the previous get_buffer function and adds support for META events. Once we’ve gotten the next event to process we have to determine what kind of event it is. The MIDI event byte 0xff indicates the event is a META event. META events provide extra data used by processors and applications. They are not typically sent to the output device but they may affect how the MIDI file is processed.

The wiki page discusses each META event in great detail. For our purposes the only thing we need to understand is that META events begin with the MIDI event byte 0xff, are followed by the META event byte, a length and length number of data bytes. All of the META events may be ignored except one, the tempo event.

As shown below, skipping META events is easy, read the event byte, read the length byte, then skip over the next length number of bytes.

#define TEMPO_EVT 1

if(evt.event == 0xff) // meta-event
{
	evt.data++; // skip the event byte
	unsigned char meta = *evt.data++; // read the meta-event byte
	unsigned int len;

	switch(meta)
	{
	case 0x51:
	{
		unsigned char a, b, c;
		len = *evt.data++; // get the length byte, should be 3
		a = *evt.data++;
		b = *evt.data++;
		c = *evt.data++;

		msg = ((unsigned long)TEMPO_EVT << 24) |
			  ((unsigned long)a << 16) |
			  ((unsigned long)b << 8) |
			  ((unsigned long)c << 0);

		while(streamlen + 2 > streambuflen)
		{
			unsigned int* tmp = NULL;
			streambuflen *= 2;
			tmp = (unsigned int*)realloc(streambuf, sizeof(unsigned int) * streambuflen);
			if(tmp != NULL)
			{
				streambuf = tmp;
			}
			else
			{
				goto error;
			}
		}
		streambuf[streamlen++] = time;
		streambuf[streamlen++] = msg;
	}
	break;
	default: // ignore any of the other META events
		len = *evt.data++; // get the data length
		evt.data += len; // skip over the data
		break;
	}

	tracks[idx].buf = evt.data;
}

The tempo META event is used to adjust the tempo of the score. The tempo is expressed in microseconds per quarter note. This value is used to calculate beats per minute (BPM). The default BPM if no tempo is specified is 120. There are 1000 microseconds per millisecond and 1000 milliseconds per second which equals 1,000,000 microseconds per second. 120 BPM = 60 * 1,000,000 / 120 = 500,000 microseconds per quarter note.

In previous examples we discussed the PPQN and PPQN clock. PPQN stands for pulses per quarter note. This is the number of clock ticks per quarter note. In the MIDI header is the ticks field that specifies the PPQN. The tempo value (defaulted to 500,000 if none is specified) divided by the PPQN will give you the number of microseconds per tick. As an example, 500,000 microseconds per quarter note / 96 PPQN = 5208 microseconds per tick.

Note that 5208 microseconds = 5.208 milliseconds. This is why the Sleep() function is not precise enough for accurate MIDI score timing. A more accurate method will be described later.

The tempo META event is the value 0x51 followed by a length byte which is always 3, and 3 data bytes. The 3 data bytes form a 24-bit integer representing the number of microseconds per quarter note as described above. Again these bytes are in big-endian format and will need to be converted to little-endian before they can be used correctly.

The tempo event is passed to the caller in the stream in the same way the other events are passed. Because the most significant byte in the stream message is always ignored for normal events (and set to 0) we use it to pass a special value to the caller indicating that the message should be treated as a tempo change rather than a normal message to be passed to midiOutShortMsg(). We define TEMPO_EVT to be 1 (because it’s not 0) and place it in the most significant byte. The remaining data bytes are placed into the lower three bytes of the message in the correct (little-endian) format. The main loop processing the stream buffer will be able to find the tempo change event and process it accordingly.

All of any of the other META events, if encountered will be skipped. Because they provide their length information, there is no need to actually decode the event. Instead, the length is read and that number of bytes are skipped.

Running Mode

If the event is not a META event, we check to see if running mode is being used. Running mode is a method of compression used by the MIDI file format where the event byte may be omitted if the event is the same as the previous event. Running mode is determined by checking the most significant bit of the event byte. If the bit is set then the byte is the actual event and the following bytes are the data bytes. If the bit is not set (0), the byte is actually the first data byte (data bytes always have a most significant bit of 0) and the previous event byte is assumed. For example, to play the C-major cord you may use the following commands:

 0x90, 0x3c, 0x7f
 0x90, 0x40, 0x7f
 0x90, 0x43, 0x7f

Because they are all the same note-on event, the note-on (0x90) byte for the second two events may be omitted:

 0x90, 0x3c, 0x7f
 0x40, 0x7f
 0x43, 0x7f

To decode running mode we need to be able to remember previously encountered event bytes. The last_event field of our trk structure is used for this purpose. When a normal event is encountered, it is recorded in the last_event field. If running mode is detected, the event is pulled from the last_event field and the data bytes are processed accordingly. Obviously, there must be one normal event specified first before running mode can be used. If not, the MIDI file is malformed.

else if(!(evt.event & 0x80)) // running mode
{
	unsigned char last = tracks[idx].last_event;
	msg = ((unsigned long)last) |
		  ((unsigned long)*evt.data++ << 8);

	if(!((last & 0xf0) == 0xc0 || (last & 0xf0) == 0xd0))
		msg |= ((unsigned long)*evt.data++ << 16);
	while(streamlen + 2 > streambuflen)
	{
		unsigned int* tmp = NULL;
		streambuflen *= 2;
		tmp = (unsigned int*)realloc(streambuf, sizeof(unsigned int) * streambuflen);
		if(tmp != NULL)
		{
			streambuf = tmp;
		}
		else
		{
			goto error;
		}
	}

	streambuf[streamlen++] = time;
	streambuf[streamlen++] = msg;

	tracks[idx].buf = evt.data;
}

This last example is exactly like the normal mode from the previous article example. However, we now check to make sure the output stream is big enough to hold the parsed MIDI data. If the stream buffer is not large enough, it is resized to accommodate more data. This should be able to hold arbitrarily large MIDI files.

Another type of event is the System Exclusive event. They are not usually in most MIDI files (but they can be) and we are not supporting them yet. If we find something that is not one of our previously described events, we’ll bail out.

else if((evt.event & 0xf0) != 0xf0) // normal command
{
	tracks[idx].last_event = evt.event;
	evt.data++; // skip the event byte
	msg = ((unsigned long)evt.event) |
		  ((unsigned long)*evt.data++ << 8);

	if(!((evt.event & 0xf0) == 0xc0 || (evt.event & 0xf0) == 0xd0))
		msg |= ((unsigned long)*evt.data++ << 16);

	while(streamlen + 2 > streambuflen)
	{
		unsigned int* tmp = NULL;
		streambuflen *= 2;
		tmp = (unsigned int*)realloc(streambuf, sizeof(unsigned int) * streambuflen);
		if(tmp != NULL)
		{
			streambuf = tmp;
		}
		else
		{
			goto error;
		}
	}

	streambuf[streamlen++] = time;
	streambuf[streamlen++] = msg;

	tracks[idx].buf = evt.data;
}
else
{
	// not handling sysex events yet
	printf("unknown event %2x", evt.event);
	exit(1);
}

MIDI Score Timing

The usleep() function provides a more precise delay for MIDI score timing. It provides better precision than Sleep() but precision is still dependent on the precision of the high-resolution performance counters on an individual system. This can and will vary from system to system.

void usleep(int waitTime) {
    LARGE_INTEGER time1, time2, freq;

    if(waitTime == 0)
    	return;

    QueryPerformanceCounter(&time1);
    QueryPerformanceFrequency(&freq);

    do {
        QueryPerformanceCounter(&time2);
    } while((time2.QuadPart - time1.QuadPart) * 1000000ll / freq.QuadPart < waitTime);
}

QueryPerformanceCounter() returns the current value of the high-resolution performance counter of the system. This counter ticks at a frequency based on the CPU clock (but is not necessarily the same as the CPU clock). QueryPerformanceFrequency() returns how many times the performance counter ticks every second.

This function does not sleep but instead spins in a tight loop until a specified number of counter ticks has passed. To convert ticks to microseconds, we calculate the difference in ticks, multiply by 1,000,000 (microseconds/second) and divide by the frequency of ticks per second. When this value is greater than or equal to the number of microseconds passed in, we return.

This function is not exact but is pretty good. Some things may throw this off for instance the precision of the high-resolution counter is not likely to be down to a single microsecond. If you want to wait 10 microseconds and the precision/frequency is one tick every 6 microseconds, your 10 microsecond wait will actually be no less than 12 microseconds. Again, this frequency is system dependent and will vary from system to system. Also, Windows is not a real-time operating system. A process may be preempted at any time and it is up to Windows to decide when the process is rescheduled. The application may be preempted in the middle of usleep() and not restarted again until long after the expected wait time has elapsed. There really isn't much you can do about it. If you've ever heard MIDI music stutter in games this is likely the reason why.

unsigned int example8()
{
	unsigned char* midibuf = NULL;
	unsigned int midilen = 0;

	unsigned int* streambuf = NULL;
	unsigned int streamlen = 0;

	unsigned int err, msg;
	HMIDIOUT out;
	unsigned int PPQN_CLOCK;
	unsigned int i;

	struct _mid_header* hdr;

	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*)"example8.mid", &midilen);
	if(midibuf == NULL)
	{
		printf("could not open example8.mid\n");
		return 0;
	}

	hdr = (struct _mid_header*)midibuf;

	PPQN_CLOCK = 500000 / swap_bytes_short(hdr->ticks);

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

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

		usleep(time * PPQN_CLOCK);

		msg = streambuf[i++];
		if(msg & 0xff000000) // tempo change
		{
			msg = msg & 0x00ffffff;
			PPQN_CLOCK = msg / swap_bytes_short(hdr->ticks);
		}
		else
		{
			err = midiOutShortMsg(out, msg);
			if(err != MMSYSERR_NOERROR)
				printf("error sending command: %08x error: %d\n", msg, err);
		}
	}

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

	return 0;
}

Finally, this last function retrieves the stream buffer and plays the score. It is still much like the previous examples with the exception of the added tempo adjustments. First, instead of the PPQN_CLOCK value being hard-coded at 5 milliseconds, we calculate it based on the default 500,000 microseconds per quarter note and the PPQN ticks from the MIDI header. Second, we replace the Sleep() function with our improved usleep() function passing in the delay time in microseconds instead of milliseconds. Third, if we encounter a tempo change META event in the stream, we pull out the new microseconds per quarter note value and recalculate the PPQN_CLOCK value. Otherwise, the message is treated as before.

These last few articles focused on the low-level details of parsing and playing MIDI files. By this time you should have a pretty good understanding of how that works. In the next article I'll talk about some of the mid-level APIs for off loading some of that work to Windows and possibly the sound card if supported. We'll still need to parse the files but things like timing the events we won't have to worry about as much.


In the near future, when it is completed (enough for my liking) I'll post a fully functioning set of APIs for manipulating MIDI files. Also, I'm working on the same for playing MUS files as found in DOOM and Hexen which I'll also post.
UPDATE: I've actually posted this library to SourceForge as part of my DooM port. If you're interested you can check it out here!

As always, if you find any errors, omissions, have suggestions or improvements, please comment below or email me. I want these tutorials to be as correct as possible.

Related files:

Further reading:

Category(s): C, Multimedia, Programming
Tags: , , , , ,

Leave a Reply

Your email address will not be published. Required fields are marked *