C Programming

Al implements a C++ version of the Midifile C function libraries that parse MIDI files. He then takes a look at Bjarne Stroustrup's The C++ Programming Language, Third Edition.



May 01, 1998
URL:http://www.drdobbs.com/cpp/c-programming/184410563

MIDIFile: Standard MIDI Format File Parsing

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


Recently, I undertook some projects that involved reading and processing data from files written in the Standard MIDI Format (SMF) -- an architecture that, among other things, records MIDI messages in data streams, also called "sequences." Sequences of MIDI messages are what a sequencer program or device sends in real time to electronic musical instruments to play a production. SMF prescribes a standard format to record the real-time messages on disk, as well as other information -- tempo, for example -- that a playback system needs to accurately reproduce the production, and information meaningful to the human MIDI programmer -- key signature, time signature, and so on. So, irrespective of a particular sequencer's internal representation of these data, if the sequencer can generate an SMF file, other sequencers can play back the production.

Furthermore, a computer program that can read an SMF file can process MIDI production data in support of various applications, which is the point of this discussion.

Each message in an SMF file represents an event that a MIDI system processes in the course of the playback of a musical production. There are three kinds of events: MIDI events, metaevents, and system-exclusive events. Each event includes a time stamp that specifies when the event is to be processed. It is expressed as a delta number of clock ticks counted from the beginning of the track in which the event is stored. More about tracks later.

MIDI events are real-time messages. They play notes on multiple simulated instruments, specify "aftertouch" values, encode controller events (volume, pan, and sustain pedal, for example), process pitch bends, assign channels to instrument sounds (patches), control lighting devices, and so on.

Metaevents specify such things as tempo, how to relate clock ticks to tempo, time signature, key signature, channel assignments, and text fields to name the song, the instruments, and the tracks.

System-exclusive events contain data defined by the manufacturer of the device or program that processes the SMF file.

An SMF file comprises a header chunk followed by one or more track chunks. There are three SMF formats.

All of this suggests that the SMF architecture is complex, bewildering, and begging to be encapsulated so programmers can ignore the details and work at higher levels of abstraction.

MIDI tends to embrace an open, cooperative culture. Much of the software that MIDI users need is available as shareware or freeware. An Internet search revealed two freeware C-function libraries that parse MIDI files. One library is called Midifile (http://www.nosuch.com/ midifile/midifile.zip). It is authored by Tim Thompson and augmented by Michael Czeiszperger. The other library is also called Midifile (http://www.ooblick.com/ software/midifile.tar.gz) and is a rewrite of Thompson's library by Andrew Arensburger. He rewrote the library because he "wasn't quite satisfied with [Thompson's] implementation." Both libraries were written in the 1980s in K&R C, and both work in essentially the same way, with these differences: Arensburger's rewrite does not include Czeiszperger's enhancements to write SMF files; the rewrite does not support system-exclusive events; the rewrite sends error conditions to the library user whereas the original program simply exits the program upon encountering the first error. (Tim's site at http://www.nosuch.com/ includes lots of other MIDI software that programmers might find interesting. In particular, check out his KeyKit MIDI programming environment.)

The Midifile API of both libraries for parsing SMF files comprises a set of function pointers. An application assigns the addresses of custom functions to the pointers and calls a library function to begin parsing the file. The library processes the file and calls each function as the parser encounters the header, the tracks, and the events in the file. It is up to the using program to do something meaningful with the data. If the application does not initialize a pointer, the event associated with that pointer is parsed and bypassed. One notable feature of the Midifile API is the requirement for the application to manage opening, closing, and reading bytes from the SMF file. One of the function pointers expects to point to a user-provided function that returns the next sequential byte in the file.

Czeiszperger's enhancements permit an application to write SMF files. You initialize pointers to write characters to the stream and to write tracks. Then you call a function that writes the header and calls your track function for each track. From within that function, you call functions to write events.

Midifile contains all of the functionality that I needed for my project and more, so I decided to use the two libraries as inspiration for a rewrite in C++.

The MIDIFile Class

Besides representing a complex problem domain in need of object-oriented encapsulation, there are two facets of both Midifile implementations to which C++ is particularly well suited. First, error processing is better handled by C++ exception handling than by the common C idiom of returning error conditions. Second, the function pointer API mechanism is better represented by virtual functions in a C++ base class than by a set of global function pointers that the library user initializes. With C++, the application derives a class from an abstract base class that encapsulates the details of parsing the SMF file. The base class contains virtual member functions that the file parsing algorithm calls for each of the events. The derived class overrides only those virtual functions related to the events that the application wants to view. For example, a MIDI jukebox application would not need to see the time and key signature metaevents, but would need to see the tempo metaevent. The same application might want to display the text that identifies the production -- the song title, for example, but would not be interested in the instrument name text that identifies each track.

The complete project -- including all source code and a MIDI song file that one of the example programs creates -- is available electronically (see "Resource Center," page 3). Midifile.h, for instance, is the header file that defines the MIDIFile class. MIDIFile is an abstract base class. To use it for parsing or generating an SMF file, an application derives a class from it, provides some arguments, and overrides some virtual functions. The MIDIFile member functions are defined in a file named midifile.cpp, which is not published here because of its length.

The file begins with typedefs that define the aliases, Short and Long. The purpose for these typedefs is to ensure that certain data types are 16 and 32 bits in width, necessary data types for some of the internal SMF data representations. When you instantiate an object of a class derived from MIDIFile, the constructor makes the following assertion:

assert(sizeof(Short)==2&&sizeof(Long)==4);

The next part of midifile.h declares the exception classes that MIDIFile throws. First is a #define macro named MFX that abbreviates the declarations. Following that are invocations of MFX to declare classes derived from std::runtime_error and to provide text to describe the nature of the exceptions. (Whenever people deprecate the C++ preprocessor and wish it would go away, I think of handy constructions such as the MFX macro, and I smile.)

A list of const declarations define symbols for the MIDI events, and an EventBuffer class manages the allocation of memory to hold the data values of variable-length events.

Next is the MIDIFile class declaration. Objects of the class can be instantiated to parse an existing SMF file or to create a new one depending on which constructor you use. Each constructor initializes a pointer to either an ifstream or an ofstream object and initialized the other pointer to zero. Subsequent member functions use these values to read or write the SMF file and also to determine whether the object was instantiated for input or output. The constructors and destructor are protected to ensure that you derive a class from MIDIFile rather than instantiating an object of MIDIFile. (Well, almost; a class derived from MIDIFile could itself instantiate a MIDIFile object, but there would be no reason to do so.)

MIDIFile includes private member functions to manage the way that SMF files store numerical values. SMF uses a byte order that is the reverse of that used by Intel processors, so some juggling is necessary when reading and writing those formats between memory and the file. Some values are stored in two bytes. The song's tempo is stored in three bytes. The header length and track length fields are stored in four bytes. All values are treated as twos-complement signed integers. Event data lengths and delta times are stored in a variable-length format wherein all but the last byte has the most significant bit set to one. Only bits 0-7 of the bytes in a variable-length integer contain numerical data. If you download the project, you can view these conversion functions in midifile.cpp.

Reading an SMF File

An application reads an SMF file by first deriving a class from MIDIFile, overriding virtual functions that process each of the MIDIFile components. The constructor of the derived class passes to the base MIDIFile class constructor a reference to an open SMF file. The MIDIFile constructor has a second argument, a bool that tells MIDIFile whether to bypass system-exclusive events (true) or to allocate memory for them on the heap and process them (false). System-exclusive events can involve large memory allocations. This mechanism permits a program to ignore them.

The application opens the SMF file and instantiates an object of the derived class. The application then calls the MIDIFile::ReadMIDIFile() function, which parses the file, calls the virtual functions for each event, and throws exceptions when it finds errors in the file.

What the application does depends on which virtual functions it overrides and what it does with the SMF data. Miditest.cpp (available electronically) is an application that overloads all the virtual functions and displays the contents of the SMF file on the console. The program catches exceptions and displays their text on the console. As such, miditest is a good utility program for testing the validity of SMF files and reporting what and where the problems are. Actually, if I had not written this program first, getting the next one to work and debugging the part of Midifile that creates an SMF file would have been much more difficult.

Each overridden function represents one part of the SMF file. The first function called is the Header function. Next is the StartTrack function for the first track. After that come the metaevent and MIDI-event functions for the track with the EndOfTrack function signifying that there are no more events in the track. The track and event functions repeat for each track in the file.

Each overridden function is passed a delta time value as its first argument. If the application overrides all the functions, the delta value is the same one recorded in the file with the events. Otherwise, the value passed is the delta time since the last event that was intercepted by an overridden function. An overridden function must not call the base class function it overrides. If it does, the delta time computation is compromised.

Writing an SMF File

An application writes an SMF file also by deriving a class from MIDIFile. In this case the constructor passes to the MIDIFile constructor a reference to an ofstream object and variables that specify the SMF format (0, 1, or 2), the number of tracks, and a value that specifies how many clock ticks there are in a quarter note. The derived class overrides the StartTrack virtual function. The overriding function uses the track number to determine which track is to be written, and calls MIDIFile member functions to write each track's events.

Listing One is playmidi.cpp, an application that generates a simple SMF file that plays the first part of the "William Tell Overture" (no royalties to pay, that's why) on the piano and drums. The SMF file is included in the files you can download with this project. The application derives the WmTellOverture class from MIDIFile as explained previously, opens the ofstream file, instantiates an object of the class for the file, and calls the WriteMIDIFile function. Observe that the MIDIFile functions that WmTellOverture::StartTrack calls to write events to the track are the same virtual functions that an application overrides to read an existing SMF file.

MIDIfile Enhancements

Even though the MIDIFile class encapsulates much of the SMF file processing, you still view it from a relatively low level. The application has to know how to convert between musical and SMF notation. For example, an application has to know that the denominator of a time signature is really a power of two so that a march in 6/8 time is expressed as 6,3 (six and two raised to the third power). I don't know why, but that's how they did it. Key signature is a number between -7 and +7 to specify the number of flats or sharps and a boolean integer to indicate whether the key is minor (1) or major (0). Tempo and event time is another enigma. The header specifies the number of delta field clock ticks in a quarter note unless the number is negative, in which case it represents the number of ticks in a second. The tempo field itself is the number of microseconds in a quarter note. Notes are just numbers rather than in notation that musicians would understand. There's more, but I won't go into it here. The point is that to use Midifile, you have to understand these algorithms. A class library at a higher level of abstraction would understand musical notation, implement that in the API, and do the conversions for you. As I use this library, I expect to understand better where further encapsulation will make it more usable.

The C++ Programming Language, Third Edition

Bjarne Stroustrup's The C++ Programming Language, Third Edition (Addison-Wesley, 1997) has been available for several months. This work, by the creator of C++, is the definitive treatment of the subject and has been since its first edition in 1987. I must confess that I did not care for the first edition. I had expected a tutorial approach as elegant as the classic K&R white book. But then, K&R was about C, a programming language that supported a familiar programming model. The C++ programming model was new to most of us ten years ago, and Stroustrup's first edition was daunting, to say the least. Looking at it now, I find it far less so and much easier to read.

Comparing the first and third editions of The C++ Programming Language provides insight into how the C++ language has grown and changed in the past decade. The third edition has almost three times the number of pages and a slightly different organization. Whereas the first edition included a 67-page language reference manual at the end, the third edition includes only a language grammar section to represent formal language definition. This is appropriate. The ANSI/ISO Standard document, which is now the formal language and library definition, is itself about 750 pages long. Stroustrup plans to publish The Annotated C++ Language Standard (coauthored by Andrew Koenig, the ANSI C++ committee's Project Editor) sometime this year.

The third edition takes a tutorial approach with many of Stroustrup's personal programming philosophies. The author's explanations of how he uses language features provide examples for learning the behavior of those features. He also explains code idioms that some programmers routinely use but that he finds inappropriate.

As much as possible, the third edition reflects Standard C++. When small language features are found to be missing, particularly new ones, Stroustrup pledges to add them to a future printing.

The book includes many code examples. There is no diskette or CD-ROM, because Stroustrup avoids a teaching approach wherein readers compile and run examples. His examples are mostly code fragments that demonstrate the points he makes and the issues he addresses. The code fragments are readable, meaningful, and neither frivolous nor cute, and since you do not compile them, you need not worry that your compiler does not fully support Standard C++.

There are four parts to the body of the book: "Part I: Basic Facilities;" "Part II: Abstract Mechanisms;" "Part III: The Standard Library;" and "Part IV: Design Using C++." Even if you are already a seasoned C++ programmer, Part IV, which is a rewrite of several chapters from the second edition, is worth the price of the book. It describes Stroustrup's philosophies on the design and development cycle of a software project involving C++. In his words, Part IV aims "to bridge the gap between would-be language-independent design and programming that is myopically focused on details."

The three appendixes are: "A: The C++ Grammar;" "B: Compatibility;" and "C: Technicalities." Appendix B discusses the differences between C and C++ and explains how the languages have become more compatible over time. Some of this convergence results from changes being made to the C specification (double-slash comments and no implicit int, for example). The appendix also discusses the issues related to porting C++ code from older C++ implementations, advising that, where possible, you should use the latest implementation of a compiler so that newer features are available to you.

Appendix C is about technical details that a programmer faces that are not necessarily language issues. I particularly like the discussion on the problems associated with traditional multidimensioned arrays as compared to using STL containers to achieve the same result without the headaches.

This book is an essential addition to a C++ programmer's library. It is not for dummies, and it wouldn't be my first choice for an entry-level, self-help tutorial on C++ for beginning programmers. It is, however, an excellent textbook for programmers who are self-motivated and students who study under the watchful care of a skilled instructor. As an experienced C++ programmer, I find the book useful as a reference to language usage and behavior. The author invented the language and then stayed close to the standardization and innovation process for the duration, always maintaining a careful vigilance over the evolution of his brainchild. Consequently, this book serves, for those who do not care to pore over the ANSI/ISO document (or the promised annotated version), as the authority on the Standard C++ language, how it works, and how you should use it.

DDJ

Listing One

// ------ playmidi.cpp#include <fstream>
#include <iostream>
#include "midifile.h"


class WmTellOverture : public MIDIFile { void StartTrack(int trackno); static int m_nNotes[][2]; public: WmTellOverture(std::ofstream& rfile) : MIDIFile(rfile, 1, 3, 120) { } }; static const int iv = 100; // note interval static const int nt = 60; // 1st note of song int WmTellOverture::m_nNotes[][2] = { // {note, delta} {nt,0},{nt, iv/2},{nt,iv/2},{nt,iv},{nt,iv/2},{nt,iv/2}, {nt,iv},{nt, iv/2},{nt+5,iv/2},{nt+7,iv},{nt+9,iv},{nt,iv}, {nt,iv/2},{nt,iv/2},{nt,iv},{nt,iv/2},{nt+5,iv/2},{nt+9,iv}, {nt+9,iv/2},{nt+7,iv/2},{nt+4,iv},{nt,iv},{nt,iv},{nt, iv/2}, {nt,iv/2},{nt,iv},{nt, iv/2},{nt,iv/2},{nt,iv},{nt,iv/2}, {nt+5,iv/2},{nt+7,iv},{nt+9,iv},{nt+5,iv},{nt+9,iv/2}, {nt+12,iv/2},{nt+10,iv*3},{nt+9,iv/2},{nt+7,iv/2}, {nt+5,iv/2},{nt+9,iv},{nt+5,iv}, {0,iv} }; void WmTellOverture::StartTrack(int trackno) { switch (trackno) { case 1: // ---- tempo track TextEvent(0, META_SEQTRKNAME, 21, "William Tell Overture"); Tempo(0, 250000); break; case 2: // ---- piano track TextEvent(0, META_SEQTRKNAME, 5, "Piano"); ProgramChange(0, 0, 0); // channel 0 = acoustic piano // --- play the notes int i; for (i = 0; m_nNotes[i][0] != 0; i++) { if (i > 0) NoteOff(m_nNotes[i][1],0,m_nNotes[i-1][0],0); NoteOn(0,0,m_nNotes[i][0],64); } NoteOff(m_nNotes[i][1],0,m_nNotes[i-1][0],0); break; case 3: { // ---- drum track const int shot = 42; const int crash = 49; TextEvent(0, META_SEQTRKNAME, 5, "Drums"); ProgramChange(0, 9, 0); for (i = 0; m_nNotes[i][0] != 0; i++) NoteOn(m_nNotes[i][1],9,shot,64); NoteOn(iv,9,crash,100); break; } default: break; } } int main() { std::ofstream ifile("WTO.mid", std::ios::binary); WmTellOverture wto(ifile); wto.WriteMIDIFile(); return 0; }

Back to Article


Copyright © 1998, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.