//
// Programmer:    Craig Stuart Sapp <craig@ccrma.stanford.edu>
// Creation Date: Thu Oct 24 00:44:06  2002
// Last Modified: Thu Oct 24 09:14:26 PDT 2002
// Filename:      ...sig/doc/examples/sig/sigfile/vogran/vogran.cpp
// Syntax:        C++; sig
//
// Description:   Granulate a soundfile by layers of grain voices.
//
// Input parameters:
// *  number of voices (envelope)
// *  grain size (envelope) milliseconds [default 50]
// *  grain size variation (envelope) milliseconds [default 0]
// *  transition width in milliseconds (envelope) [default 5]
// *  start time in sound file in seconds [default 0]
// *  duration of sound in input file in seconds [default -1 = end of sound]
// *  duration of output sound in seconds [default = -1 = duration of input]
// *  gap time in milliseconds (envelope) [default 10]
// *  gap variation in milliseconds (envelope) [default 0]
// *  time variation for synchrony of voices/grains (envelope) in ms [default 0]
//
// *  optional normalization level
// *  optional output amplitude envelope
// *  density check.
//
// Note: Parameter memory is a hog.
//

#include "sigAudio.h"
#include "SoundFileWrite.h"
#include "SoundFileRead.h"

#include <stdlib.h>

#ifndef OLDCPP
   #include <iostream>
   using namespace std;
#else
   #include <iostream.h>
#endif

class voiceItem {
   public:
      int voice;       // voice to which this item belongs
      int grainsize;   // size of grain in samples
      int transition;  // size of transition in samples
      int start;       // start time of opening transition
      int offset;      // time offset from input in samples
   
      void clear(void) {
         voice = grainsize = transition = start = offset = 0;
      }
      void print(void) {
         cout << "v: "     << voice      << "\t";
         cout << "grnsz: " << grainsize  << "\t";
         cout << "trans: " << transition << "\t";
         cout << "strt: "  << start      << "\t";
         cout << "ofst: "  << offset     << "\n";
      }
};
      

// function declarations:
void checkOptions(Options& opts);
void example(void);
void usage(const char* command);
void fillInputBuffer(CircularBuffer<double>& inputsound, const char* infile, 
                         double start, double dur);
void doGranulations(CircularBuffer<double>& inputdata, 
                         Array<double>& outputdata);
int  randomvar(int base, int var);
void addVoiceGrains(Array<voiceItem>& grains, int voice, 
                       Array<int>& voiceenv, Array<int>& grainsize, 
                       Array<int>& grainvarsize, Array<int>& gapsize, 
                       Array<int>& gapvarsize, Array<int>& transitions);
void insertGrain(Array<double>& outputdata, 
                       CircularBuffer<double>& inputdata, voiceItem& grain);
void makeHalfWindow(Array<double>& window, int direction);


// input parameters:
int    densityQ  =  0;                 // used with --density
int    verboseQ  =  0;                 // used with --verbose option
double starttime =  0;                 // used with -s option
double duration  = -1;                 // used with -d option
double outputdur = -1;                 // used with --od option
const char* voiceenv   = "1";          // used with -v option
const char* grainenv   = "0 50 1 50";  // used with -g option
const char* grainvarenv= "0 0 1 0";    // used with --gv option
const char* gapenv     = "0 50 1 50";  // used with --gap option
const char* gapvarenv  = "0 0 1 0";    // used with --gapv option
const char* transenv   = "0 5 1 5";    // used with -t option


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

int main(int argc, char* argv[]) {
   Options options(argc, argv);
   checkOptions(options);

   CircularBuffer<double> inputdata;
   fillInputBuffer(inputdata, options.getArg(1), starttime, duration);

   Array<double> outputdata;
   int outputsize = 0;
   if (outputdur <= 0) {
      outputsize = inputdata.getSize();
   } else {
      outputsize = (int)(outputdur * 44100 + 0.5); // assume 44100 srate
   }
   outputdata.setSize(outputsize);
   outputdata.setAll(0.0);

   doGranulations(inputdata, outputdata);
   int i;
  
   // apply the amplitude envelope to the output soundfile if one is given
   if (options.getBoolean("amplitude")) {
      Envelope amp(options.getString("amp-env"), outputdata.getSize());
      for (i=0; i<outputdata.getSize(); i++) {
         amp.tick(i);
         outputdata[i] *= amp.output(0);
      }
   }   

   // normalize the data to specified max amplitude if requested 
   if (options.getBoolean("normalize")) {
      double max = -1;
      for (i=0; i<outputdata.getSize(); i++) {
         if (fabs(outputdata[i]) > max) {
            max = fabs(outputdata[i]);
         }
      }
      double scale = options.getDouble("normalization") / max;
      for (i=0; i<outputdata.getSize(); i++) {
         outputdata[i] *= scale;
      }
   }

   // write the sound data to the output file
   SoundHeader header;
   header.setHighMono();
   SoundFileWrite outsound(options.getArg(2), header);
   for (i=0; i<outputdata.getSize(); i++) {
      outsound.writeSampleDouble(outputdata[i]);
   }

   return 0;
}


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



//////////////////////////////
//
// doGranulations -- do all of the work.
//

void doGranulations(CircularBuffer& inputdata, Array& outputdata) {

   Array<voiceItem> grains;
   grains.setSize(1000000);
   grains.setGrowth(1000000);
   grains.setSize(0);

   Array<int> voices(outputdata.getSize());
   Array<int> grainsize(outputdata.getSize());
   Array<int> grainvarsize(outputdata.getSize());
   Array<int> gapsize(outputdata.getSize());
   Array<int> gapvarsize(outputdata.getSize());
   Array<int> transitions(outputdata.getSize());

   Envelope voiceenv(voiceenv, outputdata.getSize());
   Envelope grainsizeenv(grainenv, outputdata.getSize());
   Envelope grainvarsizeenv(grainvarenv, outputdata.getSize());
   Envelope transitionenv(transenv, outputdata.getSize());
   Envelope gapsizeenv(gapenv, outputdata.getSize());
   Envelope gapvarsizeenv(gapvarenv, outputdata.getSize());
   
   
   int maxvoice = 0;
   int i;
   for (i=0; i<outputdata.getSize(); i++) {
      voiceenv.tick(i);
      voices[i] = int(voiceenv.output(0) + 0.5);
      if (voices[i] > maxvoice) {
         maxvoice = voices[i];
      }

      grainsizeenv.tick(i);
      grainsize[i] = (int)(grainsizeenv.output(0)/1000.0*44100.0 + 0.5);

      grainvarsizeenv.tick(i);
      grainvarsize[i] = (int)(grainvarsizeenv.output(0)/1000.0*44100.0 + 0.5);

      gapsizeenv.tick(i);
      gapsize[i] = (int)(gapsizeenv.output(0)/1000.0*44100.0 + 0.5);

      gapvarsizeenv.tick(i);
      gapvarsize[i] = (int)(gapvarsizeenv.output(0)/1000.0*44100.0 + 0.5);

      transitionenv.tick(i);
      transitions[i] = (int)(transitionenv.output(0)/1000.0*44100.0 + 0.5);

   }

   for (i=1; i<=maxvoice; i++) {
      addVoiceGrains(grains, i, voices, grainsize, grainvarsize, gapsize, 
          gapvarsize, transitions);
   }

   if (verboseQ) {
      for (i=0; i<grains.getSize(); i++) {
         cout << "Grain " << i+1 << ":\t";
         grains[i].print();
      }
   }

   for (i=0; i<grains.getSize(); i++) {
      insertGrain(outputdata, inputdata, grains[i]);
   }
}


//////////////////////////////
//
// insertGrain -- 
//

void insertGrain(Array<double>& outputdata, CircularBuffer<double>& inputdata, 
      voiceItem& grain) {

   if (densityQ) {
      outputdata[grain.start] = 0.1;
//      return;
   }

   static Array<double> tStart(0);
   static Array<double> tStop(0);

   if (grain.transition != tStart.getSize()) {
      tStart.setSize(grain.transition);
      makeHalfWindow (tStart, 0);
   }
   if (grain.transition != tStop.getSize()) {
      tStop.setSize(grain.transition);
      makeHalfWindow (tStop, -1);
   }
   int i;

   int inputstart = rand() % (inputdata.getSize()-22050);

   // put the starting window on.
   for (i=0; i<tStart.getSize(); i++) {
      outputdata[grain.start + i] += inputdata[inputstart + i] * tStart[i];
   }

   // copy the constant portion of the grain
   for (i=0; i<grain.grainsize; i++) {
      outputdata[grain.start + tStart.getSize() + i] += 
         inputdata[inputstart + tStart.getSize() + i];
   }

   // put the trailing window on.
   for (i=0; i<tStop.getSize(); i++) {
      outputdata[grain.start + grain.grainsize + tStart.getSize() + i] += 
         inputdata[inputstart + grain.grainsize + tStart.getSize() + i] * tStop[i];
   }

}


//////////////////////////////
//
// addVoiceGrains --
//

void addVoiceGrains(Array<voiceItem>& grains, int voice, 
      Array<int>& voices, Array<int>& grainsize, Array<int>& grainvarsize, 
      Array<int>& gapsize, Array<int>& gapvarsize, Array<int>& transitions) {

   voiceItem tempitem;
   int gapsamples;

   int i;
   for (i=0; i<voices.getSize(); i++) {
      if (voice <= voices[i]) {
         tempitem.voice = voice;
         tempitem.grainsize = randomvar(grainsize[i], grainvarsize[i]);
         tempitem.transition = transitions[i];
         tempitem.start = i;
         tempitem.offset = rand() % 1000;
         gapsamples = randomvar(gapsize[i], gapvarsize[i]);
         if (tempitem.start+tempitem.transition*2+tempitem.grainsize < 
               voices.getSize()) {
            grains.append(tempitem);
         }
         i += tempitem.transition * 2 + tempitem.grainsize + gapsamples;
      }
   }
}



//////////////////////////////
//
// randomvar --
//

int randomvar(int base, int var) {
   int value = (int)(base + var * (2.0 * rand()/RAND_MAX - 1.0) + 0.5);
   if (value < 0) {
      value = 0;
   }
   return value;
}



//////////////////////////////
//
// makeHalfWindow --
//

#define MY_PI 3.1415926535897932384626
#ifndef TWOPI
   #define TWOPI 6.2831853
#endif

void makeHalfWindow(Array& window, int direction) {
   int samples = window.getSize();
   int i;
   double phase = 0.0;
   if (direction == -1) {
      phase = MY_PI/2;
   }
   double freq = 1.0 / (samples * 4);
   for (i=0; i<samples; i++) {
      window[i] = sin(TWOPI * freq * i + phase);
      window[i] *= window[i];
   }
}



//////////////////////////////
//
// fillInputBuffer -- read in the input sound which is source material
//     for the granulation.
//

void fillInputBuffer(CircularBuffer<double>& inputsound, 
      const char* infile, double start, double dur) {

   SoundFileRead soundfile(infile);
   int startsample = (int) (start * soundfile.getSrate());
   soundfile.gotoSample(startsample);
   int samplecount = 0;
   if (duration <= 0) {
      samplecount = soundfile.getSamples() - startsample;
   } else {
      samplecount = (int)(duration * soundfile.getSrate());
   }

   inputsound.setSize(samplecount);
   inputsound.reset();

   int i;
   for (i=0; i<samplecount; i++) {
      inputsound[i] = soundfile.getCurrentSampleDouble(0);  // 0 = first chan.
      soundfile.incrementSample(); 
   }
}



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

void checkOptions(Options& opts) {
   opts.define("a|amp|amplitude|amp-env=s:0 1 1 1");
   opts.define("n|norm|normalize|normalization=d:1.0");
   opts.define("d|dur|duration=d:-1.0 seconds");
   opts.define("od|output-duration=d:-1.0 seconds");
   opts.define("s|start-time=d:0.0 seconds");
   opts.define("v|voices|voice-env=s:0 1 1 1");
   opts.define("g|grain|grain-env=s:0 50 1 50");
   opts.define("gv|grain-var|grain-var-env=s:0 0 1 0");
   opts.define("gap|gap-env=s:0 50 1 50");
   opts.define("gapv|gap-var|gap-var-env=s:0 0 1 0");
   opts.define("t|transition-envelope|transenv=s:0 5 1 5");
   opts.define("verbose=b");
   opts.define("density=b");

   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, Oct 2002" << endl;
      exit(0);
   }
   if (opts.getBoolean("version")) {
      cout << "compiled: " << __DATE__ << endl;
      cout << SIG_VERSION << endl;
      exit(0);
   }
   if (opts.getBoolean("help")) {
      usage(opts.getCommand());
      exit(0);
   }
   if (opts.getBoolean("example")) {
      example();
      exit(0);
   }

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

   starttime  = opts.getDouble("start-time");
   duration   = opts.getDouble("duration");
   voiceenv   = opts.getString("voice-env");
   grainenv   = opts.getString("grain-env");
   grainvarenv= opts.getString("grain-var-env");
   gapenv     = opts.getString("gap-env");
   gapvarenv  = opts.getString("gap-var-env");
   outputdur  = opts.getDouble("output-duration");
   transenv   = opts.getString("transition-envelope");
   verboseQ   = opts.getBoolean("verbose");
   densityQ   = opts.getBoolean("density");

   cout << "Start time = " << starttime << endl;
   cout << "duration   = " << duration << endl;

}
   


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

void example(void) {
   cout << 
   "# osc examples:                                                          \n"
   "#   a quick sweep in frequency from 0 to 22050 Hz with a saw-like        \n"
   "#   amplitude envelope:                                                  \n"
   "       osc chirp.snd -d 0.5 -f \"0 0 1 22050\" -a \"0 0 1 1 10 0\"       \n"
   << endl;
}



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

void usage(const char* command) {
   cout << 
   "                                                                         \n"
   "Creates a frequency and amplitude varying sinusoid.                      \n"
   "                                                                         \n"
   "Usage: " << command << " [-d duration][-f freqEnv][-a ampEnv] outsound   \n"
   "                                                                         \n"
   "Options:                                                                 \n"
   "   -d = duration of the input soundfile to use.                          \n"
   "   -s = start time in seconds.                                           \n"
   "   -a = amplitude envelope (default \"0 1, 1 1\").                       \n"
   "   --options = list of all options, aliases and default values.          \n"
   "                                                                         \n"
   "                                                                         \n"
   << endl;
}



// md5sum: a842d2ddfaf67cafdb103f318799c704 vogran.cpp [20050403]