//
// Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
// Creation Date: Tue Apr 16 22:10:12 PDT 2002
// Last Modified: Fri Apr 19 13:03:58 PDT 2002
// Last Modified: Thu Oct 23 20:59:02 PDT 2003 (compiling to a file)
// Filename:      ...sig/doc/examples/all/timeplay/timeplay.cpp
// Syntax:        C++ 
//
// Description:   Play **kern entries in a Humdrum file through MIDI
//                by following a **time spine.
//

#include "synthImprov.h"
#include "humdrum.h"
#include "MidiFile.h"
#include <string.h>

/*----------------- beginning of improvization algorithms ---------------*/

SigTimer timer;           // for playing the humdrum file
EventBuffer eventBuffer;  // for storing note offs when playing notes

HumdrumFile data;         // humdrum file to play
MidiFile    midifile;     // for compiling into a MIDI file
Array<uchar> mididata(3);
int linenum    = 0;       // for keeping track of current line in file
int velocity   = 64;      // default velocity to play MIDI notes
int echoTextQ  = 1;       // boolean for displaying input file
int fileNumber = 1;       // current file number being played
int initial_time = 0;     // starting time of t_time

int     mine              =  0;    // used with the -m option
int     shortenQ          =  0;    // used with the -s option
int     shortenamount     =  0;    // used with the -s option
int     debugQ            =  0;    // used with the --debug option
int     midifileQ         =  0;    // used with the -o option
char    midifilename[2048] = {0};  // used with the -o option

Array<double> timings;

// function declarations:
void   checkOptions(void);
void   inputNewFile(HumdrumFile& data, Array<double>& timings);
void   playdata(HumdrumFile& data, int& linenum, SigTimer& timer, 
                           Array<double>& timings);
void   printInputLine(HumdrumRecord& record);
void   processNotes(HumdrumFile& data, int line, Array<double>& timings);
double getDeltaTimeValue(HumdrumFile& data, int linenum, 
                           Array<double>& timings);
void   getTimings(HumdrumFile& data, Array<double>& timings);
int    getendline(HumdrumFile& data, int startline, double duration);

/*--------------------- maintenance algorithms --------------------------*/


void description(void) { 
   printboxtop();      
   psl(" TIMEPLAY -- by Craig Stuart Sapp <craig@ccrma.stanford.edu> "
                                                        "-- 16 Apr 2002\n");
   printboxbottom();
}


void initialization(void) { 
   checkOptions();

   timer.setPeriod(500); 
   timer.reset();
   eventIdler.setPeriod(0);
   eventBuffer.setPollPeriod(10);
   if (midifileQ) {
      midifile.setTicksPerQuarterNote(1000);
      midifile.allocateEvents(0, 100000);
      midifile.absoluteTime();
      cout << "Saving MIDI data to the file " << midifilename << endl;
   }
   initial_time = t_time;
}


void finishup(void) { 
   eventBuffer.off();
   if (midifileQ) {
      midifile.sortTracks();
      cout << "Checking: midifilename is " << midifilename << endl;
      midifile.write(midifilename);
   }
}

/*-------------------- main loop algorithms -----------------------------*/

void mainloopalgorithms(void) { 
   eventBuffer.checkPoll();
   if (timer.expired()) {
      playdata(data, linenum, timer, timings);
      if (linenum >= data.getNumLines()) {
         if (midifileQ) {
            finishup();
            exit(0);
         }
         inputNewFile(data, timings);
      }
   }
}

/*-------------------- triggered algorithms -----------------------------*/

void keyboardchar(int key) { 
   switch (key) {
      case ' ':               // toggle display of file while playing
         echoTextQ = !echoTextQ;
         if (echoTextQ) {
            cout << "!! FILE DISPLAY TURNED ON" << endl;
         } else {
            cout << "!! FILE DISPLAY TURNED OFF" << endl; 
         }
         break;
      case 's':    // silence notes
         eventBuffer.off();
         break;
   }
}


/*------------------ end improvization algorithms -----------------------*/


//////////////////////////////
//
// checkOptions --  process command-line options and setup the
//   humdrum data to play.
//

void checkOptions(void) {
   options.define("o|output=s:output.mid");
   options.define("p|pause=d:1.0", "pause time in seconds between files");
   options.define("q|quiet=b", "Turn off data echoing while playing");
   options.define("v|velocity=i:64", "Default MIDI key velocity");
   options.define("m|min=i:0",      "minimum tick duration of notes");
   options.define("s|shorten=i:0",  "shortening tick value for note durations");
   options.define("debug=b",        "debug messages turned on");
   options.process();

   midifileQ = options.getBoolean("output");
   strcpy(midifilename, options.getString("output"));
   debugQ = options.getBoolean("debug");
   velocity = options.getInteger("velocity");
   if (options.getBoolean("quiet")) {
      echoTextQ = 0;
   }

   if (options.getArgCount() < 1) {
      data.read(cin);
   } else {
      inputNewFile(data, timings);
   }

   mine = options.getInteger("min");
   if (mine < 0) {
      mine = 0;
   }
   shortenQ = options.getBoolean("shorten");
   shortenamount = options.getInteger("shorten");
}



//////////////////////////////
//
// inputNewFile -- load in a new Humdrum file.
//

void inputNewFile(HumdrumFile& data, Array& timings) {
   data.clear();
   linenum = 0;    

   int count = options.getArgCount();
   if (fileNumber > count) {
      finishup();
      exit(0);
   }

   data.read(options.getArg(fileNumber));
   data.analyzeRhythm("4");
   getTimings(data, timings);

   if (fileNumber > 1) {
      millisleep((float)(1000 * options.getDouble("pause")));
   }
   fileNumber++;
}



//////////////////////////////
//
// getTimings -- get the absolute timing values from the humdrum file.
//

void getTimings(HumdrumFile& data, Array& timings) {
   int i, j;
   timings.setSize(data.getNumLines());
   timings.allowGrowth(0);
   timings.setAll(-1.0);
   for (i=0; i<data.getNumLines(); i++) {
      if (data[i].isData()) {
         // find the **time spine and extract the timing info
         // (in milliseconds only for now
         for (j=0; j<data[i].getFieldCount(); j++) {
            if (strcmp(data[i].getExInterp(j), "**time") == 0) {
               timings[i] = atof(data[i][j]);
               break;
            }
         }
      } else if (strncmp(data[i][0], "**", 2) == 0) {
         // check for **time spine
         int found = 0;
         for (j=0; j<data[i].getFieldCount(); j++) {
            if (strcmp(data[i][j], "**time") == 0) {
               found = 1; 
               break;
            }
         }
         if (!found) {
            cout << "Error: The input humdrum file does not "
                 << "have a **time spine" << endl;
            exit(1);
         }
         timings[i] = -1.0; 
      } else {
         // if it is not data then the time is just equal to the next
         // data element; store -1 in the array for now and fill in later
         timings[i] = -1.0; 
      }
   }

   // now work backwards to generate the timing information for 
   // 0 duration lines.
   double current = -1.0;
   for (i=timings.getSize()-1; i>=0; i--) {
      if (timings[i] == -1.0) {
         timings[i] = current;
      } else {
         current = timings[i];
      }
   }

   // print timings
   if (debugQ) {
      for (int i=0; i<timings.getSize(); i++) {
         cout << timings[i] << "\t" << data[i] << endl;
      }
   }

}


//////////////////////////////
//
// playdata -- play the next line of the humdrum file, update the 
//     line number and the time for the next events to be read 
//     from the file.
//

void playdata(HumdrumFile& data, int& linenum, SigTimer& timer, 
      Array<double>& timings) {
   double duration = 0.0;     // duration of the current line;
   while (linenum < data.getNumLines() && duration == 0.0) {
      duration = getDeltaTimeValue(data, linenum, timings);
      if (data[linenum].isData()) {
         processNotes(data, linenum, timings);
      } 
      if (echoTextQ) {
         printInputLine(data[linenum]);
      }
      if (duration > 0.0) {
         timer.setPeriod(duration);
         timer.reset();
      }
      linenum++;
   }
}



//////////////////////////////
//
// printInputLine -- print the current line of the file,
//  omitting the duration field at the end of the line
//

void printInputLine(HumdrumRecord& record) {
   cout << record.getLine() << endl;
}



//////////////////////////////
//
// processNotes -- play all kern notes in the current record and
//    return the shortest note duration.
//

void processNotes(HumdrumFile& data, int line, Array& timings) {
   HumdrumRecord& record = data[line];
   NoteEvent note;
   int pitch = 0;
   double duration = 0.0;
   int staccatoQ = 0;
   int accentQ = 0;
   int sforzandoQ = 0;
   int i, j;
   int notecount = 0;
   int endline = 0;
   char buffer[128] = {0};
   for (i=0; i<record.getFieldCount(); i++) {
      if (record.getExInterpNum(i) == E_KERN_EXINT) {
         notecount = record.getTokenCount(i);
         if (strcmp(record[i], ".") == 0) {
            continue;
         }
         for (j=0; j<notecount; j++) {
            record.getToken(buffer, i, j);
            if (strchr(buffer, '[')) {
               // total tied note durations
               endline = getendline(data, line, data.getTiedDuration(line,i,j));
               duration = timings[endline] - timings[line];
            } else {
               endline = getendline(data,line,Convert::kernToDuration(buffer));
               duration = timings[endline] - timings[line];
            }
            pitch = Convert::kernToMidiNoteNumber(buffer); 
            // skip rests
            if (pitch < 0) {
               continue;
            }

            // skip tied notes
            if (strchr(buffer, '_') || strchr(buffer, ']')) {
               continue;
            }

            accentQ = (int)strchr(buffer, '^');
            sforzandoQ = (int)strchr(buffer, 'z');
            staccatoQ = (int)strchr(buffer, '\'');
            note.setChannel(0);
            note.setKey(pitch);
            note.setOnTime(t_time);
            if (shortenQ) {
               // duration -= shortenamount;
               if (duration < mine) {
                  duration = mine;
               }
            }
            note.setDur((int)(duration+0.5));
            if (staccatoQ) {
               note.setDur((int)(0.5 * note.getDur()));
            }
            note.setVelocity(velocity);
            if (accentQ) {
               note.setVelocity((int)(note.getVelocity() * 1.3));
            }
            if (sforzandoQ) {
               note.setVelocity((int)(note.getVelocity() * 1.5));
            }
 
            if (!midifileQ) {
               note.activate();
               note.action(eventBuffer);
               eventBuffer.insert(note);
            } else {
               mididata[0] = (0x0f & note.getChannel()) | 0x90;
               mididata[1] = note.getKey();
               mididata[2] = note.getVelocity();
               midifile.addEvent(0, t_time - initial_time, mididata);
               mididata[2] = 0;
               midifile.addEvent(0, 
                     t_time - initial_time + (int)(duration+0.5), 
                     mididata);
            }
         }
      }
   }
}



//////////////////////////////
//
// getendline -- get the absolute duration in milliseconds of the given
//    score position
//

int getendline(HumdrumFile& data, int startline, double duration) {
   double starttime = data[startline].getAbsBeat();
   double endtime = starttime;
   int i = startline;
   double matchdur = 0.0;
   while (i<data.getNumLines()) {
      i++;
      endtime = data[i].getAbsBeat();
      matchdur = endtime - starttime;
      if (matchdur >= duration) {
         return i;
      } 
   }
   return i;
}



//////////////////////////////
//
// getDeltaTimeValue -- return the current absolute time minus
//    the previous absolute time.
//

double getDeltaTimeValue(HumdrumFile& data, int linenum, 
      Array<double>& timings) {
   if (linenum == timings.getSize()-1) {
      return 0.0;
   }
   return timings[linenum+1] - timings[linenum];
}




// md5sum: a55e37e4741c297a838bd18b67886ecb timeplay.cpp [20050403]