mirror of
https://github.com/thestk/stk
synced 2026-01-13 13:01:52 +00:00
368 lines
12 KiB
C++
368 lines
12 KiB
C++
/**********************************************************************/
|
|
/*! \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 - 2009.
|
|
*/
|
|
/**********************************************************************/
|
|
|
|
#include "MidiFileIn.h"
|
|
#include <string.h>
|
|
#include <iostream>
|
|
|
|
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_ ) {
|
|
errorString_ << "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 ) ) {
|
|
errorString_ << "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 ) {
|
|
errorString_ << "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 ) {
|
|
errorString_ << "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.
|
|
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.
|
|
unsigned int i;
|
|
for ( 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( (long) file_.tellg() );
|
|
trackPointers_.push_back( (long) 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( 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( 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( 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( 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:
|
|
case 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.
|
|
unsigned long i;
|
|
for ( 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] < tempoEvents_.size() - 1 ) {
|
|
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( 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( 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;
|
|
}
|
|
|
|
} // stk namespace
|