Get a site

VC++ 6.0 ebook chapter index
homepage aranna.altervista.org
Free counters!

MIDI and Music

The Musical Instrument Digital Interface (MIDI) was developed in the early 1980s by a consortium of manufacturers of electronic music synthesizers. MIDI is a protocol for connecting electronic music instruments among themselves and with computers. MIDI is an extremely important standard in the field of electronic music. The MIDI specification is maintained by the MIDI Manufacturers Association (MMA), which has a Web site at http://www.midi.org.

The Workings of MIDI

MIDI defines a protocol for passing digital commands through a cable. A MIDI cable uses 5-pin DIN connectors, but only three of the connectors are used. One is a shield, another is a current loop, and the third carries the data. The MIDI protocol is unidirectional at 31,250 bits per second. Each byte of data begins with a start bit and ends with a stop bit, for an effective transfer rate of 3,125 bytes per second.

It's important to understand that no actual sounds—in either an analog or digital format—are transferred through the MIDI cable. What goes through the cable are generally simple messages, usually 1, 2, or 3 bytes in length.

A simple MIDI configuration could consist of two pieces of MIDI-compatible hardware. One is a MIDI keyboard that makes no sounds by itself but serves solely to generate MIDI messages. This keyboard has a MIDI port labeled "MIDI Out." You connect a MIDI cable from this port to the "MIDI In" port of a MIDI sound synthesizer. This synthesizer may simply look like a little box with a few buttons on the front.

When you press a key on the keyboard (let's say middle C), the keyboard sends 3 bytes to the MIDI Out port. In hexadecimal, these bytes are

90 3C 40

The first byte (90) indicates a "Note On" message. The second byte is the key number, where 3C is middle C. The third byte is the velocity with which the key is struck and may range from 1 to 127. We happen to be using a keyboard here that is not velocity-sensitive, so it sends an average velocity value. This 3-byte message goes down the MIDI cable into the Midi In port of the synthesizer. The synthesizer responds by playing a tone at middle C.

When you release the key, the keyboard sends another 3-byte message to the MIDI Out port:

90 3C 00

This is the same as the Note On command, but with a zero velocity byte. This zero byte indicates a Note Off command, meaning that the note should be turned off. The synthesizer reponds by stopping the sound.

If the synthesizer is capable of polyphony (that is, playing more than one note at the same time), then you can play chords on the keyboard. The keyboard generates multiple Note On messages, and the synthesizer plays all the notes. When you release the chord, the keyboard sends multiple Note Off messages to the synthesizer.

Generally speaking, the keyboard in this configuration is known as a "MIDI controller." It is reponsible for generating MIDI messages to control a synthesizer. A MIDI controller does not have to look like a keyboard. There are MIDI wind controllers that look like clarinets or saxophones, MIDI guitar controllers, MIDI string controllers, and MIDI drum controllers. At the very least, all of these controllers generate 3-byte Note On and Note Off messages.

Rather than something that resembles a keyboard or traditional musical instrument, a controller can also be a "sequencer." This is a piece of hardware that stores sequences of Note On and Note Off messages in memory and then plays them back. Stand-alone sequencers are used much less today than they were some years ago because they have been replaced with computers. A computer equipped with a MIDI board can also generate Note On and Note Off messages to control synthesizers. MIDI authoring software, which lets you compose on screen, can store MIDI messages coming from a MIDI controller, let you manipulate them, and then send the MIDI messages to a synthesizer.

The synthesizer is sometimes also called a "sound module" or "tone generator." MIDI does not specify how the sounds are actually generated. The synthesizer could be using any one of a variety of different sound generation techniques.

In the real world, only very simple MIDI controllers (such as wind controllers) have only MIDI Out cable ports. Often a keyboard will have a built-in synthesizer, and it will have three MIDI cable ports labeled MIDI In, MIDI Out, and MIDI Thru. The MIDI In port accepts MIDI messages to play the keyboard's internal synthesizer. The MIDI Out port sends MIDI messages from the keyboard to an external synthesizer. The MIDI Thru port is an output port that duplicates the input in the MIDI In port—whatever comes into the MIDI In port is sent back out to the MIDI Thru port. (The MIDI Thru port does not contain any of the information sent out over the MIDI Out port.)

There are only two ways to connect MIDI hardware by cables: You can connect a MIDI Out on one piece of hardware to MIDI In of another, or you can connect MIDI Thru to MIDI In. The MIDI Thru port allows for the daisy-chaining of MIDI synthesizers.

The Program Change

What kind of sound does the synthesizer make? Is it a piano sound, a violin sound, a trumpet sound, or a flying saucer sound? Generally the various sounds that a synthesizer is capable of producing are stored in ROM or somewhere else. These are generally called "voices" or "instruments" or "patches." (The word "patch" comes from the days of analog synthesizers when different sounds were configured by plugging patch chords into jacks on the front of the synthesizer.)

In MIDI, the various sounds that a synthesizer is capable of producing are known as "programs." Changing the program requires sending the synthesizer a MIDI Program Change message,

C0 pp

where pp can range from 0 to 127. Often a MIDI keyboard will have a series of numbered buttons across the top that generate Program Change messages. By pressing these you can control the synthesizer voice from the keyboard. The numbering of these buttons usually begins with 1 rather than 0, so program number 1 corresponds to a Program Change byte of 0.

The MIDI specification does not indicate what program numbers should correspond with what instruments. For example, the first three programs on a Yamaha DX7 synthesizer are called "Warm Strings," "Mellow Horn," and "Pick Guitar." On a Yamaha TX81Z tone generator, they're "Grand Piano," "Upright Piano," and "Deep Grand." On a Roland MT-32 sound module, they're "Acoustic Piano 1," "Acoustic Piano 2," and "Acoustic Piano 3." So, if you don't want to be surprised when you make a program change from a keyboard, you had better know what instrument voice corresponds to each program number in the synthesizer you happen to be using.

This can be a real problem for MIDI files that contain Program Change messages—these files are not device-independent because their contents will sound different on different synthesizers. However, in recent years, a standard known as "General MIDI" (GM) has standardized the program numbers. General MIDI is supported by Windows. If a synthesizer is not in accordance with the General MIDI specification, program mappings can make it emulate a General MIDI synthesizer.

The MIDI Channel

I've discussed two MIDI messages so far. The first is Note On,

90 kk vv

where kk is the key number (0 to 127) and vv is the velocity (0 to 127). A zero velocity indicates a Note Off command. The second is the Program Change,

C0 pp

where pp ranges from 0 to 127. These are typical of MIDI messages. The first byte is called the "status" byte. Depending on what the status byte is, it is generally followed by 0, 1, or 2 "data" bytes. (The exception is for "system exclusive" messages that I'll describe shortly.) It is easy to distinguish a status byte from a data byte: the high bit is always 1 for a status byte and 0 for a data byte.

I have not yet discussed the generalized form of these two messages, however. The generalized form of the Note On message is

9n kk vv

and the Program Change is

Cn pp

In both cases, n corresponds to the lower four bits of the status byte and can range from 0 to 15. This is called the MIDI "channel." Channels are generally numbered beginning with 1, so if n is zero, that means channel 1.

The use of 16 different channels allows a MIDI cable to carry messages for 16 different voices. Generally, you'll find that a particular string of MIDI messages will begin with Program Change messages to set a voice for the various channels being used, followed by multiple Note On and Note Off commands. Later on, there might be other Program Change commands. But at any time, each channel is associated with only one voice.

Let's take a simple example: Suppose the keyboard controller I've been describing is able to generate MIDI messages for two different channels simultaneously—channel 1 and channel 2. You might begin by pressing buttons on the keyboard to send two Program Change messages to the synthesizer:

C0 01
C1 05 

Channel 1 is now set for program 2, and channel 2 is set for program 6. (Recall that channel numbers and program numbers are 1-based but encoded in a 0-based form in the messages.) Now when you press a key on the keyboard, it sends two Note On messages, one for each channel:

90 kk vv
91 kk vv

This lets you play two instrument voices simultaneously in unison.

An alternative is a "split" keyboard. The lower keys could generate Note On messages on channel 1, and the upper keys could generate Note On messages on channel 2. This lets you play two instruments independently from one keyboard.

The use of 16 channels becomes more powerful when you think about MIDI sequencing software on a PC. Each channel corresponds to a different instrument. If you have a synthesizer that can play 16 different instruments independently, you can orchestrate a composition for a 16-piece band and connnect the MIDI board with the synthesizer using just one MIDI cable.

MIDI Messages

Although the Note On and Program Change messages are the most important messages in any MIDI implementation, this is not all that MIDI can do. Figure 22-9 is a chart of the MIDI channel messages defined in the MIDI specification. As I've noted above, the status byte always has the high bit set and all data bytes that follow the status byte have a high bit equal to 0. This means that status bytes can range from 0x80 through 0xFF, while data bytes range from 0 through 0x7F.

MIDI Message Data Bytes Values
Note Off 8n kk vv kk = key number (0-127)
vv = velocity (0-127)

Note On

9n kk vv kk = key number (0-127)
vv = velocity (1-127, 0 = note off)
Polyphonic After Touch An kk tt kk = key number (0-127)
tt = after touch (0-127)

Control Change

Bn cc xx cc = controller (0-121)
xx = value (0-127)
Channel Mode Local Control Bn 7A xx xx = 0 (off), 127 (on)
All Notes Off Bn 7B 00
Omni Mode Off Bn 7C 00
Omni Mode On Bn 7D 00
Mono Mode On Bn 7E cc cc = number of channels
Poly Mode On Bn 7F 00
Program Change Cn pp pp = program (0-127)
Channel After Touch Dn tt tt = after touch (0-127)
Pitch Wheel Change En ll hh ll = low 7 bits (0-127)
hh = high 7 bits (0-127)

Figure 22-9. The MIDI Channel Messages (n = channel number, 0 through 15)

The key numbers generally correspond to the traditional notes of Western music, although they don't have to. (For a percussion voice, each key number could be a different percussion instrument, for example.) When the key numbers correspond to a piano-type keyboard, key 60 (in decimal) is middle C. The MIDI key numbers extend 21 notes below and 19 notes above the range of a normal 88-key piano. The velocity number is the velocity with which the key is depressed, which on a piano governs both loudness and the harmonic character of the sound. A particular voice can respond to key velocity in this way or other ways.

The examples I showed earlier used a Note On message with a velocity byte of zero to indicate a Note Off command. There is also a separate Note Off command for keyboards (or other controllers) that implement a key release velocity. This is very rare, however.

There are two "after-touch" messages. After-touch is a feature of some keyboards where you can change the sound in some way by pressing harder on the key after it's already depressed. One message (status byte 0xDn) is an after-touch that applies to all the notes currently being played in a channel; this is the most common. The status byte 0xAn indicates after–touch that applies to each individual key independently.

Generally keyboards have some dials or switches for further controlling the sound. These are called "controllers," and any change is indicated by a status byte of 0xBn. Controllers are identified by numbers ranging from 0 to 121. The 0xBn status byte is also used for Channel Mode messages that indicate how a synthesizer should respond to simultaneous notes in the channel.

One very important controller is a wheel that shifts the pitch up and down. This has a separate MIDI message with a status byte of 0xEn.

Missing from the chart in Figure 22-9 are messages that begin with status bytes F0 through FF. These are called system messages because they apply to the entire MIDI system rather than a particular channel. The system messages are generally used for synchronization purposes, triggering sequencers, resetting hardware, and obtaining information.

Many MIDI controllers continually send out status bytes of 0xFE, which is called the Active Sensing message. This simply indicates that the MIDI controller is still attached to the system.

One important system message is the "system exclusive" message that begins with a status byte of 0xF0. This is used for transferring chunks of data to a synthesizer in a manufacturer-dependent and synthesizer-dependent format. (For example, new voice definitions can be passed from a computer to a synthesizer in this way.) The system exclusive message is the only message that can contain more than 2 data bytes. In fact, the number of data bytes is variable, but each data byte must have its high bit set to 0. The status byte 0xF7 indicates an end of the system exclusive message.

System exclusive messages are also used for dumping data (for example, voice definitions) from the synthesizer. The data comes out of the synthesizer through the MIDI Out port. If you're attempting to program for MIDI in a device-independent manner, you should probably avoid using system exclusive messages. But they are quite valuable for defining new synthesizer voices.

A MIDI file (with the extension .MID) is a collection of MIDI messages with timing information. You can play MIDI files using MCI. However, for the remainder of this chapter, I'll be discussing the low-level midiOut functions.

An Introduction to MIDI Sequencing

The low-level MIDI API consists of functions beginning with the prefix midiIn, for reading MIDI sequences coming from an external controller, and midiOut, for playing music on the internal or external synthesizer. Despite the term "low-level," you don't need to know anything about the hardware interface of the MIDI board when using these functions.

To open a MIDI output device in preparation for playing music, you call midiOutOpen:

error = midiOutOpen (&hMidiOut, wDeviceID, dwCallBack, 
                     dwCallBackData, dwFlags) ;
					 

The function returns 0 if successful or an error code if not. If you've specified the function arguments correctly, an error will usually indicate that the MIDI device is already in use by another program.

The first argument is a pointer to a variable of type HMIDIOUT that receives a MIDI output handle for use in subsequent MIDI output functions. The second argument is the device ID. To use one of the real MIDI devices, this argument can range from 0 to one less than the number returned from midiOutGetNumDevs. Or you can use MIDIMAPPER, which is defined in MMSYSTEM.H as –1. In most cases, you'll probably set the last three arguments of midiOutOpen to NULL or 0.

Once you open a MIDI output device and obtain the handle, you can begin sending MIDI messages to the device. You do this by calling

error = midiOutShortMsg (hMidiOut, dwMessage) ;

The first parameter is the handle obtained from midiOutOpen. The second parameter is a 1-byte, 2-byte, or 3-byte MIDI message packed into a 32-bit DWORD. As I discussed earlier, MIDI messages begin with a status byte, followed by 0, 1, or 2 bytes of data. The status byte forms the least significant byte of dwMessage, the first data byte is the next significant byte, and the second data byte is the next. The most significant byte of dwMessage is 0.

For example, to play a middle C (the note 0x3C) on MIDI channel 5 with a velocity of 0x7F, you need a 3-byte Note On message:

0x95 0x3C 0x7F

The dwMessage parameter to midiOutShortMsg is 0x007F3C95.

The three essential MIDI messages are Program Change (to change the instrument voice for a particular channel), Note On, and Note Off. After opening a MIDI output device, you should always begin with a Program Change message and you should send an equal number of Note On and Note Off messages.

When you're all done playing the music you want to play, you can reset the MIDI output device to make sure that all notes are turned off:

midiOutReset (hMidiOut) ;

You can then close the device:

midiOutClose (hMidiOut) ;

The midiOutOpen, midiOutShortMsg, midiOutReset, and midiOutClose functions are the four essential functions you need for using the low-level MIDI output API.

So, let's play some music! The BACHTOCC program shown in Figure 22-10 plays the first measure of the toccata section of J. S. Bach's famous Toccata and Fugue in D Minor for organ.

Figure 22-10. The BACHTOCC Program.

BACHTOCC.C

/*---------------------------------------------------
   BACHTOCC.C -- Bach Toccata in D Minor (First Bar)
                 (c) Charles Petzold, 1998
  ---------------------------------------------------*/

#include <windows.h>

#define ID_TIMER    1

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;

TCHAR szAppName[] = TEXT ("BachTocc") ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;

     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;

     if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"),
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     }
     
     hwnd = CreateWindow (szAppName, 
                          TEXT ("Bach Toccata in D Minor (First Bar)"),
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     if (!hwnd)
          return 0 ;
     
     ShowWindow (hwnd, iCmdShow) ;
     UpdateWindow (hwnd) ;
     
     while (GetMessage (&msg, NULL, 0, 0))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return msg.wParam ;
}

DWORD MidiOutMessage (HMIDIOUT hMidi, int iStatus, int iChannel,
                                      int iData1,  int iData2)
{
     DWORD dwMessage = iStatus | iChannel | (iData1 << 8) | (iData2 << 16) ;
     
     return midiOutShortMsg (hMidi, dwMessage) ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static struct
     {
          int iDur ;
          int iNote [2] ;
     }
     noteseq [] = { 110, 69, 81,  110, 67, 79,  990, 69, 81,  220, -1, -1,
                    110, 67, 79,  110, 65, 77,  110, 64, 76,  110, 62, 74,
                    220, 61, 73,  440, 62, 74, 1980, -1, -1,  110, 57, 69,
                    110, 55, 67,  990, 57, 69,  220, -1, -1,  220, 52, 64,
                    220, 53, 65,  220, 49, 61,  440, 50, 62, 1980, -1, -1 } ;
     
     static HMIDIOUT hMidiOut ;
     static int      iIndex ;
     int             i ;
     
     switch (message)
     {
     case WM_CREATE:
               // Open MIDIMAPPER device
          
          if (midiOutOpen (&hMidiOut, MIDIMAPPER, 0, 0, 0))
          {
               MessageBeep (MB_ICONEXCLAMATION) ;
               MessageBox (hwnd, TEXT ("Cannot open MIDI output device!"),
                                 szAppName, MB_ICONEXCLAMATION | MB_OK) ;
               return -1 ;
          }
               // Send Program Change messages for "Church Organ"
          
          MidiOutMessage (hMidiOut, 0xC0,  0, 19, 0) ;
          MidiOutMessage (hMidiOut, 0xC0, 12, 19, 0) ;

          SetTimer (hwnd, ID_TIMER, 1000, NULL) ;
          return 0 ;
          
     case WM_TIMER:
              // Loop for 2-note polyphony
          
          for (i = 0 ; i < 2 ; i++)
          {
                    // Note Off messages for previous note
               
               if (iIndex != 0 && noteseq[iIndex - 1].iNote[i] != -1)
               {
                    MidiOutMessage (hMidiOut, 0x80,  0,
                                    noteseq[iIndex - 1].iNote[i], 0) ;
                    
                    MidiOutMessage (hMidiOut, 0x80, 12,
                                    noteseq[iIndex - 1].iNote[i], 0) ;
               }
                    // Note On messages for new note
               
               if (iIndex != sizeof (noteseq) / sizeof (noteseq[0]) &&
                    noteseq[iIndex].iNote[i] != -1)
               {
                    MidiOutMessage (hMidiOut, 0x90,  0,
                                    noteseq[iIndex].iNote[i], 127) ;
                    
                    MidiOutMessage (hMidiOut, 0x90, 12,
                                    noteseq[iIndex].iNote[i], 127) ;
               }
          }
          
          if (iIndex != sizeof (noteseq) / sizeof (noteseq[0]))
          {
               SetTimer (hwnd, ID_TIMER, noteseq[iIndex++].iDur - 1, NULL) ;
          }
          else
          {
               KillTimer (hwnd, ID_TIMER) ;
               DestroyWindow (hwnd) ;
          }
          return 0 ;
          
     case WM_DESTROY:
          midiOutReset (hMidiOut) ;
          midiOutClose (hMidiOut) ;
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

The first measure of the Bach D Minor Toccata is shown in Figure 22-11.

Click to view at full size.

Figure 22-11. The firstt measure of Bach's Toccata and Fugue in D Minor.

Our job here is to translate this music into a series of numbers—basically key numbers and timing information that indicate when to send Note On messages (equivalent to an organ key being depressed) and Note Off messages (a key release). Because an organ keyboard is not velocity-sensitive, we can play all the notes using the same velocities. Another simplification is to ignore the difference between staccato playing (that is, leaving a slight pause between successive notes for a sharper, crisper effect) and legato playing (a smoother overlapping blend between successive notes). We'll assume that the ending of one note is followed immediately by the beginning of the next note.

If you can read music, you'll note that the opening of the toccata consists of parallel octaves. So I created a data structure in BACHTOCC called noteseq to store a series of note durations and two key numbers. Unfortunately, continuing the music into the second measure would require a more generalized approach to storing this information. I decided that a quarter note should have a duration of 1760 milliseconds, which means that an eighth note (which has one stem on the note or rest) has a duration of 880 milliseconds, a 16th note (two stems) of 440, a 32nd note (three stems) of 220, and a 64th note (four stems) of 110.

There are two mordents in this first measure—one over the first note and the other halfway through the measure. These are indicated by squiggly lines with a short vertical line. In baroque music, the mordent sign means that the note should actually be played as three notes—the indicated note, a note a full tone below it, and then the indicated note. The first two notes should be played quickly, and the third held for the remaining duration. For example, the first note is an A with a mordent. This is played as A, G, A. I decided to make the first two notes of the mordent 64th notes; thus, each has a duration of 110 milliseconds.

There are also four fermatas in this first measure. These are indicated by semicircles with dots in the middle. The fermata sign means that the note should be held longer than its notated duration, generally at the player's discretion. For the fermatas, I decided to increase the note durations by 50 percent.

As you can see, translating even a piece of music seemingly as simple and straightforward as the opening of the D Minor Toccata is not always so simple and straightforward!

The noteseq structure array contains three numbers for every parallel note and rest in the measure. The duration of the note is followed by two MIDI key numbers for the parallel octaves. For example, the first note is an A with a duration of 110 milliseconds. Because middle C has a MIDI key number of 60, the A above middle C has a key number of 69 and the A an octave higher has a key number of 81. Thus, the first three values in the noteseq array are 110, 69, and 81. I've used note values of –1 to indicate a rest.

During the WM_CREATE message, BACHTOCC sets a Windows timer for 1000 milliseconds—meaning that the music will begin in 1 second—and then calls midiOutOpen using the MIDIMAPPER device ID.

BACHTOCC requires only one instrument voice (an organ), so it needs to use only one channel. To simplify the sending of MIDI messages, I've defined a short function in BACHTOCC called MidiOutMessage. This function accepts a MIDI output handle, a status byte, a channel number, and two bytes of data. It assembles these numbers into a packed 32-bit message and calls midiOutShortMsg.

At the end of WM_CREATE processing, BACHTOCC sends a Program Change message to select the "church organ" voice. In the General MIDI voice assignments, the church organ voice is indicated by a data byte of 19 in the Program Change message. The actual playing of notes occurs during the WM_TIMER message. A loop handles the two-note polyphony. If a previous note is still playing, BACHTOCC sends Note Off messages for that note. Then, if the new note is not a rest, it sends Note On messages to channels 0 and 12. It then resets the Windows timer to the duration of the note indicated in the noteseq structure.

After the music concludes, BACHTOCC destroys the window. During the WM_DESTROY message, the program calls midiOutReset and midiOutClose and then terminates the program.

Although BACHTOCC works and the results sound reasonable (if not exactly like a human being playing an organ), using the Windows timer for playing music in this way simply does not work in the general case. The problem is that the Windows timer is based on the PC's system clock and the resolution is not good enough for music. Moreover, the Windows timer is not asynchronous. There can be slight delays getting WM_TIMER messages if another program is busy doing something. WM_TIMER messages could even be discarded if the program cannot handle them immediately. This would start sounding like a real mess.

So, while BACHTOCC shows how to call the low-level MIDI output functions, the use of the Windows timer is clearly inadequate for accurate music reproduction. This is why Windows also includes a supplementary set of timer functions that you can take advantage of when using the low-level MIDI output functions. These functions begin with the prefix time, and you can use them to set a timer with a resolution as low as 1 millisecond. I'll show you how to use these functions in the DRUM program at the end of this chapter.

Playing a MIDI Synthesizer from the PC Keyboard

Since most PC users probably don't have a MIDI keyboard they can attach to their machines, it makes sense to substitute the keyboard everyone does have (the one with all the letters and numbers on the keys) for a musical one. Figure 22-12 shows a program called KBMIDI that lets you use the PC keyboard to play an electronic music synthesizer—either the one on your sound board or an external synthesizer hooked up to the MIDI Out port. KBMIDI gives you complete control over the MIDI output device (that is, the internal or external synthesizer), the MIDI channel, and the instrument voice. Besides being fun to use, I've found the program useful for exploring how Windows implements MIDI support.

Figure 22-12. The KBMIDI Program.

KBMIDI.C

/*---------------------------------------
   KBMIDI.C -- Keyboard MIDI Player 
               (c) Charles Petzold, 1998
  ---------------------------------------*/

#include <windows.h>

// Defines for Menu IDs
// --------------------

#define IDM_OPEN    0x100
#define IDM_CLOSE   0x101
#define IDM_DEVICE  0x200
#define IDM_CHANNEL 0x300
#define IDM_VOICE   0x400

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM);

TCHAR    szAppName [] = TEXT ("KBMidi") ;
HMIDIOUT hMidiOut ;
int      iDevice = MIDIMAPPER, iChannel = 0, iVoice = 0, iVelocity = 64 ;
int      cxCaps, cyChar, xOffset, yOffset ;

     // Structures and data for showing families and instruments on menu
     // ----------------------------------------------------------------

typedef struct
{
     TCHAR * szInst ;
     int    iVoice ;
}
INSTRUMENT ;

typedef struct
{
     TCHAR      * szFam ;
     INSTRUMENT   inst [8] ;
}
FAMILY ;

FAMILY fam [16] = { 
     
     TEXT ("Piano"),

          TEXT ("Acoustic Grand Piano"),        0,
          TEXT ("Bright Acoustic Piano"),       1,
          TEXT ("Electric Grand Piano"),        2,
          TEXT ("Honky-tonk Piano"),            3,
          TEXT ("Rhodes Piano"),                4,
          TEXT ("Chorused Piano"),              5,
          TEXT ("Harpsichord"),                 6,
          TEXT ("Clavinet"),                    7,

     TEXT ("Chromatic Percussion"),

          TEXT ("Celesta"),                     8,
          TEXT ("Glockenspiel"),                9,
          TEXT ("Music Box"),                   10,
          TEXT ("Vibraphone"),                  11,
          TEXT ("Marimba"),                     12,
          TEXT ("Xylophone"),                   13,
          TEXT ("Tubular Bells"),               14,
          TEXT ("Dulcimer"),                    15,

     TEXT ("Organ"),

          TEXT ("Hammond Organ"),               16,
          TEXT ("Percussive Organ"),            17,
          TEXT ("Rock Organ"),                  18,
          TEXT ("Church Organ"),                19,
          TEXT ("Reed Organ"),                  20,
          TEXT ("Accordian"),                   21,
          TEXT ("Harmonica"),                   22,
          TEXT ("Tango Accordian"),             23,

     TEXT ("Guitar"),

          TEXT ("Acoustic Guitar (nylon)"),     24,
          TEXT ("Acoustic Guitar (steel)"),     25,
          TEXT ("Electric Guitar (jazz)"),      26,
          TEXT ("Electric Guitar (clean)"),     27,
          TEXT ("Electric Guitar (muted)"),     28,
          TEXT ("Overdriven Guitar"),           29,
          TEXT ("Distortion Guitar"),           30,
          TEXT ("Guitar Harmonics"),            31,

     TEXT ("Bass"),

          TEXT ("Acoustic Bass"),               32,
          TEXT ("Electric Bass (finger)"),      33,
          TEXT ("Electric Bass (pick)"),        34,
          TEXT ("Fretless Bass"),               35,
          TEXT ("Slap Bass 1"),                 36,
          TEXT ("Slap Bass 2"),                 37,
          TEXT ("Synth Bass 1"),                38,
          TEXT ("Synth Bass 2"),                39,

     TEXT ("Strings"),

          TEXT ("Violin"),                      40,
          TEXT ("Viola"),                       41,
          TEXT ("Cello"),                       42,
          TEXT ("Contrabass"),                  43,
          TEXT ("Tremolo Strings"),             44,
          TEXT ("Pizzicato Strings"),           45,
          TEXT ("Orchestral Harp"),             46,
          TEXT ("Timpani"),                     47,

     TEXT ("Ensemble"),

          TEXT ("String Ensemble 1"),           48,
          TEXT ("String Ensemble 2"),           49,
          TEXT ("Synth Strings 1"),             50,
          TEXT ("Synth Strings 2"),             51,
          TEXT ("Choir Aahs"),                  52,
          TEXT ("Voice Oohs"),                  53,
          TEXT ("Synth Voice"),                 54,
          TEXT ("Orchestra Hit"),               55,

     TEXT ("Brass"),

          TEXT ("Trumpet"),                     56,
          TEXT ("Trombone"),                    57,
          TEXT ("Tuba"),                        58,
          TEXT ("Muted Trumpet"),               59,
          TEXT ("French Horn"),                 60,
          TEXT ("Brass Section"),               61,
          TEXT ("Synth Brass 1"),               62,
          TEXT ("Synth Brass 2"),               63,

     TEXT ("Reed"),

          TEXT ("Soprano Sax"),                 64,
          TEXT ("Alto Sax"),                    65,
          TEXT ("Tenor Sax"),                   66,
          TEXT ("Baritone Sax"),                67,
          TEXT ("Oboe"),                        68,
          TEXT ("English Horn"),                69,
          TEXT ("Bassoon"),                     70,
          TEXT ("Clarinet"),                    71,

     TEXT ("Pipe"),

          TEXT ("Piccolo"),                     72,
          TEXT ("Flute "),                      73,
          TEXT ("Recorder"),                    74,
          TEXT ("Pan Flute"),                   75,
          TEXT ("Bottle Blow"),                 76,
          TEXT ("Shakuhachi"),                  77,
          TEXT ("Whistle"),                     78,
          TEXT ("Ocarina"),                     79,

     TEXT ("Synth Lead"),

          TEXT ("Lead 1 (square)"),             80,
          TEXT ("Lead 2 (sawtooth)"),           81,
          TEXT ("Lead 3 (caliope lead)"),       82,
          TEXT ("Lead 4 (chiff lead)"),         83,
          TEXT ("Lead 5 (charang)"),            84,
          TEXT ("Lead 6 (voice)"),              85,
          TEXT ("Lead 7 (fifths)"),             86,
          TEXT ("Lead 8 (brass + lead)"),       87,

     TEXT ("Synth Pad"),

          TEXT ("Pad 1 (new age)"),             88,
          TEXT ("Pad 2 (warm)"),                89,
          TEXT ("Pad 3 (polysynth)"),           90,
          TEXT ("Pad 4 (choir)"),               91,
          TEXT ("Pad 5 (bowed)"),               92,
          TEXT ("Pad 6 (metallic)"),            93,
          TEXT ("Pad 7 (halo)"),                94,
          TEXT ("Pad 8 (sweep)"),               95,

     TEXT ("Synth Effects"),

          TEXT ("FX 1 (rain)"),                 96,
          TEXT ("FX 2 (soundtrack)"),           97,
          TEXT ("FX 3 (crystal)"),              98,
          TEXT ("FX 4 (atmosphere)"),           99,
          TEXT ("FX 5 (brightness)"),           100,
          TEXT ("FX 6 (goblins)"),              101,
          TEXT ("FX 7 (echoes)"),               102,
          TEXT ("FX 8 (sci-fi)"),               103,
          TEXT ("Ethnic"),
          TEXT ("Sitar"),                       104,
          TEXT ("Banjo"),                       105,
          TEXT ("Shamisen"),                    106,
          TEXT ("Koto"),                        107,
          TEXT ("Kalimba"),                     108,
          TEXT ("Bagpipe"),                     109,
          TEXT ("Fiddle"),                      110,
          TEXT ("Shanai"),                      111,

     TEXT ("Percussive"),

          TEXT ("Tinkle Bell"),                 112,
          TEXT ("Agogo"),                       113,
          TEXT ("Steel Drums"),                 114,
          TEXT ("Woodblock"),                   115,
          TEXT ("Taiko Drum"),                  116,
          TEXT ("Melodic Tom"),                 117,
          TEXT ("Synth Drum"),                  118,
          TEXT ("Reverse Cymbal"),              119,

     TEXT ("Sound Effects"),

          TEXT ("Guitar Fret Noise"),           120,
          TEXT ("Breath Noise"),                121,
          TEXT ("Seashore"),                    122,
          TEXT ("Bird Tweet"),                  123,
          TEXT ("Telephone Ring"),              124,
          TEXT ("Helicopter"),                  125,
          TEXT ("Applause"),                    126,
          TEXT ("Gunshot"),                     127 } ;

     // Data for translating scan codes to octaves and notes
     // ----------------------------------------------------

#define NUMSCANS    (sizeof key / sizeof key[0])

struct
{
     int     iOctave ;
     int     iNote ;
     int     yPos ;
     int     xPos ;
     TCHAR * szKey ;
}
key [] =
{
                                         // Scan  Char  Oct  Note
                                         // ----  ----  ---  ----
         -1, -1, -1, -1, NULL,           //   0   None
         -1, -1, -1, -1, NULL,           //   1   Esc
         -1, -1,  0,  0, TEXT (""),      //   2     1
          5,  1,  0,  2, TEXT ("C#"),    //   3     2    5    C#
          5,  3,  0,  4, TEXT ("D#"),    //   4     3    5    D#
         -1, -1,  0,  6, TEXT (""),      //   5     4
          5,  6,  0,  8, TEXT ("F#"),    //   6     5    5    F#
          5,  8,  0, 10, TEXT ("G#"),    //   7     6    5    G#
          5, 10,  0, 12, TEXT ("A#"),    //   8     7    5    A#
         -1, -1,  0, 14, TEXT (""),      //   9     8
          6,  1,  0, 16, TEXT ("C#"),    //  10     9    6    C#
          6,  3,  0, 18, TEXT ("D#"),    //  11     0    6    D#
         -1, -1,  0, 20, TEXT (""),      //  12     -
          6,  6,  0, 22, TEXT ("F#"),    //  13     =    6    F#
         -1, -1, -1, -1, NULL,           //  14    Back
          
         -1, -1, -1, -1, NULL,           //  15    Tab
          5,  0,  1,  1, TEXT ("C"),     //  16     q    5    C
          5,  2,  1,  3, TEXT ("D"),     //  17     w    5    D
          5,  4,  1,  5, TEXT ("E"),     //  18     e    5    E
          5,  5,  1,  7, TEXT ("F"),     //  19     r    5    F
          5,  7,  1,  9, TEXT ("G"),     //  20     t    5    G
          5,  9,  1, 11, TEXT ("A"),     //  21     y    5    A
          5, 11,  1, 13, TEXT ("B"),     //  22     u    5    B
          6,  0,  1, 15, TEXT ("C"),     //  23     i    6    C
          6,  2,  1, 17, TEXT ("D"),     //  24     o    6    D
          6,  4,  1, 19, TEXT ("E"),     //  25     p    6    E
          6,  5,  1, 21, TEXT ("F"),     //  26     [    6    F
          6,  7,  1, 23, TEXT ("G"),     //  27     ]    6    G
         -1, -1, -1, -1, NULL,           //  28    Ent
          
         -1, -1, -1, -1, NULL,           //  29    Ctrl
          3,  8,  2,  2, TEXT ("G#"),    //  30     a    3    G#
          3, 10,  2,  4, TEXT ("A#"),    //  31     s    3    A#
         -1, -1,  2,  6, TEXT (""),      //  32     d
          4,  1,  2,  8, TEXT ("C#"),    //  33     f    4    C#
          4,  3,  2, 10, TEXT ("D#"),    //  34     g    4    D#
         -1, -1,  2, 12, TEXT (""),      //  35     h
          4,  6,  2, 14, TEXT ("F#"),    //  36     j    4    F#
          4,  8,  2, 16, TEXT ("G#"),    //  37     k    4    G#
          4, 10,  2, 18, TEXT ("A#"),    //  38     l    4    A#
         -1, -1,  2, 20, TEXT (""),      //  39     ;
          5,  1,  2, 22, TEXT ("C#"),    //  40     `    5    C#
         -1, -1, -1, -1, NULL,           //  41     `
         -1, -1, -1, -1, NULL,           //  42    Shift
         -1, -1, -1, -1, NULL,           //  43     \  (not line continuation)
          3,  9,  3,  3, TEXT ("A"),     //  44     z    3    A
          3, 11,  3,  5, TEXT ("B"),     //  45     x    3    B
          4,  0,  3,  7, TEXT ("C"),     //  46     c    4    C
          4,  2,  3,  9, TEXT ("D"),     //  47     v    4    D
          4,  4,  3, 11, TEXT ("E"),     //  48     b    4    E
          4,  5,  3, 13, TEXT ("F"),     //  49     n    4    F
          4,  7,  3, 15, TEXT ("G"),     //  50     m    4    G
          4,  9,  3, 17, TEXT ("A"),     //  51     ,    4    A
          4, 11,  3, 19, TEXT ("B"),     //  52     .    4    B
          5,  0,  3, 21, TEXT ("C")      //  53     /    5    C
} ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     MSG      msg;
     HWND     hwnd ;
     WNDCLASS wndclass ;
     
     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;
     
     if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"),
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     }
     
     hwnd = CreateWindow (szAppName, TEXT ("Keyboard MIDI Player"),
                          WS_OVERLAPPEDWINDOW | WS_HSCROLL | WS_VSCROLL,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     if (!hwnd)
          return 0 ;
     
     ShowWindow (hwnd, iCmdShow) ;
     UpdateWindow (hwnd); 
     
     while (GetMessage (&msg, NULL, 0, 0))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return msg.wParam ;
}

// Create the program's menu (called from WndProc, WM_CREATE)
// ----------------------------------------------------------

HMENU CreateTheMenu (int iNumDevs)
{
     TCHAR       szBuffer [32] ;
     HMENU       hMenu, hMenuPopup, hMenuSubPopup ;
     int         i, iFam, iIns ;
     MIDIOUTCAPS moc ;
     
     hMenu = CreateMenu () ;
     
          // Create "On/Off" popup menu
     
     hMenuPopup = CreateMenu () ;
     
     AppendMenu (hMenuPopup, MF_STRING, IDM_OPEN, TEXT ("&Open")) ; 
     AppendMenu (hMenuPopup, MF_STRING | MF_CHECKED, IDM_CLOSE, 
                             TEXT ("&Closed")) ;
     
     AppendMenu (hMenu, MF_STRING | MF_POPUP, (UINT) hMenuPopup, 
                        TEXT ("&Status")) ;
     
          // Create "Device" popup menu
     
     hMenuPopup = CreateMenu () ;
     
          // Put MIDI Mapper on menu if it's installed
     
     if (!midiOutGetDevCaps (MIDIMAPPER, &moc, sizeof (moc)))
          AppendMenu (hMenuPopup, MF_STRING, IDM_DEVICE + (int) MIDIMAPPER,
                         moc.szPname) ;
     else
          iDevice = 0 ;
     
          // Add the rest of the MIDI devices
     for (i = 0 ; i < iNumDevs ; i++)
     {
          midiOutGetDevCaps (i, &moc, sizeof (moc)) ;
          AppendMenu (hMenuPopup, MF_STRING, IDM_DEVICE + i, moc.szPname) ;
     }
     
     CheckMenuItem (hMenuPopup, 0, MF_BYPOSITION | MF_CHECKED) ;
     AppendMenu (hMenu, MF_STRING | MF_POPUP, (UINT) hMenuPopup, 
                        TEXT ("&Device")) ;
     
          // Create "Channel" popup menu
     
     hMenuPopup = CreateMenu () ;
     
     for (i = 0 ; i < 16 ; i++)
     {
          wsprintf (szBuffer, TEXT ("%d"), i + 1) ;
          AppendMenu (hMenuPopup, MF_STRING | (i ? MF_UNCHECKED : MF_CHECKED),
                                  IDM_CHANNEL + i, szBuffer) ;
     }
     
     AppendMenu (hMenu, MF_STRING | MF_POPUP, (UINT) hMenuPopup, 
                        TEXT ("&Channel")) ;
     
          // Create "Voice" popup menu
     
     hMenuPopup = CreateMenu () ;
     
     for (iFam = 0 ; iFam < 16 ; iFam++)
     {
          hMenuSubPopup = CreateMenu () ;
          
          for (iIns = 0 ; iIns < 8 ; iIns++)
          {
               wsprintf (szBuffer, TEXT ("&%d.\t%s"), iIns + 1,
                                   fam[iFam].inst[iIns].szInst) ;
               AppendMenu (hMenuSubPopup,
                           MF_STRING | (fam[iFam].inst[iIns].iVoice ?
                                             MF_UNCHECKED : MF_CHECKED),
                           fam[iFam].inst[iIns].iVoice + IDM_VOICE,
                           szBuffer) ;
          }
          
          wsprintf (szBuffer, TEXT ("&%c.\t%s"), `A' + iFam,
                              fam[iFam].szFam) ;
          AppendMenu (hMenuPopup, MF_STRING | MF_POPUP, (UINT) hMenuSubPopup,
                                  szBuffer) ;
     }
     AppendMenu (hMenu, MF_STRING | MF_POPUP, (UINT) hMenuPopup, 
                        TEXT ("&Voice")) ;
     return hMenu ;
}

// Routines for simplifying MIDI output
// ------------------------------------

DWORD MidiOutMessage (HMIDIOUT hMidi, int iStatus, int iChannel,
                      int iData1,  int iData2)
{
     DWORD dwMessage ;
     
     dwMessage = iStatus | iChannel | (iData1 << 8) | (iData2 << 16) ;
     
     return midiOutShortMsg (hMidi, dwMessage) ;
}

DWORD MidiNoteOff (HMIDIOUT hMidi, int iChannel, int iOct, int iNote, int iVel)
{
     return MidiOutMessage (hMidi, 0x080, iChannel, 12 * iOct + iNote, iVel) ;
}

DWORD MidiNoteOn (HMIDIOUT hMidi, int iChannel, int iOct, int iNote, int iVel)
{
     return MidiOutMessage (hMidi, 0x090, iChannel, 12 * iOct + iNote, iVel) ;
}

DWORD MidiSetPatch (HMIDIOUT hMidi, int iChannel, int iVoice)
{
     return MidiOutMessage (hMidi, 0x0C0, iChannel, iVoice, 0) ;
}

DWORD MidiPitchBend (HMIDIOUT hMidi, int iChannel, int iBend)
{
     return MidiOutMessage (hMidi, 0x0E0, iChannel, iBend & 0x7F, iBend >> 7) ;
}

// Draw a single key on window
// ---------------------------

VOID DrawKey (HDC hdc, int iScanCode, BOOL fInvert)
{
     RECT rc ;
     rc.left   = 3 * cxCaps * key[iScanCode].xPos / 2 + xOffset ;
     rc.top    = 3 * cyChar * key[iScanCode].yPos / 2 + yOffset ;
     rc.right  = rc.left + 3 * cxCaps ;
     rc.bottom = rc.top  + 3 * cyChar / 2 ;
     
     SetTextColor (hdc, fInvert ? 0x00FFFFFFul : 0x00000000ul) ;
     SetBkColor   (hdc, fInvert ? 0x00000000ul : 0x00FFFFFFul) ;
     
     FillRect (hdc, &rc, GetStockObject (fInvert ? BLACK_BRUSH : WHITE_BRUSH)) ;
     
     DrawText (hdc, key[iScanCode].szKey, -1, &rc,
                    DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
     
     FrameRect (hdc, &rc, GetStockObject (BLACK_BRUSH)) ;
}

// Process a Key Up or Key Down message
// ------------------------------------

VOID ProcessKey (HDC hdc, UINT message, LPARAM lParam)
{
     int iScanCode, iOctave, iNote ;
     
     iScanCode = 0x0FF & HIWORD (lParam) ;
     
     if (iScanCode >= NUMSCANS)                       // No scan codes over 53
          return ;
     
     if ((iOctave = key[iScanCode].iOctave) == -1)    // Non-music key
          return ;
     
     if (GetKeyState (VK_SHIFT) < 0)
          iOctave += 0x20000000 & lParam ? 2 : 1 ;
     
     if (GetKeyState (VK_CONTROL) < 0)
          iOctave -= 0x20000000 & lParam ? 2 : 1 ;
     
     iNote = key[iScanCode].iNote ;
     
     if (message == WM_KEYUP)                           // For key up
     {
          MidiNoteOff (hMidiOut, iChannel, iOctave, iNote, 0) ;   // Note off
          DrawKey (hdc, iScanCode, FALSE) ;
          return ;
     }
     
     if (0x40000000 & lParam)                          // ignore typematics
          return ;
     
     MidiNoteOn (hMidiOut, iChannel, iOctave, iNote, iVelocity) ; // Note on
     DrawKey (hdc, iScanCode, TRUE) ;                 // Draw the inverted key
}

// Window Procedure
// ----------------

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static BOOL bOpened = FALSE ;
     HDC         hdc ;
     HMENU       hMenu ;
     int         i, iNumDevs, iPitchBend, cxClient, cyClient ;
     MIDIOUTCAPS moc ;
     PAINTSTRUCT ps ;
     SIZE        size ;
     TCHAR       szBuffer [16] ;
     
     switch (message)
     {
     case WM_CREATE:
               // Get size of capital letters in system font
          
          hdc = GetDC (hwnd) ;
          
          GetTextExtentPoint (hdc, TEXT ("M"), 1, &size) ;
          cxCaps = size.cx ;
          cyChar = size.cy ;
          
          ReleaseDC (hwnd, hdc) ;
          
               // Initialize "Volume" scroll bar
          
          SetScrollRange (hwnd, SB_HORZ, 1, 127, FALSE) ;
          SetScrollPos   (hwnd, SB_HORZ, iVelocity, TRUE) ;
          
               // Initialize "Pitch Bend" scroll bar
          
          SetScrollRange (hwnd, SB_VERT, 0, 16383, FALSE) ;
          SetScrollPos   (hwnd, SB_VERT, 8192, TRUE) ;
          
               // Get number of MIDI output devices and set up menu
          
          if (0 == (iNumDevs = midiOutGetNumDevs ()))
          {
               MessageBeep (MB_ICONSTOP) ;
               MessageBox (hwnd, TEXT ("No MIDI output devices!"),
                                 szAppName, MB_OK | MB_ICONSTOP) ;
               return -1 ;
          }
          SetMenu (hwnd, CreateTheMenu (iNumDevs)) ;
          return 0 ;
          
     case WM_SIZE:
          cxClient = LOWORD (lParam) ;
          cyClient = HIWORD (lParam) ;
          
          xOffset = (cxClient - 25 * 3 * cxCaps / 2) / 2 ;
          yOffset = (cyClient - 11 * cyChar) / 2 + 5 * cyChar ;
          return 0 ;
          
     case WM_COMMAND:
          hMenu = GetMenu (hwnd) ;
          
              // "Open" menu command
          
          if (LOWORD (wParam) == IDM_OPEN && !bOpened)
          {
               if (midiOutOpen (&hMidiOut, iDevice, 0, 0, 0))
               {
                    MessageBeep (MB_ICONEXCLAMATION) ;
                    MessageBox (hwnd, TEXT ("Cannot open MIDI device"),
                                szAppName, MB_OK | MB_ICONEXCLAMATION) ;
               }
               else
               {
                    CheckMenuItem (hMenu, IDM_OPEN,  MF_CHECKED) ;
                    CheckMenuItem (hMenu, IDM_CLOSE, MF_UNCHECKED) ;
                    
                    MidiSetPatch (hMidiOut, iChannel, iVoice) ;
                    bOpened = TRUE ;
               }
          }
          
               // "Close" menu command
          
          else if (LOWORD (wParam) == IDM_CLOSE && bOpened)
          {
               CheckMenuItem (hMenu, IDM_OPEN,  MF_UNCHECKED) ;
               CheckMenuItem (hMenu, IDM_CLOSE, MF_CHECKED) ;
               
                    // Turn all keys off and close device
               
               for (i = 0 ; i < 16 ; i++)
                    MidiOutMessage (hMidiOut, 0xB0, i, 123, 0) ;
               
               midiOutClose (hMidiOut) ;
               bOpened = FALSE ;
          }
          
               // Change MIDI "Device" menu command
          
          else if (LOWORD (wParam) >= IDM_DEVICE - 1 && 
                   LOWORD (wParam) <  IDM_CHANNEL)
          {
               CheckMenuItem (hMenu, IDM_DEVICE + iDevice, MF_UNCHECKED) ;
               iDevice = LOWORD (wParam) - IDM_DEVICE ;
               CheckMenuItem (hMenu, IDM_DEVICE + iDevice, MF_CHECKED) ;
               
                    // Close and reopen MIDI device
               
               if (bOpened)
               {
                    SendMessage (hwnd, WM_COMMAND, IDM_CLOSE, 0L) ;
                    SendMessage (hwnd, WM_COMMAND, IDM_OPEN,  0L) ;
               }
          }
          
               // Change MIDI "Channel" menu command
          
          else if (LOWORD (wParam) >= IDM_CHANNEL && 
                   LOWORD (wParam) <  IDM_VOICE)
          {
               CheckMenuItem (hMenu, IDM_CHANNEL + iChannel, MF_UNCHECKED);
               iChannel = LOWORD (wParam) - IDM_CHANNEL ;
               CheckMenuItem (hMenu, IDM_CHANNEL + iChannel, MF_CHECKED) ;
               
               if (bOpened)
                    MidiSetPatch (hMidiOut, iChannel, iVoice) ;
          }
          
               // Change MIDI "Voice" menu command
          
          else if (LOWORD (wParam) >= IDM_VOICE)
          {
               CheckMenuItem (hMenu, IDM_VOICE + iVoice, MF_UNCHECKED) ;
               iVoice = LOWORD (wParam) - IDM_VOICE ;
               CheckMenuItem (hMenu, IDM_VOICE + iVoice, MF_CHECKED) ;
               
               if (bOpened)
                    MidiSetPatch (hMidiOut, iChannel, iVoice) ;
          }
          
          InvalidateRect (hwnd, NULL, TRUE) ;
          return 0 ;
          
          // Process a Key Up or Key Down message
          
     case WM_KEYUP:
     case WM_KEYDOWN:
          hdc = GetDC (hwnd) ;
          
          if (bOpened)
               ProcessKey (hdc, message, lParam) ;
          
          ReleaseDC (hwnd, hdc) ;
          return 0 ;
          
          // For Escape, turn off all notes and repaint
          
     case WM_CHAR:
          if (bOpened && wParam == 27)
          {
               for (i = 0 ; i < 16 ; i++)
                    MidiOutMessage (hMidiOut, 0xB0, i, 123, 0) ;
               
               InvalidateRect (hwnd, NULL, TRUE) ;
          }
          return 0 ;
          
          // Horizontal scroll: Velocity
          
     case WM_HSCROLL:
          switch (LOWORD (wParam))
          {
          case SB_LINEUP:         iVelocity -= 1 ;  break ;
          case SB_LINEDOWN:       iVelocity += 1 ;  break ;
          case SB_PAGEUP:         iVelocity -= 8 ;  break ;
          case SB_PAGEDOWN:       iVelocity += 8 ;  break ;
          case SB_THUMBPOSITION:  iVelocity = HIWORD (wParam) ;  break ;
          default:                return 0 ;
          }
          iVelocity = max (1, min (iVelocity, 127)) ;
          SetScrollPos (hwnd, SB_HORZ, iVelocity, TRUE) ;
          return 0 ;
          
          // Vertical scroll:  Pitch Bend
     
     case WM_VSCROLL:
          switch (LOWORD (wParam))
          {
          case SB_THUMBTRACK:    iPitchBend = 16383 - HIWORD (wParam) ;  break ;
          case SB_THUMBPOSITION: iPitchBend = 8191 ;                     break ;
          default:               return 0 ;
          }
          iPitchBend = max (0, min (iPitchBend, 16383)) ;
          SetScrollPos (hwnd, SB_VERT, 16383 - iPitchBend, TRUE) ;
          
          if (bOpened)
               MidiPitchBend (hMidiOut, iChannel, iPitchBend) ;
          return 0 ;
     
     case WM_PAINT:
          hdc = BeginPaint (hwnd, &ps) ;
          
          for (i = 0 ; i < NUMSCANS ; i++)
               if (key[i].xPos != -1)
                    DrawKey (hdc, i, FALSE) ;
               
          midiOutGetDevCaps (iDevice, &moc, sizeof (MIDIOUTCAPS)) ;
          wsprintf (szBuffer, TEXT ("Channel %i"), iChannel + 1) ;
     
          TextOut (hdc, cxCaps, 1 * cyChar, 
                        bOpened ? TEXT ("Open") : TEXT ("Closed"),
                        bOpened ? 4 : 6) ;
          TextOut (hdc, cxCaps, 2 * cyChar, moc.szPname,
                        lstrlen (moc.szPname)) ;
          TextOut (hdc, cxCaps, 3 * cyChar, szBuffer, lstrlen (szBuffer)) ;
          TextOut (hdc, cxCaps, 4 * cyChar,
                        fam[iVoice / 8].inst[iVoice % 8].szInst,
               lstrlen (fam[iVoice / 8].inst[iVoice % 8].szInst)) ;
     
          EndPaint (hwnd, &ps) ;
          return 0 ;
               
     case WM_DESTROY :
          SendMessage (hwnd, WM_COMMAND, IDM_CLOSE, 0L) ;
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

When you run KBMIDI, the window shows how the keys of the keyboard correspond to the keys of a traditional piano or organ. The Z key at the lower left corner plays an A at 110 Hz. Moving across the bottom row of the keyboard, you reach middle C at the right, with the sharps and flats on the second-to-bottom row. The top two rows continue the scale, from middle C to G#. Thus, the range is 3 octaves. Pressing the Ctrl key drops the entire range by 1 octave, and pressing the Shift key raises it by 1 octave, giving an effective range of 5 octaves.

If you start trying to play immediately, however, you won't hear anything. You first must select Open from the Status menu. This will open a MIDI output device. If the port is successfully opened, pressing a key will send a MIDI Note On message to the synthesizer. Releasing the key generates a Note Off message. Depending on the rollover characteristics of your keyboard, you might be able to play several notes at once.

Select Close from the Status menu to close the MIDI device. This is handy if you want to run some other MIDI software under Windows without terminating the KBMIDI program.

The Device menu lists the installed MIDI output devices. These are obtained from the midiOutGetDevCaps function. One of these will probably be a MIDI Out port to an external synthesizer that might or might not be present. The list also includes the MIDI Mapper device. This is the MIDI synthesizer selected in the Multimedia applet of the Control Panel.

The Channel menu lets you select a MIDI channel from 1 through 16. By default, channel 1 is selected. All MIDI messages that the KBMIDI program generates are sent on the selected channel.

The final menu on KBMIDI is labeled Voice. This is a double-nested menu from which you can select one of the 128 instrument voices defined by the General MIDI specification and implemented in Windows. The 128 instrument voices are divided into 16 instrument families with 8 instruments each. These 128 instrument voices are called the melodic voices because different MIDI key numbers correspond to different pitches.

General MIDI also defines a wide range of nonmelodic percussion instruments. To play the percussion instruments, use the Channel menu to select channel 10. Also select the first instrument voice (Acoustic Grand Piano) from the Voice menu. After you do this, each key plays a different percussion sound. There are 47 different percussion sounds, from MIDI key number 35 (the B two octaves below middle C) to 81 (the A nearly two octaves above middle C). We'll take advantage of the percussion channel in the DRUM program coming up.

The KBMIDI program has horizontal and vertical scroll bars. Because a PC keyboard is not velocity-sensitive, the horizontal scroll bar controls the note velocity. Generally, this corresponds to the volume of the notes that you play. After setting the horizontal scroll bar, all Note On messages will use that velocity.

The vertical scroll bar generates a MIDI message known as "Pitch Bend." To use this feature, press down one or more keys and manipulate the vertical scroll bar thumb with the mouse. As you raise the scroll bar thumb, the frequency of the note increases, and as you lower it, the frequency decreases. Releasing the scroll bar returns the pitch to normal.

These two scroll bars can be tricky to use: As you manipulate a scroll bar, keyboard messages do not come through the program's message loop. Therefore, if you press a key and begin manipulating one of the scroll bars with the mouse and then release the key before finishing with the scroll bar, the note will continue to sound. Thus, you shouldn't press or release any keys during the time you're manipulating the scroll bars. A similar rule applies to the menu—do not try to select anything from the menu while a key is depressed. Also, do not change the octave shift using the Ctrl or Shift keys between the time you press a key and release it.

If one or more notes get "stuck" and continue to sound after being released, press the Esc key. This stops the sounds by sending 16 "All Notes Off" messages to the 16 channels of the MIDI synthesizer.

KBMIDI does not have a resource script and instead creates its own menu from scratch. The device names are obtained from the midiOutGetDevCaps function, and the instrument voice families and names are stored in the program in a large data structure.

KBMIDI has a few short functions for simplifying the MIDI messages. I've discussed these messages previously, except for the Pitch Bend message. This message uses two 7-bit values that comprise a 14-bit pitch-bend level. Values between 0 and 0x1FFF lower the pitch, and values between 0x2001 and 0x3FFF raise the pitch.

When you select Open from the Status menu, KBMIDI calls midiOutOpen for the selected device and, if successful, calls its MidiSetPatch function. When changing a device, KBMIDI must close the previous device, if necessary, and then reopen the new device. KBMIDI must also call MidiSetPatch when you change the MIDI device, the MIDI channel, or the instrument voice.

KBMIDI processes WM_KEYUP and WM_KEYDOWN messages to turn notes on and off. A data structure within KBMIDI maps keyboard scan codes to octaves and notes. For example, the Z key on an American English keyboard has a scan code of 44, and the structure identifies this as octave 3 and note 9—an A. In the MidiNoteOn function in KBMIDI, these are combined to form a MIDI key number of 45 (12 times 3, plus 9). This same data structure is used for drawing the keys on the window: each key has a particular horizontal and vertical position and a text string shown inside the rectangle.

Horizontal scroll bar processing is straightforward: all that need be done is store the new velocity level and set the new scroll bar position. Vertical scroll bar processing to control pitch bend is a little unusual, however. The only scroll bar commands it processes are SB_THUMBTRACK, which occurs when you manipulate the scroll bar thumb with the mouse, and SB_THUMBPOSITION, activated when you release the thumb. On an SB_THUMBPOSITION command, KBMIDI sets the scroll bar position to its middle level and calls MidiPitchBend with a value of 8192.

A MIDI Drum Machine

Some percussion instruments, such as a xylophone or timpani, are termed "melodic" or "chromatic" because they can play tones in different pitches. A xylophone has wooden blocks corresponding to different pitches, and timpani can be tuned. These two instruments, as well as several other melodic percussion instruments, can be selected from the Voice menu in KBMIDI.

However, many other percussion instruments are nonmelodic. They cannot be tuned and usually contain too much noise to be associated with a particular pitch. In the General MIDI specification, these nonmelodic percussion voices are available through channel 10. Different key numbers correspond to 47 different percussion instruments.

The DRUM program shown in Figure 22-13 is a computer drum machine. This program lets you construct a sequence of up to 32 notes using 47 different percussion sounds. The program plays the sequence repetitively at a selectable tempo and volume.

Figure 22-13. The DRUM Program.

DRUM.C

/*-------------------------------------
   DRUM.C -- MIDI Drum Machine
             (c) Charles Petzold, 1998
  -------------------------------------*/

#include <windows.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include "drumtime.h"
#include "drumfile.h"
#include "resource.h"

LRESULT CALLBACK WndProc   (HWND, UINT, WPARAM, LPARAM) ;
BOOL    CALLBACK AboutProc (HWND, UINT, WPARAM, LPARAM) ;
                            
void  DrawRectangle (HDC, int, int, DWORD *, DWORD *) ;
void  ErrorMessage  (HWND, TCHAR *, TCHAR *) ;
void  DoCaption     (HWND, TCHAR *) ;
int   AskAboutSave  (HWND, TCHAR *) ;

TCHAR * szPerc [NUM_PERC] =
{
     TEXT ("Acoustic Bass Drum"), TEXT ("Bass Drum 1"),     
     TEXT ("Side Stick"),         TEXT ("Acoustic Snare"),     
     TEXT ("Hand Clap"),          TEXT ("Electric Snare"),
     TEXT ("Low Floor Tom"),      TEXT ("Closed High Hat"), 
     TEXT ("High Floor Tom"),     TEXT ("Pedal High Hat"),     
     TEXT ("Low Tom"),            TEXT ("Open High Hat"),
     TEXT ("Low-Mid Tom"),        TEXT ("High-Mid Tom"),    
     TEXT ("Crash Cymbal 1"),     TEXT ("High Tom"),           
     TEXT ("Ride Cymbal 1"),      TEXT ("Chinese Cymbal"),
     TEXT ("Ride Bell"),          TEXT ("Tambourine"),      
     TEXT ("Splash Cymbal"),      TEXT ("Cowbell"),            
     TEXT ("Crash Cymbal 2"),     TEXT ("Vibraslap"),
     TEXT ("Ride Cymbal 2"),      TEXT ("High Bongo"),      
     TEXT ("Low Bongo"),          TEXT ("Mute High Conga"),    
     TEXT ("Open High Conga"),    TEXT ("Low Conga"),
     TEXT ("High Timbale"),       TEXT ("Low Timbale"),     
     TEXT ("High Agogo"),         TEXT ("Low Agogo"),          
     TEXT ("Cabasa"),             TEXT ("Maracas"),
     TEXT ("Short Whistle"),      TEXT ("Long Whistle"),    
     TEXT ("Short Guiro"),        TEXT ("Long Guiro"),         
     TEXT ("Claves"),             TEXT ("High Wood Block"),
     TEXT ("Low Wood Block"),     TEXT ("Mute Cuica"),      
     TEXT ("Open Cuica"),         TEXT ("Mute Triangle"),      
     TEXT ("Open Triangle")
} ;

TCHAR   szAppName  [] = TEXT ("Drum") ;
TCHAR   szUntitled [] = TEXT ("(Untitled)") ;
TCHAR   szBuffer [80 + MAX_PATH] ;
HANDLE  hInst ;
int     cxChar, cyChar ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;
     
     hInst = hInstance ;
     
     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (hInstance, szAppName) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = szAppName ;
     wndclass.lpszClassName = szAppName ;
          
     if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"),
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     }

     hwnd = CreateWindow (szAppName, NULL,
                          WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU |
                                   WS_MINIMIZEBOX | WS_HSCROLL | WS_VSCROLL,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, szCmdLine) ;
     
     ShowWindow (hwnd, iCmdShow) ;
     UpdateWindow (hwnd) ;
     
     while (GetMessage (&msg, NULL, 0, 0))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return msg.wParam ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static BOOL  bNeedSave ;
     static DRUM  drum ;
     static HMENU hMenu ;
     static int   iTempo = 50, iIndexLast ;
     static TCHAR szFileName  [MAX_PATH], szTitleName [MAX_PATH] ;
     HDC          hdc ;
     int          i, x, y ;
     PAINTSTRUCT  ps ;
     POINT        point ;
     RECT         rect ;
     TCHAR      * szError ;
     
     switch (message)
     {
     case WM_CREATE:
               // Initialize DRUM structure
          
          drum.iMsecPerBeat = 100 ;
          drum.iVelocity    =  64 ;
          drum.iNumBeats    =  32 ;
          
          DrumSetParams (&drum) ;
          
               // Other initialization
          
          cxChar = LOWORD (GetDialogBaseUnits ()) ;
          cyChar = HIWORD (GetDialogBaseUnits ()) ;

          GetWindowRect (hwnd, &rect) ;
          MoveWindow (hwnd, rect.left, rect.top, 
                            77 * cxChar, 29 * cyChar, FALSE) ;
          
          hMenu = GetMenu (hwnd) ;
          
               // Initialize "Volume" scroll bar
          
          SetScrollRange (hwnd, SB_HORZ, 1, 127, FALSE) ;
          SetScrollPos   (hwnd, SB_HORZ, drum.iVelocity, TRUE) ;
          
               // Initialize "Tempo" scroll bar
          
          SetScrollRange (hwnd, SB_VERT, 0, 100, FALSE) ;
          SetScrollPos   (hwnd, SB_VERT, iTempo, TRUE) ;
          
          DoCaption (hwnd, szTitleName) ;
          return 0 ;
          
     case WM_COMMAND:
          switch (LOWORD (wParam))
          {
          case IDM_FILE_NEW:
               if (bNeedSave && IDCANCEL == AskAboutSave (hwnd, szTitleName))
                    return 0 ;
               
                    // Clear drum pattern
               
               for (i = 0 ; i < NUM_PERC ; i++)
               {
                    drum.dwSeqPerc [i] = 0 ;
                    drum.dwSeqPian [i] = 0 ;
               }
               
               InvalidateRect (hwnd, NULL, FALSE) ;
               DrumSetParams (&drum) ;
               bNeedSave = FALSE ;
               return 0 ;
               
          case IDM_FILE_OPEN:
                    // Save previous file
               
               if (bNeedSave && IDCANCEL ==
                    AskAboutSave (hwnd, szTitleName))
                    return 0 ;
               
                    // Open the selected file
               
               if (DrumFileOpenDlg (hwnd, szFileName, szTitleName))
               {
                    szError = DrumFileRead (&drum, szFileName) ;
                    
                    if (szError != NULL)
                    {
                         ErrorMessage (hwnd, szError, szTitleName) ;
                         szTitleName [0] = `\0' ;
                    }
                    else
                    {
                              // Set new parameters
                         
                         iTempo = (int) (50 *
                              (log10 (drum.iMsecPerBeat) - 1)) ;
                         
                         SetScrollPos (hwnd, SB_VERT, iTempo, TRUE) ;
                         SetScrollPos (hwnd, SB_HORZ, drum.iVelocity, TRUE) ;
                         
                         DrumSetParams (&drum) ;
                         InvalidateRect (hwnd, NULL, FALSE) ;
                         bNeedSave = FALSE ;
                    }
                    
                    DoCaption (hwnd, szTitleName) ;
               }
               return 0 ;
               
          case IDM_FILE_SAVE:
          case IDM_FILE_SAVE_AS:
                    // Save the selected file
               
               if ((LOWORD (wParam) == IDM_FILE_SAVE && szTitleName [0]) ||
                         DrumFileSaveDlg (hwnd, szFileName, szTitleName))
               {
                    szError = DrumFileWrite (&drum, szFileName) ;
                    
                    if (szError != NULL)
                    {
                         ErrorMessage (hwnd, szError, szTitleName) ;
                         szTitleName [0] = `\0' ;
                    }
                    else
                         bNeedSave = FALSE ;
                    
                    DoCaption (hwnd, szTitleName) ;
               }
               return 0 ;
               
          case IDM_APP_EXIT:
               SendMessage (hwnd, WM_SYSCOMMAND, SC_CLOSE, 0L) ;
               return 0 ;
               
          case IDM_SEQUENCE_RUNNING:
                    // Begin sequence
               
               if (!DrumBeginSequence (hwnd))
               {
                    ErrorMessage (hwnd,
                         TEXT ("Could not start MIDI sequence -- ")
                         TEXT ("MIDI Mapper device is unavailable!"),
                         szTitleName) ;
               }
               else
               {
                    CheckMenuItem (hMenu, IDM_SEQUENCE_RUNNING,   MF_CHECKED) ;
                    CheckMenuItem (hMenu, IDM_SEQUENCE_STOPPED, MF_UNCHECKED) ;
               }
               return 0 ;
               
          case IDM_SEQUENCE_STOPPED:
                    // Finish at end of sequence
               
               DrumEndSequence (FALSE) ;
               return 0 ;
               
          case IDM_APP_ABOUT:
               DialogBox (hInst, TEXT ("AboutBox"), hwnd, AboutProc) ;
               return 0 ;
          }
          return 0 ;
                    
     case WM_LBUTTONDOWN:
     case WM_RBUTTONDOWN:
          hdc = GetDC (hwnd) ;
          
               // Convert mouse coordinates to grid coordinates
          
          x =     LOWORD (lParam) / cxChar - 40 ;
          y = 2 * HIWORD (lParam) / cyChar -  2 ;
               // Set a new number of beats of sequence
          
          if (x > 0 && x <= 32 && y < 0)
          {
               SetTextColor (hdc, RGB (255, 255, 255)) ;
               TextOut (hdc, (40 + drum.iNumBeats) * cxChar, 0, TEXT (":|"), 2);
               SetTextColor (hdc, RGB (0, 0, 0)) ;
               
               if (drum.iNumBeats % 4 == 0)
                    TextOut (hdc, (40 + drum.iNumBeats) * cxChar, 0,
                             TEXT ("."), 1) ;
               
               drum.iNumBeats = x ;
               
               TextOut (hdc, (40 + drum.iNumBeats) * cxChar, 0, TEXT (":|"), 2);
               
               bNeedSave = TRUE ;
          }
          
               // Set or reset a percussion instrument beat
          
          if (x >= 0 && x < 32 && y >= 0 && y < NUM_PERC)
          {
               if (message == WM_LBUTTONDOWN)
                    drum.dwSeqPerc[y] ^= (1 << x) ;
               else
                    drum.dwSeqPian[y] ^= (1 << x) ;
               
               DrawRectangle (hdc, x, y, drum.dwSeqPerc, drum.dwSeqPian) ;
               
               bNeedSave = TRUE ;
          }
          
          ReleaseDC (hwnd, hdc) ;
          DrumSetParams (&drum) ;
          return 0 ;
          
     case WM_HSCROLL:
               // Change the note velocity
          
          switch (LOWORD (wParam))
          {
          case SB_LINEUP:         drum.iVelocity -= 1 ;  break ;
          case SB_LINEDOWN:       drum.iVelocity += 1 ;  break ;
          case SB_PAGEUP:         drum.iVelocity -= 8 ;  break ;
          case SB_PAGEDOWN:       drum.iVelocity += 8 ;  break ;
          case SB_THUMBPOSITION:
               drum.iVelocity = HIWORD (wParam) ;
               break ;
               
          default:
               return 0 ;
          }
          
          drum.iVelocity = max (1, min (drum.iVelocity, 127)) ;
          SetScrollPos (hwnd, SB_HORZ, drum.iVelocity, TRUE) ;
          DrumSetParams (&drum) ;
          bNeedSave = TRUE ;
          return 0 ;
     
     case WM_VSCROLL:
               // Change the tempo
          
          switch (LOWORD (wParam))
          {
          case SB_LINEUP:         iTempo -=  1 ;  break ;
          case SB_LINEDOWN:       iTempo +=  1 ;  break ;
          case SB_PAGEUP:         iTempo -= 10 ;  break ;
          case SB_PAGEDOWN:       iTempo += 10 ;  break ;
          case SB_THUMBPOSITION:
               iTempo = HIWORD (wParam) ;
               break ;
               
          default:
               return 0 ;
          }
          
          iTempo = max (0, min (iTempo, 100)) ;
          SetScrollPos (hwnd, SB_VERT, iTempo, TRUE) ;
          
          drum.iMsecPerBeat = (WORD) (10 * pow (100, iTempo / 100.0)) ;
          
          DrumSetParams (&drum) ;
          bNeedSave = TRUE ;
          return 0 ;
     
     case WM_PAINT:
          hdc = BeginPaint (hwnd, &ps) ;
          
          SetTextAlign (hdc, TA_UPDATECP) ;
          SetBkMode (hdc, TRANSPARENT) ;
          
               // Draw the text strings and horizontal lines
          for (i = 0 ; i < NUM_PERC ; i++)
          {
               MoveToEx (hdc, i & 1 ? 20 * cxChar : cxChar,
                             (2 * i + 3) * cyChar / 4, NULL) ;
               
               TextOut (hdc, 0, 0, szPerc [i], lstrlen (szPerc [i])) ;
               
               GetCurrentPositionEx (hdc, &point) ;
               
               MoveToEx (hdc,  point.x + cxChar, point.y + cyChar / 2, NULL) ;
               LineTo   (hdc,       39 * cxChar, point.y + cyChar / 2) ;
          }
          
          SetTextAlign (hdc, 0) ;
          
               // Draw rectangular grid, repeat mark, and beat marks
          
          for (x = 0 ; x < 32 ; x++)
          {
               for (y = 0 ; y < NUM_PERC ; y++)
                    DrawRectangle (hdc, x, y, drum.dwSeqPerc, drum.dwSeqPian) ;
               
               SetTextColor (hdc, x == drum.iNumBeats - 1 ?
                                   RGB (0, 0, 0) : RGB (255, 255, 255)) ;
               
               TextOut (hdc, (41 + x) * cxChar, 0, TEXT (":|"), 2) ;
               
               SetTextColor (hdc, RGB (0, 0, 0)) ;
               
               if (x % 4 == 0)
                    TextOut (hdc, (40 + x) * cxChar, 0, TEXT ("."), 1) ;
          }
          
          EndPaint (hwnd, &ps) ;
          return 0 ;
          
     case WM_USER_NOTIFY:
               // Draw the "bouncing ball"
          
          hdc = GetDC (hwnd) ;
          
          SelectObject (hdc, GetStockObject (NULL_PEN)) ;
          SelectObject (hdc, GetStockObject (WHITE_BRUSH)) ;
          
          for (i = 0 ; i < 2 ; i++)
          {
               x = iIndexLast ;
               y = NUM_PERC + 1 ;
               
               Ellipse (hdc, (x + 40) * cxChar, (2 * y + 3) * cyChar / 4,
                    (x + 41) * cxChar, (2 * y + 5) * cyChar / 4);
               
               iIndexLast = wParam ;
               SelectObject (hdc, GetStockObject (BLACK_BRUSH)) ;
          }
          
          ReleaseDC (hwnd, hdc) ;
          return 0 ;
          
     case WM_USER_ERROR:
          ErrorMessage (hwnd, TEXT ("Can't set timer event for tempo"),
                        szTitleName) ;
          
                                             // fall through
     case WM_USER_FINISHED:
          DrumEndSequence (TRUE) ;
          CheckMenuItem (hMenu, IDM_SEQUENCE_RUNNING,   MF_UNCHECKED) ;
          CheckMenuItem (hMenu, IDM_SEQUENCE_STOPPED, MF_CHECKED) ;
          return 0 ;
          
     case WM_CLOSE:
          if (!bNeedSave || IDCANCEL != AskAboutSave (hwnd, szTitleName))
               DestroyWindow (hwnd) ;
          
          return 0 ;
          
     case WM_QUERYENDSESSION:
          if (!bNeedSave || IDCANCEL != AskAboutSave (hwnd, szTitleName))
               return 1L ;
          
          return 0 ;
          
     case WM_DESTROY:
          DrumEndSequence (TRUE) ;
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

BOOL CALLBACK AboutProc (HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
     switch (message)
     {
     case WM_INITDIALOG:
          return TRUE ;
          
     case WM_COMMAND:
          switch (LOWORD (wParam))
          {
          case IDOK:
               EndDialog (hDlg, 0) ;
               return TRUE ;
          }
          break ;
     }
     return FALSE ;
}

void DrawRectangle (HDC hdc, int x, int y, DWORD * dwSeqPerc, DWORD * dwSeqPian)
{
     int iBrush ;
     
     if (dwSeqPerc [y] & dwSeqPian [y] & (1L << x))
          iBrush = BLACK_BRUSH ;
     
     else if (dwSeqPerc [y] & (1L << x))
          iBrush = DKGRAY_BRUSH ;
     
     else if (dwSeqPian [y] & (1L << x))
          iBrush = LTGRAY_BRUSH ;
     
     else
          iBrush = WHITE_BRUSH ;
     
     SelectObject (hdc, GetStockObject (iBrush)) ;
     
     Rectangle (hdc, (x + 40) * cxChar    , (2 * y + 4) * cyChar / 4,
                     (x + 41) * cxChar + 1, (2 * y + 6) * cyChar / 4 + 1) ;
}

void ErrorMessage (HWND hwnd, TCHAR * szError, TCHAR * szTitleName)
{
     wsprintf (szBuffer, szError,
          (LPSTR) (szTitleName [0] ? szTitleName : szUntitled)) ;
     
     MessageBeep (MB_ICONEXCLAMATION) ;
     MessageBox (hwnd, szBuffer, szAppName, MB_OK | MB_ICONEXCLAMATION) ;
}

void DoCaption (HWND hwnd, TCHAR * szTitleName)
{
     wsprintf (szBuffer, TEXT ("MIDI Drum Machine - %s"),
               (LPSTR) (szTitleName [0] ? szTitleName : szUntitled)) ;
     
     SetWindowText (hwnd, szBuffer) ;
}

int AskAboutSave (HWND hwnd, TCHAR * szTitleName)
{
     int iReturn ;
     
     wsprintf (szBuffer, TEXT ("Save current changes in %s?"),
               (LPSTR) (szTitleName [0] ? szTitleName : szUntitled)) ;
     
     iReturn = MessageBox (hwnd, szBuffer, szAppName,
                           MB_YESNOCANCEL | MB_ICONQUESTION) ;
     
     if (iReturn == IDYES)
          if (!SendMessage (hwnd, WM_COMMAND, IDM_FILE_SAVE, 0))
               iReturn = IDCANCEL ;
          
     return iReturn ;
}

DRUMTIME.H

/*------------------------------------------------------------
   DRUMTIME.H Header File for Time Functions for DRUM Program
  ------------------------------------------------------------*/

#define NUM_PERC         47
#define WM_USER_NOTIFY   (WM_USER + 1)
#define WM_USER_FINISHED (WM_USER + 2)
#define WM_USER_ERROR    (WM_USER + 3)

#pragma pack(push, 2)

typedef struct
{
     short iMsecPerBeat ;
     short iVelocity ;
     short iNumBeats ;
     DWORD dwSeqPerc [NUM_PERC] ;
     DWORD dwSeqPian [NUM_PERC] ;
}
DRUM, * PDRUM ;

#pragma pack(pop)

void DrumSetParams     (PDRUM) ;
BOOL DrumBeginSequence (HWND)  ;
void DrumEndSequence   (BOOL)  ;

DRUMTIME.C

/*-----------------------------------------
   DRUMFILE.C -- Timer Routines for DRUM
                 (c) Charles Petzold, 1998
  -----------------------------------------*/

#include <windows.h>
#include "drumtime.h"

#define minmax(a,x,b) (min (max (x, a), b))

#define TIMER_RES   5

void CALLBACK DrumTimerFunc (UINT, UINT, DWORD, DWORD, DWORD) ;

BOOL     bSequenceGoing, bEndSequence ;
DRUM     drum ;
HMIDIOUT hMidiOut ;
HWND     hwndNotify ;
int      iIndex ;
UINT     uTimerRes, uTimerID ;

DWORD MidiOutMessage (HMIDIOUT hMidi, int iStatus, int iChannel,
                      int iData1, int iData2)
{
     DWORD dwMessage ;
     
     dwMessage = iStatus | iChannel | (iData1 << 8) | (iData2 << 16) ;
     
     return midiOutShortMsg (hMidi, dwMessage) ;
}

void DrumSetParams (PDRUM pdrum)
{
     CopyMemory (&drum, pdrum, sizeof (DRUM)) ;
}

BOOL DrumBeginSequence (HWND hwnd)
{
     TIMECAPS tc ;
     
     hwndNotify = hwnd ;           // Save window handle for notification
     DrumEndSequence (TRUE) ;      // Stop current sequence if running
     
          // Open the MIDI Mapper output port
     
     if (midiOutOpen (&hMidiOut, MIDIMAPPER, 0, 0, 0))
          return FALSE ;
     
          // Send Program Change messages for channels 9 and 0
     
     MidiOutMessage (hMidiOut, 0xC0, 9, 0, 0) ;
     MidiOutMessage (hMidiOut, 0xC0, 0, 0, 0) ;
     
          // Begin sequence by setting a timer event
     
     timeGetDevCaps (&tc, sizeof (TIMECAPS)) ;
     uTimerRes = minmax (tc.wPeriodMin, TIMER_RES, tc.wPeriodMax) ;
     timeBeginPeriod (uTimerRes) ;
     
     uTimerID = timeSetEvent (max ((UINT) uTimerRes, (UINT) drum.iMsecPerBeat),
                              uTimerRes, DrumTimerFunc, 0, TIME_ONESHOT) ;
     
     if (uTimerID == 0)
     {
          timeEndPeriod (uTimerRes) ;
          midiOutClose (hMidiOut) ;
          return FALSE ;
     }
     
     iIndex = -1 ;
     bEndSequence = FALSE ;
     bSequenceGoing = TRUE ;
     
     return TRUE ;
}

void DrumEndSequence (BOOL bRightAway)
{
     if (bRightAway)
     {
          if (bSequenceGoing)
         
 {
                    // stop the timer
               if (uTimerID)
                    timeKillEvent (uTimerID) ;
               timeEndPeriod (uTimerRes) ;

                    // turn off all notes
               MidiOutMessage (hMidiOut, 0xB0, 9, 123, 0) ;
               MidiOutMessage (hMidiOut, 0xB0, 0, 123, 0) ;
               
                    // close the MIDI port
               midiOutClose (hMidiOut) ;
               bSequenceGoing = FALSE ;
          }
     }
     else
          bEndSequence = TRUE ;
}

void CALLBACK DrumTimerFunc (UINT  uID, UINT uMsg, DWORD dwUser,
                             DWORD dw1, DWORD dw2)
{
     static DWORD dwSeqPercLast [NUM_PERC], dwSeqPianLast [NUM_PERC] ;
     int          i ;
     
         // Note Off messages for channels 9 and 0
     
     if (iIndex != -1)
     {
          for (i = 0 ; i < NUM_PERC ; i++)
          {
               if (dwSeqPercLast[i] & 1 << iIndex)
                    MidiOutMessage (hMidiOut, 0x80, 9, i + 35, 0) ;
               
               if (dwSeqPianLast[i] & 1 << iIndex) 
                    MidiOutMessage (hMidiOut, 0x80, 0, i + 35, 0) ;
          }
     }
     
          // Increment index and notify window to advance bouncing ball
     
     iIndex = (iIndex + 1) % drum.iNumBeats ;
     PostMessage (hwndNotify, WM_USER_NOTIFY, iIndex, timeGetTime ()) ;
     
          // Check if ending the sequence
     
     if (bEndSequence && iIndex == 0)
     {
          PostMessage (hwndNotify, WM_USER_FINISHED, 0, 0L) ;
          return ;
     }
     
          // Note On messages for channels 9 and 0
     
     for (i = 0 ; i < NUM_PERC ; i++)
     {
          if (drum.dwSeqPerc[i] & 1 << iIndex)
               MidiOutMessage (hMidiOut, 0x90, 9, i + 35, drum.iVelocity) ;
          
          if (drum.dwSeqPian[i] & 1 << iIndex)
               MidiOutMessage (hMidiOut, 0x90, 0, i + 35, drum.iVelocity) ;
          
          dwSeqPercLast[i] = drum.dwSeqPerc[i] ;
          dwSeqPianLast[i] = drum.dwSeqPian[i] ;
     }
          // Set a new timer event
     
     uTimerID = timeSetEvent (max ((int) uTimerRes, drum.iMsecPerBeat),
                              uTimerRes, DrumTimerFunc, 0, TIME_ONESHOT) ;
     
     if (uTimerID == 0)
     {
          PostMessage (hwndNotify, WM_USER_ERROR, 0, 0) ;
     }
}

DRUMFILE.H

/*-------------------------------------------------------
   DRUMFILE.H Header File for File I/O Routines for DRUM
  -------------------------------------------------------*/

BOOL    DrumFileOpenDlg (HWND, TCHAR *, TCHAR *) ;
BOOL    DrumFileSaveDlg (HWND, TCHAR *, TCHAR *) ;

TCHAR * DrumFileWrite   (DRUM *, TCHAR *) ;
TCHAR * DrumFileRead    (DRUM *, TCHAR
 *) ;
 

DRUMFILE.C

/*------------------------------------------
   DRUMFILE.C -- File I/O Routines for DRUM
                 (c) Charles Petzold, 1998
  -----------------------------------------
-*/
#include <windows.h>
#include <commdlg.h>
#include "drumtime.h"
#include "drumfile.h"

OPENFILENAME ofn = { sizeof (OPENFILENAME) } ;

TCHAR * szFilter[] = { TEXT ("Drum Files (*.DRM)"),  
                       TEXT ("*.drm"), TEXT ("") } ;

TCHAR szDrumID   [] = TEXT ("DRUM") ;
TCHAR szListID   [] = TEXT ("LIST") ;
TCHAR szInfoID   [] = TEXT ("INFO") ;
TCHAR szSoftID   [] = TEXT ("ISFT") ;
TCHAR szDateID   [] = TEXT ("ISCD") ;
TCHAR szFmtID    [] = TEXT ("fmt ") ;
TCHAR szDataID   [] = TEXT ("data") ;
char  szSoftware [] = "DRUM by Charles Petzold, Programming Windows" ;

TCHAR szErrorNoCreate    [] = TEXT ("File %s could not be opened for writing.");
TCHAR szErrorCannotWrite [] = TEXT ("File %s could not be written to. ") ;
TCHAR szErrorNotFound    [] = TEXT ("File %s not found or cannot be opened.") ;
TCHAR szErrorNotDrum     [] = TEXT ("File %s is not a standard DRUM file.") ;
TCHAR szErrorUnsupported [] = TEXT ("File %s is not a supported DRUM file.") ;
TCHAR szErrorCannotRead  [] = TEXT ("File %s cannot be read.") ;

BOOL DrumFileOpenDlg (HWND hwnd, TCHAR * szFileName, TCHAR * szTitleName)
{
     ofn.hwndOwner         = hwnd ;
     ofn.lpstrFilter       = szFilter [0] ;
     ofn.lpstrFile         = szFileName ;
     ofn.nMaxFile          = MAX_PATH ;
     ofn.lpstrFileTitle    = szTitleName ;
     ofn.nMaxFileTitle     = MAX_PATH ;
     ofn.Flags             = OFN_CREATEPROMPT ;
     ofn.lpstrDefExt       = TEXT ("drm") ;
     
     return GetOpenFileName (&ofn) ;
}

BOOL DrumFileSaveDlg (HWND hwnd, TCHAR * szFileName, TCHAR * szTitleName)
{
     ofn.hwndOwner         = hwnd ;
     ofn.lpstrFilter       = szFilter [0] ;
     ofn.lpstrFile         = szFileName ;
     ofn.nMaxFile          = MAX_PATH ;
     ofn.lpstrFileTitle    = szTitleName ;
     ofn.nMaxFileTitle     = MAX_PATH ;
     ofn.Flags             = OFN_OVERWRITEPROMPT ;
     ofn.lpstrDefExt       = TEXT ("drm") ;
     
     return GetSaveFileName (&ofn) ;
}

TCHAR * DrumFileWrite (DRUM * pdrum, TCHAR * szFileName)
{
     char        szDateBuf [16] ;
     HMMIO       hmmio ;
     int         iFormat = 2 ;
     MMCKINFO    mmckinfo [3] ;
     SYSTEMTIME  st ;
     WORD        wError = 0 ;
     
     memset (mmckinfo, 0, 3 * sizeof (MMCKINFO)) ;
     
          // Recreate the file for writing
     
     if ((hmmio = mmioOpen (szFileName, NULL,
               MMIO_CREATE | MMIO_WRITE | MMIO_ALLOCBUF)) == NULL)
          return szErrorNoCreate ;
     
          // Create a "RIFF" chunk with a "CPDR" type
     
     mmckinfo[0].fccType = mmioStringToFOURCC (szDrumID, 0) ;
     
     wError |= mmioCreateChunk (hmmio, &mmckinfo[0], MMIO_CREATERIFF) ;
     
          // Create "LIST" sub-chunk with an "INFO" type
     
     mmckinfo[1].fccType = mmioStringToFOURCC (szInfoID, 0) ;
     
     wError |= mmioCreateChunk (hmmio, &mmckinfo[1], MMIO_CREATELIST) ;
     
          // Create "ISFT" sub-sub-chunk
     
     mmckinfo[2].ckid = mmioStringToFOURCC (szSoftID, 0) ;
     
     wError |= mmioCreateChunk (hmmio, &mmckinfo[2], 0) ;
     wError |= (mmioWrite (hmmio, szSoftware, sizeof (szSoftware)) !=
                                              sizeof (szSoftware)) ;
     wError |= mmioAscend (hmmio, &mmckinfo[2], 0) ;
     
          // Create a time string
     GetLocalTime (&st) ;
     
     wsprintfA (szDateBuf, "%04d-%02d-%02d", st.wYear, st.wMonth, st.wDay) ;
     
          // Create "ISCD" sub-sub-chunk
     
     mmckinfo[2].ckid = mmioStringToFOURCC (szDateID, 0) ;
     
     wError |= mmioCreateChunk (hmmio, &mmckinfo[2], 0) ;
     wError |= (mmioWrite (hmmio, szDateBuf, (strlen (szDateBuf) + 1)) !=
                                       (int) (strlen (szDateBuf) + 1)) ;
     wError |= mmioAscend (hmmio, &mmckinfo[2], 0) ;
     wError |= mmioAscend (hmmio, &mmckinfo[1], 0) ;
     
          // Create "fmt " sub-chunk
     
     mmckinfo[1].ckid = mmioStringToFOURCC (szFmtID, 0) ;
     
     wError |= mmioCreateChunk (hmmio, &mmckinfo[1], 0) ;
     wError |= (mmioWrite (hmmio, (PSTR) &iFormat, sizeof (int)) !=
                                                   sizeof (int)) ;
     wError |= mmioAscend (hmmio, &mmckinfo[1], 0) ;
     
          // Create the "data" sub-chunk
     
     mmckinfo[1].ckid = mmioStringToFOURCC (szDataID, 0) ;
     
     wError |= mmioCreateChunk (hmmio, &mmckinfo[1], 0) ;
     wError |= (mmioWrite (hmmio, (PSTR) pdrum, sizeof (DRUM)) !=
                                                sizeof (DRUM)) ;
     wError |= mmioAscend (hmmio, &mmckinfo[1], 0) ;
     wError |= mmioAscend (hmmio, &mmckinfo[0], 0) ;
     
          // Clean up and return
     
     wError |= mmioClose (hmmio, 0) ;
     
     if (wError)
     {
          mmioOpen (szFileName, NULL, MMIO_DELETE) ;
          return szErrorCannotWrite ;
     }
     return NULL ;
}

TCHAR * DrumFileRead (DRUM * pdrum, TCHAR * szFileName)
{
     DRUM     drum ;
     HMMIO    hmmio ;
     int      i, iFormat ;
     MMCKINFO mmckinfo [3] ;
     
     ZeroMemory (mmckinfo, 2 * sizeof (MMCKINFO)) ;
     
         // Open the file
     
     if ((hmmio = mmioOpen (szFileName, NULL, MMIO_READ)) == NULL)
          return szErrorNotFound ;
     
          // Locate a "RIFF" chunk with a "DRUM" form-type
     
     mmckinfo[0].ckid = mmioStringToFOURCC (szDrumID, 0) ;
     
     if (mmioDescend (hmmio, &mmckinfo[0], NULL, MMIO_FINDRIFF))
     {
          mmioClose (hmmio, 0) ;
          return szErrorNotDrum ;
     }
     
          // Locate, read, and verify the "fmt " sub-chunk
     
     mmckinfo[1].ckid = mmioStringToFOURCC (szFmtID, 0) ;
     
     if (mmioDescend (hmmio, &mmckinfo[1], &mmckinfo[0], MMIO_FINDCHUNK))
     {
          mmioClose (hmmio, 0) ;
          return szErrorNotDrum ;
     }
     
     if (mmckinfo[1].cksize != sizeof (int))
     {
          mmioClose (hmmio, 0) ;
          return szErrorUnsupported ;
     }
     
     if (mmioRead (hmmio, (PSTR) &iFormat, sizeof (int)) != sizeof (int))
     {
          mmioClose (hmmio, 0) ;
          return szErrorCannotRead ;
     }
     
     if (iFormat != 1 && iFormat != 2)
     {
          mmioClose (hmmio, 0) ;
          return szErrorUnsupported ;
     }
     
          // Go to end of "fmt " sub-chunk
     
     mmioAscend (hmmio, &mmckinfo[1], 0) ;
     
          // Locate, read, and verify the "data" sub-chunk
     
     mmckinfo[1].ckid = mmioStringToFOURCC (szDataID, 0) ;
  
     if (mmioDescend (hmmio, &mmckinfo[1], &mmckinfo[0], MMIO_FINDCHUNK))
     {
          mmioClose (hmmio, 0) ;
          return szErrorNotDrum ;
     }
     
     if (mmckinfo[1].cksize != sizeof (DRUM))
     {
          mmioClose (hmmio, 0) ;
          return szErrorUnsupported ;
     }
     
     if (mmioRead (hmmio, (LPSTR) &drum, sizeof (DRUM)) != sizeof (DRUM))
     {
          mmioClose (hmmio, 0) ;
          return szErrorCannotRead ;
     }
     
          // Close the file 
     
     mmioClose (hmmio, 0) ;

          // Convert format 1 to format 2 and copy the DRUM structure data

     if (iFormat == 1)
     {
          for (i = 0 ; i < NUM_PERC ; i++)
          {
               drum.dwSeqPerc [i] = drum.dwSeqPian [i] ;
               drum.dwSeqPian [i] = 0 ;
          }
     }
     
     memcpy (pdrum, &drum, sizeof (DRUM)) ;
     return NULL ;
}

DRUM.RC (excerpts)

//Microsoft Developer Studio generated resource script.

#include "resource.h"
#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////
// Menu

DRUM MENU DISCARDABLE 
BEGIN
    POPUP "&File"
    BEGIN
        MENUITEM "&New",                        IDM_FILE_NEW
        MENUITEM "&Open...",                    IDM_FILE_OPEN
        MENUITEM "&Save",                       IDM_FILE_SAVE
        MENUITEM "Save &As...",                 IDM_FILE_SAVE_AS
        MENUITEM SEPARATOR
        MENUITEM "E&xit",                       IDM_APP_EXIT
    END
    POPUP "&Sequence"
    BEGIN
        MENUITEM "&Running",                    IDM_SEQUENCE_RUNNING
        MENUITEM "&Stopped",                    IDM_SEQUENCE_STOPPED
        , CHECKED
    END
    POPUP "&Help"
    BEGIN
        MENUITEM "&About...",                   IDM_APP_ABOUT
    END
END

/////////////////////////////////////////////////////////////////////////////
// Icon

DRUM                    ICON    DISCARDABLE     "drum.ico"

/////////////////////////////////////////////////////////////////////////////
// Dialog

ABOUTBOX DIALOG DISCARDABLE  20, 20, 160, 164
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Dialog"
FONT 8, "MS Sans Serif"
BEGIN
    DEFPUSHBUTTON   "OK",IDOK,54,143,50,14
    ICON            "DRUM",IDC_STATIC,8,8,21,20
    CTEXT           "DRUM",IDC_STATIC,34,12,90,8
    CTEXT           "MIDI Drum Machine",IDC_STATIC,7,36,144,8
    CONTROL         "",IDC_STATIC,"Static",SS_BLACKFRAME,8,88,144,46
    LTEXT           "Left Button:\t\tDrum sounds",IDC_STATIC,12,92,136,8
    LTEXT           "Right Button:\t\tPiano sounds",IDC_STATIC,12,102,136,8
    LTEXT           "Horizontal Scroll:\t\tVelocity",IDC_STATIC,12,112,136,8
    LTEXT           "Vertical Scroll:\t\tTempo",IDC_STATIC,12,122,136,8
    CTEXT           "Copyright (c) Charles Petzold, 1998",IDC_STATIC,8,48,
                    144,8
    CTEXT           """Programming Windows,"" 5th Edition",IDC_STATIC,8,60,
                    144,8
END

RESOURCE.H (excerpts)

// Microsoft Developer Studio generated include file.
// Used by Drum.rc

#define IDM_FILE_NEW                    40001
#define IDM_FILE_OPEN                   40002
#define IDM_FILE_SAVE                   40003
#define IDM_FILE_SAVE_AS                40004
#define IDM_APP_EXIT                    40005
#define IDM_SEQUENCE_RUNNING            40006
#define IDM_SEQUENCE_STOPPED            40007
#define IDM_APP_ABOUT                   40008

When you first run DRUM, you'll see the 47 different percussion instruments listed by name in the left half on the window in two columns. The grid at the right is a two-dimensional array of percussion sound vs. time. Each instrument is associated with a row in the grid. The 32 columns are 32 beats. If you think of these 32 beats as occuring within a measure of 4/4 time (that is, four quarter notes per measure), then each beat corresponds to a 32nd note.

When you select Running from the Sequence menu, the program will attempt to open the MIDI Mapper device. If it's unsuccessful, you'll get a message box. Otherwise, you'll see a little "bouncing ball" skip across the bottom of the grid as each beat is played.

You can click with the left mouse button mouse anywhere within the grid to play a percussion sound during that beat. The square will turn dark gray. You can also add some piano beats using the right mouse button. The square will turn light gray. If you click with both mouse buttons, either together or independently, the square will turn black and the percussion and piano sounds will be heard. Clicking again with either or both buttons will turn off the sound for that beat.

Across the top of the grid is a dot every 4 beats. Those dots simply make it easy to pinpoint your button clicks without too much counting. At the upper righthand corner of the grid is a colon and bar (:|) that together resemble a repeat sign used in traditional music notation. This indicates the length of the sequence. You can click with the mouse anywhere above the grid to put the repeat sign somewhere else. The sequence plays up to, but not including, the beat under the repeat sign. If you want to create a waltz rhythm, for example, you should set the repeat mark for some multiple of 3 beats.

The horizontal scroll bar controls the velocity byte in the MIDI Note On messages. This generally affects the volume of the sounds, although it can also affect timbre in some synthesizers. The program initially sets the velocity scroll bar thumb in the center position. The vertical scroll bar controls the tempo. This is a logarithmic scale, ranging from 1 second per beat when the thumb is at the bottom to 10 milliseconds per beat at the top. The program initially sets the tempo at 100 milliseconds (1/10th second) per beat, with the scroll bar thumb in the center.

The File menu allows you to save and retrieve files with the extension .DRM, which is a format that I invented. These files are fairly small and use the RIFF file format, which is recommended for all new multimedia data files. The About option from the Help menu displays a dialog box containing a very brief summary of the use of the mouse on the grid and the functions of the two scroll bars.

Finally, the Stopped option from the Sequence menu stops the music and closes the MIDI Mapper device after finishing with the current sequence.

The Multimedia time Functions

You'll notice that DRUM.C makes no calls to any multimedia functions. All the real action occurs in the DRUMTIME module.

Although the normal Windows timer is certainly simple to use, it's a disaster for time-critical applications. As we saw in the BACHTOCC program, playing music is one such time-critical application for which the Windows timer is simply inadequate. To provide the accuracy needed for playing MIDI sequences on the PC, the multimedia API includes a high-resolution timer implemented through the use of seven functions beginning with the prefix time. One of these functions is superfluous, and DRUMTIME demonstrates the use of the other six. The timer functions work with a callback function that runs in a separate thread. This callback function is called by the system according to a timer delay value specified by the program.

When dealing with the multimedia timer, you specify two different times, both in milliseconds. The first is the delay time, and the second is called the resolution. You can think of the resolution as a tolerable error. If you specify a delay of 100 milliseconds with a resolution of 10 milliseconds, the actual timer delay can range anywhere from 90 to 110 milliseconds.

Before you begin using the timer, you should obtain the timer device capabilities:

timeGetDevCaps (&timecaps, uSize) ;

The first argument is a pointer to a structure of type TIMECAPS, and the second argument is the size of this structure. The TIMECAPS structure has only two fields, wPeriodMin and wPeriodMax. These are the minimum and maximum resolution values supported by the timer device driver. If you look at these values after calling timeGetDevCaps, you'll find that wPeriodMin is 1 and wPeriodMax is 65535, so this function may not seem crucial. However, it's a good idea to get these resolution values anyway and use them in the other timer function calls.

The next step is to call

timeBeginPeriod (uResolution) ;

to indicate the lowest timer resolution value that your program requires. This value should be within the range given in the TIMECAPS structure. This call allows the timer device driver to best provide for multiple programs that might be using the timer. Every call to timeBeginPeriod must be paired with a later call to timeEndPeriod, which I'll describe shortly.

Now you're ready to actually set a timer event:

idTimer = timeSetEvent (uDelay, uResolution, CallBackFunc, dwData, uFlag) ;

The idTimer returned from the call will be zero if an error occurs. Following this call, the function CallBackFunc will be called from Windows in uDelay milliseconds with an allowable error specified by uResolution. The uResolution value must be greater than or equal to the resolution value passed to timeBeginPeriod. The dwData parameter is program-defined data later passed to CallBackFunc. The last parameter can be either TIME_ONESHOT to get a single call to CallBackFunc in uDelay number of milliseconds or TIME_PERIODIC to get calls to CallBackFunc every uDelay milliseconds.

To stop a one-shot timer event before CallBackFunc is called, or to halt periodic timer events, call

timeKillEvent (idTimer) ;

You don't need to kill a one-shot timer event after CallBackFunc is called. When you're finished using the timer in your program, call

timeEndPeriod (wResolution) ;

with the same argument passed to timeBeginPeriod.

Two other functions begin with the prefix time. The function

dwSysTime = timeGetTime () ;

returns the system time in milliseconds since Windows first started up. The function

timeGetSystemTime (&mmtime, uSize) ;

requires a pointer to an MMTIME structure as the first argument and the size of this structure as the second. Although the MMTIME structure can be used in other circumstances to get the system time in formats other than milliseconds, in this case it always returns the time in milliseconds. So, timeGetSystemTime is superfluous.

The callback function is limited in the Windows function calls it can make. The callback function can call PostMessage, four timer functions (timeSetEvent, timeKillEvent, timeGetTime, and the superfluous timeGetSystemTime), two MIDI output functions (midiOutShortMsg and midiOutLongMsg), and the debugging function OutputDebugStr.

Obviously, the multimedia timer is designed specifically for playing MIDI sequences and has very limited use for anything else. You can, of course, use PostMessage for informing a window procedure of timer events, and the window procedure can do whatever it likes, but it won't be responding with the accuracy of the timer callback itself.

The callback function has five parameters, but only two of them are used: the timer ID number returned from timeSetEvent and the dwData value originally passed as an argument to timeSetEvent.

The DRUM.C module calls the DrumSetParams function in DRUMTIME.C at various times—when DRUM's window is created, when the user clicks on the grid or manipulates the scroll bars, when the program loads a .DRM file from disk, or when the grid is cleared. The single argument to DrumSetParams is a pointer to a structure of type DRUM, defined in DRUMTIME.H. This structure stores the beat time in milliseconds, the velocity (which generally corresponds to the volume), the number of beats in the sequence, as well as two sets of forty-seven 32-bit integers for storing the grid settings for the percussion and piano sounds. Each bit in these 32-bit integers corresponds to a beat of the sequence. The DRUM.C module maintains a structure of type DRUM in static memory and passes a pointer to it when calling DrumSetParams. DrumSetParams simply copies the contents of the structure.

To start the sequence going, DRUM calls the DrumBeginSequence function in DRUMTIME. The only argument is a window handle. This is used for notification purposes. DrumBeginSequence opens the MIDI Mapper output device and, if successful, sends Program Change messages to select instrument voice 0 for MIDI channels 0 and 9. (These are zero-based, so 9 actually refers to MIDI channel 10, the percussion channel. The other channel is used for the piano sounds.) DrumBeginSequence continues by calling timeGetDevCaps and then timeBeginPeriod. The desired timer resolution defined in the TIMER_RES constant is 5 milliseconds, but I've defined a macro called minmax to calculate a resolution within the limits returned from timeGetDevCaps.

The next call is timeSetEvent, specifying the beat time, the calculated resolution, the callback function DrumTimerFunc, and the constant TIME_ONESHOT. DRUMTIME uses a one-shot timer rather than a periodic timer so that the tempo can be dynamically changed while a sequence is running. After the timeSetEvent call, the timer device driver will call DrumTimerFunc after the delay time has elapsed.

The DrumTimerFunc callback is the function in DRUMTIME.C where most of the heavy action takes place. The variable iIndex stores the current beat in the sequence. The callback begins by sending MIDI Note Off messages for the sounds currently playing. An initial -1 value of iIndex prevents this first happening when the sequence first begins.

Next, iIndex is incremented and its value is delivered to the window procedure in DRUM with a user-defined message called WM_USER_NOTIFY. The wParam message argument is set to iIndex so that WndProc in DRUM.C can move the "bouncing ball" at the bottom of the grid.

DrumTimerFunc finishes up by sending Note On messages to the synthesizer for both channels 0 and 9, saving the grid values so that the sounds can be turned off the next time through, and then setting a new one-shot timer event by calling timeSetEvent.

To stop the sequence, DRUM calls DrumEndSequence with a single argument that can be set to either TRUE or FALSE. If TRUE, DrumEndSequence ends the sequence right away by killing any pending timer event, calling timeEndPeriod, sending "all notes off" messages to the two MIDI channels, and then closing the MIDI output port. DRUM calls DrumEndSequence with a TRUE argument when the user has decided to terminate the program.

However, when the user selects Stop from the Sequence menu in DRUM, the program instead calls DrumEndSequence with a FALSE argument. This allows the sequence to complete the current cycle before ending. DrumEndSequence responds to this call by setting the bEndSequence global variable to NULL. If bEndSequence is TRUE and the beat index has been set to zero, DrumTimerFunc posts a user-defined message called WM_USER_FINISHED to WndProc. WndProc must respond to this message by calling DrumEndSequence with a TRUE argument to properly close down the use of the timer and the MIDI port.

RIFF File I/O

The DRUM program can also save and retrieve files containing the information stored in the DRUM structure. These files are in the Resource Interchange File Format (RIFF) recommended for multimedia file types. You can read and write RIFF files by using standard file I/O functions, of course, but an easier approach is provided by functions beginning with the prefix mmio (for "multimedia input/output").

As we saw when examining the .WAV format, RIFF is a tagged file format, which means that the data in the file is organized in blocks of various lengths (called "chunks"), each of which is identified by a tag. A tag is simply a 4-byte ASCII string. This makes it easy to compare tag names with 32-bit integers. The tag is followed by the length of the chunk and the data for the chunk. Tagged file formats are versatile because the information in the file is not located at fixed offsets from the beginning of the file but is instead identified by tags. Thus, the file format can be enhanced by adding additional tags. When reading the file, programs can easily find the data they need and skip tags they don't need or don't understand.

A RIFF file in Windows consists solely of chunks, which are blocks of information in the file. A chunk is composed of a chunk type, a chunk size, and chunk data. The chunk type is a 4-character ASCII tag. It must have no embedded blanks but is possibly padded at the end with blanks. The chunk size is a 4-byte (32-bit) value that indicates the size of the chunk data. Chunk data must occupy an even number of bytes and is padded at the end with an extra zero byte if necessary. Thus, every component of a chunk is word-aligned with the beginning of the file. The chunk size does not include the 8 bytes required for the chunk type and the chunk size, and it does not reflect the padding of the data.

For some chunk types, the chunk size can be the same regardless of the particular file. This is the case when the chunk data is a fixed-length structure containing information. In other cases, the chunk size is variable depending on the particular file.

There are two special types of chunks, called RIFF chunks and LIST chunks. In a RIFF chunk, the chunk data begins with a 4-character ASCII form type, which is then followed by one or more sub-chunks. The LIST chunk is similar except that the data begins with a 4-character ASCII list type. A RIFF chunk is used for the overall RIFF file, and the LIST chunk is used within the file to consolidate related sub-chunks.

A RIFF file is a RIFF chunk. Thus, a RIFF file begins with the character string "RIFF" and a 32-bit value that indicates the size of the file less 8 bytes. (Actually, the file might be one byte longer if data padding is required.)

The multimedia API includes 16 functions beginning with the prefix mmio, specifically designed for working with RIFF files. Several of these functions are used in DRUMFILE.C to read and write DRUM data files.

To open a file using the mmio functions, the first step is to call mmioOpen. The function returns a handle to the file. The mmioCreateChunk function creates a chunk in the file. This uses an MMCKINFO to define the name and characteristics of the chunk. The mmioWrite function writes the chunk data. After writing the chunk data, you call mmioAscend. The MMCKINFO structure passed to mmioAscend must be the same MMCKINFO structure passed earlier to mmioCreateChunk to create the chunk. The mmioAscend function works by subtracting the dwDataOffset field of the structure from the current file pointer, which will now be at the end of the chunk data, and storing that value before the data. The mmioAscend function also takes care of data padding if the chunk data is not a multiple of two bytes in length.

RIFF files are composed of nested levels of chunks. To make mmioAscend work correctly, you must maintain multiple MMCKINFO structures, each of which is associated with a level in the file. The DRUM data files have three levels. Hence, in the DrumFileWrite function in DRUMFILE.C, I've defined an array of three MMCKINFO structures, which can be referenced as mmckinfo[0], mmckinfo[1], and mmckinfo[2]. The mmckinfo[0] structure is used in the first mmioCreateChunk call to create a chunk type of RIFF with a form type of DRUM. This is followed by a second mmioCreateChunk call using mmckinfo[1] to create a chunk type of LIST with a list type of INFO.

A third mmioCreateChunk call using mmckinfo[2] creates a chunk type of ISFT, which identifies the software that created the data file. Following the mmioWrite call to write the string szSoftware, a call to mmioAscent using mmckinfo[2] fills in the chunk size field for this chunk. This is the first completed chunk. The next chunk is also within the LIST chunk. The program proceeds with another mmioCreateChunk call to create a ISCD ("creation data") chunk, again using mmckinfo[2]. After the mmioWrite call to write the chunk data, a call to mmioAscend using mmckinfo[2] fills in the chunk size. That's the end of this chunk, and it's also the end of the LIST chunk. So, to fill in the chunk size field of the LIST chunk, mmioAscend is called again, this time using mmckinfo[1], which was originally used to create the LIST chunk.

To create the "fmt " and "data" chunks, mmioCreateChunk uses mmckinfo[1]; the mmioWrite calls are followed by mmioAscend, also using mmckinfo[1]. At this point, all the chunk sizes have been filled in except for the RIFF chunk itself. That requires one more call to mmioAscend using mmckinfo[0]. There's only one more call, and that's to mmioClose.

It may seem as if an mmioAscend call changes the current file pointer, and it certainly might to fill in the chunk size, but by the time the function returns, the file pointer is restored to its position after the end of the chunk data (or perhaps incremented by one byte for data padding). From the application's perspective, all writing to the file is sequential from beginning to end.

After a successful mmioOpen call, nothing can really go wrong except for the running out of disk space. I use the variable wError to accumulate error codes from the mmioCreateChunk, mmioWrite, mmioAscend, and mmioClose calls, each of which could fail if insufficient disk space is available. If that happens, the file is deleted using mmioOpen with the MMIO_DELETE constant and an error message is returned to the caller.

Reading a RIFF file is similar to creating one, except that mmioRead is called instead of mmioWrite, and mmioDescend is called rather than mmioCreateChunk. To "descend" into a chunk means to locate a chunk and put the file pointer after the chunk size (or after the form type or list type for a RIFF or LIST chunk type). To "ascend" from a chunk means to move the file pointer to the end of the chunk data. Neither the mmioDescend nor mmioAscend functions move the file pointer to an earlier position in the file.

An earlier version of the DRUM program was published in PC Magazine in 1992. At that time, Windows supported two different levels of MIDI synthesizers (called "base" and "extended"). Files written from that program have a format identifier of 1. The DRUM program in this chapter sets the format identifier to 2. It can read the earlier format, however, and convert them. This is done in the DrumFileRead routine.