/*****************************************************************
au record & play (duplex, for Linux)

$Id: auduplex.c,v 1.24 2003/02/27 23:54:59 purnhage Exp $

Source file: auduplex.c

(c) Heiko Purnhagen 2000-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:
31-mai-00   HP      first version, based on plax and auecho
06-jun-00   HP      added non-blocking au i/o
13-Nov-2001 HP      added input and output fifos for non-blocking i/o
19-Nov-2001 HP      non-blocking works (buffer over/under warnings tbd)
22-Nov-2001 HP      some fine-tuning ...
19-Dec-2001 HP      MAX_CHAN ...
21-Dec-2001 HP      added SNDCTL_DSP_CHANNELS support (Linux > 2 ch)
04-Jan-2002 HP      added strerror(errno)
20-Mar-2002 HP      fixed bug in "odd" read(), write()
2003-01-07  HP      changed caps debug output
**********************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <signal.h>
#include <ctype.h>

#include "common.h"		/* common module */
#include "cmdline.h"		/* command line module */
#include "austream.h"		/* audio i/o streams */

#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/time.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/soundcard.h>


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

#define PROGVER "auduplex V1.1.8 07-Jan-2003"
#define CVSID "$Id: auduplex.c,v 1.24 2003/02/27 23:54:59 purnhage Exp $"
#ifndef DISTVER		/* set in Makefile */
#define DISTVER "<unknown>"
#endif

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

#define MAX_CHAN (64)
#define STRLEN (255)
#define NAPTIME (0.001)

#ifndef PI
#define PI 3.14159265358979323846264338327950288
#endif


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

/* command line module */

static char *progName = "<no program name>";
static char *recFileName;
static int numChan;
static float fSample;
static char *audioDevice;
static float gainRec;
static float gainPlay;
static float delayTime;
static float duration;
static float bufTime;
static float fragTime;
static int fragTimeUsed;
static float napTime;
static int napTimeUsed;
static float latTime;
static float recBufTime;
static float playBufTime;

static int debugLevel;
static int *varArgIdx;

static CmdLinePara paraList[] = {
  {&varArgIdx,NULL,"<play input .au file (- = stdin)>"},
  {NULL,NULL,NULL}
};

static CmdLineSwitch switchList[] = {
  {"h",NULL,NULL,NULL,NULL,"print help"},
  {"o",&recFileName,"%s","-",NULL,
   "record output .au file (- = stdout)"},
  {"c",&numChan,"%d","1",NULL,
   "number of channels"},
  {"s",&fSample,"%f","8000.0",NULL,
   "sampling frequency [Hz]"},
  {"ad",&audioDevice,"%s","/dev/dsp",NULL,
   "audio device name"},
  {"gr",&gainRec,"%f","1.0",NULL,
   "rec. amplitude gain"},
  {"gp",&gainPlay,"%f","1.0",NULL,
   "play amplitude gain"},
  {"ts",&delayTime,"%f","0.0",NULL,
   "start time [sec] (+: dly play / -: dly rec.)"},
  {"td",&duration,"%f","0.0",NULL,
   "max. duration [sec] (0=unlimited or as input)"},
  {"b",&bufTime,"%f","0.1",NULL,
   "transfer block size [sec]"},
  {"bf",&fragTime,"%f",NULL,&fragTimeUsed,
   "fragment size [sec] (recommended if only 2 fragments)"},
  {"bn",&napTime,"%f",NULL,&napTimeUsed,
   "nap time [sec] (dflt: 0.3*block size)"},
  {"bl",&latTime,"%f","0.2",NULL,
   "buffering latency [sec] (play)"},
  {"br",&recBufTime,"%f","0.5",NULL,
   "record fifo size [sec]"},
  {"bp",&playBufTime,"%f","0.5",NULL,
   "play fifo size [sec]"},
  {"d",&debugLevel,"%i","0",NULL,
   "debug level (on stderr) (0=off 1=basic 2=med 3=high)\n"
   "(+4*austreamDbgLvl)"},
  {NULL,NULL,NULL,NULL,NULL,NULL}
};

static int stop = 0;


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

void stopproc (int sig)
{
  stop = 1;
  if (sig==SIGPIPE) {
    stop = 2;
    signal(SIGPIPE,stopproc);	/* catch broken output pipe -- again */
  }
  /* 2nd SIGINT will abort immediately */
}


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

int main (int argc, char *argv[])
{
  char playFileName[STRLEN];
  AuStream *inas,*outas;
  int infd,outfd;
  int audev;
  int result;

  int inNumChan;
  float inFSample;
  long inNumSamp;
  long recBufLen,playBufLen;
  long bufLen;
  long latLen;
  long numClip,recClip,playClip;
  short *recBuf;
  short *playBuf;
  long i,tmpReq,tmpAck;
  int tmp;
  int recPeak,playPeak;
  char level[17];

  long readNumSamp;
  long playNumSamp;
  long recNumSamp;
  long writeNumSamp;
  long delayNumSamp;
  long durNumSamp,playDurNumSamp;
  int samplerate,channels;
  int caps,err,tmperr,triggered;
  fd_set rfds,wfds;
  struct timeval tv;
  int mfs;
  long recW,recR,recF,recDev,recFile;
  long playW,playR,playF,playDev,playFile;
  int rF,rD,pF,pD;
  audio_buf_info ainfo;
  int fragsize;
  char recs[20],plays[20]; /* 5 is sufficient, but snprintf is not ANSI-C */
  long recOver,playUnder;
  long recSlipped,playSlipped;
  int recMaxUsed,playMinUsed;

  /* 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);
      fprintf(stderr,
	      "example a)\n"
	      "  mkfifo fifo.au\n"
	      "  cat fifo.au | linux/fifo | "
	      "linux/auduplex -d 1 -ts 1 - -o fifo.au\n"
	      "\n"
	      "example b)\n"
	      "  linux/augen -c 2 -s 48000 -o noise.au "
	      "\"n 0 1 0.1 0.1 0 0\" \"n 1 1 0 0 0.1 0.1\"\n"
	      "  linux/auduplex -c 2 -s 48000 -d 2 noise.au -o response.au\n"
	      "\n");
      exit (1);
    }
    else
      CommonExit(1,"command line error (\"-h\" for help)");
  }
  AuInit((debugLevel>>2)&3);
  debugLevel &= 3;

  if (debugLevel)
    fprintf(stderr,"%s: %s\n",progName,PROGVER "   " DISTVER);
  if (debugLevel>1)
    fprintf(stderr,"%s\n",CVSID);
  if (debugLevel>=3)
    fprintf(stderr,"debugLevel=%d\n",debugLevel);

  if (varArgIdx[0]>=0) {
    if (varArgIdx[1]>=0)
      CommonExit(1,"max one input file allowed");
      strncpy(playFileName,argv[varArgIdx[0]],STRLEN);
  }
  else
    /* strcpy(playFileName,"-"); */
    CommonExit(1,"input file required");

  if (numChan > MAX_CHAN) {
    numChan = MAX_CHAN;
    CommonWarning("setting number of channels to %2d",numChan);
  }
  delayNumSamp = numChan*(long)(fSample*delayTime+.5);
  if (duration < 0) {
    duration = 0;
    CommonWarning("setting duration to %8.3f",duration);
  }
  durNumSamp = playDurNumSamp = numChan*(long)(fSample*duration+.5);
  if (bufTime < 2*NAPTIME) {
    bufTime = 2*NAPTIME;
    CommonWarning("setting transfer block size to %8.3f",bufTime);
  }
  if (fragTimeUsed && (fragTime <= 0 || fragTime > bufTime/2)) {
    fragTime = bufTime/2;
    CommonWarning("setting fragment size to %8.3f",fragTime);
  }
  if (!napTimeUsed)
    napTime = 0.3*bufTime;
  if (napTime < NAPTIME) {
    napTime = NAPTIME;
    CommonWarning("setting nap time to %8.3f",napTime);
  }
  recBufTime = max(bufTime,recBufTime);
  playBufTime = max(bufTime,playBufTime);
  if (latTime < 2*bufTime) {
    latTime = 2*bufTime;
    CommonWarning("setting latency to %8.3f",latTime);
  }
  bufLen = numChan*(long)(fSample*bufTime+.5);
  latLen = numChan*(long)(fSample*latTime+.5);

  recBufLen = numChan*(long)(fSample*recBufTime+.5);
  playBufLen = numChan*(long)(fSample*playBufTime+.5);
  if(!(recBuf = (short *)malloc(recBufLen*sizeof(short))))
    CommonExit(1,"failed to allocate recBuf");
  if(!(playBuf = (short *)malloc(playBufLen*sizeof(short))))
    CommonExit(1,"failed to allocate playBuf");


  signal(SIGINT,stopproc);	/* catch ^C */
  signal(SIGPIPE,stopproc);	/* catch broken output pipe */

  if (debugLevel)
    fprintf(stderr,
	    "%s: audio device -> %s & %s -> audio device\n",progName,
	    recFileName,playFileName);

  /* open audio device */
  samplerate = (int)(fSample+.5);
  channels = numChan;
  if ((audev=open(audioDevice,O_RDWR,0))==-1)
    CommonExit(1,"Can not open \"%s\"",audioDevice);
  /* non-blocking audio i/o is better done via
        SNDCTL_DSP_GETISPACE and SNDCTL_DSP_GETOSPACE ...
      than using
        fcntl(audev,F_SETFL,fcntl(audev,F_GETFL,0)|O_NONBLOCK);
  */
  ioctl(audev,SNDCTL_DSP_SETDUPLEX,0);
  err = ioctl(audev,SNDCTL_DSP_GETCAPS,&caps);
  if (debugLevel>=3)
    fprintf(stderr,"caps=0x%x dup=0x%x (%d) trig=0x%x (%d) err=%d\n",
	    caps,DSP_CAP_DUPLEX,(caps&DSP_CAP_DUPLEX)?1:0,
	    DSP_CAP_TRIGGER,(caps&DSP_CAP_TRIGGER)?1:0,err);

  /* use 20 ms .. 40 ms fragement size */
  if (fragTimeUsed) {
    tmp = 4;
    fragsize = (int)(fragTime*samplerate+.5)*2*channels;
  }
  else {
    tmp = 9;				/* min 512 bytes/fragment */
    fragsize = (int)((min(0.020,bufTime)*samplerate+.5)*2*channels);
  }
  while (1<<tmp < fragsize)
    tmp++;
  tmp |= 0x7fff0000;
  err = ioctl(audev,SNDCTL_DSP_SETFRAGMENT,&tmp);

  tmp = AFMT_S16_LE;			/* aka 16 */
  err = ioctl(audev,SNDCTL_DSP_SAMPLESIZE,&tmp);
  if (tmp != AFMT_S16_LE)
    CommonExit(1,"16 bit audio not supported");
  tmperr = 1;
  if (channels<=2) {
    /* try SNDCTL_DSP_STEREO first */
    tmp = (channels>1)?1:0;
    tmperr = ioctl(audev,SNDCTL_DSP_STEREO,&tmp);
    if (!tmperr)
      channels = tmp+1;
  }
  if (tmperr) {
    tmp = channels;
    err = err || ioctl(audev,SNDCTL_DSP_CHANNELS,&tmp);
    channels = tmp;
  }
  tmp = samplerate;
  err = err || ioctl (audev,SNDCTL_DSP_SPEED,&tmp);
  samplerate = tmp;
  tmp = ~PCM_ENABLE_OUTPUT & ~PCM_ENABLE_INPUT; /* disable */
  err = err || ioctl(audev,SNDCTL_DSP_SETTRIGGER,&tmp);
  /* err = err || ioctl(audev,SNDCTL_DSP_GETBLKSIZE,&fragsize); */
  ioctl(audev,SNDCTL_DSP_GETISPACE,&ainfo);
  fragsize = ainfo.fragsize;
  if (err)
    CommonExit(1,"Unable to init dsp");
  if (debugLevel>1)
    fprintf (stderr,"audio device: samplerate=%d channels=%d fragsize=%d\n"
	            "              fragstotal=%d (%5.3f/%5.3f s)\n",
	     samplerate,channels,fragsize,ainfo.fragstotal,
	     (fragsize/channels/2)/(float)samplerate,
	     ((ainfo.fragstotal*fragsize)/channels/2)/(float)samplerate);
  if (channels!=numChan && samplerate!=fSample)
    CommonExit(1,"wrong input file format (numChan / fSample)");
  if (fragsize%(2*channels))
    CommonWarning("fragsize no multiple of 2*channel (%d)",fragsize);
  if (sizeof(short)!=2)
    CommonExit(1,"sizeof(short)!=2 (%d)",sizeof(short));

  if (bufLen>fragsize/2)
    bufLen = ((bufLen*2)/fragsize)*fragsize/2;
  if (recBufLen>fragsize/2)
    recBufLen = ((recBufLen*2)/fragsize)*fragsize/2;
  if (playBufLen>fragsize/2)
    playBufLen = ((playBufLen*2)/fragsize)*fragsize/2;
 
  if (debugLevel>=3)
    fprintf(stderr,
	    "bufLen=%ld latLen=%ld recBufLen=%ld playBufLen=%ld\n",
	    bufLen,latLen,recBufLen,playBufLen);

  if (delayNumSamp>=0) {
    writeNumSamp = 0;
    readNumSamp = -delayNumSamp;
  }
  else {
    writeNumSamp = delayNumSamp;
    readNumSamp = 0;
  }
  recNumSamp = playNumSamp = 0;
  recW = recR = recF = recDev = recFile = 0;
  playW = playR = playF = playDev = playFile = 0;
  recClip = playClip = 0;
  recPeak = playPeak = 0;
  recSlipped = playSlipped = 0;
  recMaxUsed = 0;
  playMinUsed = playBufLen;
  recOver = playUnder = 0;
  infd = outfd = -1;
  triggered = 0;
  inas = outas = (AuStream*)NULL;
  for (i=0; i<playBufLen; i++)
    playBuf[i] = 0;

  if (debugLevel)
    fprintf(stderr,"%8s %4s %8s %8s %4s %8s %16s %16s\n",
	   "rec  [s]","bufr","writ [s]","read [s]","bufr","play [s]",
	   "-96dB..rec...0dB","-96dB..play..0dB");

  /* main loop (delayTime samples pre-roll, echo max. duration samples) */
  do {
    if (debugLevel) {
      if (recOver)
	strcpy(recs,"SLIP");
      else
	sprintf(recs,"%3.0f%%",recF*100.0/recBufLen);
      if (playUnder)
	strcpy(plays,"SLIP");
      else
	sprintf(plays,"%3.0f%%",playF*100.0/playBufLen);
      fprintf(stderr,"%s%8.3f %4s %8.3f %8.3f %4s %8.3f ",
	      (debugLevel<=2)?"\r":"",
	      recNumSamp/numChan/fSample,
	      recs,
	      writeNumSamp/numChan/fSample,
	      readNumSamp/numChan/fSample,
	      plays,
	      playNumSamp/numChan/fSample);
      fflush(stderr);
    }

    if (triggered && recMaxUsed<recF)
      recMaxUsed = recF;
    if (triggered && playMinUsed>playF)
      playMinUsed = playF;
    recSlipped += recOver;
    playSlipped += playUnder;
    recOver = playUnder = 0;

    /* open files ? */
    if (writeNumSamp>=0 && !outas) {
      if (writeNumSamp>0)
	CommonExit(1,"Ooops ... writeNumSamp>0");
      /* open output file */
      if (debugLevel>1) {
	fprintf(stderr,"\ntrying to open output %s ...\n",recFileName);
	fflush(stderr);
      }
      if (!(outas=AuOpenWriteNB(recFileName,1,0,
				numChan,fSample,&outfd)))
	CommonExit(1,"can not open output \"%s\"",recFileName);
      if (debugLevel>1) {
	fprintf(stderr,
		"opened output=%s numChan=%d fSample=%.1f fd=%d\n",
		recFileName,numChan,fSample,outfd);
	fflush(stderr);
      }
    }
    if (readNumSamp>=0 && !inas) {
      if (readNumSamp>0)
	CommonExit(1,"Ooops ... readNumSamp>0");
      /* open input file */
      if (debugLevel>1) {
	fprintf(stderr,"\ntrying to open input %s ...\n",playFileName);
	fflush(stderr);
      }
      if (!(inas=AuOpenReadNB(playFileName,1,0,0,
			      &inNumChan,&inFSample,&inNumSamp,&infd)))
	CommonExit(1,"can not open input \"%s\"",playFileName);
      if (debugLevel>1)
	fprintf(stderr,
		"opened  input=%s numChan=%d fSample=%.1f numSamp=%ld fd=%d\n",
		recFileName,inNumChan,inFSample,inNumSamp,infd);
      
      if (inNumChan!=numChan || inFSample!=fSample)
	CommonExit(1,"wrong input file format (numChan / FSample)");
      if (inNumSamp!=-1) {
	if (durNumSamp==0)
	  durNumSamp = inNumSamp;
	playDurNumSamp = min(durNumSamp,inNumSamp);
      }
    }

    /* determine transfer capabilites */
    if (inas) {
      playFile = playBufLen-playF;
      if (durNumSamp)
	playFile = min(playFile,playDurNumSamp-readNumSamp);
      playFile = max(playFile,0);
      if (playFile==0 && (playBufLen-playF)>0) {
	tmp = playBufLen-playF;
	for (i=0; i<tmp; i++)
	  playBuf[(playW+i)%playBufLen] = 0;
	playF += tmp;
	playW = (playW+tmp)%playBufLen;

      }
    }
    else {
      tmp = min(playBufLen-playF,-readNumSamp);
      playF += tmp;
      readNumSamp += tmp;
      playW = (playW+tmp)%playBufLen;
      playFile = 0;
    }

    /* playDev = min(playF,bufLen); */
    /* playDev = playF; */
    playDev = bufLen;

    if (triggered)
      /* recDev = min(recBufLen-recF,bufLen); */
      /* recDev = recBufLen-recF; */
      recDev = bufLen;
    else
      recDev = 0;

    if (outas) {
      recFile = recF;
      if (durNumSamp)
	recFile = min(recFile,durNumSamp-writeNumSamp);
      recFile = max(recFile,0);
      if (recFile==0 && recF>0) {
	recF = 0;
	recR = recW;
      }
    }
    else {
      tmp = min(recF,-writeNumSamp);
      recF -= tmp;
      writeNumSamp += tmp;
      recR = (recR+tmp)%recBufLen;
      recFile = 0;
    }

    /* select for non-blocking*/
    mfs = -1;
    FD_ZERO(&rfds);
    FD_ZERO(&wfds);
    if (recDev) {
      FD_SET(audev,&rfds);
      mfs = audev;
    }
    if (playDev) {
      FD_SET(audev,&wfds);
      mfs = audev;
    }
    if (playFile) {
      FD_SET(infd,&rfds);
      mfs = max(mfs,infd);
    }
    if (recFile) {
      FD_SET(outfd,&wfds);
      mfs = max(mfs,outfd);
    }
    if (mfs==-1)
      CommonExit(1,"mfs==-1");
    tv.tv_sec = 1;
    tv.tv_usec = 0;
    err = select(mfs+1,&rfds,&wfds,NULL,&tv);
    if (recDev)
      rD = FD_ISSET(audev,&rfds);
    else
      rD = 0;
    if (playDev)
      pD = FD_ISSET(audev,&wfds);
    else
      pD = 0;
    if (playFile)
      pF = FD_ISSET(infd,&rfds);
    else
      pF = 0;
    if (recFile)
      rF = FD_ISSET(outfd,&wfds);
    else
      rF = 0;
    if (debugLevel>=3) {
      fprintf(stderr,"I%cP%cR%cO%c ",
	      pF?'+':'-',pD?'+':'-',rD?'+':'-',rF?'+':'-');
      fprintf(stderr,"%5.3f %5.3f ",recF/numChan/fSample,
	      playF/numChan/fSample);
      fflush(stderr);
    }
    
    if (debugLevel>=3) {
      fprintf(stderr,
	      "\nplayFile=%6ld playDev=%6ld recDev=%6ld recFile=%6ld\n"
	      "readNS=%10ld playNS=%10ld recNS=%10ld writeNS=%10ld\n",
	      playFile,playDev,recDev,recFile,
	      readNumSamp,playNumSamp,recNumSamp,writeNumSamp);
      fprintf(stderr,
	      "BUFFER pR=%6ld pF=%6ld pW=%6ld %s rR=%6ld rF=%6ld rW=%6ld %s\n",
	      playR,playF,playW,((playR+playF)%playBufLen==playW)?"OK ":"ERR",
	      recR,recF,recW,((recR+recF)%recBufLen==recW)?"OK ":"ERR");
      fflush(stderr);
    }

    /* read file */
    if (inas && pF) {
      tmpReq = min(playFile,playBufLen-playW);
      if (debugLevel>=3)
	fprintf(stderr,"READ  req=%6ld ",tmpReq);
      if (tmpReq)
	tmpAck = AuReadData(inas,playBuf+playW,tmpReq);
      else
	tmpAck = 0;
      if (debugLevel>=3) {
	fprintf(stderr,"ack=%6ld\n",tmpAck);
	fflush(stderr);
      }
      if (tmpAck)
	playPeak = 0;
      numClip = 0;
      for (i=0; i<tmpAck; i++) {
	tmp = ((long)(gainPlay*playBuf[playW+i]+32768.5))-32768;
	if (tmp>=32767) {
	  tmp = 32767;
	  numClip++;
	}
	if (tmp<=-32768) {
	  tmp = -32768;
	  numClip++;
	}
	if (playPeak < tmp)
	  playPeak = tmp;
	if (playPeak < -tmp)
	  playPeak = -tmp;
	playBuf[playW+i] = tmp;
      }
      playClip += numClip;
      readNumSamp += tmpAck;
      playW = (playW+tmpAck)%playBufLen;
      playF += tmpAck;
    }

    /* play */
    if (pD && playDev) {
      tmpReq = min(playDev,playBufLen-playR);
      ioctl (audev,SNDCTL_DSP_GETOSPACE,&ainfo);
      if (debugLevel>=3)
	fprintf(stderr,"ospace.bytes=%6d ",ainfo.bytes);
      if (ainfo.bytes) /* shoudn't be zero if pD is set ... */
	tmpReq = min(tmpReq,ainfo.bytes/2);
      if (triggered) {
	/* calc num samples in device play buffer */
	err = ioctl(audev,SNDCTL_DSP_GETODELAY,&tmp);
	if (err)
	  /* bad alternative approach ... */
	  tmp = ainfo.fragstotal*fragsize-ainfo.bytes;
	tmpReq = min(tmpReq,max(0,latLen-tmp/2));
      }
      tmpReq = ((tmpReq*2)/fragsize)*fragsize/2;
      if (debugLevel>=3)
	fprintf(stderr,"PLAY  req=%6ld ",tmpReq);
      if (tmpReq)
	tmpAck = write(audev,playBuf+playR,tmpReq*2);
      else
	tmpAck = 0;
      if (tmpAck==-1) {
	if (errno!=EAGAIN)
	  CommonExit(1,"error writing to audio device (%s)",
		     strerror(errno));
	else
	  tmpAck = 0;
      }
      if (tmpAck%2) {
	if (debugLevel>=3)
	  fprintf(stderr,"play ODD ");
	tmp = -1;
	while (tmp==-1 && errno==EAGAIN) {
	  FD_ZERO(&wfds);
	  FD_SET(audev,&wfds);
	  select(audev,NULL,&wfds,NULL,NULL);   /* wait for 2nd byte */
	  tmp = write(audev,((char*)(playBuf+playR))+tmpAck,1);
	}
	tmpAck++;
      }
      if (debugLevel>=3) {
	fprintf(stderr,"ack=%6ld\n",tmpAck/2);
	fflush(stderr);
      }
      tmpAck /= 2;
      playNumSamp += tmpAck;
      if (!triggered && playNumSamp >= latLen) {
	tmp = PCM_ENABLE_OUTPUT | PCM_ENABLE_INPUT; /* enable */
	ioctl(audev,SNDCTL_DSP_SETTRIGGER,&tmp);
	triggered = 1;
	if (debugLevel>=3) {
	  fprintf(stderr,"SETTRIGGER\n");
	  fflush(stderr);
	}
      }
      playR = (playR+tmpAck)%playBufLen;
      playF -= tmpAck;
      if (playF<0) {
	playUnder = -playF;
	if (debugLevel>=3) {
	  fprintf(stderr,"UNDERFLOW PLAY %6ld\n",playUnder);
	  fflush(stderr);
	}
	playF = 0;
	playW = playR;
      }
      playDev -= tmpAck;
    }

    /* rec  */
    if (rD && recDev) {
      tmpReq = min(recDev,recBufLen-recW);
      ioctl (audev,SNDCTL_DSP_GETISPACE,&ainfo);
      if (debugLevel>=3)
	fprintf(stderr,"ispace.bytes=%6d ",ainfo.bytes);
      if (ainfo.bytes) /* 0 if overflow ..., but rD is set */
	tmpReq = min(tmpReq,ainfo.bytes/2);
      tmpReq = ((tmpReq*2)/fragsize)*fragsize/2;
      if (debugLevel>=3)
	fprintf(stderr,"REC   req=%6ld ",tmpReq);
      if (tmpReq)
	tmpAck = read(audev,recBuf+recW,tmpReq*2);
      else
	tmpAck = 0;
      if (tmpAck==-1) {
	if (errno!=EAGAIN)
	  CommonExit(1,"error reading from audio device (%s)",
		     strerror(errno));
	else
	  tmpAck = 0;
      }
      if (tmpAck%2) {
	if (debugLevel>=3)
	  fprintf(stderr,"rec ODD ");
	tmp = -1;
	while (tmp==-1 && errno==EAGAIN) {
	  FD_ZERO(&rfds);
	  FD_SET(audev,&rfds);
	  select(audev,&rfds,NULL,NULL,NULL);   /* wait for 2nd byte */
	  tmp = read(audev,((char*)(recBuf+recW))+tmpAck,1);
	}
	tmpAck++;
      }
      if (debugLevel>=3) {
	fprintf(stderr,"ack=%6ld\n",tmpAck/2);
	fflush(stderr);
      }
      tmpAck /= 2;

      if (tmpAck)
	recPeak = 0;
      numClip = 0;
      for (i=0; i<tmpAck; i++) {
	tmp = ((long)(gainRec*recBuf[recW+i]+32768.5))-32768;
	if (tmp>=32767) {
	  tmp = 32767;
	  numClip++;
	}
	if (tmp<=-32768) {
	  tmp = -32768;
	  numClip++;
	}
	if (recPeak < tmp)
	  recPeak = tmp;
	if (recPeak < -tmp)
	  recPeak = -tmp;
	recBuf[recW+i] = tmp;
      }
      recClip += numClip;
      recNumSamp += tmpAck;
      recW = (recW+tmpAck)%recBufLen;
      recF += tmpAck;
      if (recF>recBufLen) {
	recOver = recF-recBufLen;
	if (debugLevel>=3) {
	  fprintf(stderr,"OVERFLOW REC %6ld\n",recOver);
	  fflush(stderr);
	}
	recF = recBufLen;
	recR = recW;
      }
      recDev -= tmpAck;
    }

    /* write file */
    if (outas && rF) {
      tmpReq = min(recFile,recBufLen-recR);
      if (debugLevel>=3)
	fprintf(stderr,"WRITE req=%6ld ",tmpReq);
      if (tmpReq)
	tmpAck = AuWriteData(outas,recBuf+recR,tmpReq);
      else
	tmpAck = 0;
      if (debugLevel>=3) {
	fprintf(stderr,"ack=%6ld\n",tmpAck);
	fflush(stderr);
      }
      writeNumSamp += tmpAck;
      recR = (recR+tmpAck)%recBufLen;
      recF -= tmpAck;
    }

    if (debugLevel) {
      tmp = recPeak;
      for (i=0; i<16; i++) {
	level[i] = (tmp)?'*':'-';
	tmp >>= 1;
      }
      level[16]='\0';
      fprintf(stderr,"%s ",level);
      tmp = playPeak;
      for (i=0; i<16; i++) {
	level[i] = (tmp)?'*':'-';
	tmp >>= 1;
      }
      level[16]='\0';
      fprintf(stderr,"%s ",level);
      if (debugLevel<=2)
	fflush(stderr);
      else
	fprintf(stderr," \n");
    }

    /* nap bufTime */
    tv.tv_sec = 0;
    tv.tv_usec = (int)(min(1000000,napTime)*1000000.);
    tv.tv_usec = min(999999,tv.tv_usec);
    select(0,NULL,NULL,NULL,&tv);
  } while (!stop && (durNumSamp==0 ||
		     writeNumSamp < durNumSamp ||
		     readNumSamp < playDurNumSamp));

  /* close audio device */ 
  ioctl(audev,SNDCTL_DSP_SYNC,0);
  close(audev);

  /* close files */
  if (outas)
    AuClose(outas);
  if (inas)
    AuClose(inas);
  
  if (debugLevel && debugLevel<=2) {
    fprintf(stderr,"\n");
    fflush(stderr);
  }
  if (debugLevel>1)
    fprintf(stderr,
	    "rec/written: %ld/%ld samples (%.3f/%.3f sec)\n"
	    "  max buffer usage/size: %.3f/%.3f sec %3.0f%% "
	    "(slipped %ld samples)\n"
	    "read/played: %ld/%ld samples (%.3f/%.3f sec)\n"
	    "  min buffer usage/size: %.3f/%.3f sec %3.0f%% "
	    "(slipped %ld samples)\n",
	    recNumSamp,writeNumSamp,
	    recNumSamp/(numChan*fSample),writeNumSamp/(numChan*fSample),
	    recMaxUsed/(numChan*fSample),recBufLen/(numChan*fSample),
	    recMaxUsed*100.0/recBufLen,recSlipped,
	    readNumSamp,playNumSamp,
	    readNumSamp/(numChan*fSample),playNumSamp/(numChan*fSample),
	    playMinUsed/(numChan*fSample),playBufLen/(numChan*fSample),
	    playMinUsed*100.0/playBufLen,playSlipped);
  if (recClip)
    CommonWarning("rec: %ld samples clipped!",recClip);
  if (playClip)
    CommonWarning("play: %ld samples clipped!",playClip);

  CmdLineEvalFree(paraList);

  if (debugLevel)
    fprintf(stderr,"%s: %s%s\n",progName,
	    stop?"aborted":"finished",(stop==2)?" (broken pipe)":"");

  exit (0);
}

/* end of auduplex.c */
