/* linuxplay.c - play a sound file on the speaker
 **
 ** Copyright (C) 1995 by Markus Gutschke (gutschk@math.uni-muenster.de)
 **
 ** Parts of this code were inspired by sunplay.c, which is copyright 1989 by
 ** Jef Poskanzer and 1991,92 by Jamie Zawinski; c.f. sunplay.c for further
 ** information.
 **
 ** Permission to use, copy, modify, and distribute this software and its
 ** documentation for any purpose and without fee is hereby granted, provided
 ** that the above copyright notice appear in all copies and that both that
 ** copyright notice and this permission notice appear in supporting
 ** documentation.  This software is provided "as is" without express or
 ** implied warranty.
 **
 ** Currently, only SunAudio, Wave, and RAW file formats are actively
 ** supported; VOC file format is detected (and rejected :-( )
 */

#define HEADERSZ  48    /* has to be at least as big as the biggest header   */
#define SNDBUFSZ  2048  /* has to be at least as big as HEADERSZ             */

/* XEmacs beta testers say:  undef this by default. */
#undef NOVOLUMECTRLFORMULAW /* Changing the volume for uLaw-encoded
			       samples sounds very poor; possibly,
			       this is true only for the PC-Snd
			       driver, so undefine this symbol at your
			       discretion */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <errno.h>
#include <fcntl.h>
#include <linux/soundcard.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/fcntl.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include <sys/signal.h>
#include <unistd.h>

#ifdef LINUXPLAYSTANDALONE
#define perror(str) fprintf(stderr,"audio: %s %s\n",str,strerror(errno));
#define warn(str)   fprintf(stderr,"audio: %s\n",str);
#else
#include "lisp.h"
#define perror(str) message("audio: %s, %s ",str,strerror(errno))
#define warn(str)   message("audio: %s ",GETTEXT(str))
#endif
 
#ifdef __GNUC__
#define UNUSED(x) ((void)(x))
#else
#define UNUSED(x)
#define __inline__
#endif

static __sighandler_t sighup_handler;
static __sighandler_t sigint_handler;

static union {
  struct {
    int           align;
    enum {wvMain,wvSubchunk,wvOutOfBlock,wvSkipChunk,
	  wvSoundChunk,wvFatal,wvFatalNotify} state;
    size_t        left;
    unsigned char leftover[HEADERSZ];
    unsigned long chunklength;
  } wave;
} parsestate;

static unsigned char sndbuf[SNDBUFSZ];
static int           mix_fd    = -1;
static int           audio_vol = -1;
static int           audio_fd  = -1;
static char         *audio_dev = "";

typedef enum {fmtIllegal,fmtRaw,fmtVoc,fmtWave,fmtSunAudio} fmtType;

static void sighandler(int sig)
{
  if (audio_fd > 0) {
    ioctl(audio_fd,SNDCTL_DSP_RESET,NULL);
    close(audio_fd);
    audio_fd = -1; }
  if (mix_fd > 0) {
    if (audio_vol >= 0) {
      ioctl(mix_fd,SOUND_MIXER_WRITE_VOLUME,&audio_vol);
      audio_vol = -1; }
    close(mix_fd);
    mix_fd = -1; }
  if (sig == SIGHUP && sighup_handler)      sighup_handler(sig);
  else if (sig == SIGINT && sigint_handler) sigint_handler(sig);
  else exit(1);
}

static size_t parseraw(void **data,size_t *sz,void **outbuf)
{
  int rc = *sz;

  *outbuf = *data;
  *sz = 0;
  return(rc);
}

static size_t parsevoc(void **data,size_t *sz,void **outbuf)
{
  UNUSED(data);
  UNUSED(sz);
  UNUSED(outbuf);
  return(0);
}

static __inline__ int waverequire(void **data,size_t *sz,size_t rq)
{
  int rc = 1;

  if (rq > HEADERSZ) {
    warn("Header size exceeded while parsing WAVE file");
    parsestate.wave.state = wvFatal;
    *sz = 0;
    return(0); }
  if ((rq -= parsestate.wave.left) <= 0)
    return(rc);
  if (rq > *sz) {rq = *sz; rc = 0;}
  memcpy(parsestate.wave.leftover+parsestate.wave.left,
	 *data,rq);
  parsestate.wave.left     += rq;
  ((unsigned char *)*data) += rq;
  *sz                      -= rq;
  return(rc);
}

static __inline__ void waveremove(size_t rq)
{
  if (parsestate.wave.left <= rq)
    parsestate.wave.left = 0;
  else {
    parsestate.wave.left -= rq;
    memmove(parsestate.wave.leftover,
	    parsestate.wave.leftover+rq,
	    parsestate.wave.left); }
  return;
}

static size_t parsewave(void **data,size_t *sz,void **outbuf)
{
  for (;;)
    switch (parsestate.wave.state) {
    case wvMain:
      if (!waverequire(data,sz,20))
	return(0);
      /* Keep compatibility with Linux 68k, etc. by not relying on byte-sec  */
      parsestate.wave.chunklength = parsestate.wave.leftover[16] +
	256*(parsestate.wave.leftover[17] +
	     256*(parsestate.wave.leftover[18] +
		  256*parsestate.wave.leftover[19]));
      waveremove(20);
      parsestate.wave.state = wvSubchunk;
      break;
    case wvSubchunk:
      if (!waverequire(data,sz,parsestate.wave.chunklength))
	return(0);
      parsestate.wave.align = parsestate.wave.chunklength < 14 ? 1
	: parsestate.wave.leftover[2] * parsestate.wave.leftover[12];
      if (parsestate.wave.align != 1 &&
	  parsestate.wave.align != 2 &&
	  parsestate.wave.align != 4) {
	warn("Illegal datawidth detected while parsing WAVE file");
	parsestate.wave.state = wvFatal; }
      else
	parsestate.wave.state = wvOutOfBlock;
      waveremove(parsestate.wave.chunklength);
      break;
    case wvOutOfBlock:
      if (!waverequire(data,sz,8))
	return(0);
      /* Keep compatibility with Linux 68k, etc. by not relying on byte-sec  */
      parsestate.wave.chunklength = parsestate.wave.leftover[4] +
	256*(parsestate.wave.leftover[5] +
	     256*(parsestate.wave.leftover[6] +
		  256*parsestate.wave.leftover[7]));
      if (memcmp(parsestate.wave.leftover,"data",4))
	parsestate.wave.state = wvSkipChunk;
      else
	if (parsestate.wave.chunklength % parsestate.wave.align) {
	  warn("Datawidth does not match chunksize in WAVE file");
	  parsestate.wave.state = wvFatal; }
	else
	  parsestate.wave.state = wvSoundChunk;
      waveremove(8);
      break;
    case wvSkipChunk:
      if (*sz < parsestate.wave.chunklength) {
	parsestate.wave.chunklength -= *sz;
	*sz = 0; }
      else {
	*sz -= parsestate.wave.chunklength;
	((unsigned char *)*data) += parsestate.wave.chunklength;
	parsestate.wave.state = wvOutOfBlock; }
      break;
    case wvSoundChunk: {
      size_t count,rq;
      if (parsestate.wave.left) { /* handle leftover bytes from last
				     alignment operation */
	count = parsestate.wave.left;
	rq    = HEADERSZ-count;
	if (rq > parsestate.wave.chunklength)
	  rq = parsestate.wave.chunklength;
	if (!waverequire(data,sz,rq)) {
	  parsestate.wave.chunklength -= parsestate.wave.left - count;
	  return(0); }
	parsestate.wave.chunklength -= rq;
	*outbuf                      = parsestate.wave.leftover;
	parsestate.wave.left         = 0;
	return(rq); }
      if (*sz >= parsestate.wave.chunklength) {
	count  = parsestate.wave.chunklength;
	rq     = 0; }
      else {
	count  = *sz;
	count -= rq = count % parsestate.wave.align; }
      *outbuf                   = *data;
      ((unsigned char *)*data) += count;
      *sz                      -= count;
      if ((parsestate.wave.chunklength -= count) <= 0)
	parsestate.wave.state = wvOutOfBlock;
      else if (rq)
	/* align data length to a multiple of datasize; keep additional data
	   in "leftover" buffer --- this is neccessary to ensure proper
	   functioning of the sndcnv... routines */
	waverequire(data,sz,rq);
      return(count); }
    case wvFatalNotify:
      warn("Irrecoverable error while parsing WAVE file");
      parsestate.wave.state = wvFatal;
      break;
    case wvFatal:
    default:
      *sz = 0;
      return(0); }
}

static size_t sndcnvnop(void **data,size_t *sz,void **outbuf)
{
  int rc = *sz;

  *outbuf = *data;
  *sz = 0;
  return(rc);
}

static size_t sndcnv2mono(void **data,size_t *sz,void **outbuf)
{
  register unsigned char *src;
  register unsigned char *dest;
  int rc,count;

  count = *sz / 2;
  if (count > SNDBUFSZ) {
    *sz  -= 2*SNDBUFSZ;
    count = SNDBUFSZ; }
  else
    *sz   = 0;
  rc      = count;
  src     = *data;
  *outbuf =
    dest  = sndbuf;
  while (count--)
    *dest++ = (unsigned char)(((int)*((unsigned char *)src)++ +
			       (int)*((unsigned char *)src)++) / 2);
  *data   = src;
  return(rc);
}

static size_t sndcnv2byte(void **data,size_t *sz,void **outbuf)
{
  register unsigned char *src;
  register unsigned char *dest;
  int rc,count;

  count = *sz / 2;
  if (count > SNDBUFSZ) {
    *sz  -= 2*SNDBUFSZ;
    count = SNDBUFSZ; }
  else
    *sz   = 0;
  rc      = count;
  src     = *data;
  *outbuf =
    dest  = sndbuf;
  while (count--) {
    *dest++ = (unsigned char)(((signed char *)src)[1] + 0x80);
    ((char *)src) += 2; }
  *data   = src;
  return(rc);
}

static size_t sndcnv2monobyte(void **data,size_t *sz,void **outbuf)
{
  register unsigned char *src;
  register unsigned char *dest;
  int rc,count;

  count = *sz / 4;
  if (count > SNDBUFSZ) {
    *sz  -= 4*SNDBUFSZ;
    count = SNDBUFSZ; }
  else
    *sz   = 0;
  rc      = count;
  src     = *data;
  *outbuf =
    dest  = sndbuf;
  while (count--) {
    *dest++ = (unsigned char)(((int)((signed char *)src)[1] +
			       (int)((signed char *)src)[3]) / 2 + 0x80);
    ((char *)src) += 4; }
  *data   = src;
  return(rc);
}

static fmtType analyze_format(unsigned char *format,int *fmt,int *speed,
			      int *tracks,
			      size_t (**parsesndfile)(void **,size_t *sz,
						      void **))
{
  /* Keep compatibility with Linux 68k, etc. by not relying on byte-sec  */
  if (!memcmp(format,"Creative Voice File\0x1A\0x1A\x00",22) &&
      (format[22]+256*format[23]+0x1233)&0xFFFF ==
      (format[24]+256*format[25])) { /* VOC */
    *fmt          = AFMT_U8;
    *speed        = 8000;
    *tracks       = 2;
    *parsesndfile = parsevoc;
    return(fmtVoc); }
  else if (!memcmp(format,"RIFF",4) &&
	   !memcmp(format+8,"WAVEfmt ",8)) { /* WAVE */
    if (memcmp(format+20,"\001\000\001"/* PCM mono */,4) &&
	memcmp(format+20,"\001\000\002"/* PCM stereo */,4))
      return(fmtIllegal);
    *fmt          = format[32] == 1 ? AFMT_U8 : AFMT_S16_LE;
    *tracks       = format[22];
    /* Keep compatibility with Linux 68k, etc. by not relying on byte-sec  */
    *speed        = format[24]+256*(format[25]+256*
				    (format[26]+256*format[27]));
    *parsesndfile = parsewave;
    return(fmtWave); }
  else if (!memcmp(format,".snd",4)) { /* Sun Audio */
    *fmt          = AFMT_MU_LAW;
    *speed        = 8000;
    *tracks       = 1;
    *parsesndfile = parseraw;
    return(fmtSunAudio); }
  else {
    *fmt          = AFMT_U8;
    *speed        = 8000;
    *tracks       = 1;
    *parsesndfile = parseraw;
    return(fmtRaw); }
}

static int audio_init(int mix_fd,int audio_fd,int fmt,int speed,
		      int tracks,int *volume,
		      size_t (**sndcnv)(void **,size_t *sz,void **))
{
  int i;

  *sndcnv = sndcnvnop;

  if (ioctl(audio_fd,SNDCTL_DSP_SYNC,NULL) < 0) {
    perror("SNDCTL_DSP_SYNC");
    return(0); }

  if (((i=fmt),ioctl(audio_fd,SNDCTL_DSP_SETFMT,&i)) < 0 ||
      fmt != i) {
    if (fmt == AFMT_S16_LE) {
      *sndcnv = sndcnv2mono;
      if (((i=fmt=AFMT_U8),ioctl(audio_fd,SNDCTL_DSP_SETFMT,&i)) < 0 ||
	  fmt != i) {
	perror("SNDCTL_DSP_SETFMT");
	return(0); } }
    else {
      perror("SNDCTL_DSP_SETFMT");
      return(0); } }

  if (((i=tracks-1),ioctl(audio_fd,SNDCTL_DSP_STEREO,&i)) < 0 ||
      tracks-1 != i) {
    if (tracks == 2) {
      *sndcnv = *sndcnv == sndcnv2mono ? sndcnv2monobyte : sndcnv2byte;
      if (((i = 0),ioctl(audio_fd,SNDCTL_DSP_STEREO,&i)) < 0 || i) {
	perror("SNDCTL_DSP_STEREO");
	return(0); } }
    else {
      perror("SNDCTL_DSP_STEREO");
      return(0); } }

  if (((i=speed),ioctl(audio_fd,SNDCTL_DSP_SPEED,&i)) < 0 ||
      speed*11 < i*10 || speed*9 > i*10) {
    char buffer[256];
    sprintf(buffer,"SNDCTL_DSP_SPEED (req: %d, rtn: %d)",speed,i);
    perror(buffer);
    return(0); }

  if (mix_fd > 0) {
    int vol = *volume & 0xFF;
    if (ioctl(mix_fd,SOUND_MIXER_READ_VOLUME,volume) < 0)
      *volume = -1;
    if (vol < 0) vol = 0; else if (vol > 100) vol = 100;
#ifdef NOVOLUMECTRLFORMULAW
    if (fmt == AFMT_MU_LAW)
      vol = 100;
#endif
    /* vol  = (127*vol)/100; */ /* not using full volume sounds a lot better */
    vol |= 256*vol;
    /* Do not signal an error, if volume control is unavailable! */
    ioctl(mix_fd,SOUND_MIXER_WRITE_VOLUME,&vol); }
  return(1);
}

static void linux_play_data_or_file(int fd,unsigned char *data,
				    int length,int volume)
{
  size_t         (*parsesndfile)(void **data,size_t *sz,void **outbuf);
  size_t         (*sndcnv)(void **data,size_t *sz,void **);
  fmtType        ffmt;
  int            fmt,speed,tracks;
  unsigned char *pptr,*optr,*cptr,*sptr;
  int            wrtn,rrtn,crtn,prtn;

  if (!data || length < HEADERSZ)
    if (fd < 0) return;
    else {
      length = read(fd,sndbuf,SNDBUFSZ);
      if (length < HEADERSZ)
	return;
      data   = sndbuf;
      length = SNDBUFSZ; }

  ffmt = analyze_format(data,&fmt,&speed,&tracks,&parsesndfile);

  if (ffmt != fmtRaw && ffmt != fmtSunAudio && ffmt != fmtWave) {
    warn("Unsupported file format (neither RAW, nor SunAudio, nor WAVE)");
      return; }

  mix_fd = open("/dev/mixer",(O_WRONLY|O_NDELAY),0);

  if ((audio_fd = open((audio_dev = ffmt==fmtSunAudio?"/dev/audio":"/dev/dsp"),
		       (O_WRONLY|O_NDELAY),0)) < 0) {
    perror(audio_dev);
    if (mix_fd > 0) { close(mix_fd); mix_fd = -1; }
    return; }

  sighup_handler = signal(SIGHUP,(__sighandler_t)sighandler);
  sigint_handler = signal(SIGINT,(__sighandler_t)sighandler);

  if (!audio_init(mix_fd,audio_fd,fmt,speed,tracks,&volume,&sndcnv))
    goto END_OF_PLAY;
  audio_vol = volume;

  memset(&parsestate,0,sizeof(parsestate));

  rrtn = length;
  do {
    for (pptr = data; (prtn = parsesndfile((void **)&pptr,&rrtn,
					   (void **)&optr)) > 0; )
      for (cptr = optr; (crtn = sndcnv((void **)&cptr,&prtn,
				       (void **)&sptr)) > 0; ) {
	for (;;) {
	  if ((wrtn = write(audio_fd,sptr,crtn)) < 0) {
	    perror("write"); goto END_OF_PLAY; }
	  else if (wrtn) break;
	  else if (ioctl(audio_fd,SNDCTL_DSP_SYNC,NULL) < 0) {
	    perror("SNDCTL_DSP_SYNC"); goto END_OF_PLAY; } }
	if (wrtn != crtn) {
	  char buf[255];
	  sprintf(buf,"play: crtn = %d, wrtn = %d",crtn,wrtn);
	  warn(buf);
	  goto END_OF_PLAY; } }
    if (fd >= 0) {
      if ((rrtn = read(fd,sndbuf,SNDBUFSZ)) < 0) {
	perror("read"); goto END_OF_PLAY; } }
    else
      break;
  } while (rrtn > 0);

  if (ffmt == fmtWave && parsestate.wave.state != wvOutOfBlock &&
      parsestate.wave.state != wvFatal)
    warn("Unexpected end of WAVE file");

  ioctl(audio_fd,SNDCTL_DSP_SYNC,NULL);
END_OF_PLAY:
  ioctl(audio_fd,SNDCTL_DSP_RESET,NULL);

  signal(SIGHUP,sighup_handler);
  signal(SIGINT,sigint_handler);

  close(audio_fd);
  audio_fd = -1;

  if (mix_fd > 0) {
    if (audio_vol >= 0) {
      ioctl(mix_fd,SOUND_MIXER_WRITE_VOLUME,&audio_vol);
      audio_vol = -1; }
    close(mix_fd); mix_fd = -1; }

  return;
}

void play_sound_file (char *sound_file, int volume)
{
  int fd;

  if ((fd=open(sound_file,O_RDONLY,0)) < 0) {
    perror(sound_file);
    return; }
  linux_play_data_or_file(fd,NULL,0,volume);
  close(fd);
  return;
}

void play_sound_data (unsigned char *data, int length, int volume)
{
  linux_play_data_or_file(-1,data,length,volume);
  return;
}
