/* Copyright (c) 1991-2002 Doshita Lab. Speech Group, Kyoto University */
/* Copyright (c) 2000-2002 Speech and Acoustics Processing Lab., NAIST */
/*   All rights reserved   */

/* adin_mic_linux_alsa.c --- adin microphone library for ALSA native API */

/* $Id: adin_mic_linux_alsa.c,v 1.5 2002/11/25 01:05:38 ri Exp $ */

/*
 * From rev.3.3p2: alsa-0.9.0 (tested on rc2)
 *
 */

/*
 * Use mixer program like alsamixer or alsactl to setup mic device
 * (mute/unmute, volume control, etc.)
 *
 * !!Note that ALSA drivers first mute all audio devices by default!!
 */

/* see http://www.alsa-project.org/ for information about ALSA, */
/* Advanced Linux Sound Architecture */

#include <sent/stddefs.h>
#include <sent/adin.h>

#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <alsa/asoundlib.h>

static snd_pcm_t *handle;	/* audio handler */
static snd_pcm_hw_params_t *hwparams; /* store device hardware parameters */
static char *pcm_name = "hw:0,0"; /* name of the PCM device */

static boolean need_swap;	/* whether samples need byte swap */
static int latency = 50;	/* lantency time (msec) */

static struct pollfd *ufds;
static int count;


boolean
adin_mic_standby(int sfreq, void *dummy)
{
  int err;
  int exact_rate;		/* sample rate returned by hardware */
  int dir;			/* comparison result of exact rate and given rate */

  /* allocate hwparam structure */
  snd_pcm_hw_params_alloca(&hwparams);

  /* open device (for resource test, open in non-block mode) */
  if ((err = snd_pcm_open(&handle, pcm_name, SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK)) < 0) {
    j_printerr("Error: cannot open PCM device %s (%s)\n", pcm_name, snd_strerror(err));
    return(FALSE);
  }
  
  /* set device to non-block mode */
  if ((err = snd_pcm_nonblock(handle, 0)) < 0) {
    j_printerr("Error: cannot set PCM device to block mode\n");
    return(FALSE);
  }

  /* initialize hwparam structure */
  if ((err = snd_pcm_hw_params_any(handle, hwparams)) < 0) {
    j_printerr("Error: cannot initialize PCM device parameter structure (%s)\n", snd_strerror(err));
    return(FALSE);
  }

  /* set interleaved read/write format */
  if ((err = snd_pcm_hw_params_set_access(handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
    j_printerr("Error: cannot set PCM device access mode (%s)\n", snd_strerror(err));
    return(FALSE);
  }

  /* set sample format */
#ifdef WORDS_BIGENDIAN
  /* try big endian, then little endian with byte swap */
  if ((err = snd_pcm_hw_params_set_format(handle, hwparams, SND_PCM_FORMAT_S16_BE)) >= 0) {
    need_swap = FALSE;
  } else if ((err = snd_pcm_hw_params_set_format(handle, hwparams, SND_PCM_FORMAT_S16_LE)) >= 0) {
    need_swap = TRUE;
  } else {
    j_printerr("Error: cannot set PCM device format to 16bit-signed (%s)\n", snd_strerror(err));
    return(FALSE);
  }
#else  /* LITTLE ENDIAN */
  /* try little endian, then big endian with byte swap */
  if ((err = snd_pcm_hw_params_set_format(handle, hwparams, SND_PCM_FORMAT_S16_LE)) >= 0) {
    need_swap = FALSE;
  } else if ((err = snd_pcm_hw_params_set_format(handle, hwparams, SND_PCM_FORMAT_S16_BE)) >= 0) {
    need_swap = TRUE;
  } else {
    j_printerr("Error: cannot set PCM device format to 16bit-signed (%s)\n", snd_strerror(err));
    return(FALSE);
  }
#endif
  
  /* set sample rate (if the exact rate is not supported by the hardware, use nearest possible rate */
  exact_rate = snd_pcm_hw_params_set_rate_near(handle, hwparams, sfreq, &dir);
  if (exact_rate < 0) {
    j_printerr("Error: cannot set PCM device sample rate to %d (%s)\n", sfreq, snd_strerror(err));
    return(FALSE);
  }
  if (dir != 0) {
    j_printerr("Warning: the rate %d Hz is not supported by your PCM hardware.\n         ==> Using %d Hz instead.\n", sfreq, exact_rate);
  }

  /* set number of channels */
  if ((err = snd_pcm_hw_params_set_channels(handle, hwparams, 1)) < 0) {
    j_printerr("Error: cannot set PCM monoral channel (%s)\n", snd_strerror(err));
    return(FALSE);
  }

  /* set period size */
  {
    int periodsize;		/* period size (bytes) */
    int exact_size;
    int maxsize, minsize;
    
    /* get hardware max/min size */
    dir = 0;
    maxsize = snd_pcm_hw_params_get_period_size_max(hwparams, &dir);
    minsize = snd_pcm_hw_params_get_period_size_min(hwparams, &dir);

    /* set apropriate period size */
    periodsize = exact_rate * latency / 1000 * sizeof(SP16);
    if (periodsize < minsize) {
      j_printerr("Warning: PCM latency of %d ms (%d bytes) too small, use device minimum %d bytes\n", latency, periodsize, minsize);
      periodsize = minsize;
    } else if (periodsize > maxsize) {
      j_printerr("Warning: PCM latency of %d ms (%d bytes) too large, use device maximum %d bytes\n", latency, periodsize, maxsize);
      periodsize = maxsize;
    }
    
    /* set size (near value will be used) */
    exact_size = snd_pcm_hw_params_set_period_size_near(handle, hwparams, periodsize, &dir);
    if (exact_size < 0) {
      j_printerr("Error: cannot set PCM record period size to %d (%s)\n", periodsize, snd_strerror(err));
      return(FALSE);
    }
    if (dir != 0) {
      j_printerr("Warning: PCM period size: %d bytes (%d ms) -> %d bytes\n", periodsize, latency, exact_size);
    }
    /* set number of periods ( = 2) */
    if ((err = snd_pcm_hw_params_set_periods(handle, hwparams, 2, 0)) < 0) {
      j_printerr("Error: cannot set PCM number of periods to %d (%s)\n", 1, snd_strerror(err));
      return(FALSE);
    }
  }

  /* apply the configuration to the PCM device */
  if ((err = snd_pcm_hw_params(handle, hwparams)) < 0) {
    j_printerr("Error: cannot set PCM hardware parameters (%s)\n", snd_strerror(err));
    return(FALSE);
  }

  /* prepare for recording */
  if ((err = snd_pcm_prepare(handle)) < 0) {
    j_printerr("Error: cannot prepare audio interface (%s)\n", snd_strerror(err));
  }

  /* prepare for polling */
  count = snd_pcm_poll_descriptors_count(handle);
  if (count <= 0) {
    j_printerr("Error: invalid PCM poll descriptors count\n");
    return(FALSE);
  }
  ufds = mymalloc(sizeof(struct pollfd) * count);

  if ((err = snd_pcm_poll_descriptors(handle, ufds, count)) < 0) {
    j_printerr("Error: unable to obtain poll descriptors for PCM recording (%s)\n", snd_strerror(err));
    return(FALSE);
  }

  return(TRUE);
}

static int
xrun_recovery(snd_pcm_t *handle, int err)
{
  if (err == -EPIPE) {    /* under-run */
    err = snd_pcm_prepare(handle);
    if (err < 0)
      j_printerr("Can't recovery from PCM buffer underrun, prepare failed: %s\n", snd_strerror(err));
    return 0;
  } else if (err == -ESTRPIPE) {
    while ((err = snd_pcm_resume(handle)) == -EAGAIN)
      sleep(1);       /* wait until the suspend flag is released */
    if (err < 0) {
      err = snd_pcm_prepare(handle);
      if (err < 0)
	j_printerr("Can't recovery from PCM buffer suspend, prepare failed: %s\n", snd_strerror(err));
    }
    return 0;
  }
  return err;
}

/* start recording */
boolean
adin_mic_start()
{
  int err;
  snd_pcm_state_t status;

  /* check hardware status */
  while(1) {			/* wait till prepared */
    status = snd_pcm_state(handle);
    switch(status) {
    case SND_PCM_STATE_PREPARED: /* prepared for operation */
      if ((err = snd_pcm_start(handle)) < 0) {
	j_printerr("Error: cannot start PCM (%s)\n", snd_strerror(err));
	return (FALSE);
      }
      return(TRUE);
      break;
    case SND_PCM_STATE_RUNNING:	/* capturing the samples of other application */
      if ((err = snd_pcm_drop(handle)) < 0) { /* discard the existing samples */
	j_printerr("Error: cannot drop PCM (%s)\n", snd_strerror(err));
	return (FALSE);
      }
      break;
    case SND_PCM_STATE_XRUN:	/* buffer overrun */
      if ((err = xrun_recovery(handle, -EPIPE)) < 0) {
	j_printerr("Error: PCM XRUN recovery failed (%s)\n", snd_strerror(err));
	return(FALSE);
      }
      break;
    case SND_PCM_STATE_SUSPENDED:	/* suspended by power management system */
      if ((err = xrun_recovery(handle, -ESTRPIPE)) < 0) {
	j_printerr("Error: PCM XRUN recovery failed (%s)\n", snd_strerror(err));
	return(FALSE);
      }
      break;
    }
  }

  return(TRUE);
}
  
/* stop recording */
boolean
adin_mic_stop()
{
  return(TRUE);
}

/* read samples from audio device */
/* try to read `sampnum' samples and returns actual sample num recorded */
int
adin_mic_read(SP16 *buf, int sampnum)
{
  int cnt;
  snd_pcm_sframes_t avail;

  while ((avail = snd_pcm_avail_update(handle)) <= 0) {
    usleep(latency * 1000);
  }
  if (avail < sampnum) {
    cnt = snd_pcm_readi(handle, buf, avail);
  } else {
    cnt = snd_pcm_readi(handle, buf, sampnum);
  }

  if (cnt < 0) {
    j_printerr("Error: PCM read failed (%s)\n", snd_strerror(cnt));
    return(-2);
  }

  if (need_swap) {
    swap_sample_bytes(buf, cnt);
  }
  return(cnt);
}
