/*
 *  PCM-Player fuer 16bit signed Dateien auf Ice1712 Pro mit ALSA/Linux/x86
 *  Aufruf [Programm] [Dateiname]
 *
 *  (Compilieren fuer Mono oder Stereo und auf eine feste Samplingrate zw.
 *   8000 und 48000Hz, hat bisher keine eigene Erkennung fuer WAV-Header!)
 *  Benoetigte Library: asound
 *
 *  Frei kopierbarer  haesslicher  "proof of concept"  Code  ohne jegliche
 *  Garantien, nicht mal die, die oben versprochene Funktion zu erfuellen.
 *
 *  Geschrieben von Eric Auer, Tipps von Dan Hollis und Christian Dressler
 *  Achtung: Der ALSA Treiber fuer diese Karte erlaubt nur,  ALLE  Kanaele
 *  gleichzeitig zu oeffnen, daher funktioniert dafuer auch keine OSS-Emu.
 *  Fuer Ausgabe sind das exakt 10, fuer Eingabe exakt 12 Monokanaele.
 *
 *  Ausserdem ist 32bit signed little endian das einzig moegliche Format,
 *  wobei nur die  24 MSB  vom Wandler verarbeitet werden.  Das  ALSA PCM 
 *  Plugin System bietet aber bei Bedarf Formatanpassungen per Software.
 *
 *  22.05.2000
 */
 
#define FORCE_REGULAR 0
// 0 = auch Pipes etc. erlaubt
// #define snd_inSTEREO
/* Wenn define, wird die Datei von Stereo auf Mono runtergemischt */
#define snd_HZ 22050 /* z.B. 22050 */ 
/* Ice1712pro/Linux kann nur S32_LE, aber 8-48kHz Samplingrate... */

#define iCARD signed int
#define iFILE signed short int
#define nCHN 10

/* ***************************************************************** */

#include <sys/asoundlib.h>
#include <linux/asound.h>
// #include <linux/asoundid.h>   // Kartentypen
// #include <linux/asequencer.h> // Sequencer
#include <stdio.h>
#include <string.h> // strncpy
#include <sys/types.h>
#include <errno.h> // ...
#include <fcntl.h> // open
#include <sys/stat.h> // stat
#include <stdlib.h> // malloc
#include <unistd.h> // usleep
#include <sys/uio.h> // iovec

/* ***************************************************************** */

// #define DEBUG
#define USLEEP
#define WBUF 4096 /* WriteBufferSize in Samples */

#define snd_inBITS (8*sizeof(iFILE))
/* *** Achtung, zur Zeit werden nur 16 Bit signed verwendet!!! *** */
#define snd_SHL (31-snd_inBITS) /* 24 MSB of 32 used bei ice1712 */
// Ich habe ein MSB weggelassen, besser analog auf volles Volume bringen...
// 386: __BYTE_ORDER == __LITTLE_ENDIAN

// ice1712pro kann nur genau 10 voices playback und 12 voices capture
// ausserdem geht im stream mode nur write/interleaved richtig!?

/* ***************************************************************** */

void help(void) { printf("Usage: Give me a filename and I will play it,\n"
             "assuming the data is signed %dbit "
#ifndef snd_inSTEREO
             "monaural"
#else
             "stereo"
#endif
             " PCM %dHz.\n",
             snd_inBITS,snd_HZ); 
 _exit(1); return; };

/* auf files gibts z.B. getc() und feof() und int getw()
 * coz EOF is a valid int, ferror() should be used to detect getw() errors.
 * interessant bei sun: man -s 5 intro -> Tipps fuer Manpages aus Sektion 5,
 * z.B. regex, math, pam_unix, socket, signal/siginfo, ... flockfile fread
 */

/* ***************************************************************** */

int file_size(char * filenam, int force_regular)
{
struct stat status;
int fd,fsize;
fd = open(filenam,O_RDONLY /* | 0400000 */ ); // 04... ist O_NOFOLLOW
if (!fd) { perror("could not open sound file"); return 0; };
if (fstat(fd,&status)) 
   { perror("could not stat file");
     close(fd);
     return 0; 
   };
close(fd);
fsize = status.st_size / sizeof(iFILE);
#ifdef snd_inSTEREO
fsize >>= 1;
#endif
if (force_regular)
   {
     if (!S_ISREG(status.st_mode)) 
        { fprintf(stderr,"not a regular file\n"); 
          return 0; };
   } else
   {
     if (!S_ISREG(status.st_mode)) 
        { fprintf(stderr,"not a regular file, assuming pipe\n"); 
          return 12*3600*snd_HZ;
        };
   };
return (fsize>0) ? fsize : 0;
};

/* ***************************************************************** */

int open_sound(snd_pcm_t ** handle /* ptr auf ptr */)
{
int retval;
snd_pcm_channel_params_t params;

if ((retval = snd_pcm_open(handle, 0 /* card */, 0 /* device */, 
                           SND_PCM_OPEN_PLAYBACK)))
   { fprintf(stderr,"Error opening sound "); perror(snd_strerror(retval));
     return 1; };
   // auch mgl.: snd_pcm_open_subdevice(h,c,d, s ,m) ...
   // mode: | SND_PCM_OPEN_NONBLOCK ist mgl.

params.channel=0; 
params.format.format=SND_PCM_SFMT_S32_LE;
   // je nach iCARD !
   // Ice1712pro/Linux does only this, and without the use of plugins,
   // ALSA offers no conversion.
   // btw. the hardware only uses the 24 MSBs.
params.format.interleave = 1;
   // interleaved vs. blocked data format
   // for vector based i/o, interleave sucks.
params.format.rate = snd_HZ;
params.format.voices = nCHN;
   // Ice1712pro/Linux driver supports only opening all voices at once!
   // for capture, voices *must* be 12, for playback, it *must* be 10.
params.channel = SND_PCM_CHANNEL_PLAYBACK; // or ... CAPTURE
params.mode = SND_PCM_MODE_STREAM; // or ... BLOCK
  // not set: params.digital.*, params.format.reserved, 
  // params.reserved, params.format.special
params.start_mode = SND_PCM_START_DATA; // or ... FULL or ... GO
params.stop_mode = SND_PCM_STOP_STOP;
  // ... STOP or ... ERASE or ... ROLLOVER
  // STOP klappt, aber ROLLOVER muss nach einem Buffer
  // overrun/underrun nicht neu angeworfen werden.
params.time = 0; // set to get gettimeofday time of beginning of transfer
                 // -> sys/time.h ... struct timeval -> tv_sec ...
params.ust_time = 0; // set to get time in UST format
  
   params.buf.stream.queue_size = (snd_HZ >> 1) * sizeof(iCARD) * nCHN;
                 // muss ein Vielfaches von ... sein, min 1/2k...
                 // 1/16 Sekunde mit (snd_HZ >> 4) * ... funktioniert z.B. ok.
                 // Groessere Queue -> mehr Latency aber weniger kritisches
                 // Timing bei Multitasking. Also sinnvoll einstellen!
   params.buf.stream.fill = SND_PCM_FILL_SILENCE_WHOLE; 
                 // or ... NONE or ... SILENCE
   params.buf.stream.max_fill = sizeof(iCARD) * nCHN * 10;
                 // not really used if fill!=...SILENCE
/* 
 * params.buf.block.frag_size=4096 * nCHN;
 * params.buf.block.frags_min=1;
 * params.buf.block.frags_max=10;
 */
if ((retval = snd_pcm_channel_params(*handle,&params)))
   { fprintf(stderr,"Format not supported by soundcard: "); 
     fprintf(stderr,"%s\n",snd_strerror(retval)); return 1; };

if ((retval = snd_pcm_playback_prepare(*handle)))
   { fprintf(stderr,"Error preparing playback: "); 
     fprintf(stderr,"%s\n",snd_strerror(retval)); return 1; };

// je nach START setting fehlt nun noch snd_pcm_playback_go(),
// ansonsten passiert der START automatisch.
return 0;
};

/* ***************************************************************** */

int write_sound(snd_pcm_t * handle, FILE * pcmdata, int samples)
{
int left, written;
int cnt, cnt2, retval;
int towrite, toread;
// struct iovec bufv[nCHN];
iCARD mybuf[1+(WBUF*nCHN)];
iFILE filebuf[1+(WBUF*2)]; // *2 wg snd_inSTEREO
iCARD * myrest;
int restsize;
int underrun=0;

if (samples <= 0) return 1;
left = samples;
written = 0; 
#ifdef DEBUG
fprintf(stderr,"About to play %d samples\n",samples);
#endif

while (left>0)
  { 
    toread = (left>WBUF) ? WBUF : left;
    towrite = fread(&filebuf,
#ifdef snd_inSTEREO
                             2 *
#endif
                             sizeof(filebuf[0]), toread, pcmdata);
    if (ferror(pcmdata)) { perror("fread PCM file failed"); return 1; };
    if (towrite<toread)
    { if (feof(pcmdata)) 
         { if (left<WBUF) 
              { left=0; towrite=0; 
                fprintf(stderr,"EOF reached (PCM file)\n");
              };
           clearerr(pcmdata);
#ifdef DEBUG
           fprintf(stderr,"EOF ");
#endif
         } else
         { perror("Short read but no EOF?"); return 1; 
         };
      towrite = (towrite<0) ? 0 : towrite;
      underrun = 1;
    };
    clearerr(pcmdata);
    
    if (towrite)
    for (cnt=cnt2=0;cnt<towrite;cnt++)
      { 
        cnt2 = cnt * nCHN;
#ifndef snd_inSTEREO
        mybuf[cnt2] = filebuf[cnt] << snd_SHL; 
#else
        mybuf[cnt2] = (filebuf[cnt+cnt] + filebuf[cnt+cnt+1]) << (snd_SHL-1);
//      mybuf[cnt2] = filebuf[cnt+cnt] << snd_SHL; 
                     // pick one side instead of mixing
#endif
        for (cnt2++ ; cnt2<((cnt+1)*nCHN) ; cnt2++) mybuf[cnt2]=0;
      };
    // eingelesen...

    // ...ausgeben
    if (underrun && towrite)
    {
      underrun = 0;
      retval = snd_pcm_playback_go(handle);
#ifdef DEBUG
      fprintf(stderr,"snd_pcm_playback_go after underrun returned %s\n",
              snd_strerror(retval));
#endif
    };
#ifdef DEBUG
      fprintf(stderr,"Underrun state\n");
#endif
    myrest = mybuf /* &mybuf[0] */;
    restsize = towrite;
    while (restsize>0)
    {
      written = snd_pcm_write(handle, myrest, restsize*sizeof(iCARD)*nCHN );
      // snd_pcm_writev(handle, bufv, nCHN);

      if ((written<0) && (written != -EAGAIN)) 
         { fprintf(stderr,"Error with snd_pcm_write, trying to recover: %s\n",
                   snd_strerror(written));
#ifdef USLEEP
           usleep(1000);
#endif
//           (void)snd_pcm_playback_drain(handle);
//           (void)snd_pcm_playback_flush(handle);
//           retval = snd_pcm_playback_go(handle);
//           fprintf(stderr,"Tried to flush and restart playback / %s\n",
//                   snd_strerror(retval));

             if ((retval = snd_pcm_close(handle)))
                { fprintf(stderr,"Error closing sound for re-open: %s\n",
                          snd_strerror(retval)); 
                  return 1;
                };
             if ((retval = open_sound(&handle))) 
                { fprintf(stderr,"Error re-opening sound: %s\n",
                          snd_strerror(retval)); 
                  return 1; 
                };
         };
      written = (written>0) ? (written / (sizeof(iCARD) * nCHN)) : 0;
      left -= written;
#ifdef DEBUG
      if (written!=restsize) 
         { fprintf(stderr,"Only %d of %d (rest of %d) samples accepted\n", 
           written,restsize,towrite);
         } else fprintf(stderr,"%d samples written\n",written);
#endif
      restsize -= written;
      myrest += written * nCHN;
#ifdef USLEEP
      if (left>0) usleep(1000);
#endif
    }; // Ende while... write
#ifdef USLEEP
    if (!towrite) usleep(1000);
#endif
  }; // Ende while... read
return 0;
};

/* ***************************************************************** */

int main(int argc, char ** argv)
{
FILE * pcmdata;
int retval, fsize;
snd_pcm_t * soundhandle=NULL;

if ((argc!=2) || (!argv)) help(); if (!argv[1]) help();

if (! (fsize = file_size(argv[1],FORCE_REGULAR)))
   { fprintf(stderr,"%s:",argv[1]); perror("Error checking file"); return 1; };
if (! (pcmdata = fopen(argv[1],"r"))) 
   { fprintf(stderr,"%s:",argv[1]); perror("Error opening file"); return 1; };

if (open_sound(&soundhandle))
   { fprintf(stderr,"Error opening ALSA"); return 1; };

if (write_sound(soundhandle, pcmdata, fsize-1)) /* fsize hat Einheit Samples */
   { fprintf(stderr, "Error writing to PCM\n"); };
   // but hang on...
   
if ((retval = snd_pcm_close(soundhandle))) 
   { fprintf(stderr,"Error closing sound: "); 
     fprintf(stderr,"%s\n",snd_strerror(retval)); };
   // hang on if an error occured, do not return 1.

if (fclose(pcmdata)) 
   { perror("Could not close file (!)"); return 1; };

return 0;
};
