/* sqUnixSound.c -- sound support via Open Sound System
 *
 * Author: Ian Piumarta (ian.piumarta@inria.fr)
 *
 * Last edited: Thu Mar 11 18:12:34 1999 by piumarta (Ian Piumarta) on pingu
 *
 * Note: OSS-compatible drivers are built into the kernel on Linux.
 * Compatibility libraries are available for many other Unix systems.
 * See <http://www.opensound.com/> for details and downloads.
 */

/* TODO:
 * - allow for arbitrary combinations of AFMT_S16_LE and AFMT_S16_BE
 *   for both platform and sound card, then optimise when platform
 *   and sound card use the same format (Intel and PPC use opposite
 *   byte order).
 */

#include "sq.h"

#ifdef HAS_OSS


#define SOUND_DEVICE		HAS_OSS
#define BYTES_PER_SAMPLE	2		/* We IMPOSE 16-bit sound! */

#define	USE_SEMAPHORES		/* wait preferred over delay in SoundPlayer */

#define NUM_FRAGMENTS		4	/* number of buffer fragments */
#define HWM_FRAGMENTS		2	/* play semaphore hysteresis */
#define INT_FRAGMENTS			/* r/w integral fragments only */

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

static unsigned char *auBuf=	0;

static int auFd=		-1;
static int auStereo=		0;
static int auFrameCount=	0;
static int auSampleRate=	0;
static int auPlaySemaIndex=	0;
static int auBufBytes=		0;

static int auSemaWaiting=	0;


/* We assume that there's enough h/w buffer lead to soak up the
 * maximum input polling period in the event loop.  This should be
 * true even on slow machines (e.g. 133Mhz 386).  If not (i.e there
 * are glitches in the sound output even when Squeak is otherwise
 * idle) then #undef USE_SEMAPHORES above, and recompile to force the
 * SoundPlayer to poll every millisecond.  (NOTE: doing this grinds
 * performance into the ground!)
 */
void auPollForIO(void)
{
  struct audio_buf_info info;

  if ((auFd == -1) || !auSemaWaiting) return;

  if (ioctl(auFd, SNDCTL_DSP_GETOSPACE, &info) == -1) {
    perror("ioctl(SNDCTL_DSP_GETOSPACE)");
    return;
  }
  if (info.fragments > HWM_FRAGMENTS) {
    auSemaWaiting= false;
    signalSemaphoreWithIndex(auPlaySemaIndex);
  }
}


/* reimplemented because ffs() is broken on some machines
 */
static int findFirstBit(int i)
{
  int p;
  for (p= 0; i != 0; i >>= 1) ++p;
  return p;
}


/*** exported sound output functions ***/


int snd_Stop(void)
{
  if (auFd == -1) return;

  close(auFd);
  auFd= -1;

  free(auBuf);

  auBuf=	0;
  auBufBytes=	0;

  return 0;
}


int snd_Start(int frameCount, int samplesPerSec, int stereo, int semaIndex)
{
  int bytesPerFrame=	(stereo ? 2 * BYTES_PER_SAMPLE : BYTES_PER_SAMPLE);
  int bufferBytes=	((frameCount * bytesPerFrame) / 8) * 8;

#ifdef INT_FRAGMENTS
  /* round down (NOT up!!!) to nearest power of two */
  int bufferPower=	findFirstBit(bufferBytes) - 1;
  bufferBytes=		1 << bufferPower;
#endif

  if (auFd != -1) {
    snd_Stop();
  }

#ifndef USE_SEMAPHORES
  if (semaIndex != 0) {
    return false;	/* refuse to handle the semaphore */
  }
#endif

  auStereo=		(stereo ? 1 : 0);
  auFrameCount=		bufferBytes / bytesPerFrame;
  auSampleRate=		samplesPerSec;
  auPlaySemaIndex=	semaIndex;
  auBufBytes=		bufferBytes;

  if ((auFd= open(SOUND_DEVICE, O_WRONLY, 0)) == -1) {
    perror(SOUND_DEVICE);  
    return false;
  }

#ifdef INT_FRAGMENTS
  {
    int arg= (NUM_FRAGMENTS << 16) | (bufferPower);
    if (ioctl(auFd, SNDCTL_DSP_SETFRAGMENT, &arg) == -1) {
      perror("ioctl(SNDCTL_DSP_SETFRAGMENT)");
      goto closeAndFail;
    }
  }
#endif

#if 0
  {
    struct audio_buf_info info;
    if (ioctl(auFd, SNDCTL_DSP_GETOSPACE, &info) == -1) {
      perror("ioctl(SNDCTL_DSP_GETOSPACE)");
      goto closeAndFail;
    }
  }
#endif

  {
    int format= AFMT_S16_LE;  
    if (ioctl(auFd, SNDCTL_DSP_SETFMT, &format) == -1) {
      perror("ioctl(SNDCTL_DSP_SETFMT)");  
      goto closeAndFail;
    }
    if (format != AFMT_S16_LE) {  
      fprintf(stderr, "snd_Start: could not select format AFMT_S16_LE\n");
      goto closeAndFail;
    }
  }

  {
    int stereoFlag= auStereo;	/* 0 => mono, 1 => stereo */
    if (ioctl(auFd, SNDCTL_DSP_STEREO, &stereoFlag) == -1) {
      perror("ioctl(SNDCTL_DSP_STEREO)");
      goto closeAndFail;
    }
    if (auStereo != stereoFlag) {
      fprintf(stderr, "snd_Start: unsupported number of channels\n");
      goto closeAndFail;
    }
  }

  {
    int speed= samplesPerSec;

    if (ioctl(auFd, SNDCTL_DSP_SPEED, &speed) == -1) {
      perror("ioctl(SNDCTL_DSP_SPEED)");  
      goto closeAndFail;
    }  

    if (abs(speed - samplesPerSec) > (samplesPerSec / 100)) {
      /* > 1% sample rate error */
      fprintf(stderr, "snd_Start: using %d samples/second (requested %d)\n",
	      speed, samplesPerSec);
    }
  }

  auBuf= (unsigned char *)malloc(bufferBytes);

  if (auBuf != 0) {
    return true;
  }

closeAndFail:
  close(auFd);
  auFd= -1;
  return false;
}


int snd_AvailableSpace(void)
{
  audio_buf_info info;
  if (auFd == -1) return -1;

  if (ioctl(auFd, SNDCTL_DSP_GETOSPACE, &info) == -1) {
    perror("ioctl(SNDCTL_DSP_GETOSPACE)");
    return 0;
  }
  /*  return (info.bytes > auBufBytes) ? auBufBytes : info.bytes;  */
  /*  return (info.fragments == 0) ? 0 : info.fragsize;  */
  if (info.fragments == 0) {
    auSemaWaiting= true;
    return 0;
  } else {
    return info.fragsize;
  }
}


int snd_PlaySamplesFromAtLength(int frameCount, int arrayIndex, int startIndex)
{
  int framesWritten= 0;

  if (auFd == -1) return -1;

#ifdef INT_FRAGMENTS
  if (frameCount < auFrameCount) return 0;
#endif

  if (frameCount > auFrameCount)
    framesWritten= auFrameCount;
  else
    framesWritten= frameCount;

  {
    short *src= (short *)(arrayIndex + (startIndex * 4));
    short *end= (short *)(arrayIndex + ((startIndex + framesWritten) * 4));
    unsigned char *dst= auBuf;
    unsigned char *pos= dst;
    if (auStereo) {
#ifdef HAS_LSB_FIRST
      /* elide copy loop for h/w format (little-endian, 16-bit, stereo) */
      dst= (unsigned char *)end;
      pos= (unsigned char *)src;
#else /*!HAS_LSB_FIRST*/
      while (src < end) {
	short data= *src++;
	*dst++= (unsigned char)(data & 0xff);		/* lsb first */
	*dst++= (unsigned char)((data >> 8) & 0xff);
      }
#endif
    } else {
      /* mono: average the left and right channels of the source */
      while (src < end) {
	int dataL= *src++;
	int dataR= *src++;
	short data= (dataL + dataR) >> 1;
	*dst++= (unsigned char)(data & 0xff);		/* lsb first */
	*dst++= (unsigned char)((data >> 8) & 0xff);
      }
    }
    /* write data to device from auBuf to dst */
    {
      int count= dst - pos;
      while (count > 0) {
	int len;
	len= write(auFd, pos, count);
	if (len == -1) {
	  perror(SOUND_DEVICE);
	  return 0;
	}
	count-= len;
	pos+= len;
      }
    }
  }
  return framesWritten;
}


/* This primitive is impossible to implement, since the OSS is doing
 * all the necessary buffering for us and there's no way to rewrite
 * data already written.
 *
 * (We could go the whole hog and use direct DMA access to the sound
 * drivers which would allow us to mix into a buffer already partially
 * played - but: (1) OSS only supports DMA on Linux and FreeBSD
 * derivatives, (2) direct access imposes the hardware's byte order
 * and sound format, and (3) the insertSamples call is due to vanish
 * in the near future.)
 */
int snd_InsertSamplesFromLeadTime(int frameCount, int srcBufPtr,
				  int samplesOfLeadTime)
{
  if (auFd == -1) return -1;
  /* The image says we're allowed to return 0 here, but the
     SoundPlayer barfs up a subscript bounds error.  Ho hum. */
#if 0
  return 0;	/* this is the CORRECT RESPONSE, but the image barfs */
#else
  {
    /* interim solution: play at leasy one buffer's worth of sound
       immediately, suspending the current sound activity.  This is
       a compromise between discarding a buffer's worth of sound
       in every cases except the very first sound to be played, and
       introducing a slight hiccup when a sound is started over the
       top of another one.  The latter is the lesser of the two
       evils. */
    int n= snd_PlaySamplesFromAtLength(frameCount, srcBufPtr, 1);
    frameCount-= n;
    if (frameCount > 0)
      n+= snd_PlaySamplesFromAtLength(frameCount, srcBufPtr, 1 + n);
    return n;
  }
#endif
}


int snd_PlaySilence(void)
{
  if (auFd == -1) return -1;
  /* nothing to do */
  return auBufBytes;
}



/*** Recording is not yet implemented.  It's not that it's hard to
     do... I'm just feeling too lazy to do it. ***/


int snd_SetRecordLevel(int level)
{
  success(false);
  return;
}


int snd_StartRecording(int desiredSamplesPerSec, int stereo, int semaIndex)
{
  success(false);
  return;
}


int snd_StopRecording(void)
{
  return;
}


double snd_GetRecordingSampleRate(void)
{
  success(false);
  return 0.0;
}


int snd_RecordSamplesIntoAtLength(int buf, int startSliceIndex,
				  int bufferSizeInBytes)
{
  success(false);
  return 0;
}


#else /*!HAS_OSS*/

/* dummy definitions for systems without OSS */


void auPollForIO(void)
{
  return;
}

#define FAIL(X) { /*success(FALSE);*/ return X; }

/* sound output */

int snd_AvailableSpace(void)
     FAIL(8192)
int snd_InsertSamplesFromLeadTime(int frameCount, int srcBufPtr,
				  int samplesOfLeadTime)
     FAIL(frameCount)
int snd_PlaySamplesFromAtLength(int frameCount, int arrayIndex, int startIndex)
     FAIL(8192)
int snd_PlaySilence(void)
     FAIL(8192)
int snd_Start(int frameCount, int samplesPerSec, int stereo, int semaIndex)
     FAIL(1)
int snd_Stop(void)
     FAIL(0)

/* sound input */

int snd_SetRecordLevel(int level)
     FAIL(0)
int snd_StartRecording(int desiredSamplesPerSec, int stereo, int semaIndex)
     FAIL(0)
int snd_StopRecording(void)
     FAIL(0)
double snd_GetRecordingSampleRate(void)
     FAIL(0)
int snd_RecordSamplesIntoAtLength(int buf, int startSliceIndex,
				  int bufferSizeInBytes)
     FAIL(0)


#endif /*!HAS_OSS*/
