//
// Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
// Creation Date: Wed Oct 27 11:03:38 PDT 2004
// Last Modified: Wed Oct 27 12:33:26 PDT 2004
// Filename:      ...sig/examples/all/modid.cpp
// Web Address:   http://sig.sapp.org/examples/museinfo/humdrum/modid.cpp
// Syntax:        C++; museinfo
//
// Description:   Automatic modulation identification using the 
//                Krumhansl-Schmuckler key-finding algorithm.
//
// A modulation is defined with following algorithm:
//    (1) defined a window size (e.g. 16 beats).
//    (2) choose a test modulation position in the score.
//    (3) if the window of music starting at that window generate the same 
//        key analysis as a 1/2 window at the same location, then there
//        is a stable key after the test modulation position.
//    (4) if the window of music before the starting position is stable
//        according to the same method, then continue.
//    (5) if a window centered at the modulation point gives a lower
//        score for the two keys from the window before and after the
//        test modulation point, then mark the position in the score
//        as a modulation.
// 

#include "humdrum.h"

// function declarations
void   checkOptions(Options& opts, int argc, char* argv[]);
void   example(void);
void   usage(const char* command);
int    getDurationLineStart(HumdrumFile& infile, double target);
int    getDurationLineEnd(HumdrumFile& infile, double target);
void   getModulation(int& mod, int& before, HumdrumFile& infile, 
                                 int line, double window);
void   getModulationAnalysis(HumdrumFile& infile, Array<int>& modulations,
                                 Array<int>& beforemod);
void   printModulations(HumdrumFile& infile, Array<int>& modulations,
                                 Array<int>& beforemod);
void   printPitch(int value);

// user interface variables
Options      options;            // database for command-line arguments
int          appendQ  = 0;       // used with -a option
int          prependQ = 0;       // used with -p option
double       window   = 16.0;    // used with -w option
int          debugQ   = 0;       // used with --debug option

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

int main(int argc, char* argv[]) {
   // process the command-line options
   checkOptions(options, argc, argv);

   // if no command-line arguments read data file from standard input
   HumdrumFile infile;
   if (options.getArgCount() < 1) {
      infile.read(cin);
   } else {
      infile.read(options.getArg(1));
   }
   infile.analyzeRhythm();

   Array<int> modulations;
   Array<int> beforemod;

   getModulationAnalysis(infile, modulations, beforemod);
   printModulations(infile, modulations, beforemod);

   return 0;
}


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


//////////////////////////////
//
// printModulations --
//

void printModulations(HumdrumFile& infile, Array<int>& modulations,
      Array<int>& beforemod) {
   int i;
   for (i=0; i<infile.getNumLines(); i++) {
      if (appendQ) {
         cout << infile[i];
      }

      if (appendQ) {

         if (infile[i].getType() == E_humrec_data) {
            if (modulations[i] == -1) {
               cout << "\t.";
            } else if (modulations[i] == -2) {
               cout << "\t";
               printPitch(beforemod[i]);
            } else if (modulations[i] == -3) {
               cout << "\t(";
               printPitch(beforemod[i]);
               cout << ")";
            } else if (beforemod[i] == -1) {
               cout << "\t.";
            } else {
               cout << "\t";
               printPitch(beforemod[i]);
               cout << ":";
               printPitch(modulations[i]);
            }
         } else if (infile[i].getType() == E_humrec_data_measure) {
            cout << "\t" << infile[i][0];
         } else if (infile[i].getType() == E_humrec_interpretation) {
            if (strncmp(infile[i][0], "**", 2) == 0) {
               cout << "\t**mod";
            } else if (strcmp(infile[i][0], "*-") == 0) {
               cout << "\t*-";
            } else {
               cout << "\t*";
            }
         }

      } else if (prependQ) {

         if (infile[i].getType() == E_humrec_data) {
            if (modulations[i] == -1) {
               cout << ".\t";
            } else if (modulations[i] == -2) {
               printPitch(beforemod[i]);
               cout << "\t";
            } else if (modulations[i] == -3) {
               cout << "(";
               printPitch(beforemod[i]);
               cout << ")";
               cout << "\t";
            } else if (beforemod[i] == -1) {
               cout << ".\t";
            } else {
               printPitch(beforemod[i]);
               cout << ":";
               printPitch(modulations[i]);
               cout << "\t";
            }
         } else if (infile[i].getType() == E_humrec_data_measure) {
            cout << infile[i][0];
            cout << "\t";
         } else if (infile[i].getType() == E_humrec_interpretation) {
            if (strncmp(infile[i][0], "**", 2) == 0) {
               cout << "**mod\t";
            } else if (strcmp(infile[i][0], "*-") == 0) {
               cout << "*-\t";
            } else {
               cout << "*\t";
            }
         }
         cout << infile[i];

      } else {
         if (infile[i].getType() == E_humrec_data) {
            if (modulations[i] == -1) {
               cout << ".";
            } else if (modulations[i] == -2) {
               printPitch(beforemod[i]);
            } else if (modulations[i] == -3) {
               cout << "(";
               printPitch(beforemod[i]);
               cout << ")";
            } else if (beforemod[i] == -1) {
               cout << ".";
            } else {
               cout << ""; 
               printPitch(beforemod[i]);
               cout << ":";
               printPitch(modulations[i]);
            }
         } else if (infile[i].getType() == E_humrec_data_measure) {
            cout << "" << infile[i][0];
         } else if (infile[i].getType() == E_humrec_interpretation) {
            if (strncmp(infile[i][0], "**", 2) == 0) {
               cout << "**mod";
            } else if (strcmp(infile[i][0], "*-") == 0) {
               cout << "*-";
            } else {
               cout << "*";
            }
         } else {
            cout << infile[i];
         }
      }

      cout << "\n";
   }
  
}



//////////////////////////////
//
// printPitch --
//

void printPitch(int value) {
   int mode = 0;
   if (value >= 12) {
      value -= 12;
      mode = 1;  // minor key
   }

   int base40 = Convert::base12ToBase40(value);
   base40 = (base40 + 40) % 40;
   base40 = base40 + 3 * 40;
   if (mode == 1) {
      base40 += 40;
   }

   char buffer[64] = {0};
   cout << Convert::base40ToKern(buffer, base40);
}



//////////////////////////////
//
// getModulationAnalysis --
//

void getModulationAnalysis(HumdrumFile& infile, Array<int>& modulations,
   Array<int>& beforemod) {
   modulations.setSize(infile.getNumLines());
   modulations.setAll(-1);
   beforemod.setSize(infile.getNumLines());
   beforemod.setAll(-1);

   int i;
   int mod;
   int before;
   for (i=0; i<infile.getNumLines(); i++) {
      if (infile[i].getType() != E_humrec_data) {
         continue;
      }
      getModulation(mod, before, infile, i, window);
      modulations[i] = mod;
      beforemod[i] = before;
   }
}



//////////////////////////////
//
// getModulation -- 
//

void getModulation(int& mod, int& before, HumdrumFile& infile, int line, 
      double window) {
   if (infile[line].getAbsBeat() < window) {
      // not enough music before the modulation point to analyze.
      mod = -1;
      before = -1;
      return;
   }

   if (infile[infile.getNumLines()-1].getAbsBeat() - 
          infile[line].getAbsBeat() < window) {
      mod = -1;
      before = -1;
      return;
   }
  
   Array<double> scores(24);

   int wafter   = -1;  // ending window index after mod point
   int w2after  = -1;  // ending half-window index after mod point
   int wbefore  = -1;  // starting window index before mod point
   int w2before = -1;  // starting half-window index before mod point
   int midval   = -1;  
  
   double rafter     = -1.0; // rvalue for window after mod point
   double r2after    = -1.0; // rvalue for half-window after mod point
   double rbefore    = -1.0; // rvalue for window before mod point
   double r2before   = -1.0; // rvalue for half-window before mod point
   double rmidbefore = -1.0; // rvalue 1 for mid-window
   double rmidafter  = -1.0; // rvalue 1 for mid-window

   int wafterstop    = getDurationLineEnd(infile, 
                          infile[line].getAbsBeat()+window);
   int w2afterstop   = getDurationLineEnd(infile, 
                          infile[line].getAbsBeat()+window/2.0);
   int wbeforestart  = getDurationLineStart(infile, 
                          infile[line].getAbsBeat()-window);
   int w2beforestart = getDurationLineStart(infile, 
                          infile[line].getAbsBeat()-window/2.0);

   wafter     = infile.analyzeKeyKS(scores, line, wafterstop);
   rafter     = scores[wafter];
   w2after    = infile.analyzeKeyKS(scores, line, w2afterstop);
   r2after    = scores[wafter];
   wbefore    = infile.analyzeKeyKS(scores, wbeforestart, line);
   rbefore    = scores[wafter];
   w2before   = infile.analyzeKeyKS(scores, w2beforestart, line);
   r2before   = scores[wafter];
   midval     = infile.analyzeKeyKS(scores, w2beforestart, w2afterstop);
   rmidbefore = scores[wbefore];
   rmidafter  = scores[wafter];

   if (debugQ) {
      cout << "wa=" << wafter;
      cout << "\tw2a=" << w2after;
      cout << "\twb=" << wbefore;
      cout << "\tw2b=" << w2before;
      cout << endl;
   }

   mod    = -1;
   before = -1;

   if ((midval == w2before) && (midval == w2after)) {
      if ((w2before != wbefore) || (w2after != wafter)) {
         before = midval;
         mod = -3;
         return;
      }
   }

   if (wbefore != w2before)   { return;  }
   if (wafter  != w2after)    { return;  }
   if (wbefore == wafter)     { 
      if (midval == wbefore) {   // indicate a stable local region
         before = midval; 
         mod = -2;
      } 
      return;  
   }

   mod = wafter;
   before = wbefore;
}



//////////////////////////////
//
// getDurationLineEnd -- return the index of the last line
//   with the specified duration (or less).
//

int getDurationLineEnd(HumdrumFile& infile, double target) {
   int i;
   for (i=infile.getNumLines()-1; i>=0; i--) {
      if (infile[i].getAbsBeat() <= target) {
         return i;
      }      
   }
   return 0;
}



//////////////////////////////
//
// getDurationLineStart -- return the index of the first line
//   with the specified duration (or greater).
//

int getDurationLineStart(HumdrumFile& infile, double target) {
   int i;
   for (i=0; i<infile.getNumLines(); i++) {
      if (infile[i].getAbsBeat() >= target) {
         return i;
      }      
   }
   return infile.getNumLines()-1;
}



//////////////////////////////
//
// checkOptions -- validate and process command-line options.
//

void checkOptions(Options& opts, int argc, char* argv[]) {
   opts.define("a|append=b",      "append analysis to original score");
   opts.define("p|prepend=b",     "prepend analysis to original score");
   opts.define("w|window=d:16.0", "analysis window size");   

   opts.define("debug=b",       "trace input parsing");   
   opts.define("author=b",      "author of the program");   
   opts.define("version=b",     "compilation information"); 
   opts.define("example=b",     "example usage"); 
   opts.define("h|help=b",      "short description"); 
   opts.process(argc, argv);
   
   // handle basic options:
   if (opts.getBoolean("author")) {
      cout << "Written by Craig Stuart Sapp, "
           << "craig@ccrma.stanford.edu, Oct 2004" << endl;
      exit(0);
   } else if (opts.getBoolean("version")) {
      cout << argv[0] << ", version: 27 Oct 2004" << endl;
      cout << "compiled: " << __DATE__ << endl;
      cout << MUSEINFO_VERSION << endl;
      exit(0);
   } else if (opts.getBoolean("help")) {
      usage(opts.getCommand());
      exit(0);
   } else if (opts.getBoolean("example")) {
      example();
      exit(0);
   }

   appendQ  = opts.getBoolean("append");
   prependQ = opts.getBoolean("prepend");
   window   = opts.getDouble("window");
   if (window <= 0) {
      window = 16.0;
   }
   debugQ = opts.getBoolean("debug");

}



//////////////////////////////
//
// example -- example usage of the maxent program
//

void example(void) {
   cout <<
   "                                                                        \n"
   << endl;
}



//////////////////////////////
//
// usage -- gives the usage statement for the quality program
//

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


// md5sum: efada328c3889e333d3f590620aa5d30 modid.cpp [20050403]