365 lines
9.0 KiB
C++
365 lines
9.0 KiB
C++
//
|
|
// 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;
|
|
}
|
|
|
|
|
|
|