//
// Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
// Creation Date: Tue Jun 13 10:14:57 PDT 2000
// Last Modified: Tue Jul  4 21:37:29 PDT 2000
// Filename:      ...sig/doc/examples/all/batontap/batontap.cpp
// Syntax:        C++; batonImprov 2.0
//  
// Description:   Two general methods of tempo control for the radio
// 		  batons.
//

#include "batonImprov.h" 

#define MAXBEATPERIOD 2500  /* maximum time between beats, ignore if longer */
#define MAXLAGTIME      70  /* maximum time for same instant in time */
#define BASENOTE        60  /* MIDI base note for performance */
#define BATONSRATE    20.0  /* Radio Baton position reporting samping rate */


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

CircularBuffer<int> beattrigtime(1000); // times of previous beats
int avglen = 1;                         // beats to average for tempo
double idletime = 0.0;                  // time to sleep between iterations
SigTimer beatFraction;                  // for timing performance locations
double beatLocation = 0.0;              // for timing performance 
double lastBeatLocation = 0.0;          // for timing performance
int divisions = 4;                      // performance divisions per beat
int newdivisions = 4;                   // when changing the beat divisions
int divisionChange = 0;                 // for changing the beat divisions
int currentsub = -1;                    // currently playing subdivision
int numbermeaning = 'd';                // meaning of digits on keyboard
int runningQ = 0;                       // boolean for playing notes
int printsubQ = 1;                      // boolean for printing subbeats
int printbeatQ = 0;                     // boolean for printing beat info
int printpredictionQ = 0;               // boolean for printing beat pred. info
Voice voice;                            // for performance of subbeats

int predictionQ = 0;                    // boolean for beat prediction
int beatprediction = 0;                 // prediction location of next beat
int newprediction = 0;                  // newest predition of beat location
int correction = 0;                     // correction between old and new pred.
int triggerplane = 110;                 // trigger plane location


// function declarations:
double averagePeriod(CircularBuffer<int>& buffer, int avglen);
void   beattrig(int trigtime);
double periodToTempo(int mstime);
double periodToTempo(double mstime);
void   playnotes(double beatLocation, double lastBeatLocation, int& currentsub);
void   performnote(int subdivision, int totaldivision);
void   adjustTempoWithPrediction(void);
void   stick1position(void);
double tempoToPeriod(double tempo);


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

//////////////////////////////
//
// description -- this function is called by the improv interface
//     whenever a capital "D" is pressed on the computer keyboard.
//     Put a description of the program and how to use it here.
//

void description(void) {
   cout << 
   "BATONTAP by Craig Stuart Sapp, June 2000\n" 
   "Commands:\n"
   " [, ] = lower/raise the number of periods to average for tempo\n"
   " d#   = set the divisions per beat\n"
   " p    = toggle beat prediction\n"
   " u    = toggle display of subbeats on screen\n"
   " o    = toggle display of beat statistics\n"
   " i    = toggle display of beat prediction info\n"
   << endl;
} 



//////////////////////////////
//
// initialization -- this function is called by the improv
//     interface once at the start of the program.  Put items
//     here which need to be initialized at the beginning of
//     the program.
//

void initialization(void) { 
   baton.stick1position = stick1position;
   beattrigtime.reset();
   beattrigtime[0] = 0;
   beattrigtime[1] = 0;
   beattrigtime[2] = 0;
   beattrigtime[3] = 0;
   eventIdler.setPeriod(idletime);
   runningQ = 0;
   voice.setPort(synth.getPort());
   beatFraction.setTempo(80);
}



//////////////////////////////
//
// finishup -- this function is called by the improv interface
//     whenever the program is exited.  Put items here which
//     need to be taken care of when the program is finished.
//

void finishup(void) { }


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


//////////////////////////////
//
// mainloopalgorithms -- this function is called by the improv interface
//   continuously while the program is running.  The global variable t_time
//   which stores the current time is set just before this function is
//   called and remains constant while in this functions.
//

void mainloopalgorithms(void) { 
   lastBeatLocation = beatLocation;
   beatLocation = beatFraction.getPeriodCount();

   if (beattrigtime[1] != 0 && 
      beattrigtime[0] - beattrigtime[1] < MAXBEATPERIOD) {
      if (runningQ == 0) {
         lastBeatLocation = 0.0;
      }
      runningQ = 1;
   } else {
      runningQ = 0;
   }

   if (runningQ && beatLocation < 1.0) {
      playnotes(beatLocation, lastBeatLocation, currentsub);
   }
}



//////////////////////////////
//
// prepareNewPrediction -- make a new prediction when
//     the next beat will occur.
//

int prepareNewPrediction(void) {
   // get the instantaneous (average) velocity
   double velocity1 = baton.z1pb[0] - baton.z1pb[1];
   double velocity2 = baton.z1pb[1] - baton.z1pb[2];
   double velocity = (velocity1 + velocity2) / 2.0;

   // do stuff here with accelerations, etc., to improve prediction.

   // now know use trajectory velocity to figure 
   // out when we should arrive at the trigger plane

   // if heading in the wrong direction, then don't make
   // a prediction:
   if (velocity < -2) {
      return -1;
   } else if (velocity < 2) {
      return 0;
   }

   double tickcount = triggerplane / velocity;
   return t_time + (int)(BATONSRATE * tickcount);
}



//////////////////////////////
//
// playnotes -- 
//

void playnotes(double beatLocation, double lastBeatLocation, 
      int& currentsub) {
   int desiredsub = (int)(beatLocation * divisions);

   // if there is only one division per beat, then need
   // a special case:
   if (beatLocation < lastBeatLocation && divisions == 1) {
      performnote(0, 1);
      currentsub = 0;
      return;
   }

   int amount, j;
   if (desiredsub == currentsub) {
      return;
   } else if (desiredsub < currentsub) {
      // first play all notes upto the previous beat location:
      amount = divisions - 1 - currentsub;
      for (j=0; j<amount; j++) {
         performnote(currentsub + j + 1, divisions);
      }
      // now play all notes up to the desired subbeat location:
      for (j=0; j<=desiredsub; j++) {
         performnote(j, divisions);
      }
      currentsub = desiredsub;
   } else {
      amount = desiredsub - currentsub;
      for (j=0; j<amount; j++) {
         performnote(currentsub + j + 1, divisions);
      }
      currentsub = desiredsub;
   }
}


//////////////////////////////
//
// performnote -- 
//

void performnote(int subdivision, int totaldivision) {
   static int lasttime = 0;

   voice.play(BASENOTE + subdivision, 64);
   if (divisionChange && subdivision == 0) {
      divisions = newdivisions;
      divisionChange = 0;
   }

   if (printsubQ) {
      cout << subdivision << "\t(" << t_time - lasttime << ")" << endl;
   }

   lasttime = t_time;
}



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

//////////////////////////////
//
// stick1trig -- this function is called automatically whenever
//   a baton stick #1 trigger is received.
//

void stick1trig(void) { beattrig(t_time); }



//////////////////////////////
//
// stick2trig -- this function is called automatically whenever
//   a baton stick #2 trigger is received.
//

void stick2trig(void) { beattrig(t_time); }



//////////////////////////////
//
// b14plustrig -- this function is called automatically whenever
//   the b14+ button is pressed.
//

void b14plustrig(void) { beattrig(t_time); }



//////////////////////////////
//
// b15plustrig -- this function is called automatically whenever
//   the b15+ button is pressed.
//

void b15plustrig(void) { beattrig(t_time); }




//////////////////////////////
//
// b14minusuptrig -- this function is called automatically whenever
//   the b14- foot trigger is pressed.
//

void b14minusuptrig(void) { beattrig(t_time); }



//////////////////////////////
//
// b14minusdowntrig -- this function is called automatically whenever
//   the b14- foot trigger is released.
//

void b14minusdowntrig(void) { beattrig(t_time); }



//////////////////////////////
//
// b15minusuptrig -- this function is called automatically whenever
//   the b15- foot trigger is pressed.
//

void b15minusuptrig(void) { beattrig(t_time); }



//////////////////////////////
//
// b15minusdowntrig -- this function is called automatically whenever
//   the b15- foot trigger is released.
//

void b15minusdowntrig(void) { beattrig(t_time); }



///////////////////////////////
//
// keyboardchar -- this function is called by the improv interface
//     whenever a key is pressed on the computer keyboard.
//     Put commands here which will be executed when a key is
//     pressed on the computer keyboard.
//

void keyboardchar(int key) { 
   switch(key) {
      case 'p':   // toggle beat predictions
         predictionQ = !predictionQ;
         if (predictionQ) {
            cout << "Beats are predicted" << endl;
         } else {
            cout << "Beats are NOT predicted" << endl;
         }
         break;
      case 'i':   // toggle printing of beat predictions
         printpredictionQ = !printpredictionQ;
         if (printpredictionQ) {
            cout << "Prediction printing is ON" << endl;
         } else {
            cout << "Prediction printing is OFF" << endl;
         }
         break;
      case 'o':   // toggle printing of beat period information
         printbeatQ = !printbeatQ;
         if (printbeatQ) {
            cout << "Beat information is ON" << endl;
         } else {
            cout << "Beat information is OFF" << endl;
         }
         break;
      case 'u':   // toggle printing of subbeats
         printsubQ = !printsubQ;
         break;
      case ' ':   // initiate a beat
         beattrig(t_time);
         break;
      case '[':   // lower the beat averaging period count
      case '{':   // lower the beat averaging period count
         avglen--;
         if (avglen < 1) {
            avglen = 1;
         }
         cout << "Beat period averaging is: " << avglen << endl;
         break;
      case ']':   // raise the beat averaging period count
      case '}':   // raise the beat averaging period count
         avglen++;
         if (avglen > 1000) {
            avglen = 1000;
         }
         cout << "Beat period averaging is: " << avglen << endl;
         break;
      case '-':   // lower idle period
         idletime -= 1.0;
         if (idletime < 0.0) {
            idletime = 0.0;
         }
         cout << "Idle time = " << idletime << endl;
         eventIdler.setPeriod(idletime);
         break;
      case '=':   // raise idle period
         idletime += 1.0;
         cout << "Idle time = " << idletime << endl;
         eventIdler.setPeriod(idletime);
         break;
      case 'd':   // set the meaning of the numbers to divisions
         numbermeaning = 'd';
         cout << "Digits mean divisions per beat" << endl;
         break;
   }

   if (isdigit(key)) {
      switch (numbermeaning) {
         case 'd':
            if (key == '0') {
               break;
            }
            if (t_time - beattrigtime[0] < MAXLAGTIME) {
               divisions = key - '0';   // number of notes per beat
               cout << "Beat division: " << divisions << endl;
               divisionChange = 0;
            } else {
               newdivisions = key - '0';   // number of notes per beat
               cout << "Next beat divisions: " << divisions << endl;
               divisionChange = 1;
            }
            break;
      }
   }

}



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

///////////////////////////////
//
// beattrig -- trigger a new beat.
//

void beattrig(int trigtime) {
   beattrigtime.insert(trigtime);
   double avgperiod = averagePeriod(beattrigtime, avglen);
   int diff = beattrigtime[0] - beattrigtime[1];

   if (printbeatQ && beattrigtime[1] != 0 && diff < MAXBEATPERIOD) {
      cout << "T:" << trigtime
           << "\tbeat: "  << diff;
      if (avglen > 1) {
         cout << "\tAvg: "   << avgperiod;
      }
      cout << "\tTempo: " << periodToTempo(diff);
      if (avglen > 1) {
         cout << "\tAvg: " << periodToTempo(avgperiod);
      }
      cout << endl;
   }

   if (avgperiod > 0) {
      beatFraction.setPeriod(avgperiod);
      beatFraction.reset();
    
      // make a prediction for when the next beat will occur
      beatprediction = t_time + (int)avgperiod;
   }
}


///////////////////////////////
//
// beattrig -- trigger a new beat.
//

double averagePeriod(CircularBuffer& buffer, int avglen) {
   double sum = 0.0;
   int i = 0;
   int diff;
   for (i=1; i<=avglen; i++) {
      diff = buffer[i-1] - buffer[i];
      if (buffer[i] == 0) {
         break;
      } else if (diff > MAXBEATPERIOD) {
         break;
      } else {
         sum += diff;
      }
   }
   i--;
  
   if (i == 0) {
      return 0.0;
   } else {
      return sum/i;
   }
}



///////////////////////////////
//
// periodToTempo -- converts a time in milliseconds into a tempo
//    in beats per minute.
//

double periodToTempo(int mstime) {
   return (60000.0 / mstime);
}

double periodToTempo(double mstime) {
   return (60000.0 / mstime);
}

double tempoToPeriod(double tempo) {
   return (60000.0 / tempo);
}



//////////////////////////////
//
// stick1position -- called whenever a stick 1 position report comes in.
//

void stick1position(void) {
   if (predictionQ && beatLocation > 0.5) {
      adjustTempoWithPrediction();
   }
}



//////////////////////////////
//
// adjustTempoWithPrediction --
//

void adjustTempoWithPrediction(void) {
   newprediction = prepareNewPrediction();

   // don't know what is happening (stationary stick)
   if (newprediction == 0) {
      return;
   }

   // going in the wrong direction
   if (newprediction  < 0) {
      newprediction = beatprediction + 100;
      return;
   } 

   correction = beatprediction - newprediction;
   if (abs(correction) > beatFraction.getPeriod() * 0.7) {
      // error is too large, so ignore correction
      return;
   }

   // now have a half-way decent prediction, so adjust
   // tempo so that beatFraction timer will hit 1.0 at
   // newly predicted beat location.
   double oldTempo = beatFraction.getTempo();
   double newPeriod = beatFraction.getPeriod() + correction;
   double newTempo = periodToTempo(newPeriod);

   if (printpredictionQ) {
      cout << "Time: "         << t_time 
           << "\tError: "      << correction
           << "\tOld Tempo = " << oldTempo
           << "\tNew Tempo = " << newTempo
           << endl;
   }

   beatFraction.setTempo(newTempo); 
   beatprediction = newprediction;
}




// md5sum: 48f142d9e661d25f4c7746cf7651f787 batontap.cpp [20050403]