mirror of
https://github.com/thestk/stk
synced 2026-01-13 13:01:52 +00:00
Version 4.2.0
This commit is contained in:
committed by
Stephen Sinclair
parent
cf06b7598b
commit
a6381b9d38
359
src/MidiFileIn.cpp
Normal file
359
src/MidiFileIn.cpp
Normal file
@@ -0,0 +1,359 @@
|
||||
/**********************************************************************/
|
||||
/*! \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( 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.
|
||||
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( (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 || 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( 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;
|
||||
}
|
||||
Reference in New Issue
Block a user