Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

Jukebox: Covering the Basses


Jun99: C Programming

Al is a DDJ contributing editor. He can be contacted at [email protected].


Many of you know that, along with programming, music is my passion. Acoustic music and acoustic jazz music, in particular, have always been a large part of my life. I prefer acoustic over electronic music for many reasons, one being that when I was a youthful budding jazz piano player, electronic music was restricted mostly to amplified guitars. My roots aside, acoustic music contains physical elements that reach into the souls of most players. We resonate to the variations and harmonics produced by acoustic instruments when played by human players. Such sounds come from natural components that strike each other and vibrate in response to the human touch -- wooden reeds, gut and steel strings, skin drum heads, felt hammers, brass bells -- and random and accidental variations involving lip pucker and vibrations, air fluctuations, spit, the dynamics of arms, hands and fingers, feet pressing mechanical pedals, and so on. It's a human thing.

Computers can only approximate some of these things. Computer music might someday replace acoustic music made by human beings, but that time is far away, waiting either for Holodecks to be perfected or for a generation of listeners to arrive who are too busy and too uninvolved with their ambient surroundings to want to relate to them -- a generation that likes artificial fireplaces, plastic houseplants, and disposable appliances. Well, maybe that's not all that far away, but in the meantime...

Electronic music as music does not move me, but the technology that produces it fascinates me as a programmer. MIDI, the Musical Instrument Digital Interface, combines music and technology to solve certain problems, ones that are not usually among the concerns of acoustic musicians. MIDI describes a format of data packets that tell electronic instruments to play specific notes at specific times during a performance. You can make a lot of interesting sounds and music with MIDI, but they do not inspire or interest many jazz players.

One area where MIDI really excels, though, is its ability to reproduce a human player's acoustic piano performance. A piano is a unique percussive and expressive acoustic instrument. Its dynamics are a function of key combinations and attack. You can't bend notes like you can on a brass or reed instrument. Whereas a horn responds to combinations of lips, breath, saliva, and tongue, a piano knows only three things -- what key did you press, how hard did you hit it, and how long did you hold the key down. (One additional variation occurs when the sustain pedal is depressed such that the other strings vibrate with sympathetic harmonics to the strings being struck.) Contemporary MIDI playback devices can approximate most of this with such accuracy that many listeners cannot tell whether a song is being played by a live pianist at a real piano or by a sequencer and tone generator that produces the sampled sounds of a real piano playing a MIDI sequence as recorded by a live player. You don't get that kind of sound from the garden variety SoundBlaster, but high-end professional sound cards typically include very good acoustic piano samples.

I am a saloon piano player, and I play acoustic pianos whenever possible. MIDI for me is a medium for acquiring or building tools that assist practice. (An earlier column project, MidiFitz, was one such tool.) I can record a piano performance, and the sequencer lays down each track as a series of event data packets rather than as a stream of audio waveform samples. When I play the sequence back, it sounds like me, mistakes and all. If I missed a note or messed up a passage, I can repair the problem by using the editing features of a sequencer program. I can also play the hard passages slowly enough to get through the piece and then increase the tempo for playback. Sure, those tricks are cheating, but they're better than recording a difficult song over and over until someday I get it right.

Years ago I played the trumpet, trombone, and double bass in addition to the piano. I retired those instruments in the 1980s mainly to concentrate on the piano but also because I rarely found pianists who played what I wanted to hear. Recently, I have taken up the double bass again, but because time and neglect erode ability, calluses turn soft, and muscle memory fades, I need to woodshed (practice) to get back into shape and build up my chops. The string bass as a jazz instrument is best practiced in the context in which it is played -- with a group or at least with a piano player. I need a piano player whose style and harmonic conceptions match mine. Not wanting to find and hire someone, and being such a piano player myself, I decided to record a series of MIDI piano renditions so that I can play with myself (musically, of course). If I don't like the piano player it's my own fault. These sequences have become my primary bass practice regimen. MIDI plays the piano and I plunk along on the bass, getting better, I hope, with each session. Perhaps later I'll add audio bass tracks, a few comping choruses, and use the recordings to practice playing the horns. Then, who knows, perhaps an album of music on which I play all the parts. The Al Stevens Quartet. Grammy, here I come.

Why not use audio tapes, you might ask? I want the flexibility of fast song selection and key signature and tempo changing that tape does not provide. How about WAV files? Still no tempo or key changing (without long processing times) and too much hard disk space. I have about 300 songs recorded with which I practice and as many more on the list to record. MIDI packets are more economical than WAV files, and they allow me to change all kinds of things without rerecording the piece. Wonderful stuff, this MIDI.

MIDI Software

Now, about the software. (Good, you say, you were wondering when this "C Programming" column was going to get around to discussing software.) An advantage of being a programmer is that when available software will not do, you can write your own. There is a lot of MIDI software, but I never found a program that does what I need. Sequencer programs load and play one file at a time. A double bass is an awkward instrument. The computer controls are too far away when you are standing with your arm wrapped around that big bass fiddle, so even one-handed computer operation is inconvenient and error-prone. You must put the bass aside and sit at the computer to use the mouse and keyboard to load the next file. Takes too much time. Clearly I need a jukebox program to play a set of songs from a list.

MIDI jukebox programs abound, but most of them have three shortcomings. First, they go immediately from one song to another, not giving me time to look at what's coming and rest my weary hands. Second, they have no count-off. A bass player needs to know how fast the piano player is going to play the song. On the bandstand, someone usually says, "One, two, three, four." Third, they have no metronome. Half of a bass player's job is keeping time. (The other half is playing correct notes, of course.) A metronome is a good training device for learning to keep accurate time.

Jukebox

To solve those problems, I developed Jukebox, the project for this month. Jukebox maintains a list of Standard MIDI Format (SMF) files in a dialog-based MFC application. As you select a song from the list of titles, the dialog displays the song's tempo and key and time signatures. The program lets you organize the list, modify the tempo for each song, specify a number of seconds to wait between songs, toggle a metronome, and say how many measures of count-off to play at the beginning of each song. You can start playing anywhere in the list. Jukebox remembers the last song you played in each session, selecting the next song for the next session. Figure 1 is the Jukebox application dialog. The right pointing arrow icon is on the Play button. The up and down arrow buttons let you move the selected song up and down in the list. You can download the Jukebox source code; see "Resource Center," page 5. I'll discuss the more interesting aspects of the program here.

MIDIFile, MIDIInfo, and MIDIPlayer

In May of last year I wrote about the MIDIFile class library, which supports reading and writing SMF files. To read SMF files, a program derives a class from MIDIFile and overrides the member functions that process the MIDI events the program wants to process.

I modified MIDIFile for Jukebox to support a feature it did not have. Jukebox builds its list of files by reading the first track from the SMF file where it finds the MIDI events that describe the song title, tempo, and key signature. MIDIFile scans an entire SMF file looking for selected events in all the tracks. It had no mechanism to tell it to interrupt the scan and close the file. Jukebox took too long to build the list of song titles because MIDIFile scans the entire file for each song, yet the information for the list is at the beginning of each file in track 1. I added a StopReading member function to the MIDIFile class. When Jukebox's derived MIDIInfo class sees a start track event that is not for track 1, it calls StopReading, which stops the SMF file scan.

Time out. Isn't a pure object-oriented programmer supposed to use inheritance to change the behavior of an existing class? I suppose so, but this change adds a feature to an existing class, and the addition of the feature has no impact on programs that are compiled with the previous version. You'd have to recompile them if you wanted to use a common header file for all applications, old and new, but that's all.

To read an SMF file for playback, Jukebox derives the MIDIPlayer class from MIDIFile and intercepts the real-time events that control playback of the sequence.

Time out again. Isn't inheritance supposed to reflect an IS-A relationship between derived and base classes? MIDIFile is an abstract bass class that reads SMF files. MIDIInfo is a class that gathers information about the song in an SMF file. MIDIPlayer is a class that plays back the contents of an SMF file through the computer's MIDI system. Can the two derived classes really be considered specializations of the base class? Is this really an IS-A relationship? Probably not, but MIDIFile is designed so that the derived class overrides functions to intercept MIDI events in the file. MIDIFile's purpose is to be the file reading and event parsing engine that a derived class uses to select only those events it wants to process. It hides the details of those operations from its descendent classes. The inheritance mechanism is particularly good for expressing this kind of abstraction because overridden virtual functions in the derived class intercept the events the application cares about and the absence of overridden functions bypasses events that the application does not care about. MIDIInfo is unconcerned about the real-time events. It needs to get the song title, tempo, and time and key signatures to display them in the application dialog window. MIDIPlayer is concerned mainly about playback events (although the tempo event is one concern the two derived classes share).

The lesson learned here is that purist programming dogma -- even the object-oriented agenda -- is not always the only solution and does not always deliver the best implementation.

The Sequencer

MIDIPlayer is a miniature sequencer, which is a program that plays sequences through a MIDI system. Listings One and Two are midiplayer.h and midiplayer.cpp, the source code files that implement the sequencer. MIDIPlayer loads the real-time playback events into one std::vector per track and plays the events back. The events that Jukebox needs are Note On, Note Off, Controller Change, and Program Change. The functions of the first two event types are obvious from their names. Controller Change tells the device associated with the channel to change some kind of controller. On a piano, this controller is usually the sustain pedal. Program Change tells the device to use a different "patch," which is General MIDI jargon for the instrument sound selected from among 128 different instruments.

An SMF file does not store track events together in one merged stream. Instead, the file contains all the events for one track, followed by all the events for the next track, and so on. Each event has a delta time stamp specifying how much time to wait during playback since the previous event for that track before activating the current event. MIDIPlayer collects all those events into memory vectors when the program calls MIDIFile::ReadMIDIFile(). When the program calls MIDIPlayer::Play(), the function sets a real-time timer that ticks once every millisecond. The timer calls the MIDIPlayer::TimingMessage function. This is where the sequencing is actually performed.

At each tick of the timer, the program checks each track vector to see if an event in the vector is due to be activated. Another vector stores offsets into the track vectors to indicate which is the next unactivated event.

The granularity of the timer presents an unusual problem. Win32's real-time timer has a resolution of no better than one tick per millisecond. Yet the delta time ticks in some SMF files can specify a much higher resolution. Irrespective of the tempo of a song or the 32nd note resolution of an arrangement, the player can press and release notes at any time at all. It's called interpretation. A program has to process these events such that the tempo of the song and the proximity of the notes played is as close to the original rendition as possible. To understand this problem, download and play a sequence of something like "Rhapsody In Blue," wherein the MIDI programmer entered notes exactly as Gershwin (and Grofe) wrote them down. The sequence sounds wooden and mechanical because every 16th note is exactly a 16th note and so on. Now listen to a sequence of the same composition wherein a performer played the composition on a MIDI keyboard into a sequencer that recorded the performance in real time. If the instrument tone generation is any good, the playback sounds real.

How do you fire events that might come at you with delta times that represent a finer resolution than the timer can handle? You can't, but you can get a close approximation by compromising accuracy. For the solution I went to Maximum MIDI, by Paul Messick (Manning Publications, 1998, ISBN 1-884777-44-9), wherein Paul explains how to use two integers to simulate floating-point precision for the conversion of the tempo and delta time into a number that determines whether it's time to activate an event. This book is a valuable resource for anyone who wants to write MIDI software that runs under Windows. It will save you a lot of research and experimentation.

MIDI Mapper

Most sequencer programs include dialogs that let you specify which MIDI channels go to which MIDI devices. A real-time MIDI event is directed to a channel (not to be confused with a track, the two of which are often confused by MIDI novitiates). Computers can have more than one MIDI output device. Every contemporary SoundBlaster and most other sound cards include an internal synthesizer and an external MIDI OUT jack. You might want to play the drums on your sound card and the piano through the samples on your electronic keyboard. Jukebox does not include such a selection because Windows 95/98 already have one built in. The Control Panel's Multimedia applet allows you to specify custom configurations of channel assignments. An application can direct its real-time MIDI event output to the MIDI Mapper device, which uses these assignments.

The SMF File List

Jukebox maintains a list of songs that it will play. It maintains that list in the system Registry. I thought about using a database, considered the additional code it would take, and decided not to do it. My practice regimen uses the same set of songs mostly and Jukebox now works well for me. I am now happily plunking and bowing away whilst my favorite piano player tinkles. If you use Jukebox and need more than one persistent list of tunes, I suggest that you add Key values to the Registry to represent them. But, whatever you do, please don't send me e-mail chastising me because my program is deficient. I add this request because that happens a lot. Many readers of my books have written, "Dear Al: Why didn't you write your programs the way I would have written them to make them more usable to the vast majority of users?" To them I say: "You are programmers. The source code is yours. Modify it." Of course, readers of this column never make such comments, I am happy to say. Doorknob.

DDJ

Listing One

// ----- midiplayer.h
#ifndef MIDIPLAYER_H
#define MIDIPLAYER_H
#include "stdafx.h"
#include "midiinfo.h"
// ---- realtime midi event data
struct MIDIEvent    {
    Long delta;
    Short eventno;
    Short channel;
    Short param1;
    Short param2;
};
// ---- midi event data ready for sequencing
struct MIDIData {
    Long delta;     // delta time from beginning of sequence
    DWORD data;     // midi event packet
   // --- these are to let the type be contained in a std::vector
    bool operator<(const MIDIData&) const
        { return true; }
    bool operator==(const MIDIData&) const
        { return true; }
};
// ------- class for sequencing a Standard MIDI Format file
class MIDIPlayer : public MIDIFile  {
    long division;          // delta time ticks per quarter note
    CWnd* owner;            // window to notify when sequence is done
    // ----- the ticking clock variables
    long clock, period, nticks, fticks, trtime;
    Long delta;             // running delta time accumulation
    std::vector<MIDIData> track;    // track vector of events being gathered
    // --- vector of tracks
    std::vector<std::vector<MIDIData> > tracks;
    // --- vector of track event offsets
    std::vector<int> trkndx;
    HMIDIOUT hMidiOut;
    UINT timer;
    TIMECAPS tc;
    long tempo;   // microseconds per quarter note
    bool ticking; // semaphore to wait for timing message function to complete
    int countoff; // number of measures to count off
    bool metronome;         // true for metronome during playback
    int divctr;             // counts for metronome ticks
    int beatspermeasure;    // number of beats per measure (3, 4, ...)
    // --- overridden MIDIFile class functions
    void Header(Short fmt,Short trks,Short div)
        { division = div; }
    void StartTrack(int trkno)
        { delta = 0; }
    void EndOfTrack(Long delta);
    void TimeSignature(Long delta,Short numer,
                                Short denom,Short clocks,Short qnotes);
    void NoteOn(Long delta,Short channel,Short note, Short velocity);
    void NoteOff(Long delta,Short channel,Short note, Short velocity);
    void Controller(Long delta,Short channel,Short controller, Short value);
    void ProgramChange(Long delta,Short channel,Short program);
    // ----- private member functions
    void StoreEvent(const MIDIEvent& mev);
    void KillTimer();
    void StopMIDI();
    // ----- timer mechanism
    friend void CALLBACK TimerCallback(UINT, UINT, DWORD, DWORD, DWORD);
    void TimingMessage();
    static MIDIPlayer* pplay;   
                       // = "this" so TimerCallback can call TimingMessage
public:
    explicit MIDIPlayer(std::ifstream& rFile);
    // ---- play SMF file with count measures of count-off, tempo of tmpo, and
    //      metronome click if metr (if tmpo == 0, use tempo from SMF file)
    void Play(long tmpo, int count, bool metr);
    void StopPlay();                // stop playing the sequence
    void Reset();                   // reset the midi system
    void RegisterWindow(CWnd* wnd)  // register a window to 
                                    //       notify when sequence is done
        { owner = wnd; }
    void Metronome(bool onoff)      // turn the metronome on or off
        { metronome = onoff; }
    void ChangeTempo(long t)
        { tempo = t; }
};
#endif

Back to Article

Listing Two

// ---- midiplayer.cpp
#include "stdafx.h"
#include "midiplayer.h"

MIDIPlayer::MIDIPlayer(std::ifstream& rFile) : MIDIFile (rFile) 
{
    hMidiOut = 0;
    timer = 0;
    division = 5;
    owner = 0;
    ticking = false;
    countoff = 0;
    metronome = false;
    beatspermeasure = 4;
}
void MIDIPlayer::EndOfTrack(Long)
{
    if (track.size())   {
        // a track of realtime events has been accumulated, save it
        tracks.push_back(track);
        track.clear();
    }
}
void MIDIPlayer::TimeSignature(Long delta,Short numer,
                                   Short denom,Short clocks,Short qnotes)
{
    beatspermeasure = numer;
}
inline void MIDIPlayer::StoreEvent(const MIDIEvent& mev)
{
    delta += mev.delta;
    DWORD dwEvent  = mev.eventno | mev.channel | 
                             (mev.param1 << 8) | (mev.param2 << 16);
    MIDIData data = { delta, dwEvent };
    track.push_back(data);
}
void MIDIPlayer::NoteOn(Long delta,Short channel,Short note, Short velocity)
{
    MIDIEvent mev = { delta, MIDI_NOTEON, channel, note, velocity };
    StoreEvent(mev);
}
void MIDIPlayer::NoteOff(Long delta,Short channel,Short note, Short velocity)
{
    MIDIEvent mev = { delta, MIDI_NOTEOFF, channel, note, velocity };
    StoreEvent(mev);
}
void MIDIPlayer::Controller(Long delta,Short channel,Short controller, Short value)
{
    MIDIEvent mev = { delta, MIDI_CONTROL, channel, controller, value };
   StoreEvent(mev);
}
void MIDIPlayer::ProgramChange(Long delta,Short channel,Short program)
{
    MIDIEvent mev = { delta, MIDI_PROGRAM, channel, program, 0 };
    StoreEvent(mev);
}
MIDIPlayer* MIDIPlayer::pplay;
void CALLBACK TimerCallback(UINT, UINT, DWORD, DWORD, DWORD)
{
    MIDIPlayer::pplay->TimingMessage();
}
void MIDIPlayer::Play(long tmpo, int count, bool metr)
{
    if (midiOutOpen(&hMidiOut, MIDIMAPPER, 0, 0L, 0L) == 0) {
        nticks = fticks = 0;    // integer representation of 
                                //    integral and fractional parts of clock
        period = 1;             // time slice in milliseconds
        clock = 0;              // accumulated time
        trtime = (period * 1000) * division;
        countoff = count * beatspermeasure + 1;
        metronome = metr;
        if (tmpo)
            ChangeTempo(tmpo);      // playing at a specified tempo
        divctr = 0;
        for (int i = 0; i < tracks.size(); i++)
            trkndx.push_back(0);
        pplay = this;
        ticking = false;
        timeGetDevCaps(&tc, sizeof tc);
        timeBeginPeriod(tc.wPeriodMin);
        timer = timeSetEvent(period, tc.wPeriodMin, 
                                     TimerCallback, 0, TIME_PERIODIC);
        DWORD mmsg  = 0xfa;     //.start message
        midiOutShortMsg(hMidiOut, mmsg);
    }
}
void MIDIPlayer::TimingMessage()
{
    if (hMidiOut)   {
        ticking = true;
        // --- integral part of tick
        nticks = (fticks + trtime) / tempo;
        // --- fractional part of tick
        fticks += trtime - (nticks * tempo);
        // ---- process the count-off and the metronome
        if (divctr <= 0)    {
            // --- at a quarter note beat
            if (countoff)   {
                // --- in the count-off
                if (--countoff) {
                    DWORD ev  = MIDI_NOTEON | 9 | (37 << 8) | (80 << 16);
                    midiOutShortMsg(hMidiOut, ev);
                }
            }
            if (countoff == 0 && metronome) {
// ---- play metronome (except during count-off)
               DWORD ev  = MIDI_NOTEON | 9 | (37 << 8) | (80 << 16);
                midiOutShortMsg(hMidiOut, ev);
            }
            divctr = division;
        }
        divctr -= nticks;
        if (countoff == 0)  {
            // ---- sequencer code
            bool stillplaying = false;
            // --- scan the tracks for realtime midi events due for playing
            for (int i = 0; i < tracks.size(); i++) {
                // --- see if there are more events this track
                if (trkndx[i] < tracks[i].size())   {
                    stillplaying = true;
                    MIDIData& ev = tracks[i][trkndx[i]];
                    while (ev.delta <= clock)   {
                        // fire this event
                        midiOutShortMsg(hMidiOut, ev.data);
                        trkndx[i]++;
                        if (trkndx[i] == tracks[i].size())
                            break;
                        ev = tracks[i][trkndx[i]];
                    }
                }
            }
            if (!stillplaying)
                StopPlay();
            clock += nticks;
        }
        ticking = false;
    }
}
void MIDIPlayer::KillTimer()
{
    if (timer)  {
        timeKillEvent(timer);
        timer = 0;
        timeEndPeriod(tc.wPeriodMin);
    }
}
void MIDIPlayer::StopMIDI()
{
    if (hMidiOut)   {
        DWORD mmsg  = 0xfc;     // stop message
        midiOutShortMsg(hMidiOut, mmsg);
        midiOutClose(hMidiOut);
        hMidiOut = 0;
    }
}
void MIDIPlayer::StopPlay()
{
    KillTimer();
    StopMIDI();
    if (owner)
        owner->SendMessage(MM_MCINOTIFY, 0, 0);
}
void MIDIPlayer::Reset()
{
    KillTimer();
    while (ticking) // wait for TimingMessage to return
        ;
    if (hMidiOut)   {
        // --- all notes off, all channels
        DWORD ev;
        for (unsigned char channel = 0; channel < 16; channel++)    {
            ev = 0x7bb0 | channel;
            midiOutShortMsg(hMidiOut, ev);
        }
        ev = 0xff;  // system reset message
        midiOutShortMsg(hMidiOut, ev);
        StopMIDI();
    }
}

Back to Article


Copyright © 1999, Dr. Dobb's Journal

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.