[Stk] Info about SMF Files

Gary Scavone gary@ccrma.Stanford.EDU
Mon, 23 Aug 2004 10:05:14 -0400


--Apple-Mail-8--1001402596
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
	charset=US-ASCII;
	format=flowed

Hi Ravi,

I'm a little pressed at the moment to answer all your questions, but 
I'm attaching a new STK class (MidiFileIn) which I wrote about a year 
ago.  It will be in the new release of STK, which I should have ready 
within the next few weeks.  This class provides various information 
about SMFs and allows you to read events from an arbitrary track and 
get a running event time (if a time track exists, the event times will 
be "synced" to it).

--gary


--Apple-Mail-8--1001402596
Content-Disposition: attachment;
	filename=MidiFileIn.h
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
	x-unix-mode=0644;
	name="MidiFileIn.h"

/**********************************************************************/
/*! \class MidiFileIn
    \brief A standard MIDI file reading/parsing class.

    This class can be used to read events from a standard MIDI file.
    Event bytes are copied to a C++ vector and must be subsequently
    interpreted by the user.  The function getNextMidiEvent() skips
    meta and sysex events, returning only MIDI channel messages.
    Event delta-times are returned in the form of "ticks" and a
    function is provided to determine the current "seconds per tick".
    Tempo changes are internally tracked by the class and reflected in
    the values returned by the function getTickSeconds().

    by Gary P. Scavone, 2003.
*/
/**********************************************************************/

#ifndef STK_MIDIFILEIN_H
#define STK_MIDIFILEIN_H

#include "Stk.h"
#include <string>
#include <vector>
#include <fstream>
#include <sstream>

class MidiFileIn : public Stk
{
 public:
  //! Default constructor.
  /*!
      If an error occurs while opening or parsing the file header, an
      StkError exception will be thrown.
  */
  MidiFileIn( std::string fileName );

  //! Class destructor.
  ~MidiFileIn();

  //! Return the MIDI file format (0, 1, or 2).
  int getFileFormat() const;

  //! Return the number of tracks in the MIDI file.
  unsigned int getNumberOfTracks() const;

  //! Return the MIDI file division value from the file header.
  /*!
      Note that this value must be "parsed" in accordance with the
      MIDI File Specification.  In particular, if the MSB is set, the
      file uses time-code representations for delta-time values.
  */
  int getDivision() const;

  //! Move the specified track event reader to the beginning of its track.
  /*!
      The relevant track tempo value is reset as well.  If an invalid
      track number is specified, an StkError exception will be thrown.
  */
  void rewindTrack( unsigned int track = 0 );

  //! Get the current value, in seconds, of delta-time ticks for the specified track.
  /*!
      This value can change as events are read (via "Set Tempo"
      Meta-Events).  Therefore, one should call this function after
      every call to getNextEvent() or getNextMidiEvent().  If an
      invalid track number is specified, an StkError exception will be
      thrown.
  */   
  double getTickSeconds( unsigned int track = 0 );

  //! Fill the user-provided vector with the next event in the specified track and return the event delta-time in ticks.
  /*!
      MIDI File events consist of a delta time and a sequence of event
      bytes.  This function returns the delta-time value and writes
      the subsequent event bytes directly to the event vector.  The
      user must parse the event bytes in accordance with the MIDI File
      Specification.  All returned MIDI channel events are complete
      ... a status byte is provided even when running status is used
      in the file.  If the track has reached its end, no bytes will be
      written and the event vector size will be zero.  If an invalid
      track number is specified or an error occurs while reading the
      file, an StkError exception will be thrown.
  */
  unsigned long getNextEvent( std::vector<unsigned char> *event, unsigned int track = 0 );

  //! Fill the user-provided vector with the next MIDI channel event in the specified track and return the event delta time in ticks.
  /*!
      All returned MIDI events are complete ... a status byte is
      provided even when running status is used in the file.  Meta and
      sysex events in the track are skipped though "Set Tempo" events
      are properly parsed for use by the getTickSeconds() function.
      If the track has reached its end, no bytes will be written and
      the event vector size will be zero.  If an invalid track number
      is specified or an error occurs while reading the file, an
      StkError exception will be thrown.
  */
  unsigned long getNextMidiEvent( std::vector<unsigned char> *midiEvent, unsigned int track = 0 );

 protected:

  // This protected class function is used for reading variable-length
  // MIDI file values. It is assumed that this function is called with
  // the file read pointer positioned at the start of a
  // variable-length value.  The function returns true if the value is
  // successfully parsed.  Otherwise, it returns false.
  bool readVariableLength( unsigned long *value );

  std::ifstream file_;
  unsigned int nTracks_;
  int format_;
  int division_;
  bool usingTimeCode_;
  std::vector<double> tickSeconds_;
  std::vector<long> trackPointers_;
  std::vector<long> trackOffsets_;
  std::vector<long> trackLengths_;
  std::vector<char> trackStatus_;

  // This structure and the following variables are used to save and
  // keep track of a format 1 tempo map (and the initial tickSeconds
  // parameter for formats 0 and 2).
  struct TempoChange { 
    unsigned long count;
    double tickSeconds;
  };
  std::vector<TempoChange> tempoEvents_;
  std::vector<unsigned long> trackCounters_;
  std::vector<unsigned int> trackTempoIndex_;
};

#endif

--Apple-Mail-8--1001402596
Content-Disposition: attachment;
	filename=MidiFileIn.cpp
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
	x-unix-mode=0644;
	name="MidiFileIn.cpp"

/**********************************************************************/
/*! \class MidiFileIn
    \brief A standard MIDI file reading/parsing class.

    This class can be used to read events from a standard MIDI file.
    Event bytes are copied to a C++ vector and must be subsequently
    interpreted by the user.  The function getNextMidiEvent() skips
    meta and sysex events, returning only MIDI channel messages.
    Event delta-times are returned in the form of "ticks" and a
    function is provided to determine the current "seconds per tick".
    Tempo changes are internally tracked by the class and reflected in
    the values returned by the function getTickSeconds().

    by Gary P. Scavone, 2003.
*/
/**********************************************************************/

#include "MidiFileIn.h"
#include <iostream>

MidiFileIn :: MidiFileIn( std::string fileName )
{
  // Attempt to open the file.
  file_.open( fileName.c_str(), std::ios::in );
  if ( !file_ ) {
    errorString_ << "MidiFileIn: error opening or finding file (" <<  fileName << ").";
    handleError( errorString_.str(), StkError::FILE_NOT_FOUND );
  }

  // Parse header info.
  char chunkType[4];
  char buffer[4];
  SINT32 *length;
  if ( !file_.read( chunkType, 4 ) ) goto error;
  if ( !file_.read( buffer, 4 ) ) goto error;
#ifdef __LITTLE_ENDIAN__
  swap32((unsigned char *)&buffer);
#endif
  length = (SINT32 *) &buffer;
  if ( strncmp( chunkType, "MThd", 4 ) || ( *length != 6 ) ) {
    errorString_ << "MidiFileIn: file (" <<  fileName << ") does not appear to be a MIDI file!";
    handleError( errorString_.str(), StkError::FILE_UNKNOWN_FORMAT );
  }

  // Read the MIDI file format.
  SINT16 *data;
  if ( !file_.read( buffer, 2 ) ) goto error;
#ifdef __LITTLE_ENDIAN__
  swap16((unsigned char *)&buffer);
#endif
  data = (SINT16 *) &buffer;
  if ( *data < 0 || *data > 2 ) {
    errorString_ << "MidiFileIn: the file (" <<  fileName << ") format is invalid!";
    handleError( errorString_.str(), StkError::FILE_ERROR );
  }
  format_ = *data;

  // Read the number of tracks.
  if ( !file_.read( buffer, 2 ) ) goto error;
#ifdef __LITTLE_ENDIAN__
  swap16((unsigned char *)&buffer);
#endif
  if ( format_ == 0 && *data != 1 ) {
    errorString_ << "MidiFileIn: invalid number of tracks (>1) for a file format = 0!";
    handleError( errorString_.str(), StkError::FILE_ERROR );
  }
  nTracks_ = *data;

  // Read the beat division.
  if ( !file_.read( buffer, 2 ) ) goto error;
#ifdef __LITTLE_ENDIAN__
  swap16((unsigned char *)&buffer);
#endif
  division_ = (int) *data;
  double tickrate;
  usingTimeCode_ = false;
  if ( *data & 0x8000 ) {
    // Determine ticks per second from time-code formats.
    tickrate = (double) -(*data & 0x7F00);
    // If frames per second value is 29, it really should be 29.97.
    if ( tickrate == 29.0 ) tickrate = 29.97;
    tickrate *= (*data & 0x00FF);
    usingTimeCode_ = true;
  }
  else {
    tickrate = (double) (*data & 0x7FFF); // ticks per quarter note
  }

  // Now locate the track offsets and lengths.  If not using time
  // code, we can initialize the "tick time" using a default tempo of
  // 120 beats per minute.  We will then check for tempo meta-events
  // afterward.
  for ( unsigned int i=0; i<nTracks_; i++ ) {
    if ( !file_.read( chunkType, 4 ) ) goto error;
    if ( strncmp( chunkType, "MTrk", 4 ) ) goto error;
    if ( !file_.read( buffer, 4 ) ) goto error;
#ifdef __LITTLE_ENDIAN__
  swap32((unsigned char *)&buffer);
#endif
    length = (SINT32 *) &buffer;
    trackLengths_.push_back( *length );
    trackOffsets_.push_back( file_.tellg() );
    trackPointers_.push_back( file_.tellg() );
    trackStatus_.push_back( 0 );
    file_.seekg( *length, std::ios_base::cur );
    if ( usingTimeCode_ ) tickSeconds_.push_back( (double) (1.0 / tickrate) );
    else tickSeconds_.push_back( (double) (0.5 / tickrate) );
  }

  // Save the initial tickSeconds parameter.
  TempoChange tempoEvent;
  tempoEvent.count = 0;
  tempoEvent.tickSeconds = tickSeconds_[0];
  tempoEvents_.push_back( tempoEvent );

  // If format 1 and not using time code, parse and save the tempo map
  // on track 0.
  if ( format_ == 1 && !usingTimeCode_ ) {
    std::vector<unsigned char> event;
    unsigned long value, count;

    // We need to temporarily change the usingTimeCode_ value here so
    // that the getNextEvent() function doesn't try to check the tempo
    // map (which we're creating here).
    usingTimeCode_ = true;
    count = getNextEvent( &event, 0 );
    while ( event.size() ) {
      if ( ( event.size() == 6 ) && ( event[0] == 0xff ) &&
           ( event[1] == 0x51 ) && ( event[2] == 0x03 ) ) {
        tempoEvent.count = count;
        value = ( event[3] << 16 ) + ( event[4] << 8 ) + event[5];
        tempoEvent.tickSeconds = (double) (0.000001 * value / tickrate);
        if ( count > tempoEvents_.back().count )
          tempoEvents_.push_back( tempoEvent );
        else
          tempoEvents_.back() = tempoEvent;
      }
      count += getNextEvent( &event, 0 );
    }
    rewindTrack( 0 );
    for ( unsigned int i=0; i<nTracks_; i++ ) {
      trackCounters_.push_back( 0 );
      trackTempoIndex_.push_back( 0 );
    }
    // Change the time code flag back!
    usingTimeCode_ = false;
  }

  return;

 error:
  errorString_ << "MidiFileIn: error reading from file (" <<  fileName << ").";
  handleError( errorString_.str(), StkError::FILE_ERROR );
}

MidiFileIn :: ~MidiFileIn()
{
  // An ifstream object implicitly closes itself during destruction
  // but we'll make an explicit call to "close" anyway.
  file_.close(); 
}

int MidiFileIn :: getFileFormat() const
{
  return format_;
}

unsigned int MidiFileIn :: getNumberOfTracks() const
{
  return nTracks_;
}

int MidiFileIn :: getDivision() const
{
  return division_;
}

void MidiFileIn :: rewindTrack( unsigned int track )
{
  if ( track >= nTracks_ ) {
    errorString_ << "MidiFileIn::getNextEvent: invalid track argument (" <<  track << ").";
    handleError( errorString_.str(), StkError::FUNCTION_ARGUMENT );
  }

  trackPointers_[track] = trackOffsets_[track];
  trackStatus_[track] = 0;
  tickSeconds_[track] = tempoEvents_[0].tickSeconds;
}

double MidiFileIn :: getTickSeconds( unsigned int track )
{
  // Return the current tick value in seconds for the given track.
  if ( track >= nTracks_ ) {
    errorString_ << "MidiFileIn::getTickSeconds: invalid track argument (" <<  track << ").";
    handleError( errorString_.str(), StkError::FUNCTION_ARGUMENT );
  }

  return tickSeconds_[track];
}

unsigned long MidiFileIn :: getNextEvent( std::vector<unsigned char> *event, unsigned int track )
{
  // Fill the user-provided vector with the next event in the
  // specified track (default = 0) and return the event delta time in
  // ticks.  This function assumes that the stored track pointer is
  // positioned at the start of a track event.  If the track has
  // reached its end, the event vector size will be zero.
  //
  // If we have a format 0 or 2 file and we're not using timecode, we
  // should check every meta-event for tempo changes and make
  // appropriate updates to the tickSeconds_ parameter if so.
  //
  // If we have a format 1 file and we're not using timecode, keep a
  // running sum of ticks for each track and update the tickSeconds_
  // parameter as needed based on the stored tempo map.

  if ( track >= nTracks_ ) {
    errorString_ << "MidiFileIn::getNextEvent: invalid track argument (" <<  track << ").";
    handleError( errorString_.str(), StkError::FUNCTION_ARGUMENT );
  }

  event->clear();
  // Check for the end of the track.
  if ( (trackPointers_[track] - trackOffsets_[track]) >= trackLengths_[track] )
    return 0;

  unsigned long ticks = 0, bytes = 0;
  bool isTempoEvent = false;

  // Read the event delta time.
  file_.seekg( trackPointers_[track], std::ios_base::beg );
  if ( !readVariableLength( &ticks ) ) goto error;

  // Parse the event stream to determine the event length.
  unsigned char c;
  if ( !file_.read( (char *)&c, 1 ) ) goto error;
  switch ( c ) {

  case 0xFF: // A Meta-Event
    unsigned long position;
    trackStatus_[track] = 0;
    event->push_back( c );
    if ( !file_.read( (char *)&c, 1 ) ) goto error;
    event->push_back( c );
    if ( format_ != 1 && ( c == 0x51 ) ) isTempoEvent = true;
    position = file_.tellg();
    if ( !readVariableLength( &bytes ) ) goto error;
    bytes += ( (unsigned long)file_.tellg() - position );
    file_.seekg( position, std::ios_base::beg );
    break;

  case 0xF0 || 0xF7: // The start or continuation of a Sysex event
    trackStatus_[track] = 0;
    event->push_back( c );
    position = file_.tellg();
    if ( !readVariableLength( &bytes ) ) goto error;
    bytes += ( (unsigned long)file_.tellg() - position );
    file_.seekg( position, std::ios_base::beg );
    break;

  default: // Should be a MIDI channel event
    if ( c & 0x80 ) { // MIDI status byte
      if ( c > 0xF0 ) goto error;
      trackStatus_[track] = c;
      event->push_back( c );
      c &= 0xF0;
      if ( (c == 0xC0) || (c == 0xD0) ) bytes = 1;
      else bytes = 2;
    }
    else if ( trackStatus_[track] & 0x80 ) { // Running status
      event->push_back( trackStatus_[track] );
      event->push_back( c );
      c = trackStatus_[track] & 0xF0;
      if ( (c != 0xC0) && (c != 0xD0) ) bytes = 1;
    }
    else goto error;

  }

  // Read the rest of the event into the event vector.
  for ( unsigned long i=0; i<bytes; i++ ) {
    if ( !file_.read( (char *)&c, 1 ) ) goto error;
    event->push_back( c );
  }

  if ( !usingTimeCode_ ) {
    if ( isTempoEvent ) {
      // Parse the tempo event and update tickSeconds_[track].
      double tickrate = (double) (division_ & 0x7FFF);
      unsigned long value = ( event->at(3) << 16 ) + ( event->at(4) << 8 ) + event->at(5);
      tickSeconds_[track] = (double) (0.000001 * value / tickrate);
    }

    if ( format_ == 1 ) {
      // Update track counter and check the tempo map.
      trackCounters_[track] += ticks;
      TempoChange tempoEvent = tempoEvents_[ trackTempoIndex_[track] ];
      if ( trackCounters_[track] >= tempoEvent.count ) {
        trackTempoIndex_[track]++;
        tickSeconds_[track] = tempoEvent.tickSeconds;
      }
    }
  }

  // Save the current track pointer value.
  trackPointers_[track] = file_.tellg();

  return ticks;

 error:
  errorString_ << "MidiFileIn::getNextEvent: file read error!";
  handleError( errorString_.str(), StkError::FILE_ERROR );
  return 0;
}

unsigned long MidiFileIn :: getNextMidiEvent( std::vector<unsigned char> *midiEvent, unsigned int track )
{
  // Fill the user-provided vector with the next MIDI event in the
  // specified track (default = 0) and return the event delta time in
  // ticks.  Meta-Events preceeding this event are skipped and ignored.
  if ( track >= nTracks_ ) {
    errorString_ << "MidiFileIn::getNextMidiEvent: invalid track argument (" <<  track << ").";
    handleError( errorString_.str(), StkError::FUNCTION_ARGUMENT );
  }

  unsigned long ticks = getNextEvent( midiEvent, track );
  while ( midiEvent->size() && ( midiEvent->at(0) >= 0xF0 ) ) {
    for ( unsigned int i=0; i<midiEvent->size(); i++ )
      std::cout << "event byte = " << i << ", value = " << (int)midiEvent->at(i) << std::endl;
    ticks = getNextEvent( midiEvent, track );
  }

  for ( unsigned int i=0; i<midiEvent->size(); i++ )
  std::cout << "event byte = " << i << ", value = " << (int)midiEvent->at(i) << std::endl;

  return ticks;
}

bool MidiFileIn :: readVariableLength( unsigned long *value )
{
  // It is assumed that this function is called with the file read
  // pointer positioned at the start of a variable-length value.  The
  // function returns "true" if the value is successfully parsed and
  // "false" otherwise.
  *value = 0;
  char c;

  if ( !file_.read( &c, 1 ) ) return false;
  *value = (unsigned long) c;
  if ( *value & 0x80 ) {
    *value &= 0x7f;
    do {
      if ( !file_.read( &c, 1 ) ) return false;
      *value = ( *value << 7 ) + ( c & 0x7f );
    } while ( c & 0x80 );
  }

  return true;
} 

--Apple-Mail-8--1001402596
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
	charset=US-ASCII;
	format=flowed



On Sunday, August 22, 2004, at 11:12  AM, Ravi Kiran Chivukula wrote:

> Hi All,
>     I don't know whether this list is the appropriate place for
> clarifying my doubts. Please bear with me if not.
>     I am trying to develop a parser for SMF files for use in a MIDI
> software synthesizer. I have some doubts regarding the SMF file
> format.
>
> 1. As per my understanding if the 'division' word in the MThd chunk
> indicates 'ticks per quarter note' there will be a corresponding 'Set
> Tempo' meta event in the MTrk chunk. If the 'division' word contains
> SMPTE time code there will be a corresponding 'SMPTE Offset' meta
> event in the MTrk chunk. With the above information I have the
> complete timing information. If so what will be the use of MIDI
> quarter frame messages? Will these messages never occur in SMF files?
> If they occur how should a synthesizer handle them?
>
> 2. Similarly do system real time messages (midi timing clock, start,
> stop etc) ever occur in an SMF file? Since they are used for
> synchronising I feel they won't be useful for a synthesizer. How
> should they be treated if they do occur? I also read somewhere that
> song position pointer message will be redundant if we use MIDI time
> code. Is this true?
>
> 3. What is the importance of 'Time Signature' meta event? What is the
> importance of metronome click and what are these notated 32nd notes?
> Where do I use signatures like 4/4 time etc? I know the meaning of
> these signatures but not how to use them. Pardon me for this blatant
> ignorance!
>
> 4. Similarly what is the importance of 'Key Signature' meta event?
>
> 5. Can tempo be changed at any time in either format 0 or 1 files? In
> the specification it is said that for a format 1 file the tempo map
> must be stored in the first track. If so, how can we change the tempo
> at any later instant?
>
> 6. For a format 1 file should eack track be played on a separate MIDI
> channel? If so, what happens if the number of tracks exceeds 16? And
> also what exactly is the difference between format 0 and 1 files if
> the above statement is true? (The example given at the end of the
> specification uses the same musical piece to produce either format 0
> or 1 file but I didn't understand the difference as far as the output
> of the synthesis is concerned)
>
> 7. For note on/off events how do we know whether it is a whole note or
> a half note or a quarter note etc? Simply by checking the time during
> which they are on? Can a whole note exceed the time for the note as
> specified by the tempo?
>
>     Sorry for the rather long mail. Thanks a lot in advance!!
>
> Regards
> Ravi Kiran
>
> _______________________________________________
> Stk mailing list
> Stk@ccrma.stanford.edu
> http://ccrma-mail.stanford.edu/mailman/listinfo/stk

--Apple-Mail-8--1001402596--