//
// Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
// Creation Date: Thu Jul 22 18:59:27 PDT 2010
// Last Modified: Thu Jul 22 18:59:30 PDT 2010
// Filename:      tools/midiexcerpt.cpp
// URL:           https://github.com/craigsapp/midifile/blob/master/tools/midiexcerpt.cpp
// Syntax:        C++11
// vim:           ts=3
//
// Description:   Extracts a time region from a MIDI file.  Notes
//                starting before the start time will be ignored.
//                Notes not ending before the end time of the file
//                will be turned off at the given end time.
//

#include "Options.h"
#include "MidiFile.h"
#include "PerlRegularExpression.h"

#include <cstdlib>

using namespace std;
using namespace smf;


void   checkOptions        (Options& opts);
void   example             (void);
void   usage               (const char* command);
double getTimeInSeconds    (const char* timestring);
void   extractMidi         (MidiFile& outputfile, MidiFile& inputfile,
                            double starttime, double endtime);
int    getStartIndex       (MidiFile& midifile, int starttick);
int    getStopIndex        (MidiFile& midifile, int startindex, int stoptick);

// User interface variables:
double starttime = 0.0;    // used with -s option
double endtime   = 0.0;    // used with -e option


///////////////////////////////////////////////////////////////////////////

int main(int argc, char** argv) {
	int       status;
	MidiFile  inputfile;
	MidiFile  outputfile;
	Options   options(argc, argv);

	checkOptions(options);

	status = inputfile.read(options.getArg(1));
	if (status == 0) {
		cout << "Syntax error in file: " << options.getArg(1) << "\n";
	}

	extractMidi(outputfile, inputfile, starttime, endtime);
	outputfile.write(options.getArg(2));

	return 0;
}

///////////////////////////////////////////////////////////////////////////


//////////////////////////////
//
// extractMidi -- Extract a time range from a MIDI file.  If the
//      endtime is negative, then that means through the end of the
//      original MIDI file.
//
//

void extractMidi(MidiFile& outputfile, MidiFile& inputfile, double starttime,
		double endtime) {

	outputfile.absoluteTime();
	outputfile.setTicksPerQuarterNote(inputfile.getTicksPerQuarterNote());
	if (inputfile.getTrackCount() > 1) {
		outputfile.addTrack(inputfile.getTrackCount()-1);
	}
	// outputfile.joinTracks();

	int i, j;


	Array<Array<Array<int> > > notestates;
	notestates.setSize(inputfile.getTrackCountAsType1());
	for (i=0; i<notestates.getSize(); i++) {
		notestates[i].setSize(16);
		for (j=0; j<16; j++) {
			notestates[i][j].setSize(128);
			notestates[i][j].allowGrowth(0);
			notestates[i][j].setAll(0);
		}
	}

	int offtype80 = 0;
	int offtype90 = 0;

	int starttick = inputfile.getAbsoluteTickTime(starttime);
	int stoptick  = inputfile.getAbsoluteTickTime(endtime);

	int startindex = getStartIndex(inputfile, starttick);
	int stopindex  = getStopIndex(inputfile, startindex, stoptick);

	MFEvent eventcopy;
	int track;
	int pitch;
	int channel = 0;

	// insert active tempo setting, if any
	MFEvent *tempoptr = NULL;
	for (i=0; i<startindex; i++) {
		if (inputfile.getEvent(0, i).isTempo()) {
			tempoptr = &inputfile.getEvent(0, i);
		}
	}
	if (tempoptr != NULL) {
		eventcopy = *tempoptr;
		eventcopy.time = 0;
		outputfile.addEvent(eventcopy);
	}

	// insert active timbre settings, if any
	Array<Array<int> > timbres;
	timbres.setSize(notestates.getSize());
	for (i=0; i<timbres.getSize(); i++) {
		timbres[i].setSize(16);
		timbres[i].setAll(-1);
	}
	for (i=0; i<startindex; i++) {
		if (inputfile.isTimbre(0, i)) {
			int tam = inputfile.getEvent(0, i).data[1];
			track = inputfile.getTrack(0, i);
			channel = inputfile.getChannelNibble(0, i);
			timbres[track][channel] = tam;
		}
	}
	eventcopy.data.setSize(2);
	eventcopy.time = 0;
	for (track=0; track<timbres.getSize(); track++) {
		for (channel=0; channel<timbres.getSize(); channel++) {
			if (timbres[track][channel] >= 0) {
				eventcopy.track = track;
				eventcopy.data[0] = 0xc0 | channel;
				eventcopy.data[1] = timbres[track][channel];
				outputfile.addEvent(eventcopy);
			}
		}
	}

	MFEvent *ptr;
	for (i=startindex; i<stopindex; i++) {
		ptr = &inputfile.getEvent(0,i);
		if (ptr->isNoteOff()) {
			if (ptr->getCommandNibble() == 0x90) {
				offtype90++;
			} else if (ptr->getCommandNibble() == 0x80) {
				offtype80++;
			}

			track = ptr->track;
			channel = ptr->getChannelNibble();
			pitch = ptr->data[1];
			if (notestates[track][channel][pitch] > 0) {
				notestates[track][channel][pitch]--;
			} else {
				// ignore the note off, since it is from a note
				// which was turned on before the selected time region.
				continue;
			}
		} else if (ptr->isNoteOn()) {
			track = ptr->track;
			pitch = ptr->data[1];
			notestates[track][channel][pitch]++;
		}
		eventcopy = *ptr;
		eventcopy.time -= starttick;
		outputfile.addEvent(eventcopy);
	}

	// Turn off any notes which are still on...

	int k;
	eventcopy.data.setSize(3);
	eventcopy.time = stoptick - starttick;
	for (track=0; track<notestates.getSize(); track++) {
		for (channel=0; channel<16; channel++) {
			for (pitch=0; pitch<128; pitch++) {
				for (k=0; k<notestates[track][channel][pitch]; k++) {
					eventcopy.track = track;
					eventcopy.data[1] = pitch;
					if (offtype90 > offtype80) {
						eventcopy.data[0] = (uchar)(0x90 | channel);
						eventcopy.data[0] = 0;
					} else {
						eventcopy.data[0] = (uchar)(0x80 | channel);
						eventcopy.data[2] = 64;
					}
					outputfile.addEvent(eventcopy);
				}
			}
		}
	}

	outputfile.sortTracks();
}



//////////////////////////////
//
// getStartIndex --
//

int getStartIndex(MidiFile& midifile, int starttick) {
	int i;
	for (i=0; i<midifile.getNumEvents(0); i++) {
		if (starttick <= midifile.getEvent(0,i).time) {
			return i;
		}
	}

	// something bad happened
	cerr << "ERROR in getStartIndex" << endl;
	exit(1);
}



//////////////////////////////
//
// getStopIndex --
//

int getStopIndex(MidiFile& midifile, int startindex, int stoptick) {
	int i;
	for (i=startindex; i<midifile.getNumEvents(0); i++) {
		if (stoptick <= midifile.getEvent(0,i).time) {
			return i-1;
		}
	}

	// something bad happened
	cerr << "ERROR in getStartIndex" << endl;
	exit(1);
}



//////////////////////////////
//
// checkOptions -- handle command-line options.
//

void checkOptions(Options& opts) {
	opts.define("begin|start|b|s=s:0", "Excerpt start time in sec or min:sec");
	opts.define("duration|d=s:0", "Duration of the excerpt in sec or min:sec");
	opts.define("end|e=s:-1", "Ending time of the excerpt in sec or min:sec");

	opts.define("author=b");
	opts.define("version=b");
	opts.define("example=b");
	opts.define("help=b");
	opts.process();

	if (opts.getBoolean("author")) {
		cout << "Written by Craig Stuart Sapp, "
			  << "craig@ccrma.stanford.edu, July 2010" << endl;
		exit(0);
	}
	if (opts.getBoolean("version")) {
		cout << "midiextract version 1.0" << endl;
		cout << "compiled: " << __DATE__ << endl;
	}
	if (opts.getBoolean("help")) {
		usage(opts.getCommand());
		exit(0);
	}
	if (opts.getBoolean("example")) {
		example();
		exit(0);
	}

	// can only have one output filename
	if (opts.getArgCount() != 2) {
		cout << "Error: need one input MIDI file and an output filename.";
		cout << endl;
		usage(opts.getCommand());
		exit(1);
	}

	starttime = getTimeInSeconds(opts.getString("begin"));
	if (opts.getBoolean("duration")) {
		double duration = getTimeInSeconds(opts.getString("duration"));
		if (duration <= 0.0) {
			cerr << "ERROR: duration must be positive" << endl;
			exit(1);
		}
		endtime = starttime + duration;
	} else {
		endtime = getTimeInSeconds(opts.getString("end"));
	}
}



//////////////////////////////
//
// getTimeInSeconds -- return the numeric value found in the string.
//    if the string contains a colon (:), then treate the number
//    on the left side of the colon as being in minutes, and the value
//    on the right as time in seconds.  Fractional values are allowed
//    on seconds.  Also allowed on minutes, but probably should not
//    be used...
//

double getTimeInSeconds(const char* timestring) {
	PerlRegularExpression pre;
	if (pre.search(timestring, ":", "")) {
		double minutes = 0.0;
		double seconds = 0.0;
		if (pre.search(timestring, "([\\d\\.\\+-]+):", "")) {
			minutes = strtod(pre.getSubmatch(1), NULL);
		}
		if (pre.search(timestring, ":([\\d\\.\\+-]+)", "")) {
			seconds = strtod(pre.getSubmatch(1), NULL);
		}
		return minutes * 60.0 + seconds;
	} else {
		if (pre.search(timestring, "([\\d+.+-]+)", "")) {
			return strtod(pre.getSubmatch(1), NULL);
		} else {
			return 0.0;
		}
	}
}



//////////////////////////////
//
// example -- gives example calls to the midiexcerpt program.
//

void example(void) {
	cout <<
	"# textmidi examples:                                                     \n"
	<< endl;
}


//////////////////////////////
//
// usage -- how to run the midiexcerpt program on the command line.
//

void usage(const char* command) {
	cout <<
	"                                                                         \n"
	<< endl;
}



