MIDI Programming - A Complete Study
Part 3 - Timing, Windows API and MIDI Events

Written by Stéphane Richard (Mystikshadows)

INTRODUCTION:

Welcome to the third part of the MIDI Programming Series. There is a small change of plans. I was supposed to make a playback engine in this third part. But because of the contents of everything before it, I've decided to keep the engine for part 4 and instead, take the time to explain everything you'll need to know to fully understand the playback engine. As such, this part of the series will cover Timing issues, The windows API and how to manipulate standard MIDI events to get them ready to send to the MIDI devices on your computer.

To do so, we'll start by looking at some basic notions you'll need to know about sequencing and timing. We'll also take each type of MIDI event and see how we need to prepare the MIDI message to send them as valid MIDI events to the MIDI device.

TIMING RELATED NOTIONS:

When it comes to MIDI files, there's two things that can really define the quality of a MIDI software and they are resolution and Accuracy. These two combined determine how a MIDI file is played and/or recorded when you are playing on a keyboard to be recorded on your computer. Let's take a look at some definitions you'll need to consider.

TIMING CALCULATIONS AND FORMULAS:

And now it's time to get to the core of sequencing. Without timing, we have no way of knowing what's supposed to happen next when it comes to playing our MIDI song. Timing is at the core of any program that can play this type of file. The truth of the matter is that you need to know how long a given note should last on a given tempo. This value will help scale the playing of the whole song itself.

The Beats Per Minute (BPM):

Any keyboard you buy, any sequencing software gives you the ability to change the tempo of a song. The tempo determines the speed at which the song will be played. To a musician, a beat is always based on a quarter note. With this in mind a BPM of 100 means that the musician must play 100 steady quarter notes in one minute. For the other note durations you would simply multiply or divide the BPM according. For example Take a look at this list of values. Remember that there are 60,000,000 microseconds per minute. We just need to divide that by the BPM to determine the interval where each beat should be played.

Note Count And Time Lapses BPM=100 NOTE NAME COUNT/MINUTE MICROSECONDS PER BEAT ----------------- ------------ ------------ Whole Note 25 2,400,000 Half Note 50 1,200,000 Quarter Note 100 600,000 Eight Of A Note 200 300,000 16th Of A Note 400 150,000 32th Of A Note 800 75,000 64th Of A Note 1600 37,500 128th Of A Note 3200 18,750

For a quarter note, at a BPM of 100 means that every note should be played every 0.6 seconds to have 100 notes in a minute. This is is a time based calculation. As you see next, the MIDI hardware doesn't speak in terms of time based values directly. This is also where the playback resolution of a song can and will be affected. Take a look at the same chart for a BPM of 120.

Note Count And Time Lapses BPM=120 NOTE NAME COUNT/MINUTE MICROSECONDS PER BEAT ----------------- ------------ ------------ Whole Note 30 2,000,000 Half Note 60 1,000,000 Quarter Note 120 500,000 Eight Of A Note 240 250,000 16th Of A Note 480 125,000 32th Of A Note 960 62,500 64th Of A Note 1920 36,250 128th Of A Note 3840 18,125

As you can see, BPM timing is all about dividing the 60,000,000 microseconds of a minute into your Beats Per Minute. Since, in MIDI music, everything is based on the quarter note, you'll notice on both these charts that the quarter note has the BPM in questions (120 in this last case and 100 in the chart above). Let's now talk about the next biggest item in this part, the Pulse Per Quarter Note.

The Pulse Per Quarter Notes (PPQN):

What is a PPQN? You can think of them as the number of intervals a quarter note can be broken into in order to reproduce a song in time. It is dependant on the hardware used (Hardware sequencers, sound cards, etc...) and the Software/OS it is executing on. On today's hardware and software, one can usually expect a good stable PPQN of 480. This means that if you take the chart above (BPM of 120) a quarter note, which lasts 0.5 second can be split into 480 equal time slots. As such, any note that will be played must be played on one of those 480 time slots of 1041.7 microseconds each (just about a millisecond long). This is known as the resulution of the playback. Not too long ago, a PPNG of 96 seemed to be enough to reliably play a song faithfully. However, there were, back then, many songs that just weren't played electronically. This of course changed. For example, fully orchestrated classical musical pieces. It didn't take too long to notice that a recorded song would play these types of song with strange offbeat notes that felt like they didn't belong where they were being played. Hence the PPQN needed to be raised, considerably, to allow for such songs to be reproduced with the accuracy they needed.

Here's the same table as above only this time we'll include different PPQNs to give you a more visual idea of the resolution they allowed. The most widespread PPQNs up to today were 96, 192 and 480. You'll see how it affects things right here:

Note Count And Time Lapses BPM=120 MICROSECONDS NOTE RESOLUTIONS NOTE NAME COUNT/MINUTE PER BEAT 96 PPQN 192 PPQN 480 PPQN ----------------- ------------ ------------ ---------- ---------- --------- Whole Note 30 2,000,000 20,833.36 10,416.68 4,166.80 Half Note 60 1,000,000 10,416.68 5,208.34 2,083.40 Quarter Note 120 500,000 5,208.34 2,604.17 1,041.70 Eight Of A Note 240 250,000 2,604.17 1,302.09 520.85 16th Of A Note 480 125,000 1,302.09 651.04 260.43 32th Of A Note 960 62,500 651.04 325.52 130.21 64th Of A Note 1920 36,250 325.52 162.76 65.11 128th Of A Note 3840 18,125 162.76 81.38 32.55

As you can see here, the higher the PPQN, the smaller the time slots. This of course means that the notes being played can be played closer to the actual time it should have been played at when you have a smaller time slot resolution. This is what PPQN does, divide time into manageable timeslots. All you have to do after that is loop through time, collect what should be sent in the next time slot and send it off through the MIDI port. Sounds Simple? One more obstacle stands in the way of accomplishing this and that is the Windows API.

MIDI AND THE WINDOWS API:

As I mentionned previously in the series, I plan to use the Windows API and make a low level MIDI player. Of course, this means we'll need a few things from the API in order to be able to accomplish our goal. If you've ever taken a look at the mmsystem.bi include file, just doing that might seem quite overwhelming. It's a big file, but it's there to do a lot more than play a MIDI file. The first thing will do is take a look at what exactly we'll need to play a note on a MIDI device.

The needed MIDI Subs and Functions:

The purpose of this part is to show you how to play a note through the MIDI device of your choice. As I said before, you don't need the whole mmsystem.bi functions to accomplish that. As far as functions and subs are concerned, you only need five of them. Let's take a look at their declaration so you can get familiar with them.

Declare Function midiOutGetNumDevs () As Integer DECLARE FUNCTION midiOutGetDevCaps _ ALIAS "midiOutGetDevCapsA" (ByVal uDeviceID AS INTEGER, _ lpCaps AS MIDIOUTCAPS, _ ByVal uSize AS INTEGER) AS INTEGER DECLARE FUNCTION midiOutClose (ByVal hMidiOut As INTEGER) As Integer DECLARE FUNCTION midiOutOpen (lphMidiOut AS INTEGER, _ ByVal uDeviceID AS INTEGER, _ ByVal dwCallback AS INTEGER, _ ByVal dwInstance AS INTEGER, _ ByVal dwFlags AS INTEGER) AS INTEGER DECLARE FUNCTION midiOutShortMsg (ByVal hMidiOut AS INTEGER, _ ByVal dwMsg AS INTEGER) AS INTEGER

Note that the midiOutShortMsg API function is used to send all standard MIDI events (&H8X to &HEX) And we'll be cvering all of them in this 3rd part of the series. One quick look at these declarations and you can see that they all use standard data types, mostly integers, all except midiOutGetDevCaps which requires a MIDIOUTCAPS parameter. MIDIOUTCAPS is a user defined type. Here is its definition so you can see what it's made of.

Type MIDIOUTCAPS wMid AS INTEGER ' Manufacturer identifier of the device driver wPid AS INTEGER ' Product Identifier vDriverVersion AS INTEGER ' Version number of the device driver. szPname AS STRING * 32 ' Product name in a null-terminated string. wTechnology AS INTEGER ' Either MOD_FMSYNTH, MOD_MAPPER, ' MOD_MIDIPORT, MOD_SQSYNTH, MOD_SYNTH wVoices AS INTEGER ' Number of voices supported by internal synthesizer. wNotes AS INTEGER ' Maximum number of simultaneous notes wChannelMask AS INTEGER ' Channels internal synthesizer device responds to. dwSupport AS INTEGER ' Either MIDICAPS_CACHE, MIDICAPS_LRVOLUME ' MIDICAPS_STREAM, MIDICAPS_VOLUME END TYPE

This User Defined Type is used in combination with the midiOutGetDevCaps() function to get information about the different MIDI devices that are currently installed on your system. the wTechnology field uses constants as it's possible values. These constants are defined as follows:

MOD_MIDIPORT 1 <- MIDI hardware port. MOD_SYNTH 2 <- The device is a synthesizer. MOD_FMSYNTH 3 <- FM synthesizer. MOD_SQSYNTH 4 <- square wave synthesizer. MOD_MAPPER 5 <- Microsoft MIDI mapper.

Likewise, we have the dwSupport field which also needs some constant values. These values represent addition technologies and features that are supported by the hardware that you get from the driver. Here is their definitions:

MIDICAPS_VOLUME 1 <- Supports volume control. MIDICAPS_LRVOLUME 2 <- Separate left and right volume control. MIDICAPS_CACHE 4 <- Supports patch caching. MIDICAPS_STREAM 8 <- Support for the midiStreamOut function.

With all these definitions at hand, we're ready to begin the code we need to send MIDI events through the MIDI device of our choice. To do so we'll need a few steps. So let's get right to it.

Step 1 - Acquire The MIDI Devices' Information:

As probably expected in the world of programming, you need to

' --------------------------------------------- ' Include mmsystem.bi to have its definitions ' --------------------------------------------- #include "windows.bi" #include "win\mmsystem.bi" ' ---------------------- ' Variables we'll need ' ---------------------- TYPE MidiDevice DeviceID AS INTEGER DeviceName AS WSTRING * 32 VoiceCount AS INTEGER Polyphonics AS INTEGER END TYPE ' ---------------------- ' Variables we'll need ' ---------------------- DIM MIDIDevices() AS MidiDevice ' Create Dynamic Array DIM DeviceCount AS INTEGER ' Number of MIDI devices DIM Caps AS MIDIOUTCAPS DIM Counter AS INTEGER DIM MIDIHandle AS INTEGER '---------------------------------- ' Get the rest of the midi devices ' ---------------------------------- DeviceCount = midiOutGetNumDevs REDIM MIDIDevices(0 To DeviceCount - 1) AS MidiDevice ' ---------------------------- ' Index 0 is the MIDI Mapper ' ---------------------------- MIDIDevices(0).DeviceID = 0 MIDIDevices(0).DeviceName = "MIDI Mapper" ' -------------------------------------------------------- ' Loop Through devices to populate the MIDIDevices array ' -------------------------------------------------------- For Counter = 0 To (DeviceCount - 1) midiOutGetDevCaps Counter, @Caps, Len(Caps) MIDIDevices(Counter + 1).DeviceID = Counter + 1 MIDIDevices(Counter + 1).DeviceName = Caps.szPname MIDIDevices(Counter + 1).VoiceCount = Caps.wVoices MIDIDevices(Counter + 1).Polyphonics = Caps.wNotes MIDIDevices(Counter + 1).Channels = Caps.wChannelMask Next Counter

So far I think it seems simple enough. We call the midiOutGetNumDevs to get a count of the devices we have installed in Windows. Then, we call midiOutGetDevCaps API function is called in a loop to populate the rest of the MIDIDevices array with all the existing device names. This gives us the list of available MIDI Output devices. There is also the midiInGetDevCaps to allow us to get all of the devices we have that can receive information from the MIDI port. Here's what the code above gave me for output devices:

0 - MIDI Mapper 1 - SB Audigy 2 Synth B [D000] 2 - SB Audigy 2 Sw Synth [D000] 3 - SB Audigy 2 Synth A [D000]

And there you have, we now have the code to get the list of devices we can use. Of course, with that information comes the next step. And that is, actually using one of the devices and playing some notes and other things. The next section is entirely devoted to showing you just that. So, let's get right to it.

Step 2 - Accessing The MIDI Devices:

Using devices that we have is typically a simple question of opening the MIDI device we want, sending what we want MIDI to interprete, and when we're done, we just close the MIDI Port we opened. So let's take a look at some code and I'll explain it right after.

' ---------------------------------------------------------- ' Variable to Get the handle of the MIDI Device we'll open ' ---------------------------------------------------------- DIM MIDIHandle AS INTEGER DIM CurrentDevice AS INTEGER DIM Result AS INTEGER DIM MIDIMessage AS INTEGER DIM Instrument AS BYTE DIM Volume AS BYTE DIM Channel AS BYTE DIM NoteNumber AS BYTE DIM PressureValue AS BYTE DIM Value AS BYTE DIM LeastSignificant AS BYTE DIM MostSignificant AS BYTE ' ------------------------------------------- ' First we get which Device we want to open ' ------------------------------------------- CurrentDevice = Devices(DeviceIndexNumber).DeviceID Result = midiOutOpen(MidiHandle, CurrentDevice, 0, 0, 0) If (Result <> 0) Then PRINT "Could not Open MIDI Device." End If

If all goes well at this point you have the device you want opened and ready to get outward messages. All we need to do at this point is get the MIDI messages ready and send them to that MIDI Handle. And now that we have an MIDI port, let's start defining how each type of MIDI message is prepared and sent to the MIDI device. I'll just describe them in their numeric order.

&H80 Events (Note Off Events):

The Note Off event turns the note being played off. Although no error messages are reported by turning a note that isn't playing, you can imagine that before turning a note off, you'd typically be turning it on first. You need a MIDI Channel and a Note Number variables to turn a sound off. Here is how you would do it.

' ------------------------------------ ' Assign Values to needed parameters ' ------------------------------------ Channel = 0 ' 0 to 15 for active MIDI channels NoteNumber = 70 ' 0 to 127 for the notes. ' --------------------------------------------------------- ' Prepare the MIDI Message and send it to the MIDI Device ' --------------------------------------------------------- MidiMessage = &H80 + (NoteNumber * &H100) + Channel midiOutShortMsg MidiHandle, MidiMessage

&H90 Events (Note On Events):

This, of course, does the opposite as the Note Off event. It basically sends a note, a volumn and a MIDI channel to the MIDI device. When the MIDI device receives this message it will start playing the specified note at the requested volume through the specified MIDI channel. Here's how you prepare this type of message.

' -------------------------------- ' Define the note values we want ' -------------------------------- Volume = 60 ' 0 to 127 Channel = 0 ' 0 to 15 for active MIDI channels NoteNumber = 70 ' 0 to 127 for the notes. ' ------------------------------------------------------ ' Arrange the MIDI even in the right order and send it ' ------------------------------------------------------ MidiMessage = &H90 + (NoteNumber * &H100) + (Volume * &H10000) + Channel midiOutShortMsg MidiHandle, MidiMessage

&HA0 Events (Polyphonic Pressure Events):

This type of message is also called the Polyphonic Aftertouch Event. In essence, when you press a key on a keyboard it would generate a Note On event, when you release it, and just before the Note Off event is when an Aftertouch event can be inserted. Some keyboards have some pretty special aftertouch events and are well worth the effort to have them heard when it's supported. To do so, you need the note you want to play, a Polyphonic Aftertouch value (0 to 127) and of course the MIDI channel you want to send the message to. Here's how it works.

' -------------------------------- ' Define the note values we want ' -------------------------------- PressureValue = 127 ' 0 to 127 Channel = 0 ' 0 to 15 for active MIDI channels NoteNumber = 70 ' 0 to 127 for the notes. ' ------------------------------------------------------ ' Arrange the MIDI even in the right order and send it ' ------------------------------------------------------ MidiMessage = &HA0 + (NoteNumber * &H100) + (PressureValue * &H10000) + Channel midiOutShortMsg MidiHandle, MidiMessage

&HB0 Events (Controller Change Events):

If you remember in part one of the series, the biggest table I presented there was the list of Controllers. Each type of controller work differently from others, expect different parameters and perform a very different type of functionality. If you also remember, some controllers are known as continuous controllers, others are basically control switches and then you have the normal controllers. You basically need the Controller number, in some cases a value to apply to the controller and again the MIDI Channel you want to send the event to. Here's how you would do this.

' ------------------------------------ ' Assign Values to needed parameters ' ------------------------------------ Channel = 0 ' 0 to 15 for active MIDI channels Controller = 70 ' 0 to 127 for the notes. Value = 0 ' ------------------------------------------------------ ' Arrange the MIDI even in the right order and send it ' ------------------------------------------------------ MidiMessage = &HB0 + (Controller * &H100) + (Value * &H10000) + Channel midiOutShortMsg MidiHandle, MidiMessage

&HC0 Events (Program Change Events):

This type of event serves to change which instrument a note is to be played with. In a very typical scenario you would change instruments to each track's default instrument and play the whole song. However there's nothing stoping you from changing instrument whenever you want with this type if MIDI event. You simply need to specify the Instrument Number and the channel to send the MIDI event to and then 3 messages to the MIDI device will perform the program change. Here's how it works in code.

' ---------------------------------------------- ' Set Instrument and Channel to desired values ' ---------------------------------------------- Instrument = 51 ' 0 to 127 for MIDI instruments (51. SynthStrings 1) Channel = 0 ' 0 to 15 for active MIDI channels ' ------------------------------------------------------ ' Send a Patch/Program change event to the MIDI Device ' ------------------------------------------------------ midiOutShortMsg MIDIHandle, &HC0 + Channel midiOutShortMsg MIDIHandle, 32 * &H100 + &HC0 + Channel midiOutShortMsg MIDIHandle, Instrument * &H100 + &HC0 + Channel

&HD0 Events (Channel Pressure Events):

Channel Pressure Events are quite simple events that is sent whenever the force of a note (when played on a keyboard or generated numerically) influences the strength at which the sound is played and heard. MIDI has the &D0 event to simulate and reproduce this effect on a MIDI channel. It is known as channel pressure and essentially affects the volume (regardless of the default volume value) at which the note is played. To do so you need the Channel number and the Pressure Value (0 to 127) to apply to the MIDI channel. Note that this type of event differs from the Polyphonic Aftertouch message in that the Aftertouch affects the note that is in the message where as the Channel Pressure message affects the whole channel. here's how to send a Channel Pressure message.

' ------------------------------------ ' Assign Values to needed parameters ' ------------------------------------ PressureValue = 80 ' Desired Pressure Value to Applyu Channel = 0 ' 0 to 127 for the notes. ' --------------------------------------------------------- ' Prepare the MIDI Message and send it to the MIDI Device ' --------------------------------------------------------- MidiMessage = &HD0 + (PressureValue * &H100) + Channel midiOutShortMsg MidiHandle, MidiMessage

&HE0 Events (Pitch Bending Events):

Pitch bending is the act of starting on a specific note and bending the note down or up. Typically this is done, on a keyboard with a wheel control (a pitch bending wheel) and is often used in music in order to add a certain human flavor to a sound (specifically on guitar sounds, but on other sounds as well). Pitch bending requires 2 specific values. The way the work is that a pitch bend of &H0 means the note is lowered the the minimum pitch bending value, a value of &H7F7F means that the note is bent to it's maximum value. A value of &H4000 means that the note is played at the normal pitch (no bending). These values are represented as a least significant byte and a most significant byte. Aside that, you'll need the channel number where you want to send the pitch bending message.

' ------------------------------------ ' Assign Values to needed parameters ' ------------------------------------ MostSignificant = &H7F ' These set the maximum upward bend value LeastSignificant = &H7F Channel = 0 ' 0 to 15 for active MIDI channels ' --------------------------------------------------------- ' Prepare the MIDI Message and send it to the MIDI Device ' --------------------------------------------------------- MidiMessage = &HE0 + LeastSignificant * &H100 + MostSignificant * &H10000 + Channel midiOutShortMsg MidiHandle, MidiMessage

Now, much like my file manilulation series, there is one more important step to do when you're done sending things to the MIDI device. You guessed it, you have to close the MIDI device. To do so, you simply need to make a call to the midiOutClose API with the Handle of the MIDI device as it's parameter. You do this as follows:

' ------------------------------------- ' Dont forget to close your MIDI port ' ------------------------------------- Result = midiOutClose(MidiHandle)

And there you have, you should now be able to handle all standard MIDI events that you can read from a MIDI File. Each of these types of events will become an independing subroutine in the next part of the series to help organize the code better for our MIDI playback. Each value needed will be parameters that are passed to the subroutine to make them reusable for all types of occurrences of a specific MIDI event. This, along with the timing engine will allow us to playback any type of MIDI file we can find.

IN CONCLUSION:

And there you go. I think this is plenty of information for this third part of the series, don't you? The bet advice I can give you, if you're interested in this MIDI series and learning more about it, is to play with the code samples I've supplied here. Experiment with different values for each of them so you can see exactly how these values and types of event affect the way a sound is played on your MIDI device. It will also show you if, in any case, your hardware doesn't support a given feature. Not to mention that being very familiar with everything I've mentionned in this 3rd part will really help you easily and quickly understand the next and final part of the series.

In the next part of the series, we'll attack the actual playback engine itself as I promised in the last part. The code samples here will greatly help us in the next part as we will of course be using them in the playback engine to reproduce a MIDI file. For the sake of completeness, I'll introduce you to the API needed to handle SySex events as well in the next part. This way you'll be ready to handle anything MIDI can throw at your code. It takes a bit of a twist to handle SySex and this is mainly why I'm saving it for the next part of the series. You have enough to digest right now I think. Until then, happy coding, experimenting and learning. MystikShadows
Stéphane Richard
mystikshadows@gmail.com