/**********************************************************************/ /*! \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--2019. */ /**********************************************************************/ #include "MidiFileIn.h" #include #include namespace stk { MidiFileIn :: MidiFileIn( std::string fileName ) { // Attempt to open the file. file_.open( fileName.c_str(), std::ios::in | std::ios::binary ); if ( !file_ ) { oStream_ << "MidiFileIn: error opening or finding file (" << fileName << ")."; handleError( 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 ) ) { oStream_ << "MidiFileIn: file (" << fileName << ") does not appear to be a MIDI file!"; handleError( 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 ) { oStream_ << "MidiFileIn: the file (" << fileName << ") format is invalid!"; handleError( 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 ) { oStream_ << "MidiFileIn: invalid number of tracks (>1) for a file format = 0!"; handleError( 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. signed char tmp = -(*data & 0xFF00)>>8; tickrate = (double) tmp; // 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. unsigned int i; for ( i=0; i 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_ ) { oStream_ << "MidiFileIn::getNextEvent: invalid track argument (" << track << ")."; handleError( StkError::WARNING ); return; } 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_ ) { oStream_ << "MidiFileIn::getTickSeconds: invalid track argument (" << track << ")."; handleError( StkError::WARNING ); return 0.0; } return tickSeconds_[track]; } unsigned long MidiFileIn :: getNextEvent( std::vector *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. event->clear(); if ( track >= nTracks_ ) { oStream_ << "MidiFileIn::getNextEvent: invalid track argument (" << track << ")."; handleError( StkError::WARNING ); return 0; } // 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 = (unsigned long)file_.tellg(); if ( !readVariableLength( &bytes ) ) goto error; bytes += ( (unsigned long)file_.tellg() - position ); file_.seekg( position, std::ios_base::beg ); break; case 0xF0: case 0xF7: // The start or continuation of a Sysex event trackStatus_[track] = 0; event->push_back( c ); position = (unsigned long)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. unsigned long i; for ( i=0; ipush_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] < tempoEvents_.size() - 1 ) { trackTempoIndex_[track]++; tickSeconds_[track] = tempoEvent.tickSeconds; } } } // Save the current track pointer value. trackPointers_[track] = (long)file_.tellg(); return ticks; error: oStream_ << "MidiFileIn::getNextEvent: file read error!"; handleError( StkError::FILE_ERROR ); return 0; } unsigned long MidiFileIn :: getNextMidiEvent( std::vector *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_ ) { oStream_ << "MidiFileIn::getNextMidiEvent: invalid track argument (" << track << ")."; handleError( StkError::WARNING ); return 0; } unsigned long ticks = getNextEvent( midiEvent, track ); while ( midiEvent->size() && ( midiEvent->at(0) >= 0xF0 ) ) { //for ( unsigned int i=0; isize(); i++ ) //std::cout << "event byte = " << i << ", value = " << (int)midiEvent->at(i) << std::endl; ticks = getNextEvent( midiEvent, track ); } //for ( unsigned int i=0; isize(); 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; } } // stk namespace