// module wavemaker.cpp
// Generate a WAV files: square, triangle, sawtooth, sine
// Copyright Dr.E.Huckert 1-99, 10-2020
//
// Can write wave forms: square, triangle, sawtooth or sinus (option -wf)
// The length can be given in seconds (option -secs)
// The frequency can be given in Hertz (option -h)
// Other options: see the usage() output
// The generated wave file is always mon (1 channel), 44100 samples/sec, 16 bits/sample

// This is a console application (for "black windows")
// Can be compiled and linked under Windows (32 Bit) and Linux (64 Bit)
// Not tested under 64 Bit Windows!
// Compile and Link: (Windows 7, Digital Mars C++):
//    dmc -mn -DWINDOWS wavemaker.cpp
//  Windows, g++:
//    g++ -DWINDOWS -o wavemaker.exe wavemaker.cpp -lm
//  Linux (64 Bit), g++:
//    g++ -o wavemaker wavemaker.cpp -lm

// Versions:
// V1.1   03/2018 command line arguments changed, additional error messages
// V1.2   10/2020 corrected, writeDataChunk() now OK
// V1.5   12/2020 error under WINDOWS fixed: missing "_setmode(ofd, _O_BINARY)"
//                Tested OK with Digital Mars C++ and GNU g++ 

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <math.h>
#ifdef WINDOWS
#include <io.h>
// note: for 64 Bit windows this must be changed (similar to Linux)
#define DWORD long  // for Windows 32 Bit
#define WORD short  // for Windows 32 Bit
#else
#include <unistd.h>
#include <sys/types.h>
#define DWORD int32_t
#define WORD  int16_t
#endif
#include <sys/stat.h>

typedef struct
{
  DWORD main_chunk;
  DWORD mlength;
  DWORD chunk_type;
} MAIN_CHUNK;

typedef struct
{
  DWORD sub_chunk;
  DWORD slength;
  WORD  format;
  WORD  modus;
  DWORD sample_fq;
  DWORD bytes_per_second;
  WORD  bytes_per_sample;
  WORD  bits_per_sample;
} SUB_CHUNK;

typedef struct
{
  DWORD data_chunk;
  DWORD dlength;
}  DATA_CHUNK;

// ---------------------------------------------------------------------------
long writePos   = 0L;
int  g_hertz    = 220; // = a3 (American pitch)
int  verbose    = 0;   // is settable via command line
int  gnuplot    = 0;   // settable via command line option -g

// ----------------------------------------------------------------
class WAVE
{
  private:
  unsigned long samplesSec;
  unsigned durSecs;
  unsigned waveForm;
  unsigned bitsSample;
  short    maxAmpli;
  short    minAmpli;
  unsigned noChannels;
  unsigned frequency;
  short    *pWaveBuf;   // signed values!
  //
  void basicInit();
  //
  public:
  WAVE();
  WAVE(unsigned freq, unsigned long ss, unsigned secs);
  unsigned long makeSquare();
  unsigned long makeSine();
  unsigned long makeTriangle();
  unsigned long makeSawtooth();
  //
  long writeWaveBuffer(const char *fileName);
  long writeMainChunk(int of);
  long writeFormatChunk(int of);
  long writeDataChunk(int of, int wf);
  //
  void setFrequency(unsigned hertz)
  { this->frequency = hertz; };
  void setDuration(unsigned dur)
  { this->durSecs = dur; };
  void freeWaveBuf()
  {
    if (this->pWaveBuf != NULL)
    {
      delete this->pWaveBuf;
      this->pWaveBuf = NULL;
    }
  };
};

// ---------------------------------------------------------
void WAVE::basicInit()
{
  this->waveForm    = 0;      // square
  this->durSecs     = 1;      // 1 second
  this->samplesSec  = 44100L;  // CD format
  this->noChannels  = 1;      // mono
  this->bitsSample  = 16;
  this->maxAmpli    = 0x7fff;
  this->minAmpli    = 0;
  this->pWaveBuf    = NULL;
  this->frequency   = 440;    // in hertz
}   // end WAVE::basicInit()


// ---------------------------------------------------------
// Constructor
WAVE::WAVE()
{
  this->basicInit();
}

// -----------------------------------------------------------
// Constructor
WAVE::WAVE(unsigned f,
           unsigned long ss,
           unsigned ds)
{
  this->basicInit();
  this->samplesSec = ss;
  this->frequency  = f;   // in Hertz
  this->durSecs    = ds;  // in secs
}   

// --------------------------------------------------------------------
// Schreibt den Dateikopf mit dem Dateityp "RIFF"
// Returns: the number of bytes written
long WAVE::writeMainChunk(int ofd)
{
  int nb;
  char buf[20];
  MAIN_CHUNK header;
  unsigned bytesPerSample;
  //
  memcpy((char *)&header.main_chunk, "RIFF", 4);
  bytesPerSample = (this->bitsSample + 7) / 8;
  header.mlength = (this->durSecs * this->samplesSec * bytesPerSample) + 
                   sizeof(MAIN_CHUNK) + 
                   sizeof(SUB_CHUNK);
  memcpy((char *)&header.chunk_type, "WAVE", 4);
  nb = write(ofd, (char *)&header, sizeof(header));
  //
  return nb;
}   // end WAVE::writeMainChunk()

// --------------------------------------------------------------------
// Returns: the number of bytes written
long WAVE::writeFormatChunk(int      ofd)
{
  int nb;
  char buf[20];
  SUB_CHUNK header;
  unsigned bytesPerSample;
  //
  bytesPerSample = (this->bitsSample + 7) / 8;
  memcpy((char *)&header.sub_chunk, "fmt ", 4);
  header.slength   = 16;
  header.format    = 1;
  header.modus     = this->noChannels;       
  header.sample_fq = this->samplesSec;      
  header.bytes_per_second = this->samplesSec * bytesPerSample * this->noChannels;
  header.bytes_per_sample = bytesPerSample;  
  header.bits_per_sample  = this->bitsSample; 
  nb = write(ofd, (char *)&header, sizeof(header));
  //
  return nb;
}   // end WAVE::writeFormatChunk()

// -------------------------------------------------------------
// Den Wave buffer mit einem Sinussignal fuellen
// Returns: the number of short values generated
unsigned long WAVE::makeSine()
{ 
  double         actVal, arg1;
  double         pi = 3.14159;
  unsigned       period, x;
  long           noSamples = 0L;
  long           bufDim;
  int            amplitude =  this->maxAmpli - (this->maxAmpli / 10);
  //
  this->freeWaveBuf();
  period = this->samplesSec / this->frequency;
  bufDim = this->durSecs * (long)this->samplesSec;
  this->pWaveBuf = new short[bufDim];
  //
  x = 0;
  for (long i = 0L; i < bufDim; i++)
  {
    if (x >= period) x = 0;
    // x Argument normieren auf Winkel [0,360] Grad
    arg1 = (double)x * (360.0 / (double)period);
    x++;
    // Argument fuer sin() muss in radians ausgedrueckt werden
    // Der resultierende Wert liegt im Bereich (-1,+1]
    actVal = sin(arg1 * pi / 180.0);
    // y Wert von [-1,1] auf Bereich [minPatt,maxPatt] normieren
    actVal = actVal * (double)amplitude;
    pWaveBuf[i] = (short)actVal;
    if (::gnuplot)
      printf("%lf\n", actVal);
    noSamples++;
  }
  return noSamples;
}   // end WAVE::makeSine()

// ---------------------------------------------------------------------
// Einen Puffer mit einem Saegezahnsignal fuellen
// Returns: the number of short values generated
unsigned long WAVE::makeSawtooth()
{
  double         period    = 1.0;
  double         actVal    = 0.0;
  double         steigung  = 0.5;
  unsigned long  bufDim    = 2048;
  unsigned       amplitude =  1000;
  unsigned       iPeriod   = 0;
  //unsigned       count     = 0;
  //
  period = (double)(this->samplesSec / (double)this->frequency);
  bufDim = (unsigned long)this->durSecs * (unsigned long)this->samplesSec;
  this->freeWaveBuf();
  this->pWaveBuf = new short[bufDim];
  //
  // use formula y        = m * x  (where m=steigung)
  //             maxAmpli = steigung * period
  //             steigung = maxAmpli / period
  if (this->frequency == 0) this->frequency = 1; // avoid div by 0
  amplitude = (2 * this->maxAmpli) - (this->maxAmpli / 5);
  steigung = (double)amplitude / period;
  if (::verbose)
    ::printf("WAVE::makeSawTooth() steigung=%lf period=%lf\n", steigung, period);
  /*
  for (unsigned long i=0L; i < bufDim; i++)
  {
    this->pWaveBuf[i] =0;
  }
  */
  //
  for (unsigned long i=0L; i < bufDim; i++)
  {
    actVal = (double)iPeriod * steigung;
    if (actVal > (double)amplitude)
      // should not occur!
      actVal = (double)amplitude;
    this->pWaveBuf[i] = (unsigned short)actVal;
    //if (count == 1)
    //  ::printf("iPeriod=%04u actVal=%d\n", iPeriod, (int)(this->pWaveBuf[i]));
    if ((double)iPeriod >= period)
    {
      //count++;
      iPeriod = 0;
    }
    else
      iPeriod++;
  }   // end for ...
  //
  if (::verbose)
    ::printf("WAVE::makeSawTooth(): ret=%ld\n", bufDim);
  return bufDim;
}   // end WAVE::makeSawtooth()

// ---------------------------------------------------------------------
// Einen Puffer mit einem Dreiecksignal fuellen
// Returns: the number of short values generated
unsigned long WAVE::makeTriangle()
{
  int   period, halfPeriod, iPeriod;
  double actVal = 0.0;
  double step = 0.0;
  long  bufDim;
  int   count = 0;
  short amplitude =  this->maxAmpli - (this->maxAmpli / 10);
  //
  period = (int)(this->samplesSec / this->frequency);
  bufDim = this->durSecs * (long)this->samplesSec;
  this->freeWaveBuf();
  this->pWaveBuf = new short[bufDim];
  actVal     = -amplitude;
  halfPeriod = period / 2;
  step       = (amplitude * 2.0) / (double)halfPeriod;
  iPeriod    = 0;
  if (halfPeriod == 0) halfPeriod = 1;  // avoid div. by zero
  if (::verbose)
    ::printf("WAVE::makeTriangle(): period=%d step=%lf\n", period, step);
  //
  for (long i=0; i < bufDim; i++)
  {
    if (::verbose)
    {
     if (count++ < 2000)
      ::printf("WAVE::makeTriangle(): iPeriod=%d halfPeriod=%d actVal= %lf\n", 
                (int)iPeriod, (int)halfPeriod, (double)actVal);
    }
    if (::gnuplot)
      printf("%lf\n", actVal);
    this->pWaveBuf[i] = (short)actVal; 
    if (iPeriod <= halfPeriod)
    {
      // upward ramp
      actVal = actVal + step;
    }
    if (iPeriod > halfPeriod)
    {
      // downward   ramp
      actVal = actVal - step;
    }
    iPeriod++;
    if (iPeriod == halfPeriod)
    {
      actVal = amplitude;
      if (::verbose)
      {
        if (count < 2000)
        ::printf("WAVE::makeTriangle(): switch to downward\n");
      }
    }
    if (iPeriod >= period)
    {
      iPeriod = 0;
      actVal = -amplitude;
      if (::verbose)
      {
        if (count < 2000)
          ::printf("WAVE::makeTriangle(): switch to upward\n");
      }
    }
  }   // end for ...
  //
  if (::verbose)
    ::printf("WAVE::makeTriangle(): ret=%ld\n", bufDim);
  return bufDim;
}   // end WAVE::makeTriangle()

// ---------------------------------------------------------------------
// Einen Puffer mit einem Rechtecksignal fuellen
// Returns: the number of short values generated
unsigned long WAVE::makeSquare()
{
  int  period, period2;
  long bufDim;
  short amplitude =  this->maxAmpli - (this->maxAmpli / 10);
  //
  period = (int)(this->samplesSec / this->frequency);
  bufDim = this->durSecs * (long)this->samplesSec;
  this->freeWaveBuf();
  this->pWaveBuf = new short[bufDim];
  period2 = period / 2;
  if (::verbose)
    ::printf("WAVE::makeSquare(): period=%d\n", period);
  //
  for (long i=0; i < bufDim; i++)
  {
    if (period2 < 0)
    {
      this->pWaveBuf[i] = amplitude;
      period2++;
      if (period2 >= 0)
        period2 = period / 2;
    }
    else 
    {
      this->pWaveBuf[i] = -amplitude;
      period2--;
      if (period2 <= 0)
        period2 = 0 - (period / 2);
    }
    if (::verbose > 1)
      ::printf("WAVE::makeSquare(): period2=%d\n", period2);
  }   // end for ...
  //
  if (::verbose)
    ::printf("WAVE::makeSquare(): ret=%ld\n", bufDim);
  return bufDim;
}   // end WAVE::makeSquare()
  
// --------------------------------------------------------------------
// Returns: the number of bytes written
// Hinweis: offensichtlich darf nur ein data-frame erzeugt werden!!!
// Alle Versuche, es anders zu machen (z.B. 1 frame pro Sekunde) schlugen fehl!
// Returns: the number of short values written
//          < 0 upon error
long WAVE::writeDataChunk(int ofd,
                          int waveForm)
{
  unsigned patt, lastIdx, bufDim;
  int  i, j, offs;
  unsigned long pos = 0L;
  long cRet = 0L;
  short buf[8192 * 4];   // sollte > 1K sein!
  int   noBytes;
  long  noWritten = 0L;
  DATA_CHUNK header;
  int noFrame = 0;  
  long nb;
  unsigned count = 0;
  unsigned long bufBytes;
  unsigned short wBuf[256];
  unsigned bytesPerSample;
  //
  noFrame++;
  if (::verbose)
    ::printf("WAVE::writeDataChunk() write pos=%ld 0x%lX\n", writePos, writePos);
  //
  // Header fuer den Frame schreiben (8 Bytes)
  strncpy((char *)&header.data_chunk, "data", 4);
  bytesPerSample = (this->bitsSample + 7) / 8;
  header.dlength = this->durSecs * this->samplesSec * bytesPerSample;
  nb = ::write(ofd, (char *)&header, sizeof(header));
  if (nb < 0)
  {
    noWritten = -1L;
    goto zurueck;
  }
  writePos += nb;
  if (::verbose)
    ::printf("WAVE::writeDataChunk() pure data pos=%ld 0x%lX\n", writePos, writePos);
  // 
  // Jetzt die Daten erzeugen 
  j       = 0;
  lastIdx = 0;
  switch (waveForm)
  {
    case 0:
    {
      bufBytes = makeSquare() * sizeof(short);
      break;
    }
    case 1:
    {
      bufBytes = makeTriangle() * sizeof(short);
      break;
    }
    case 2:
    {
      bufBytes = makeSawtooth() * sizeof(short);
      break;
    }
    case 3:
    {
      bufBytes = makeSine() * sizeof(short);
      break;
    }
    default:
    {
      bufBytes = makeSquare() * sizeof(short);
      break;
    }
  }   // end switch
  //
  // Die erzeugten Daten schreiben
  if (::verbose)
    ::printf("WAVE::writeDataChunk() no.bytes to be written=%lu\n", bufBytes);
  pos    = 0L;
  //count = 0;
  //
  while (bufBytes  > 0L)
  {
    noBytes = 1024;
    if ((unsigned long)noBytes < bufBytes) noBytes = bufBytes;
    cRet = ::write(ofd, (char *)&(this->pWaveBuf[pos]), noBytes);
    if (::verbose)
      ::printf("WAVE::writeDataChunk() pos=%lu  cRet=%ld\n", pos, cRet);
    if (cRet  <= 0)
      // EOF or error
      break;
    /*
    if (count == 0)
    {
      short val;
      short * pShort = (short *)&(this->pWaveBuf[pos]);
      for (int i=0; i < cRet; i += sizeof(short))
      {
        val = *pShort++;
        printf("%05u %d\n", j, val);
        j++;
      }
    }
    count++;
    */
    pos       += cRet;
    bufBytes  = bufBytes - cRet;
    noWritten += cRet;
  }    // end while (bufBytes > 0L)
  //
  zurueck:
  
  if (::verbose)
    ::printf("WAVE::writeDataChunk() ret=%ld\n", noWritten);
  return noWritten;
}   // end WAVE::writeDataChunk()

// ---------------------------------------------------------------------
void usage()
{
  ::printf("usage: wavegen [-secs nn] [-h hertz] [-wf 0|1|2|3] [-g] [-v] -o wav_file\n");
  ::printf("       waveforms: 0=sqare 1=triangle 2=sawtooth 3=sine\n");
  ::printf("V1.5 12/2020 Dr.E.Huckert\n");
}   // end usage()

// --------------------------------------------------------------------
// Hinweis: ein Halbton ist um den Faktor 1.059463 vom nächsten entfernt
// z.B.a1=440 Hz * 1.059463 = a#1=466 Hz
int main(int argc, char *argv[])
{
  int      ofd = -1;
  int      n, nb;
  int      ret = 0;
  char     buf[512];
  long     sampleRate    = 44100L; // fuer CD Format
  unsigned noSeconds     = 1;      // Anz. Sekunden
  unsigned noChannels    = 1;      // fuer Mono
  unsigned bitsPerSample = 16;
  unsigned bytesPerSample;
  char     outFn[128]    = "";
  WAVE     *pWave = NULL;
  int waveForm = 0;
  //
  usage();
  //
  // Auswertung der Kommandozeile
  if (argc < 3)
  {
    ret = -1;
    printf("ERROR: not enough arguments\n");
    goto zurueck;
  }
  for (n=1; n < argc; n++)
  {
    if (::strcmp(argv[n], "-secs") == 0)
    {
      noSeconds = ::atoi(argv[n + 1]);
      if (::verbose)
        ::printf("-secs=%d\n", noSeconds);
      continue;
    }
    if (::strcmp(argv[n], "-wf") == 0)
    {
      // 0=square,1=triangle,2=saw,3=sine
      waveForm = ::atoi(argv[n + 1]);
      if (::verbose)
        ::printf("-wf=%d\n", waveForm);
      continue;
    }
    if (::strcmp(argv[n], "-h") == 0)
    {
      // value=Hertz = cycles/sec
      g_hertz = ::atoi(argv[n + 1]);
      if (::verbose)
        ::printf("-h=%d\n", g_hertz);
      continue;
    }
    if (::strcmp(argv[n], "-o") == 0)
    {
      ::strcpy(outFn, argv[n + 1]);
      if (::verbose)
        printf("output file=%s\n", outFn);
    }
    if (::strcmp(argv[n], "-v") == 0)
    {
      ::verbose = 1;
    }
    if (::strcmp(argv[n], "-g") == 0)
    {
      ::gnuplot = 1;
    }
  }   // end for n...
  //
  if (::verbose)
  {    
    ::printf("wave form=%d\n", waveForm);
    ::printf("hertz=%d\n", g_hertz);
    ::printf("seconds=%d\n", noSeconds);
  }
  bytesPerSample = (bitsPerSample + 7) / 8;
  //
  pWave = new WAVE(g_hertz, sampleRate, noSeconds);
  //
  if (::strlen(outFn) == 0)
  {
    ret = -2;
    printf("main(): ERROR: no output file name given as argument\n");
    goto zurueck;
  }
#ifdef WINDOWS
  //_set_fmode(_O_BINARY);  // sets global binary mode for IO
  ofd = ::creat(outFn, O_WRONLY | S_IWRITE);
  _setmode(ofd, _O_BINARY);  // _setmode() instead of setmode()!
#else
  ofd = ::creat(outFn, O_WRONLY | S_IRWXU);
#endif
  if (ofd < 0)
  {
    ret = -3;
    printf("main(): ERROR: cannot open output file\n");
    goto zurueck;
  }
  //
  // den Dateikopf schreiben
  nb = pWave->writeMainChunk(ofd);
  if (nb < 0)
  {
    ret = -4;
    ::printf("main(): ERROR: write error output file\n");
    goto zurueck;
  }
  writePos += nb;
  if (::verbose)
    ::printf("main(): after main chunk write pos=%ld\n", writePos);
  //
  // den Kopf fuer den Datenchunk schreiben
  nb = pWave->writeFormatChunk(ofd);
  if (nb < 0)
  {
    ret = -5;
    goto zurueck;
  }
  writePos += nb;
  if (::verbose)
    ::printf("main(): after format chunk write pos=%ld\n", writePos);
  //
  // die Daten erzeugen und schreiben
  nb = pWave->writeDataChunk(ofd, waveForm);
  if (nb < 0)
  {
    ret = -6;
    goto zurueck;
  }
  //
  ret = 1;  // means OK
  //
  zurueck:
  if (ofd > -1)
    ::close(ofd);
  if (pWave != NULL)
    delete pWave;
  ::printf("main(): wavegen result=%d\n", ret);
  return ret;
}   // end main()
