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:
- Game Audio Programming (Charles River Media Game Development)
- Maximum MIDI : Music Applications in C++
- The MIDI Manual, Third Edition: A Practical Guide to MIDI in the Project Studio
]]>