/* Audio duplex mmap test program
 * Stéphane Doyon <s.doyon@videotron.ca>
 * January 20th 2002
 * Two tests are available (see main.c). Compile with -DECHO for second test.
 */

/* Newer versions of the emu10k1 driver use non-standard mmap semantics */
//#define EMU10K1_MMAP_SEMANTICS

#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <linux/types.h>
#include <sys/time.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <linux/types.h>
#include <stdarg.h>
#include <assert.h>
#include <stdlib.h>

#include <linux/soundcard.h>

void newLine()
{
  fprintf(stderr,"\n");
}

void terminate()
{
  exit(1);
}

void errDie(char *fmt, ...)
{
  va_list argp;

  va_start(argp, fmt);

  vfprintf(stderr, fmt, argp);
  fprintf(stderr, ": %s\n", strerror(errno));
  va_end(argp);

  terminate();
}

void dieMsg(char *fmt, ...)
{
  va_list argp;

  va_start(argp, fmt);

  vfprintf(stderr, fmt, argp);
  fprintf(stderr,"\n");
  va_end(argp);

  terminate();
}

void dieOutMem()
{
  dieMsg("Out of memory!");
}

static struct timeval progStart;

void getNow(struct timeval *ptr)
{
  struct timezone tz;
  gettimeofday(ptr,&tz);
}

void initTime()
{ getNow(&progStart); }

int diffTime(struct timeval *t1, struct timeval *t2)
{
  return (t2->tv_sec - t1->tv_sec) * 1000
    + (t2->tv_usec - t1->tv_usec) / 1000;
}

unsigned elapsedSinceProgStart(struct timeval *t)
{
  return diffTime(&progStart, t);
}

unsigned elapsed(struct timeval *t)
{
  struct timeval now;
  getNow(&now);
  return(diffTime(t, &now));
}

unsigned timeStamp()
{
  return elapsed(&progStart);
}

void printProgTime()
{
  fprintf(stderr,"%u", timeStamp());
}

void print(char *fmt, ...)
{
  va_list argp;

  va_start(argp, fmt);

  printProgTime();
  fprintf(stderr,": ");
  vfprintf(stderr,fmt, argp);
  fflush(stdout);
  va_end(argp);
}

#define TRACEPRINT(level, args...) \
    ({ if(debug >= level) print(args); newLine(); })
#define TRACE(args...) \
    ({ print(__FUNCTION__ ": "); fprintf(stderr, args); newLine(); \
    })



typedef struct sample_struct {
  __s16 c1;
  __s16 c2;
} __attribute((packed)) sample;

#define BYTES_PER_SAMP 4

#define SAMPS2BYTES(len) ((len)*BYTES_PER_SAMP)
#define BYTES2SAMPS(len) ((len)/BYTES_PER_SAMP)

#define SPEED 22050

#define TIME2SAMPS(msec) \
    ((int)( (double)(msec)/1000.0 * (double)(SPEED) ) )
#define SAMPS2TIME(s) \
    ((int)( (double)(s) / (double)(SPEED) *1000.0 ) )

#define STEREO 1 /*stereo*/
#define FORMAT AFMT_S16_LE /* signed 16bits little-endian */

/* NB correct operation is not possible with only 2 frags, we need
   at least 4. */

#define FRAGSIZECODE 14
/* fragment size = 2^14=16384 = 0.1857secs
   at 22.05KHz 16bits stereo */

/* With some version of es1370, although the buffer is 128K I can't
   seem to mmap more than 64K. */
#if 0
#define MAXDMA (64*1024)
/* Whether to enforce a limit on the number of fragments */
#define NFRAGS (MAXDMA/(1<<FRAGSIZECODE))
#else
#define NFRAGS 0xFFFF
#endif

int audio_fd=-1;
static int fragSize, nFrags;
sample *ibuf, *obuf, *iend, *oend;
double fragTime;
int sampsPerFrag, totalSamps;

void cardClose()
{
  if(audio_fd>=0) {
    close(audio_fd);
    TRACE("Closed soundcard");
  }
  audio_fd=-1;
}

static void cardSetup()
{
  int frag = (NFRAGS << 16) | FRAGSIZECODE;
  int caps = 0;
  int stereo = STEREO;
  int speed = SPEED;
  int format = FORMAT;

  TRACE("About to open sound device");
  if((audio_fd = open("/dev/dsp",O_RDWR |O_NONBLOCK, 0)) <= 0)
    errDie("open");
  TRACE("Opened sound device");

  if(ioctl(audio_fd, SNDCTL_DSP_GETCAPS, &caps)==-1)
    errDie("ioctl SNDCTL_DSP_GETCAPS");
  TRACE("Obtained caps: 0x%x", caps);

  if(!(caps & DSP_CAP_DUPLEX))
    dieMsg("Soundcard is not full-duplex");
  if(!(caps & DSP_CAP_TRIGGER))
    dieMsg("Soundcard does not support trigger");
  if(!(caps & DSP_CAP_MMAP))
    dieMsg("Soundcard does not support mmap");
  if(!(caps & DSP_CAP_REALTIME))
    dieMsg("Soundcard does not support realtime / precise position reporting");
  TRACE("caps are OK");

  if (ioctl(audio_fd, SNDCTL_DSP_SETDUPLEX, 0)==-1)
    errDie("ioctl SNDCTL_DSP_SETDUPLEX");
  TRACE("set duplex");

  if (ioctl(audio_fd, SNDCTL_DSP_SETFRAGMENT, &frag)==-1)
    errDie("ioctl SNDCTL_DSP_SETFRAGMENT");
  if( (frag & 0xFFFF) != FRAGSIZECODE )
    dieMsg("Fragment size of %d was refused, proposed size was %d",
	   FRAGSIZECODE, (frag & 0xFFFF));
  TRACE("set fragments %x", frag);

  if (ioctl(audio_fd, SNDCTL_DSP_SETFMT, &format)==-1)
    errDie("ioctl SNDCTL_DSP_SETFMT");
  if(format != AFMT_S16_LE)
    dieMsg("Soundcard does not support 16bits signed sample format.\n");
  TRACE("set format 0x%x", format);

  if (ioctl(audio_fd, SNDCTL_DSP_STEREO, &stereo)==-1)
    errDie("ioctl SNDCTL_DSP_STEREO");
  if(stereo != STEREO)
    dieMsg("Stereo setting was ignored!");
  TRACE("set stereo %d", stereo);
  if (ioctl(audio_fd, SNDCTL_DSP_SPEED, &speed)==-1)
    errDie("ioctl SNDCTL_DSP_SPEED");
  if(speed != SPEED)
    dieMsg("Speed selection of %d was translated to speed %d!",
	   SPEED, speed);
  TRACE("set speed %d", speed);
}

static void cardDoMmap()
{
  int size = nFrags*fragSize;
  int sizeSamps = size / BYTES_PER_SAMP;
#ifdef EMU10K1_MMAP_SEMANTICS
  if((obuf = mmap(NULL, 2*size, PROT_READ|PROT_WRITE, 
		  MAP_FILE|MAP_SHARED, audio_fd, 0)) == (void *)-1)
    errDie("mmap (input)");
  oend = obuf+sizeSamps;
  ibuf = oend;
  iend = ibuf+sizeSamps;
  TRACE("mmapp'ed play to %p, up to %p", obuf,oend);
  TRACE("mmapp'ed record to %p, up to %p", ibuf,iend);
#else
  if((ibuf = mmap(NULL, size, PROT_READ, 
		  MAP_FILE|MAP_SHARED, audio_fd, 0)) == (void *)-1)
    errDie("mmap (input)");
  iend = ibuf+sizeSamps;
  TRACE("mmapp'ed PROT_READ to %p, up to %p", ibuf,iend);
  if((obuf = mmap(NULL, size, PROT_WRITE, 
		  MAP_FILE|MAP_SHARED, audio_fd, 0)) == (void *)-1)
    errDie("mmap (output)");
  oend = obuf +sizeSamps;
  TRACE("mmapp'ed PROT_WRITE to %p, up to %p", obuf, oend);
#endif
}

void cardSilence()
{
  memset(obuf, 0, totalSamps *BYTES_PER_SAMP);
  TRACE("memset'ed obuf to silence");
}

void cardOpen()
{
  audio_buf_info info;
  cardClose();
  cardSetup();

  if(ioctl(audio_fd, SNDCTL_DSP_GETOSPACE, &info)==-1)
    errDie("ioctl SNDCTL_DSP_GETOSPACE");
  fragSize = info.fragsize;
  if(fragSize != (1<<FRAGSIZECODE))
    dieMsg("SNDCTL_DSP_GETOSPACE reports unexpected fragment size: "
	   "%d instead of %d\n",
	   fragSize, (1<<FRAGSIZECODE));
  nFrags = info.fragstotal;
  TRACE("Confirming fragSize = %d, nFrags = %d", fragSize, nFrags);
  fragTime = (double)fragSize / (double)(BYTES_PER_SAMP * SPEED);
  sampsPerFrag = fragSize / BYTES_PER_SAMP;
  totalSamps = nFrags * sampsPerFrag;
  TRACE("Calculating: fragTime %.5f secs, sampsPerFrag %d, totalSamps %d, "
	 "max buf time %.3f secs",
	 fragTime, sampsPerFrag, totalSamps, fragTime*(double)nFrags);

  cardDoMmap();
  cardSilence();
}

void cardStart()
{
  int trig = 0;
  if(ioctl(audio_fd, SNDCTL_DSP_SETTRIGGER, &trig) == -1)
    errDie("ioctl SNDCTL_DSP_SETTRIGGER");
  if(trig != 0)
    dieMsg("Bad trigger when setting to 0: %d", trig);
  TRACE("Set trigger to disable");
  trig = PCM_ENABLE_INPUT | PCM_ENABLE_OUTPUT;
  TRACE("Enabling duplex trigger");
  if(ioctl(audio_fd, SNDCTL_DSP_SETTRIGGER, &trig) == -1)
    errDie("ioctl SNDCTL_DSP_SETTRIGGER");
  if(trig != (PCM_ENABLE_INPUT | PCM_ENABLE_OUTPUT))
    dieMsg("Bad trigger when starting duplex play/record: %d", trig);
  TRACE("Soundcard triggered");
}

sample *cardGetIPtr()
{
  sample *ptr;
  count_info info;
  if(ioctl(audio_fd, SNDCTL_DSP_GETIPTR, &info) == -1)
    errDie("ioctl SNDCTL_DSP_GETIPTR");
  ptr = ibuf + info.ptr/BYTES_PER_SAMP;
  TRACE("iptr: samples: %d, byets: %d, frags: %d, ptr: %p",
	 info.ptr/BYTES_PER_SAMP, info.ptr, info.ptr/fragSize, ptr);
  return ptr;
}
sample *cardGetOPtr()
{
  sample *ptr;
  count_info info;
  if(ioctl(audio_fd, SNDCTL_DSP_GETOPTR, &info) == -1)
    errDie("ioctl SNDCTL_DSP_GETOPTR");
  ptr = obuf + info.ptr/BYTES_PER_SAMP;
  TRACE("optr: samples: %d, byets: %d, frags: %d, ptr: %p",
	 info.ptr/BYTES_PER_SAMP, info.ptr, info.ptr/fragSize, ptr);
  return ptr;
}

int cardWaitFrag(int msec)
{
  struct timeval timeo, *t;
  fd_set set;
  int ret;

  if(msec>=0) {
    t = &timeo;
    timeo.tv_sec = msec/1000;
    timeo.tv_usec = (msec%1000)*1000;
  }else t = NULL;
  FD_ZERO(&set);
  FD_SET(audio_fd, &set);
  while(1) {
    if((ret = select(audio_fd+1, NULL, &set, NULL, t)) <0) {
      if(errno == EINTR) continue;
      dieMsg("select");
    }
    return ret;
  }
  /* GETIPTR and GETOPTR to clear before calling again! */
}
void clearSelect()
{
  cardGetOPtr(); cardGetIPtr();
}

void noise()
{
  sample *s = obuf;
  int i = 0;
  while(s<oend) {
    if(i<=0) {
      s->c1 = 0x8000; s->c2 = 0x8000;
    }else{
      s->c1 = 0x7FFF; s->c2 = 0x7FFF;
    }
    s++;
    if(++i > 64)
      i=-63;
  }
  TRACE("wrote noise to obuf");
}

int main()
{
  initTime();
  cardOpen();
  cardStart();

#ifndef ECHO /* the simple beep test */
  noise();
  {
    int i;
    for(i=0; i<20; i++) {
      cardWaitFrag(-1);
      cardGetIPtr();
      cardGetOPtr();
    }
  }
#endif

#ifdef ECHO /* simplified echo test */
  {
    /* assumes we never skip an interrupt... good enough for testing! */
    int i;
    sample *iptr, *optr;
    for(i=0; i<30; i++) {
      cardWaitFrag(-1);
      iptr = cardGetIPtr();
      iptr = ibuf + ((iptr-ibuf)/sampsPerFrag*sampsPerFrag);
      optr = cardGetOPtr();
      optr = obuf + ((optr-obuf)/sampsPerFrag*sampsPerFrag);

      iptr -= sampsPerFrag;
      if(iptr<ibuf) {
	iptr += totalSamps;
	assert(iptr >= ibuf);
	assert(iptr < iend);
      }
      optr += sampsPerFrag;
      if(optr>=oend) {
	optr -= totalSamps;
	assert(optr >= obuf);
	assert(optr < oend);
      }
      assert( (sample *)(((char *)iptr) +fragSize) <= iend);
      assert( (sample *)(((char *)optr) +fragSize) <= oend);
      memcpy(optr, iptr, fragSize);
    }
  }
#endif

  cardClose();
  return 0;
}
