/**********************************************************************
auquant -- generate audio signals

$Id: auquant.c,v 1.17 2003/01/19 23:17:09 purnhage Exp $

Source file: auquant.c

(c) Heiko Purnhagen 1993-2003
e-mail: purnhage@tnt.uni-hannover.de
WWW:    http://www.tnt.uni-hannover.de/~purnhage/

This program is available under the terms of the GNU General Public
License. See file COPYING for further details. All usual disclaimers
apply!

Authors:
HP    Heiko Purnhagen, Uni Hannover <purnhage@tnt.uni-hannover.de>

Changes:
21-apr-93   HP      stereo_xquant.c
13-may-93   HP      modified for acc (double->float)
18-may-93   HP      fixed e.g. reading of short files
19-may-93   HP      fixed noise shaping
12-jan-99   HP      now using cmdline & audio
27-jan-99   HP      fix rawFormat
28-jan-99   HP      bugfixes, debug output on stderr
18-feb-99   HP      progress in seconds


References:
Robert A. Wannemaker: "Psychoacoustically Optimal Noise Shaping,"
J. Audio Eng. Soc., Vol. 40, No. 7/8, 1992 July/August.

**********************************************************************/


#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "common.h"
#include "cmdline.h"
#include "audio.h"

/* ---------- declarations ---------- */

#define PROGVER "auquant V1.2.1 19-Dec-2001"
#define CVSID "$Id: auquant.c,v 1.17 2003/01/19 23:17:09 purnhage Exp $"
#ifndef DISTVER		/* set in Makefile */
#define DISTVER "<unknown>"
#endif

#ifndef MSVC
#define min(a,b) ((a) < (b) ? (a) : (b))
#define max(a,b) ((a) > (b) ? (a) : (b))
#endif

#define STRLEN 255

#define MAX_FS (48000.0)
#define MAX_CHAN (16)
#define BUF_TIME (1.)		/* max. length of buffer in seconds */

#define AUDIO_PATH_ENV "AUQUANTPATH"
#define AUDIO_IN_EXT ".au"
#define AUDIO_OUT_EXT "_q.au"
#define RAWENV "RAWAUDIOFILE"
#define RAWDEFAULT "integer16,0,48000.,native,1,1.0"


/* ---------- variables ---------- */

static char *outputFileName;
static int outputFileNameUsed;
static float startTime;
static float duration;
static float gain;
static int quantSteps;
static float scaling;
static int scalingUsed;
static int dithering;
static float noiseShapeFB;
static char *audioPath;
static int audioPathUsed;
static char *audioInExt;
static char *audioOutExt;
static char *rawFormat;
static int rawFormatUsed;
static float bufTime;
static int debugLevel;
static int audioDebugLevel;
static int *varArgIdx;
 
static CmdLinePara paraList[] = {
  {&varArgIdx,NULL,"<input files (- = stdin) (dflt: -)>"},
  {NULL,NULL,NULL}
};

static CmdLineSwitch switchList[] = {
  {"h",NULL,NULL,NULL,NULL,"print help"},
  {"o",&outputFileName,"%s",NULL,&outputFileNameUsed,
   "output file name (- = stdout) (dflt: as input)"},
  {"ts",&startTime,"%f","0",NULL,"start time [sec]"},
  {"td",&duration,"%f","0",NULL,"duration [sec]\n"
   "(0 = complete signal)"},
  {"g",&gain,"%f","1.0",NULL,
   "pre-quantisation amplitude gain"},
  {"q",&quantSteps,"%d","65536",NULL,
   "num quantiser steps"},
  {"s",&scaling,"%f",NULL,&scalingUsed,
   "post-quantisation amplitude scaling\n"
   "(dflt: (int)(65536/num_quantiser_steps) )"},
  {"di",&dithering,"%d","0",NULL,
   "dithering (tpdf) (0=off(0dB) 1=on(+4.77dB))"},
  {"ns",&noiseShapeFB,"%f","0",NULL,
   "noiseshaper feedback factor\n"
   "(0.0 = off(0dB)  0.9 = -5.6dB..20dB  1.0 = -6.0dB..+inf)\n\b"},
  {"p",&audioPath,"%s",NULL,&audioPathUsed,
   "audio file path (dflt: $" AUDIO_PATH_ENV ")"},
  {"ei",&audioInExt,"%s",AUDIO_IN_EXT,NULL,
   "input audio file extension"},
  {"eo",&audioOutExt,"%s",AUDIO_OUT_EXT,NULL,
   "output audio file extension"},
  {"f",&rawFormat,"%s",NULL,&rawFormatUsed,
   "raw input file format\n"
   "(dflt: $" RAWENV " / \"" RAWDEFAULT "\")"},
  {"b",&bufTime,"%f","0.2",NULL,
   "buffer time"},
  {"d",&debugLevel,"%d","0",NULL,"debug level (0=off 1=basic 2=full)"},
  {"da",&audioDebugLevel,"%i","0",NULL,"audio file debug level"},
  {NULL,NULL,NULL,NULL,NULL,NULL}
};


/* ---------- functions ---------- */

/* white noise with triangular pdf [-1 .. 1] */

float dithernoise (void)
{
  float temp;

  temp = (float) rand() / (float) RAND_MAX;
  temp += (float) rand() / (float) RAND_MAX;
  return (temp - 1.0f);
}


/* ---------- main ---------- */

int main (int argc, char *argv[])
{
  char *progName = "<no program name>";
  int result;
  int listIdx;
  char inFileName[STRLEN];
  char outFileName[STRLEN];
  AudioFile *inFile;
  AudioFile *outFile;
  float *buf[MAX_CHAN];
  long maxBufLen,bufLen;
  int numChan;
  float fSample;
  long numSample;
  long maxNumSamp;
  long bufUsed,bufTrans;
  long inNumSamp;
  long outNumSamp;
  int i,ch;
  float tmpf;
  int tmpi;
  float error[MAX_CHAN];
  long clip,totClip;
  int minValue;
  float offset;

  /* evaluate command line  */
  CmdLineInit(0);
  result = CmdLineEval(argc,argv,paraList,switchList,1,&progName);
  if (result) {
    if (result==1) {
      fprintf(stderr,
	      "%s: %s\n"
	      "%s\n",
	      progName,PROGVER "   " DISTVER,CVSID);
      CmdLineHelp(progName,paraList,switchList,stderr);
      exit (1);
    }
    else
      CommonExit(1,"command line error (\"-h\" for help)");
  }

  if (!audioPathUsed)
    audioPath = getenv(AUDIO_PATH_ENV);

  if (debugLevel)
    fprintf(stderr,"%s: %s\n",progName,PROGVER "   " DISTVER);
  if (debugLevel>1)
    fprintf(stderr,"%s\n",CVSID);
  
  maxBufLen = (long)(BUF_TIME*MAX_FS);
  for (ch=0; ch<MAX_CHAN; ch++)
    if (!(buf[ch]=(float*)malloc(sizeof(float)*maxBufLen)))
      CommonExit(1,"error allocating memory");

  if (!rawFormatUsed)
    if ((rawFormat=getenv(RAWENV))==NULL)
      rawFormat = RAWDEFAULT;
  AudioInit(rawFormat,0,audioDebugLevel);

  if (varArgIdx[0]>=0 && varArgIdx[1]>=0) {
    if (outputFileNameUsed)
      CommonExit(1,"max one input file allowed when using -o");
    listIdx = 0;
    while (varArgIdx[listIdx]>=0) {
      if (!strcmp(argv[varArgIdx[listIdx]],"-"))
	CommonExit(1,"max one input file allowed when using \"-\"");
      listIdx++;
    }
  }

  if (quantSteps < 2)
    CommonExit(1,"quantSteps < 2");
  if (noiseShapeFB < -1 || noiseShapeFB > 1)
    CommonWarning("noiseshaper not minimal phase ...");
  dithering = (dithering)?1:0;
  if (!scalingUsed)
    scaling = (float)(int)(65536.0/quantSteps);
  minValue = - (quantSteps / 2);
  offset = (float)((quantSteps%2)?0:(int)(0.5*scaling));
  if (debugLevel>1)
    fprintf(stderr,
	    "quantised values in [%d .. %d]\n"
	    "scaled to [%.1f .. %.1f]\n"
	    "(scaling=%f offset=%f)\n",
	    minValue,minValue+quantSteps-1,
	    scaling*minValue+offset,scaling*(minValue+quantSteps-1)+offset,
	    scaling,offset);

  /* process all files on command line or read from stdin */
  listIdx = 0;
  while (listIdx==0 || listIdx>0 && varArgIdx[listIdx]>=0) {
    if (ComposeFileName((varArgIdx[listIdx]>=0)?
			argv[varArgIdx[listIdx]]:"-",
			0,audioPath,audioInExt,inFileName,STRLEN))
      CommonExit(1,"composed input file name too long \"%s\"",
		 argv[varArgIdx[listIdx]]);
    if (outputFileNameUsed) {
      if (ComposeFileName(outputFileName,
			  0,audioPath,audioOutExt,outFileName,STRLEN))
	CommonExit(1,"composed output file name too long \"%s\"",
		   outputFileName);
    }
    else
      if (ComposeFileName((strcmp(inFileName,"-"))?
			  argv[varArgIdx[listIdx]]:"-",
			  1,audioPath,audioOutExt,outFileName,STRLEN))
	CommonExit(1,"composed output file name too long \"%s\"",
		   argv[varArgIdx[listIdx]]);
    
    if (debugLevel)
      fprintf(stderr,
	      "%s: quantising %s -> %s\n",progName,inFileName,outFileName);

    /* open audio files */
    if (!(inFile=AudioOpenRead(inFileName,&numChan,&fSample,&numSample)))
      CommonExit(1,"error opening input audio file %s",inFileName);
    if (numChan > MAX_CHAN)
      CommonExit(1,"more than %d channels",MAX_CHAN);
    if (!(outFile=AudioOpenWrite(outFileName,outFileName,numChan,fSample)))
      CommonExit(1,"error opening output audio file %s",outFileName);
    if (debugLevel>1)
      fprintf(stderr,"numChan=%d fSample=%f numSample=%ld\n",
	      numChan,fSample,numSample);
    bufLen = (long)(fSample*bufTime+.5);
    bufLen = max(bufLen,1);
    bufLen = min(bufLen,maxBufLen);
    if (debugLevel>1)
      fprintf(stderr,"bufLen=%ld maxBufLen=%ld\n",bufLen,maxBufLen);

    /* skip samples */
    inNumSamp = 0;
    if (startTime <= 0)
      startTime = 0;
    else {
      maxNumSamp = (long)(fSample*startTime+.5);
      do {
	if (debugLevel) {
	  fprintf(stderr,(debugLevel<=2)?"\r~%7.3f ":"~%7.3f\n",
		  inNumSamp/fSample);
	  fflush(stderr);
	}
	bufTrans = maxNumSamp-inNumSamp;
	if (bufTrans>bufLen)
	  bufTrans = bufLen;
	bufUsed = AudioReadData(inFile,buf,bufTrans);
	inNumSamp += bufUsed;
      } while (bufUsed);
    }

    /* process samples */
    outNumSamp = 0;
    totClip = 0;
    for (ch=0; ch<numChan; ch++)
      error[ch] = 0;
    if (duration <= 0)
      maxNumSamp = 0;
    else
      maxNumSamp = (long)(fSample*(startTime+duration)+.5);
    do {
      /* read buffer */
      if (debugLevel) {
	fprintf(stderr,(debugLevel<=2)?"\r%8.3f ":"%8.3f\n",
		inNumSamp/fSample);
	fflush(stderr);
      }
      bufTrans = (maxNumSamp)?maxNumSamp-inNumSamp:bufLen;
      if (bufTrans>bufLen)
	bufTrans = bufLen;
      bufUsed = AudioReadData(inFile,buf,bufTrans);
      inNumSamp += bufUsed;

      /* process buffer */
      clip = 0;
      for (i=0; i<bufUsed; i++)
	for (ch=0; ch<numChan; ch++) {
	  tmpf = buf[ch][i]/65536.0f*gain+0.5f;
	  tmpf = tmpf*quantSteps-error[ch]*noiseShapeFB;
	  tmpi = (int)(tmpf+dithernoise()*dithering+1)-1;
	  if (tmpi < 0) {
	    tmpi = 0;
	    clip++;
	  }
	  if (tmpi > quantSteps-1) {
	    tmpi = quantSteps-1;
	    clip++;
	  }
	  error[ch] = tmpi-tmpf+0.5f;
	  buf[ch][i] = (tmpi+minValue)*scaling+offset;
	}

      totClip += clip;
      if (!totClip && clip)
	CommonWarning("clipping ...");

      /* write buffer */
      AudioWriteData(outFile,buf,bufUsed);
      outNumSamp += bufUsed;
    } while (bufUsed);
      
    /* close audio files */
    AudioClose(inFile);
    AudioClose(outFile);

    if (debugLevel && debugLevel<=2) {
      fprintf(stderr," \n");
      fflush(stderr);
    }

    if (debugLevel>1)
      fprintf(stderr,
	      "in: %ld samples (%.3f sec)   out: %ld samples (%.3f sec)\n",
	      inNumSamp,inNumSamp/fSample,outNumSamp,outNumSamp/fSample);
  
    if (totClip)
      CommonWarning("%d samples clipped",totClip);

    if (varArgIdx[listIdx]>=0)
      listIdx++;
    else
      listIdx = -1;
  }

  CmdLineEvalFree(paraList);

  if (debugLevel)
    fprintf(stderr,"%s: finished\n",progName);

  return 0;
}

/* end of auquant.c */
