/*
 * FILE:    auddev_freebsd.c
 * PROGRAM: RAT
 * AUTHOR:  Colin Perkins / Orion Hodson
 *
 * $Revision: 1.1.1.1 $
 * $Date: 1998/08/15 19:16:50 $
 *
 * Copyright (c) 1996,1997 University College London
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, is permitted, for non-commercial use only, provided
 * that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *      This product includes software developed by the Computer Science
 *      Department at University College London
 * 4. Neither the name of the University nor of the Department may be used
 *    to endorse or promote products derived from this software without
 *    specific prior written permission.
 * Use of this software for commercial purposes is explicitly forbidden
 * unless prior written permission is obtained from the authors.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */


#include "assert.h"
#include "bat_include.h"
#if defined(FreeBSD) || defined(__DragonFly__)

#include <sys/soundcard.h>

int          can_read  = FALSE;
int          can_write = FALSE;
int          iport     = AUDIO_MICROPHONE;
audio_format format;

#define bat_to_device(x)  ((x) * 100 / MAX_AMP)
#define device_to_bat(x)  ((x) * MAX_AMP / 100)

int 
audio_open_rw(char rw)
{
  int mode     = AFMT_S16_LE;	/* 16bit linear, little-endian */
  int stereo   = 0;		/* 0=mono, 1=stereo            */
  int speed    = 8000;
  int frag     = 0x7fff0007;	/* 128 bytes fragments         */
  int volume   = 100;
  int reclb    = 0;
  int audio_fd = -1;
  char buffer;			/* sigh. */

  switch (rw) {
  case O_RDONLY: can_read  = TRUE;
                 can_write = FALSE;
		 break;
  case O_WRONLY: can_read  = FALSE;
                 can_write = TRUE;
		 break;
  case O_RDWR  : can_read  = TRUE;
                 can_write = TRUE;
		 break;
  default      : abort();
  }

  audio_fd = open("/dev/audio", rw | O_NDELAY);
  if (audio_fd > 0) {
    if (ioctl(audio_fd, SNDCTL_DSP_SETDUPLEX, 0) == -1) {
      /* Cannot enable full-duplex mode. Oh well. */
#ifdef DEBUG
            printf("Cannot set full-duplex.\n");
#endif
    }
    if (ioctl(audio_fd, SNDCTL_DSP_SETFRAGMENT, &frag) == -1) {
#ifdef DEBUG
      printf("Cannot set fragment size (ignored)\n");
#endif
    }
    if ((ioctl(audio_fd, SNDCTL_DSP_SETFMT, &mode) == -1) || (mode != AFMT_S16_LE)) { 
      printf("Device doesn't support 16bit linear format!\n");
      return -1;
    }
    if ((ioctl(audio_fd, SNDCTL_DSP_STEREO, &stereo) == -1) || (stereo != 0)) {
      printf("Device doesn't support mono!\n");
      return -1;
    }
    if ((ioctl(audio_fd, SNDCTL_DSP_SPEED, &speed) == -1) || (speed != 8000)) {
      printf("Device doesn't support 8kHz sampling!\n");
      return -1;
    }

    /* Set global gain/volume to maximum values. This may fail on */
    /* some cards, but shouldn't cause any harm when it does..... */ 
    ioctl(audio_fd, MIXER_WRITE(SOUND_MIXER_VOLUME), &volume);
    ioctl(audio_fd, MIXER_WRITE(SOUND_MIXER_RECLEV), &volume);
    /* Set the gain/volume properly. We use the controls for the  */
    /* specific mixer channel to do this, relative to the global  */
    /* maximum gain/volume we've just set...                      */
    audio_set_gain(audio_fd, MAX_AMP / 2);
    audio_set_volume(audio_fd, MAX_AMP / 2);
    /* Select microphone input. We can't select output source...  */
    audio_set_iport(audio_fd, iport);
    /* Turn off loopback from input to output... */
    ioctl(audio_fd, MIXER_WRITE(SOUND_MIXER_IMIX), &reclb);
    read(audio_fd, &buffer, 1);	/* Device driver bug in linux-2.0.28: we must read some data before the ioctl */
                                /* to tell us how much data is waiting works....                              */
    return audio_fd;
  } else {
    perror("audio_open");
    close(audio_fd);
    return -1;
  }
}

/* Try to open the audio device.              */
/* Return TRUE if successful FALSE otherwise. */
int
audio_open(audio_format fmt)
{
  format = fmt;
  if (audio_duplex(-1)) {
    return audio_open_rw(O_RDWR);
  } else {
    return audio_open_rw(O_WRONLY);
  }
}

/* Close the audio device */
void
audio_close(int audio_fd)
{
  audio_drain(audio_fd);
  close(audio_fd);
}

/* Flush input buffer */
void
audio_drain(int audio_fd)
{
  /* oops! */
}

int
audio_duplex(int audio_fd)
{
  /* Find out if the device supports full-duplex operation. The device must
   * be open to do this, so if we're passed -1 as a file-descriptor we open
   * the device, do the ioctl, and then close it again...
   */
  if (audio_fd == -1) {
    audio_fd = audio_open_rw(O_RDONLY);
    if (ioctl(audio_fd, SNDCTL_DSP_SETDUPLEX, 0) == -1) {
      audio_close(audio_fd);
      return FALSE;
    }
    audio_close(audio_fd);
    return TRUE;
  }
  if (ioctl(audio_fd, SNDCTL_DSP_SETDUPLEX, 0) == -1) {
    return FALSE;
  }
  return TRUE;
}

/* Gain and volume values are in the range 0 - MAX_AMP */
void
audio_set_gain(int audio_fd, int gain)
{
  int volume = bat_to_device(gain) << 8 | bat_to_device(gain);

  if (audio_fd <= 0) {
    return;
  }
  switch (iport) {
  case AUDIO_MICROPHONE : if (ioctl(audio_fd, MIXER_WRITE(SOUND_MIXER_MIC), &volume) == -1) {
                            perror("Setting gain");
                          }
			  return;
  case AUDIO_LINE_IN    : if (ioctl(audio_fd, MIXER_WRITE(SOUND_MIXER_LINE), &volume) == -1) {
                            perror("Setting gain");
                          }
			  return;
  }
  printf("ERROR: Unknown iport in audio_set_gain!\n");
  abort();
}

int
audio_get_gain(int audio_fd)
{
  int volume;

  if (audio_fd <= 0) {
    return (0);
  }
  switch (iport) {
  case AUDIO_MICROPHONE : if (ioctl(audio_fd, MIXER_READ(SOUND_MIXER_MIC), &volume) == -1) {
                            perror("Getting gain");
                          }
			  break;
  case AUDIO_LINE_IN    : if (ioctl(audio_fd, MIXER_READ(SOUND_MIXER_LINE), &volume) == -1) {
                            perror("Getting gain");
                          }
			  break;
  default               : printf("ERROR: Unknown iport in audio_set_gain!\n");
                          abort();
  }
  return device_to_bat(volume & 0xff);
}

void
audio_set_volume(int audio_fd, int vol)
{
  int volume;

  if (audio_fd <= 0) {
    return;
  }
  volume = vol << 8 | vol;
  if (ioctl(audio_fd, MIXER_WRITE(SOUND_MIXER_PCM), &volume) == -1) {
    perror("Setting volume");
  }
}

int
audio_get_volume(int audio_fd)
{
  int volume;

  if (audio_fd <= 0) {
    return (0);
  }
  if (ioctl(audio_fd, MIXER_READ(SOUND_MIXER_VOLUME), &volume) == -1) {
    perror("Getting volume");
  }
  return device_to_bat(volume & 0x000000ff); /* Extract left channel volume */
}

int
audio_read(int audio_fd, sample *buf, int samples)
{
  if (can_read) {
    int            len;
    audio_buf_info info;

    /* Figure out how many bytes we can read before blocking... */
    ioctl(audio_fd, SNDCTL_DSP_GETISPACE, &info);
    if (info.bytes > (samples * BYTES_PER_SAMPLE)) {
      info.bytes = (samples * BYTES_PER_SAMPLE);
    }
    /* Read the data... */
    if ((len = read(audio_fd, (char *)buf, info.bytes)) < 0) {
      return 0;
    }
    return len / BYTES_PER_SAMPLE;
  } else {
    /* The value returned should indicate the time (in audio samples) */
    /* since the last time read was called.                           */
    int                   i;
    int                   diff;
    static struct timeval last_time;
    static struct timeval curr_time;
    static int            first_time = 0;

    if (first_time == 0) {
      gettimeofday(&last_time, NULL);
      first_time = 1;
    }
    gettimeofday(&curr_time, NULL);
    diff = (((curr_time.tv_sec - last_time.tv_sec) * 1e6) + (curr_time.tv_usec - last_time.tv_usec)) / 125;
    if (diff > samples) diff = samples;
    if (diff <      80) diff = 80;
    xmemchk();
    for (i=0; i<diff; i++) {
      buf[i] = L16_AUDIO_ZERO;
    }
    xmemchk();
    last_time = curr_time;
    return diff;
  }
}

int
audio_write(int audio_fd, sample *buf, int samples)
{
  int  done, len;
  char  *p;

  if (can_write) {
    p   = (char *) buf;
    len = samples * BYTES_PER_SAMPLE;
    while (1) {
      if ((done = write(audio_fd, p, len)) == len) {
        break;
      }
      if (errno != EINTR) {
        perror("Error writing device");
        return samples - ((len - done) / BYTES_PER_SAMPLE);
      }
      len -= done;
      p   += done;
    }
    return samples;
  } else {
    return samples;
  }
}

/* Check if the audio output has run out of data */
int
audio_is_dry(int audio_fd)
{
  return 0;
}

/* Set ops on audio device to be non-blocking */
void
audio_non_block(int audio_fd)
{
  int frag = 1;
  if ( ioctl(audio_fd, SNDCTL_DSP_NONBLOCK, &frag) == -1 ) {
          perror("Setting non blocking i/o");
  }
}

/* Set ops on audio device to block */
void
audio_block(int audio_fd)
{
          int frag = 0;
          if ( ioctl(audio_fd, SNDCTL_DSP_NONBLOCK, &frag) == -1 ) {
                  perror("Setting blocking i/o");
          }
}

/*
 * Check to see whether another application has requested the
 * audio device.
 */
int
audio_requested(int audio_fd)
{
  return 0;
}

void
audio_set_oport(int audio_fd, int port)
{
  /* There appears to be no-way to select this with OSS... */
  return;
}

int
audio_get_oport(int audio_fd)
{
  /* There appears to be no-way to select this with OSS... */
  return AUDIO_HEADPHONE;
}

int
audio_next_oport(int audio_fd)
{
  /* There appears to be no-way to select this with OSS... */
  return AUDIO_HEADPHONE;
}

void
audio_set_iport(int audio_fd, int port)
{
  int recmask;
  int recsrc;
  int gain;

  if (ioctl(audio_fd, MIXER_READ(SOUND_MIXER_RECMASK), &recmask) == -1) {
    printf("WARNING: Unable to read recording mask!\n");
    return;
  }
  switch (port) {
    case AUDIO_MICROPHONE : if (recmask & SOUND_MASK_MIC) {
			      recsrc = SOUND_MASK_MIC;
			      if ((ioctl(audio_fd, MIXER_WRITE(SOUND_MIXER_RECSRC), &recsrc) == -1) && !(recsrc & SOUND_MASK_MIC)) {
			        printf("WARNING: Unable to select recording source!\n");
				return;
			      }
			      gain = audio_get_gain(audio_fd);
                              iport = port;
			      audio_set_gain(audio_fd, gain);
			    }
                            break;
    case AUDIO_LINE_IN    : if (recmask & SOUND_MASK_LINE) {
			      recsrc = SOUND_MASK_LINE;
			      if ((ioctl(audio_fd, MIXER_WRITE(SOUND_MIXER_RECSRC), &recsrc) == -1) && !(recsrc & SOUND_MASK_LINE)){
			        printf("WARNING: Unable to select recording source!\n");
				return;
			      }
			      gain = audio_get_gain(audio_fd);
                              iport = port;
			      audio_set_gain(audio_fd, gain);
			    }
                            break;
    default               : printf("audio_set_port: unknown port!\n");
                            abort();
  };
  return;
}

int
audio_get_iport(int audio_fd)
{
  return iport;
}

int
audio_next_iport(int audio_fd)
{
  switch (iport) {
    case AUDIO_MICROPHONE : audio_set_iport(audio_fd, AUDIO_LINE_IN);
                            break;
    case AUDIO_LINE_IN    : audio_set_iport(audio_fd, AUDIO_MICROPHONE);
                            break;
    default               : printf("Unknown audio source!\n");
  }
  return iport;
}

void
audio_switch_out(int audio_fd, cushion_struct *ap)
{
  if (!audio_duplex(audio_fd) && !can_write) {
    audio_close(audio_fd);
    audio_open_rw(O_WRONLY);
  }
}

void
audio_switch_in(int audio_fd)
{
  if (!audio_duplex(audio_fd) && !can_read) {
    audio_close(audio_fd);
    audio_open_rw(O_RDONLY);
  }
}

#endif /* FreeBSD */

