Author: David Rager
Date: December 31, 2011
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 the number 3, and then the 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 current 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. 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:
Author: David Rager
Date: December 30, 2011
Jump to: Part 1, Part 2, Part 3, Part 4, Part 5
In the last article we demonstrated playing a very simple single track MIDI file. In this article we will build on that and add support for multiple tracks. This example will still only support very small MIDI files and is only guaranteed to work correctly for the provided example MIDI file. Related source and example files can be downloaded here: mididemo.zip In the previous example, each event was preceded by a delta-time value. This delta-time value is the amount of time to wait before executing the MIDI event. This time is relative to the previous event so a delta-time of say 10 ticks means “wait 10 ticks from the last event before executing the next event”. In the single track example, this was pretty straight forward. In multiple track MIDI files this becomes a little more complicated. The delta-time values are relative to the previous event in the same track. So for example, if one track executes an event every 10 ticks and another simultaneous track executes an event every 25 ticks, they will need to be merged (or mixed) into a single stream and each delta-time will need to be adjusted based on the events from both tracks. For example:
Ticks | Track 1 | Track 2 ==> Merged
---------------------------------------------------
0 | 0 - e1 | 0 - e1 | 0 - t1e1
| | | 0 - t2e1
5 | | |
10 | 10 - e2 | | 10 - t1e2
15 | | |
20 | 10 - e3 | | 10 - t1e3
25 | | 25 - e2 | 5 - t2e2
30 | 10 - e4 | | 5 - t1e4
35 | | |
40 | 10 - e5 | | 10 - t1e5
45 | | |
50 | 10 - e6 | 25 - e3 | 10 - t1e6
| | | 0 - t2e3
...
These two tracks begin simultaneously executing their first event. 10 ticks later an event from the first track is executed followed by another after another 10 ticks. Now it starts to get interesting. Because the second event from track 2 is executed after 25 ticks, we must take into account the events from track 1 that have been executed so far and subtract that value from the delta-time for the track 2 event. Therefore, when the tracks are merged, the second track 2 event will need to be executed only 5 ticks after the third track 1 event. Also, since the second event from track 2 is executed 5 ticks after the third event from track 1, the delta-time for the fourth event for track one must be adjusted to account for that and is executed 5 ticks later. At this point in the example things get back on track until the 50 tick mark when two events must be executed simultaneously again. A total of 10 ticks must pass before executing one of the events, and because they are to be executed simultaneously, 0 ticks must pass before executing the second event. This gets seemingly more complicated as more tracks are added, however the algorithm is relatively simple. A counter is needed to keep track of the absolute time from the beginning of the score. Another counter for each track is also needed to keep track of the absolute time processed so far within the track. The algorithm goes something like this:
- Loop through each track.
- If a track is at the end-of-track marker, skip the track.
- If all the tracks are at the end-of-track marker, we are done.
- Select the event closest to the current absolute time.
- Extract the event.
- Advance the track pointer to the next event in the track.
- Advance the absolute time for the track by the extracted event’s delta-time.
- The difference between the absolute time for the track and the absolute time for the score is used as the new delta-time for the event.
- The absolute time for the score is advanced by the new delta-time.
- The delta-time and event are added to the stream as in the previous example.
- Continue from step 1.
To put this into practice we’ll introduces some new structures and helper functions.
struct trk {
struct _mid_track* track;
unsigned char* buf;
unsigned char last_event;
unsigned int absolute_time;
};
The trk
structure will keep track of the processing of each MIDI track. The track
field is a pointer to the Track header as described in the last article. buf
will be initialized to point to the first delta-time of the first event. The absolute_time
field will be initialized to 0 and will keep track of the absolute time of all processed events. The last_event
field we won’t worry about for now. It will be used in the next article.
struct evt {
unsigned int absolute_time;
unsigned char* data;
unsigned char event;
};
The evt
structure will be used to represent the extracted event data from the track. The absolute_time
field will represent the absolute time of the event from the beginning of the track. The data
field will point to the beginning of the event (first byte past the delta-time value). The event
field will store the event byte.
unsigned short swap_bytes_short(unsigned short in)
{
return ((in << 8) | (in >> 8));
}
unsigned long swap_bytes_long(unsigned long in)
{
unsigned short *p;
p = (unsigned short*)∈
return ( (((unsigned long)swap_bytes_short(p[0])) << 16) |
(unsigned long)swap_bytes_short(p[1]));
}
Because numeric data in MIDI files is stored in big-endian format, swap_bytes_short()
and swap_bytes_long()
are helper functions used to convert the numeric data to little-endian format.
struct evt get_next_event(const struct trk* track)
{
unsigned char* buf;
struct evt e;
unsigned int bytesread;
unsigned int time;
buf = track->buf;
time = read_var_long(buf, &bytesread);
buf += bytesread;
e.absolute_time = track->absolute_time + time;
e.data = buf;
e.event = *e.data;
return e;
}
The get_next_event()
helper function simply reads the next event from the track. It does not advance the track pointer or absolute time, it simply returns the next event available. If the returned event is the one that will be selected, the track pointer will have to be advanced later. This function uses read_var_long()
as discussed in the previous article to read the delta-time from the track buffer. That value is added to the track’s absolute_time
to get the event’s absolute_time
value. The data
field is set to the next byte past the delta-time in the track buffer and the event byte is copied into the event
field.
int is_track_end(const struct evt* e)
{
if(e->event == 0xff) // meta-event?
if(*(e->data + 1) == 0x2f) // track end?
return 1;
return 0;
}
Finally, the is_track_end()
helper function determines if the returned event is the end-of-track marker.
unsigned int get_buffer_ex7(unsigned char* buf, unsigned int len, unsigned int** out, unsigned int* outlen)
{
struct _mid_header* hdr = NULL;
struct trk* tracks = NULL;
unsigned short nTracks = 0;
unsigned int i;
unsigned int* streambuf = NULL;
unsigned int streambuflen = 1024;
unsigned int streamlen = 0;
unsigned char* tmp = buf;
unsigned int currTime = 0;
streambuf = (unsigned int*)malloc(sizeof(unsigned int) * streambuflen);
memset(streambuf, 0, sizeof(unsigned int) * streambuflen);
hdr = (struct _mid_header*)tmp;
tmp += sizeof(struct _mid_header);
nTracks = swap_bytes_short(hdr->tracks);
tracks = (struct trk*)malloc(nTracks * sizeof(struct trk));
for(i = 0; i < nTracks; i++)
{
tracks[i].track = (struct _mid_track*)tmp;
tracks[i].buf = tmp + sizeof(struct _mid_track);
tracks[i].absolute_time = 0;
tracks[i].last_event = 0;
tmp += sizeof(struct _mid_track) + swap_bytes_long(tracks[i].track->length);
}
while(TRUE)
{
unsigned int time = (unsigned int)-1;
unsigned char cmd;
unsigned int msg = 0;
unsigned int idx = -1;
struct evt evt;
// get the next event
for(i = 0; i < nTracks; i++)
{
evt = get_next_event(&tracks[i]);
if(!(is_track_end(&evt)) && (evt.absolute_time < time))
{
time = evt.absolute_time;
idx = i;
}
}
if(idx == -1)
break; // we're done
evt = get_next_event(&tracks[idx]);
tracks[idx].absolute_time = evt.absolute_time;
time = tracks[idx].absolute_time - currTime;
currTime = tracks[idx].absolute_time;
cmd = *evt.data++;
if((cmd & 0xf0) != 0xf0) // normal command
{
msg = ((unsigned long)cmd) |
((unsigned long)*evt.data++ << 8);
if(!((cmd & 0xf0) == 0xc0 || (cmd & 0xf0) == 0xd0))
msg |= *evt.data++ << 16;
streambuf[streamlen++] = time;
streambuf[streamlen++] = msg;
tracks[idx].buf = evt.data;
}
}
*out = streambuf;
*outlen = streamlen;
free(tracks);
return 0;
}
This function first parses the MIDI header to determine how many tracks are in the file. It then allocates an array of that number of trk
structures. Each trk
structure is initialized with the beginning of the track data for each track within the buffer. Once the trk
structures are initialized, processing begins. We loop through each trk
structure looking for the next event that is not the end-of-track marker and has the lowest absolute_time
value. If more than one event have the same absolute_time
, the first event is chosen. (the next iteration will choose the next event, and so on…) If all the tracks are at the end-of-track marker, our stream buffer is filled and we can return to the caller. With the next event in hand, we update the absolute time for the track from which the event came to be the absolute time of the event. We calculate the new delta-time for the event by subtracting the score absolute time value from the track absolute time value. Finally, we update the absolute time for the score to be the absolute time of the event (track). The event is processed in the same way as described in the previous article. However, the data field in the evt
structure is used to read from the track buffer to determine where the data bytes are and find the beginning of the delta-time for the next event in the track. Once the event is processed, evt.data
is pointing to the beginning of the next delta-time. The buf
field of the trk
structure is advanced to this location.
unsigned int example7()
{
unsigned char* midibuf = NULL;
unsigned int midilen = 0;
unsigned int* streambuf = NULL;
unsigned int streamlen = 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*)"example7.mid", &midilen);
if(midibuf == NULL)
{
printf("could not open example7.mid\n");
return 0;
}
get_buffer_ex7(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;
}
Because the multiple tracks have been merged into a single stream, the code that sends the messages to midiOutShortMsg()
is exactly the same as in the previous example. In this article we looked at merging multiple MIDI tracks into a single stream. In the next article we will look at supporting more advanced features such as running mode and META events. We will also look at a more precise timing mechanism for timing MIDI events instead of using the Sleep()
function. The result will be a (mostly) complete example capable of playing (most) midi files thrown at it. Related files:
Further reading:
]]>
Author: David Rager
Date: December 29, 2011
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:
]]>
Author: David Rager
Date: December 23, 2011
Jump to: Part 1, Part 2, Part 3, Part 4, Part 5
How do you read a MIDI file into memory and play it? I’m not talking about calling a function passing the file name like play("song.mid")
but rather play from a buffer filled with the contents of a MIDI file. I was kind of surprised to find that neither the Windows Multimedia API nor DirectX provide a way to directly play MIDI files loaded from a resource or loaded into a memory buffer. They do supply APIs and interfaces for playing MIDI data, however they require you to do a bit of processing of the MIDI file manually first. This is fine I suppose but if you’re looking for something more high level you’ll have to look for a third party library, or write your own. The following articles will show how to process MIDI files and play them using the Windows Multimedia API. MIDI files are basically a set of one or more tracks containing time-stamped commands (or events). The commands are instructions for the MIDI sequencer describing instrument patches to use, tempo, controller instructions (volume, pan, etc…) and of course the notes to play. This wiki article describes the MIDI file format and command messages in great detail. In order to play MIDI commands you have to first open a MIDI device that will process them. Windows keeps a list of devices that are capable of processing MIDI commands. In addition to the MIDI devices there is the MIDI Mapper which allows MIDI output to be routed to different devices. By default, the MIDI Mapper maps to the default MIDI device. To open the MIDI Mapper you must call midiOutOpen()
passing the MIDI_MAPPER
value as the device ID.
Example 1
unsigned int example1()
{
unsigned int err;
HMIDIOUT out;
err = midiOutOpen(&out, MIDI_MAPPER, 0, 0, CALLBACK_NULL);
if (err != MMSYSERR_NOERROR)
printf("error opening MIDI Mapper: %d\n", err);
else
printf("successfully opened MIDI Mapper\n");
midiOutClose(out);
return 0;
}
Once the device is no longer needed, it must be closed by calling midiOutClose()
passing the device handle retrieved from midiOutOpen()
. Actual MIDI devices are assigned IDs beginning with 0. If there is more than one MIDI capable device, the default device is always given ID 0. The remaining devices are numbered, 1, 2, etc… To open the default MIDI device call midiOutOpen()
passing 0 as the device ID.
unsigned int example2()
{
unsigned int err;
HMIDIOUT out;
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");
midiOutClose(out);
return 0;
}
The actual number of devices may be retrieved by calling midiOutGetNumDevs()
. Capabilities for each device may be determined by passing the device ID to midiOutGetDevCaps()
which will fill a MIDIOUTCAPS
structure with information about the device capabilities. With an open device handle we can start sending MIDI commands. For the next few examples we will use the midiOutShortMsg()
function to write the commands. midiOutShortMsg()
takes the handle to the device that was returned from midiOutOpen()
and a DWORD
containing the actual message. The MIDI commands are similar in format as described in the MIDI file format specification. The low-order byte is the MIDI command (or status) byte. This is the same value as specified in the MIDI file. The next two bytes take the first and second data byte for the MIDI command (if it is needed). Whether or not a data byte is needed depends on the message. The highest-order byte is unused and may be set to 0.
| 31 - 24 | 23 - 16 | 15 - 8 | 7 - 0 |
--------------------------------------------------
| Unused | Data 2 | Data 1 | Status byte |
midiOutShortMsg()
also supports a running status as described in the MIDI file format specification. If running status is used, the command byte is omitted and the previously sent command byte is assumed. The lowest-order two bytes then become the data bytes and the highest-order two bytes are unused.
| 31 - 24 | 23 - 16 | 15 - 8 | 7 - 0 |
----------------------------------------------
| Unused | Unused | Data 2 | Data 1 |
The following example demonstrates playing a single note (Middle-C) held for one second.
unsigned int example3()
{
unsigned int err;
HMIDIOUT out;
err = midiOutOpen(&out, 0, 0, 0, CALLBACK_NULL);
if (err != MMSYSERR_NOERROR)
{
printf("error opening default MIDI device: %d\n", err);
return 0;
}
else
printf("successfully opened default MIDI device\n");
midiOutShortMsg(out, 0x00403C90);
Sleep(1000);
midiOutShortMsg(out, 0x00003C90);
midiOutClose(out);
return 0;
}
Note that the message parameter is in little-endian byte order. This is important because data in the MIDI file format is stored in big-endian format. When these values are read from the MIDI file they must be packed in the message in the correct order. The lowest-order byte 0x90
is the MIDI command for note-on (play note, key down, etc…). The next byte 0x3c
is the first data parameter for the note-on command. This is the actual note number for Middle-C. The next byte 0x40
is the velocity for which the note is played (e.g. how hard the key is pressed). The highest order byte is not used and is set to 0. This message will cause the note to start playing. In order to hear the note we need to pause, in this case we call Sleep()
for 1 second. When one second is up we have to stop the note from playing. The MIDI command for note-off (stop note, key up, etc…) is 0x80
. But wait! You are passing another note-on event (0x90
). Well, to take advantage of running mode, a MIDI note-on command (0x90
) with a velocity value of 0 is equivalent to a note-off command. I’ll discuss running mode later. So, the low order byte 0x90
is the MIDI command for note-on. The second byte 0x3c
is the first parameter indicating the note we are controlling, Middle-C. The third byte 0x00
is the velocity. The value of 0x00
turns the note-on event into a note-off event. The highest-order byte is unused and is set to 0. Finally, we call midiOutClose()
and exit. The next example will play a chord. This will demonstrate playing multiple events simultaneously. In particular, Middle-C, E and G will be played, held for one second and then stopped. This example is not much different from the previous example.
unsigned int example4()
{
unsigned int err;
HMIDIOUT out;
err = midiOutOpen(&out, 0, 0, 0, CALLBACK_NULL);
if (err != MMSYSERR_NOERROR)
{
printf("error opening default MIDI device: %d\n", err);
return 0;
}
else
printf("successfully opened default MIDI device\n");
midiOutShortMsg(out, 0x00403C90);
midiOutShortMsg(out, 0x00404090);
midiOutShortMsg(out, 0x00404390);
Sleep(1000);
midiOutShortMsg(out, 0x00003C90);
midiOutShortMsg(out, 0x00004090);
midiOutShortMsg(out, 0x00004390);
midiOutClose(out);
return 0;
}
The following example builds on the previous concepts and plays a scale. It also introduces the concept of time ticks (or pulses) and the PPQN clock.
Example 5
unsigned int example5()
{
unsigned int err;
HMIDIOUT out;
const unsigned int PPQN_CLOCK = 5;
unsigned int i;
unsigned int cmds[] = {
0x007f3c90,
0x60003c90,
0x007f3e90,
0x60003e90,
0x007f4090,
0x60004090,
0x007f4190,
0x60004190,
0x007f4390,
0x60004390,
0x007f4590,
0x60004590,
0x007f4790,
0x60004790,
0x007f4890,
0x60004890,
0};
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");
i = 0;
while(cmds[i])
{
unsigned int time = cmds[i] >> 24;
Sleep(time * PPQN_CLOCK);
err = midiOutShortMsg(out, cmds[i]);
if(err != MMSYSERR_NOERROR)
printf("error sending command: %d\n", err);
i++;
}
midiOutClose(out);
return 0;
}
First, the commands are stored in an array. They should look familiar except for one additional piece of information. The high-order byte is being used to store the command ticks. The tick value is the number of time ticks (or pulses) to wait since the previous command was executed, before executing the next command. The first command again, starts playing a Middle-C. The high-order byte is 0x00
meaning play immediately. The next command stops playing the Middle-C. The high-order byte is 0x60
which says wait for 96
(0x60
) ticks before playing the note-off command. The next command starts playing the next note (D) after 0 ticks from when the last note-off command was sent (immediately). The PPQN_CLOCK
(pulses per quarter note) value specifies how often a tick (pulse) is generated. NOTE, Sleep()
does not provide enough precision for accurate MIDI score timing, it is being used here for ease and illustration. The PPQN_CLOCK
value of 5
milliseconds (generate a tick every 5 miliseconds) and 96
ticks (pulses) per quarter note will give about 120
beats-per-minute. More on this later. This example loops through each command in the array, pulls out the time ticks and sleeps for that amount of time. Once it is done sleeping the command is sent to the MIDI device with midiOutShortMsg()
. Note that the high order byte is not cleared. This does not matter, it is not used by midiOutShortMsg()
and is ignored. When it reaches the end of the commands list, the loop exits and the device is closed. NOTE, storing the ticks in the command value like this is not part of any standard. It is just something I decided to do for this example and in fact is insufficient for the full MIDI file format support. The time values are stored differently in the MIDI file format and may be larger than a value that can be stored in a single byte. Another MIDI API that will be discussed later will also handle the time ticks differently. In the next article we’ll start getting into parsing the MIDI file format and playing MIDI files. Related files:
Further reading:
]]>
Author: David Rager
Date: December 22, 2011
I was messing with the windows multimedia APIs and ran into this strange error running my application:
Run-Time Check Failure #0 - The value of ESP was not properly saved
I was adding a callback used by midiStreamOpen()
:
static void mid_callback_proc(
HMIDIOUT hmo,
UINT wMsg,
DWORD_PTR dwInstance,
DWORD_PTR dwParam1,
DWORD_PTR dwParam2)
{
...
}
...
err = midiStreamOpen(
&p->stream,
&p->device,
1,
(DWORD_PTR)mid_callback_proc,
(DWORD_PTR)p,
CALLBACK_FUNCTION);
It seems I forgot the CALLBACK
macro in the function definition:
static void CALLBACK mid_callback_proc(
HMIDIOUT hmo,
UINT wMsg,
DWORD_PTR dwInstance,
DWORD_PTR dwParam1,
DWORD_PTR dwParam2)
{
...
}
The CALLBACK
macro sets up the correct calling convention for the callback function. By default the calling convention is __cdecl
in which the caller is required to clean up the stack. The CALLBACK
macro tells the compiler to use the __stdcall
calling convention for the callback function in which the called function is responsible for cleaning up the stack. If the callback uses the __cdecl
calling convention, the stack is not cleaned up properly which results in the above error. Easy to miss if you’re not paying attention. ]]>
Author: David Rager
Date: December 21, 2011
An unpatched critical flaw in 64-bit Windows 7 can lead to a ‘blue screen of death’ system crash. The flaw is triggered by viewing a malicious HTML page containing an IFRAME element with a sufficiently large height attribute while using the Safari web browser. The flaw seems to be exploitable only with Safari at the moment. However, such a flaw could lead to the ability to execute arbitrary code in the kernel.]]>