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++

A MIDI Class in C++


SP 96: A MIDI Class in C++

George has been a member of the information systems/decision sciences department at Loyola College in Baltimore since 1987. He can be reached at [email protected].


The musical instrument digital interface (MIDI) specification spells out a compact, digital representation of a piece of music. The MIDI file format was established under the auspices of the International MIDI Association to standardize the way keyboards and synthesizers send and receive musical data. By the mid-1980s, PCs were being used to control synthesizers, and MIDI programming began to evolve. Most of us who have programmed for MIDI have built on the work of Jim Conger and Michael Czeiszperger, authors of numerous books and articles on the subject. Their work was the inspiration for this article.

In connection with one MIDI project, I needed to convert some old MIDI routines in C to C++. Because the hierarchical structure of a MIDI file lends itself to the object paradigm, implementing MIDI object classes seemed to be the ideal approach. The abstraction of the object classes greatly simplifies the programming (once the classes are written, of course).

Given MIDI classes with basic functionality, application programs for MIDI-file manipulation are greatly simplified. Quick-and-dirty programs of three or four lines can read in a MIDI file, display it as a human-readable musical score, add a missing key signature, extract a particular track to another MIDI file, or transpose the key of the file (with appropriate key-signature changes for scoring programs). In this article, I'll present MIDI classes and several example programs.

MIDI Files and Events

A MIDI file consists of a collection of one or more tracks. Format 0 MIDI files have all events in one track; format 1 MIDI files (the most common) have multiple tracks. The "conductor track" in a format 1 file contains all necessary information on tempo, key signature, title, copyright notice, and the like. The conductor track is usually track 1. Other tracks contain musical information, usually one track per instrument represented in the piece of music. Each instrumental or channel track consists of many events. Channel events produce, change, or stop a musical note. Such events include a note-on event, a note-off event, an event signaling action of a pedal or lever, an event modifying the sustain or volume of a note, or a pitch-changing event.

Along with the usual channel events, there are other events called "metaevents," which include the information events in the conductor's track, instrument names, lyrics, and cue points. System-exclusive events signal the beginning or continuation of a series of arbitrary bytes, of meaning only to the particular device receiving them. No matter what the type, each MIDI event consists of a time, type signature, and series of data bytes.

Listing One presents the event header file, EVENT.H. Note that the MIDI event class has three private data members. The m_DeltaTime member is the elapsed time since the last event in the same track. The time is measured in arbitrary ticks. Actual elapsed time depends on the tempo established in the conductor's track. Member m_EventType is one of an enumeration of types:

  • Channel is an event on a certain channel, such as a note.
  • Meta is a metaevent, such as text.
  • SysEx is an event exclusive to a certain type of MIDI equipment.
  • SysExCont is a continued system exclusive.
  • Undefined is an unknown type of event.
  • Error indicates an error condition.

Member m_Data is an array of bytes of type CByteArray. The class CByteArray takes advantage of the collection classes that are available with IBM's VisualAge C++ for OS/2. Using the statement, typedef ISequence <char> CByteArray allows you to take advantage of the ISequence template. This gives you access to an efficient implementation of an abstract class with a complete, systematic combination of basic properties; see Figure 1. Using the ISequence template gives you a byte array with a full set of methods for adding, locating, testing, and removing the data bytes of a MIDI event.

Listing One shows the usual complement of methods for an abstract class: constructors, destructors, operations, and member get and set functions. The workhorse functions--WriteData() and ReadData()--handle MIDI event I/O to and from a stream. There's also a Printf() function that outputs a formatted, readable MIDI event dump to cout. The implementation of the event functions appears in Listing Two. ReadData() is the most elaborate, because it has to cope with the wide variety of MIDI events, plus some other wrinkles. For one thing, different channel events have different numbers of data bytes involved. This is easily handled (after Czeiszperger) with a static table of additional bytes needed.

Another wrinkle with reading (and writing) MIDI events is the concept of running status. This concept is used to cut down on the number of bytes that must be transmitted in the MIDI stream. If running status is set, the byte indicating the type of event is skipped. It's assumed that the current event type is the same as the last. Since most of the events are four bytes long, this usually yields a 25 percent savings. When the high bit in the high-order byte is set, it indicates running status is set.

A third wrinkle is apparent in the private methods of the event class. The GetVar, PutVar, ReadVar, and WriteVar functions are necessary to deal with variable-length numbers. In the interest of efficiency, the MIDI specification requires that the number of bytes used to represent a number be minimal. Only the number of bytes needed to represent a number is used. In a variable-length number, the last byte has the most-significant bit (MSB) cleared; all other bytes have it set. As a variable-length number is read, bytes are read until one with the MSB cleared is found. The 7-bit values are then reassembled into 8-bit bytes.

Also apparent in Listing Two is the heavy use of the ISequence collection class methods as member functions of the m_Data byte array. Functions such as numberOfElements(), elementAtPosition(), and addAsLast() all come from the ISequence template in Figure 1.

The last line of code in Listing One, typedef ISequence <CEvent> CEventArray; again makes use of the ISequence template to generate a new class, CEventArray. The CEventArray class, to be used as a member of each MIDI track, has all the functionality declared in Figure 1.

MIDI Tracks

A MIDI track consists of a header, track length, and number of events. The track class defined in track.h (available electronically; see "Availability," page 3) represents a MIDI track. Every MIDI track has a track signature, MTrk. Since all tracks have the same signature, you can represent the header as a static member. I make use of the String class, also included with VisualAge C++, to store the track header. The String class is similar to string-handling routines commonly found in C++ textbooks. Track length is stored as an unsigned 32-bit integer. The declaration uses UINT32, typedefed as an unsigned long. The events that make up the rest of the track are handled compactly by the CEventArray type, typedefed to an ISequence of CEvents.

track.h also shows the usual constructors, destructors, operators, get and set functions, and utility functions. These are implemented in the MIDI track class implementation; see track.cpp (available electronically). Implementation of the track class is much shorter than the implementation of the event class, because you're able to build on the event code as well as on the ISequence code.

ReadTrack, the track class member function for reading a MIDI track, simply clears an event buffer, calls ReadData for the event buffer, adds the event to the CEventArray member, and loops until no more events remain. The WriteTrack function steps through the CEventArray, calling the event WriteData function for each. Both the ReadTrack and WriteTrack functions make use of utility functions for reading and writing 32-bit unsigned integers. These functions, adapted from David Charlap (see "The BMP File Format, Part I," Dr. Dobb's Journal, March 1995), are available electronically. They let you handle 16- and 32-bit quantities without worrying about the underlying byte-ordering of the host platform. The track implementation closes with another invocation of the ISequence template. This gives all the functionality of Figure 1 for a collection of MIDI tracks.

MIDI Files

The file midi.cpp (available electronically) shows the definition of the MIDI file object. The file midi.h (also available electronically) contains the private members of the object. Each MIDI file begins with the header MThd. The m_MThd member is stored as a static String object, since you only need one copy no matter how many MIDI files you have. The header length m_HeaderLength can also be stored as static, because it never varies from file to file.

The MIDI file specification defines three formats, called simply "0," "1," and "2." A piece of music can be stored in a MIDI file of any of the three formats and still sound the same when the file is played. It's just a matter of how the data is stored internally. In type 0 format, all information is stored in a single, multichannel track. Type 1 format, the most common, stores several simultaneous tracks with the same tempo and time signature. Type 2 allows for multiple tracks, each with its own tempo and time signature. Only types 0 and 1 are supported by this code.

The number of tracks in the file is stored in m_TrackCount. The member m_Division contains information about timing. If the most significant of the 16 bits is cleared, the remaining 15 bits indicate the resolution of a quarter note in ticks per quarter note. If the MSB is set, the rest of the high byte is the Society of Motion Picture and Television Engineers (SMPTE) frame rate, and the low byte is the resolution of a frame in ticks per frame. SMPTE timing is a standardized time code used in the analog world of tape and video recording. In such applications, SMPTE timing can synchronize MIDI events to external events, such as actions on an accompanying video tape track. SMPTE timing is not supported in this code. The last member of the MIDI object is m_TrackArray, the collection of one or more tracks. The data type is the CTrackArray class, generated from the ISequence template in track.cpp, the track implementation.

The MIDI object implementation (midi .cpp) mimics the track implementation in that most operations call corresponding methods for constituent tracks. The MIDI object is the only one that has methods to read from and write to disk. Export simply writes the header items and then steps through the CTrackArray in m_TrackArray, calling the WriteTrack function for each track. The Import function uses m_TrackCount to loop through clearing each track buffer, calling ReadTrack, and calling ISequence::addAsLast to install the track into m_TrackArray.

Using the Classes

Once all the up-front work of class definition is done, the MIDI file becomes an abstract data type (ADT). This leads to very short application programs. The musical fragment in Figure 2 shows the power of the MIDI ADT. This is the file Czeiszperger treats in detail (see "References") as an example. (For the sake of convenience, I've changed Czeiszperger's type 0 file to a type 1 MIDI file.) Figure 2 shows no key signature, suggesting that the fragment is in the key of C major or the key of A minor. A closer look at the second measure shows a D-minor chord. It's probably either a fragment of a piece in A minor with an unresolved subdominant D-minor chord or a piece in D minor with a missing key signature.

Might there be any text metaevents in the MIDI file that would provide you with a clue as to the proper key signature? Example 1 will print the contents of the MIDI file named "cz.mid," the file that appears in score in Figure 2. Figure 3 is a partial listing of the result, showing only track 1, where you would expect to find a key signature. The track contains a time signature, a tempo, and a title, but there is no key signature. This isn't a surprise. Many MIDI files, never intended for scoring as in Figure 2, lack the proper key signature. This isn't a problem during playback, of course, since each note has its proper pitch value. It is a problem, however, when the file is scored. In the absence of the right key signature, the scoring program simply places an accidental for every sharp and flat note.

Assume the Czeiszperger piece is in D minor with a missing key signature, to wit, one flat. We'd like to add the key signature to the MIDI file so that, when scored, it would follow common notational conventions. Example 2 will read a file named "cz.mid," add the one-flat key signature, set the minor flag, and write the result to a file named "czkey.mid." The key signature addition is accomplished by the call to CMidi::AddKeySignature() with the number of sharps (a positive integer) or flats (a negative integer) and the minor flag (0 for major key, 1 for minor key) as arguments. The file midi.cpp shows that the MIDI-level function for adding a key signature always adds the key signature to track 1. The MIDI-level function then passes the argument to the track-level function.

The add is done in CTrack::AddKeySignature(). As can be seen in track.cpp (available electronically), the track-level key signature function builds a 4-byte array consisting of the proper event type, proper data length, number of sharps or flats, and minor flag. The byte array is placed into an event constructed to add the key signature at 0 delta time. The new event is then placed at the beginning of the track, and the track length is adjusted accordingly. Figure 4 shows the result. Any decent scoring package will pick up the one-flat key signature. It should also add the cancel sign for the treble-clef B natural accidental in the first measure. (I used Voyetra's Orchestrator Plus to prepare the figures, and it did both.)

A more common and more difficult requirement is transposition. This problem frequently arises when orchestrating a MIDI piece for play by live performers. Perhaps the key is wrong for a singer's vocal range. Perhaps no bassoon is available, so the bassoon part must be transposed for baritone sax. Transpositions, along with appropriate key signature changes, can be accomplished easily with the functionality of the MIDI ADT.

To continue with our example, the code of Example 3 transposes the MIDI file czkey.mid down one half step. Once again, follow the strategy of calling a function only at the MIDI file level. In midi .cpp, CMidi::Transpose() defines a cursor (part of the template code of Figure 1) to iterate over each track in the MIDI file. For each track, the track level transpose function is called. In track.cpp, CTrack:: Transpose() behaves similarly. It defines a cursor to iterate over each event, calling the event-level function.

The work of transposition is accomplished in Listing Two's CEvent::Transpose(). This function begins with key signature data in a static array. Standard key signatures are used. (Some composers write in nonstandard keys--such as Db minor instead of C# minor--which require more accidentals than the standard keys. This code doesn't support nonstandard keys.)

Only two types of events are affected by transposition, the key signature metaevent and the note on/off channel events. Each event is checked to see if it is one of these two types. If the event is a key signature, the current signature is found in the table, and then the new signature is found at the requested transposition offset. The key signature doesn't change, of course, if the transposition is one or more octaves, a multiple of 12 half steps. If the event is a note-on or a note-off, the transposition offset is applied to the note value. Since note values can't be lower than 0 (C five octaves below middle C) or higher than 127 (G six octaves above middle C), transpositions beyond these ranges are set to the highest or lowest possible octave. The transposition of Figure 4 by the code of Example 3 appears in Figure 5. The four sharps of C# minor appear, and the appropriate accidental has been added in the treble clef, first measure.

Conclusion

In developing the event, track, and MIDI file classes, collection class templates are an enormous help. The collection classes supplied with IBM's VisualAge C++ provided efficient, function-rich code for handling the file/track/event hierarchies in MIDI files. The code I present here is sufficiently complete to allow you to expand the functionality to other MIDI file processing, such as normalization of all note velocities (volumes).

The catch is efficiency. While the template code is sparse and easy to code at high levels of abstraction, the overhead of the underlying code is concealed. Large MIDI files bring correspondingly long run times. VisualAge C++'s trace facility showed that a lot of time was spent allocating memory for event arrays and deallocating it when destructors were called. This may be partially because I used VisualAge's Equality Sequence template, which has the richest set of functionality available. Now that I have a better idea what functions I need for the MIDI ADT, it may be possible to choose a simpler and more efficient template.

The next step is to try the MIDI ADTs on other platforms. A prototype port to Visual C++ 1.5 and Microsoft Foundation Classes (MFC) under Windows was successful in principle, but memory was exhausted by MIDI files of more than 2000 bytes. Another drawback was that VC++ 1.5 does not support templates. However, my next project is to try Visual C++ 4.0 under Windows NT. The MFC distributed with Version 4.0 includes template support and isn't limited to the medium memory model.

References

Charlap, D. "The BMP File Format, Part 1." Dr. Dobb's Journal, March 1995.

Conger, J. C Programming for MIDI. New York, NY: M&T Books, 1989.

------. "MIDI Programming in C, Part One: MIDI Input and Output." Electronic Musician, September 1989.

------. "MIDI Programming in C, Part Three: Patch Librarian Basics." Electronic Musician, November 1989.

------. "MIDI Programming in C, Part Two: MIDI Data Debugger." Electronic Musician, October 1989.

------. MIDI Sequencing in C. New York, NY: M&T Books, 1989.

Czeiszperger, M.S. "Introducing Standard MIDI Files," Electronic Musician, April 1989.

Murray, R.B. C++ Strategy and Tactics. Reading, MA: Addison-Wesley, 1993.

Pohl, I. C++ for C Programmers, Second Edition. Redwood City, CA: Benjamin/Cummings, 1994.

Wyatt, D. Standard MIDI files 0.06. Available at http://www.id.ethz.ch:80/~parish/ midi/midi_file_format.txt, March 1988.

Figure 1: IBM's Sequence declarations.

template < class Element >
class ISequence {
public:
  class Cursor : ICursor {
    Element& element  ();
    void setToLast ();
    void setToPrevious  ();
    Boolean operator==  (Cursor const& cursor);
    Boolean operator!=  (Cursor const& cursor);
  };
                ISequence            (INumber numberOfElements = 100);
                ISequence            (ISequence < Element > const&);
ISequence < Element >&
                operator =           (ISequence < Element > const&);
                ~ISequence           ();
Boolean         add                  (Element const&);
Boolean         add                  (Element const&, ICursor&);
void            addAllFrom           (ISequence < Element > const&);
Element const&  elementAt            (ICursor const&) const;
Element&        elementAt            (ICursor const&);
Element const&  anyElement           () const;
void            removeAt             (ICursor const&);
INumber         removeAll            (Boolean (*property)
                                             (Element const&, void*),
                                             void* additionalArgument = 0);
void            replaceAt            (ICursor const&, Element const&);
void            removeAll            ();
Boolean         isBounded            () const;
INumber         maxNumberOfElements  () const;
INumber         numberOfElements     () const;
Boolean         isEmpty              () const;
Boolean         isFull               () const;
ICursor*        newCursor            () const;
Boolean         setToFirst           (ICursor&) const;
Boolean         setToNext            (ICursor&) const;
Boolean         allElementsDo        (Boolean (*function) (Element&, void*),
                                             void* additionalArgument = 0);
Boolean         allElementsDo        (IIterator <Element>&);
Boolean         allElementsDo        (Boolean (*function)
                                             (Element const&, void*),
                                             void* additionalArgument = 0) const;
Boolean         allElementsDo        (IConstantIterator <Element>&) const;
Boolean         isConsistent         () const;
void            removeFirst          ();
void            removeLast           ();
void            removeAtPosition     (IPosition);
Element const&  firstElement         () const;
Element const&  lastElement          () const;
Element const&  elementAtPosition    (IPosition) const;
Boolean         setToLast            (ICursor&) const;
Boolean         setToPrevious        (ICursor&) const;
void            setToPosition        (IPosition, ICursor&) const;
Boolean         isFirst              (ICursor const&) const;
Boolean         isLast               (ICursor const&) const;
long            compare              (ISequence < Element > const&,
                                             long (*comparisonFunction)
                                             (Element const&,
                                             Element const&)) const;
void            addAsFirst           (Element const&);
void            addAsFirst           (Element const&, ICursor&);
void            addAsLast            (Element const&);
void            addAsLast            (Element const&, ICursor&);
void            addAsNext            (Element const&, ICursor&);
void            addAsPrevious        (Element const&, ICursor&);
void            addAtPosition        (IPosition, Element const&);
void            addAtPosition        (IPosition, Element const&, ICursor&);
void            sort                 (long (*comparisonFunction)
                                              (Element const&,
                                              Element const&));
};

Figure 2: Czeiszperger's MIDI example.

Figure 3: Listing of Track 1 in readable form.

--------------- Track 1 --------------
Header Signature: MTrk
    Track Length: 53
Delta Time: 0
 Data Size: 6
      Type: time signature, (type 58).
      Data: 58 4 3 2 18 8
Delta Time: 0
 Data Size: 5
      Type: set tempo, (type 51).
      Data: 51 3 7 a1 20
Delta Time: 0
 Data Size: 32
      Type: text, (type 1).
      Data: TYPE 1 MIDI FILE
Delta Time: 0
 Data Size: 2
      Type: end of track, (type 2f).
      Data: 2f 0

Figure 4: MIDI example with D minor key signature added.

Figure 5: MIDI example transposed to C# minor.

Example 1: Code to print a MIDI file in readable form.

#include "midi.h"
void main(void)
{
  CMidi tempmidi;
  tempmidi.Import("cz.mid");
  tempmidi.Printf(); // method to print in readable form
}

Example 2: Code to add a D minor key signature.

#include "midi.h"
void main(void)
{
   CMidi tempmidi;
   tempmidi.Import("czkey.mid");
   tempmidi.Transpose(-1);
   tempmidi.Export("czcsmin.mid");
}

Example 3: Code to transpose down one-half step.

#include "midi.h"
void main(void)
{
   CMidi tempmidi;
   tempmidi.Import("cz.mid");
   tempmidi.AddKeySig(-1, 1); // 1 flat, minor = 1 => d minor
   tempmidi.Export("czkey.mid");
}

Listing One

// event.h---declares event class
#ifndef __C_EVENT_H_____LINEEND____
#define __C_EVENT_H_____LINEEND____
#include <iglobals.h>
#include <iseq.h>
#include <fstream.h>
const int TRUE = 1;
typedef ISequence <char> CByteArray;
enum EventType {Channel, Meta, SysEx, SysExCont, Undefined, Error};
class CEvent
{                      
public:
   CEvent();
   CEvent(const unsigned long& time, const EventType& TypeVal,
      const CByteArray& Data);
   CEvent(const CEvent& Event);
   ~CEvent();
                  CEvent&   operator = (const CEvent& Event);
                  int       operator == (const CEvent& Event) const;
                  int       operator != (const CEvent& Event) const;
                  void      Clear(void);
                  int       GetDataLength(void) const;                  
   const unsigned long&     GetDeltaTime(void) const;
   const          EventType GetEventType(void) const;
                  void      Printf(void) const;
                  int       ReadData(ifstream&);
                  CEvent&   SetDeltaTime(const unsigned long& time);
                  CEvent&   SetEventType(const EventType& TypeVal);
                  void      Transpose(const int);
                  void      WriteData(ofstream&) const;
   
private:
   unsigned long       m_DeltaTime;
            EventType  m_EventType;
            CByteArray m_Data;
            
            int        GetVar(const CByteArray&, unsigned long *) const;
            int        PutVar(CByteArray&, const int,
                          const unsigned long);
            int        ReadVar(ifstream& ins, unsigned long *);
            void       WriteVar(ofstream&, unsigned long) const;
};
typedef ISequence <CEvent> CEventArray;
#endif

Listing Two

// event.cpp---implements event class. Based on code from Michael Czeiszperger.
#include "event.h"
#include <iomanip.h>
CEvent::CEvent() : m_DeltaTime(0l), m_EventType(Undefined), m_Data() {}
CEvent::CEvent(const unsigned long& DeltaTime,
   const EventType& TypeVal, const CByteArray& Data) :
   m_DeltaTime(DeltaTime), m_EventType(TypeVal),
   m_Data(Data) {}
CEvent::CEvent(const CEvent& Event)
{  
   if (this != &Event)
   {  
      int ElementCount = Event.m_Data.numberOfElements();
      m_DeltaTime = Event.m_DeltaTime;
      m_EventType = Event.m_EventType;
      for (int i = 1; i <= ElementCount; i++)
         m_Data.addAsLast(Event.m_Data.elementAtPosition(i));
   }
}
CEvent::~CEvent()
{ m_Data.~ISequence(); }
                                         
CEvent& CEvent::operator=(const CEvent& Event)
{ 
   if(this != &Event)
   {                        
      int ElementCount = Event.m_Data.numberOfElements();
      if (m_Data.numberOfElements() > 0) // Old data?
         m_Data.removeAll();
      m_DeltaTime = Event.m_DeltaTime;
      m_EventType = Event.m_EventType;
      for (int i = 1; i <= ElementCount; i++)
         m_Data.addAsLast(Event.m_Data.elementAtPosition(i));
   }
   return *this;
}
int CEvent::operator==(const CEvent& Event) const
{
   int ByteCount;
   if (this == &Event)
      return 1;
   if (m_DeltaTime != Event.m_DeltaTime || 
      m_EventType != Event.m_EventType)
      return 0;
   ByteCount = m_Data.numberOfElements();
   if (ByteCount != Event.m_Data.numberOfElements())
      return 0;
   for (int i = 1; i <= ByteCount; i++)
   {
      if (m_Data.elementAtPosition(i) != 
         Event.m_Data.elementAtPosition(i))
         return 0;
   }
   return 1;
}
int CEvent::operator!=(const CEvent& Event) const
{ return !(*this == Event); }
void CEvent::Clear(void)
{ m_DeltaTime = 0l; m_EventType = Undefined; m_Data.removeAll(); }
const unsigned long& CEvent::GetDeltaTime() const
{ return m_DeltaTime; }
const EventType CEvent::GetEventType() const
{ return m_EventType; }
void CEvent::Printf() const 
{             
            int  byte, chan, i, size;
      const int  LINELENGTH = 60;
   unsigned long DataLength;
   
   cout << endl
        << "Delta Time: " << dec << m_DeltaTime;
   size = m_Data.numberOfElements();
   cout << endl
        << " Data Size: " << dec << size << endl;
   if (!size)
      return;
   cout << "      Type: ";
   byte = m_Data.elementAtPosition(1);
   switch ((int)m_EventType)
   {
      case (int)Channel:        //--- channel event ----------------
         chan = (byte & 0xf) + 1;
         switch (byte & 0xf0)
         {
            case 0x80: cout << "Note off, channel " << chan << ", ";
               break;
            case 0x90: cout << "Note on, channel " << chan << ", ";
               break;
            case 0xa0: cout << "Pressure, channel " << chan << ", ";
               break;
            case 0xb0: cout << "Parameter, channel " << chan << ", ";
               break;
            case 0xe0: cout << "Pitchbend, channel " << chan << ", ";
               break;
            case 0xc0: cout << "Program, channel " << chan << ", ";
               break;
            case 0xd0: cout << "Channel pressure, channel " << chan <<
               ", "; break;
              default: cout << "Unknown event, "; break;
         }
         cout << "(type " << hex << byte << ")." << endl;
         if (size > 1)
         {  
            cout << "      Data: "; 
            CByteArray::Cursor cursor(m_Data);
            forCursor(cursor)
               cout << hex << (int)m_Data.elementAt(cursor) << " ";
            cout << endl;
         }
         break;
      case (int)Meta:               //--- meta event -------------------
         switch (byte)
         {       
            case  0x0: cout << "sequence number, "; break;
            case  0x1: cout << "text, "; break;
            case  0x2: cout << "copyright, "; break;
            case  0x3: cout << "sequence/track name, "; break;
            case  0x4: cout << "instrument name, "; break;
            case  0x5: cout << "lyric, "; break;
            case  0x6: cout << "marker, "; break;
            case  0x7: cout << "cue point, "; break;
            case 0x20: cout << "MIDI channel prefix, "; break;
            case 0x2f: cout << "end of track, "; break;
            case 0x51: cout << "set tempo, "; break;   
            case 0x54: cout << "SMPTE offset, "; break;
            case 0x58: cout << "time signature, "; break;
            case 0x59: cout << "key signature, "; break;
            case 0x7f: cout << "sequencer-specific, "; break;
              default: cout << "Unknown meta event, "; break;
         }
         cout << "(type " << hex << byte << ")." << endl;
         if (size > 1)
         {                
            cout << "      Data: ";  
            if (0x0 <= byte && byte <= 0x5)  // if text
               for (i = 3; i <= size; i++)   // print char bytes
                  cout << (char)m_Data.elementAtPosition(i);
            else for (i = 1; i <= size; i++) // dump hex bytes
               cout << hex << (int)m_Data.elementAtPosition(i) << " ";
            cout << endl;
         }
         break;
      case (int)SysEx:              //--- sysex event ------------------
      case (int)SysExCont:
         if (size > 1)
         {
            i = GetVar(m_Data, &DataLength);
            cout << "    Length: " << DataLength << endl;
            cout << "SysEx Data: ";  
            for (i = 1; i <= size; i++)
            {
               cout << hex << m_Data.elementAtPosition(i) << " ";
               if (((i * 2) % LINELENGTH) == 0)
                  cout << "\n            ";
            }
         }
         break;
      default:
         cerr << "Unexpected event type: " << (int)m_EventType
            << ".  Aborting." << endl;
         exit (1);
         break;
   }
}  
int CEvent::ReadData(ifstream& ins)
{  
   unsigned char c, c1;
   static   int  ChanType[]    = {0,0,0,0,0,0,0,0,2,2,2,2,1,1,2,0};
   static   int  EventLength   = 0;
            int  i;
   unsigned long Length;
            int  LengthLength  = 0;
            int  Needed;
   static   int  NoMerge       = 0;    
   static   int  Running       = 0;
   static   int  Status        = 0;
   static   int  SysExContinue = 0;                
   EventLength = ReadVar(ins, &m_DeltaTime);     
   c = (char)ins.get(); 
   EventLength++;
   if (SysExContinue && c != 0xf7)
   {
      cerr << "Didn't find expected continuation of sysex. Aborting." 
         << endl;
      exit (1);
   }
   if ((c & 0x80) == 0)
   {
      if (Status == 0)      
      {
         cerr << "Unexpected running status---" << endl;
         cerr << "Status = " << hex << Status << ", Running = " << dec
            << Running << ".  Aborting." << endl;
         exit(1);
      }
      Running = 1;
   }             
   else 
   {
      Running = 0;
      Status = c;
   }
   Needed = ChanType[(Status >> 4) & 0xf]; 
   if ( Needed ) // channel event?
   {
      m_EventType = Channel;
      m_Data.addAsLast(Status);
      if ( Running )
         c1 = c;
      else
      {
         c1 = ins.get(); 
         EventLength++;
      }
      m_Data.addAsLast(c1);
      if (Needed > 1)
      {
         m_Data.addAsLast(ins.get()); 
         EventLength++;
      }
   }
   else
   {
      switch (c)
      {                                                              
         case 0xff:  // meta event
            m_EventType = Meta;
            m_Data.addAsLast(ins.get()); 
            EventLength++;
            LengthLength += (int)ReadVar(ins, &Length);
            PutVar(m_Data, EventLength+1, Length);
            EventLength += LengthLength;
            for (i = 1; i <= (int)Length; i++)
            {
               m_Data.addAsLast(ins.get());
               EventLength++;
            }
            break;
         case 0xf0:  // start of system exclusive 
         case 0xf7:  // system exclusive continuation
            if (c == 0xf0)
            {
               m_EventType = SysEx;
               SysExContinue = (NoMerge == 0) ? 0 : 1;
            }
            else
               m_EventType = SysExCont;
            m_Data.addAsLast(Status);
            EventLength += ReadVar(ins, &Length);
            for (i = 1; i <= (int)Length; i++) 
               m_Data.addAsLast(ins.get());
            break;
         default:
            cerr << "Unexpected event type " << hex << (int)c 
               << ".  Aborting." << endl;
            exit (1);
            break;
      }
   }
   return EventLength;
}
   
CEvent& CEvent::SetDeltaTime(const unsigned long& Time)
{ m_DeltaTime = Time; return *this; }
CEvent& CEvent::SetEventType(const EventType& TypeVal)
{ m_EventType = TypeVal; return *this; }
void CEvent::Transpose(const int steps)
{
   //             major:  C Db   D Eb   E  F    F# G Ab   A  Bb   B
   //             minor:  A Bb   B C    C# D    Eb E F    F# G    G#
   //            sharps:         2      4       6  1      3       5
   //             flats:    5      3       1         4       2
   static int keysig[] = {0,0xfb,2,0xfd,4, 0xff,6, 1,0xfc,3, 0xfe,5 };
   // key signature == ff 59 02 sf mi: +sf == count sharps,
   // -sf == count flats, mi == 0 => major, mi == 1 => minor
   if (m_EventType == Meta)
   {
      if (m_Data.elementAtPosition(1) == 0x59)
      {
         int i, keyshift, sharpflat;
         CByteArray::Cursor cursor(m_Data);
         keyshift = steps % 12;                    // no change for octaves
         m_Data.setToPosition(3, cursor);          // position at sf
         sharpflat = m_Data.elementAtPosition(3);
         if (sharpflat == -6)
            sharpflat = 6;
         for (i = 0; i < 12; i++)
            if (keysig[i] == sharpflat)
               break;
         if (i == 12)
         {
            cerr << "Unexpected key signature " << sharpflat
               << ".  Aborting." << endl;
            exit (1);
         }
         m_Data.elementAt(cursor) = keysig[(i+keyshift)%12];
      }
   }
   else if (m_EventType == Channel)
   {
      int byte = m_Data.elementAtPosition(1) & 0xf0;
      if (byte == 0x80 || byte == 0x90)         // note off or on
      {
         CByteArray::Cursor cursor(m_Data);
         m_Data.setToPosition(2, cursor);       // position at note
         m_Data.elementAt(cursor) += steps;
         if (m_Data.elementAt(cursor) < 0)      // lower than C-5?
            m_Data.elementAt(cursor) += 12;     // then raise an octave
         else if (m_Data.elementAt(cursor) > 0x7f)  // higher than G6?
            m_Data.elementAt(cursor) -= 12;     // then lower an octave
      }
   }
}
void CEvent::WriteData(ofstream& outs) const
{     
   static int CurrentChanType = 0;            
   static int Running = 0;
                                                 
   WriteVar(outs, m_DeltaTime);
   if (m_EventType != Channel)
   {
      Running = CurrentChanType = 0;
      if (m_EventType == Meta)
         outs.put((char)0xff);
   }
   else
   {
      if (CurrentChanType == m_Data.elementAtPosition(1))
         Running = 1;
      else
      {
         Running = 0;
         CurrentChanType = m_Data.elementAtPosition(1);
      }
   }  
   for (int i = Running+1; i <= m_Data.numberOfElements(); i++)
      outs.put(m_Data.elementAtPosition(i));
}
int CEvent::GetDataLength(void) const
{ return m_Data.numberOfElements(); }
int CEvent::GetVar(const CByteArray& Data, unsigned long *Value) const
{
   int c;
   int count = 0;
   
   c = (char)Data.elementAtPosition(count++);
   *Value = (long)c;
   if (c & 0x80)
   {
      *Value &= 0x7f;
      do
      {
         c = Data.elementAtPosition(count++);
         *Value = (*Value << 7) + (c & 0x7f);
      } while (c & 0x80);
   }                     
   return count;
}
int CEvent::PutVar(CByteArray& Data, const int Position,
   const unsigned long Value)
{
   unsigned long Buffer, TempVal;
            int  i = 0;
   
   Buffer = Value & 0x7f;
   TempVal = Value;
   while ((TempVal >>= 7) > 0)
   {
      Buffer <<= 8;
      Buffer |= 0x80;
      Buffer += (TempVal & 0x7f);
   }                           
   while(TRUE)
   {  
      Data.addAsLast((char)Buffer);
      i++;
      if (Buffer & 0x80)
         Buffer >>= 8;
      else
         break;
   }
   return i;
}
int CEvent::ReadVar(ifstream& ins, unsigned long *Value)
{            
   int c;
   int count = 0;
   
   c = ins.get();
   count++;
   *Value = (long)c;
   if (c & 0x80)
   {
      *Value &= 0x7f;
      do
      {
         c = ins.get();
         count++;
         *Value = (*Value << 7) + (c & 0x7f);
      } while (c & 0x80);
   }                     
   return count;
}
   
void CEvent::WriteVar(ofstream& ofs, unsigned long Value) const
{
   unsigned long buffer;
   
   buffer = Value & 0x7f;
   while ((Value >>= 7) > 0)
   {
      buffer <<= 8;
      buffer |= 0x80;
      buffer += (Value & 0x7f);
   }                           
   while(TRUE)
   {
      ofs.put((char)buffer);
      if (buffer & 0x80)
         buffer >>= 8;
      else
         break;
   }
}
End Listings>>
<<


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.