/*
 * fifo.c  -  a buffering program intended primarily for audio CD playback
 *
 * compile with:  gcc -o fifo fifo.c
 *
 * run with argument -h or --help to read usage message
 */

#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <limits.h>
#include <fcntl.h>
#include <errno.h>

static fd_set rset, wset;
static struct timeval tout;

void waitinput();
void waitoutput();
void waitio();

int main(int argc, char **argv)
{
  char *unitstr, *buf, *top, *readptr, *writeptr;
  unsigned long bufsize, chunksize, unit, space, avail;
  long xferred;
  int eoi;

  if( argc > 3 || (argc==2 && !isdigit(*argv[1])) 
       || (argc==3 && !isdigit(*argv[2])) ) {
    fprintf(stderr, "usage: <data source> | fifo [<buffer size>[k|M|G|s] [<chunk size>[k|M|G|s]]] | <data sink>\n"
	"For example: cdparanoia 1- - | fifo | aplay -\n"
"fifo is a FIFO application intended primarily for audio CD playback.  It reads\n"
"data from its standard input and outputs it unchanged to standard output.  On\n"
"program startup, its buffer is filled completely before outputting the first\n"
"byte of data, to allow the CD ROM drive to spin up.  Then output and input are\n"
"performed in turn, with each output restricted to the chunk size to prevent the\n"
"drive from spinning down intermediately.  The buffer and chunk size can be\n"
"given in bytes, kiB, MiB, GiB or seconds of CD-quality audio (16 bit stereo at\n"
"44.1 kHz).  The default buffer size is 1 MiB, the default chunk size is half\n"
"the buffer size.\n");
    return -1;
  }
  if( argc == 2 ) {
    bufsize= strtoul(argv[1], &unitstr, 0);
    if( *unitstr=='k' || *unitstr=='K' )
      unit= 1024;
    else if( *unitstr=='m' || *unitstr=='M' )
      unit= 1024*1024;
    else if( *unitstr=='g' || *unitstr=='G' )
      unit= 1024*1024*1024;
    else if( *unitstr=='s' || *unitstr=='S' )
      unit= 4*44100;	// 16 bit stereo at 44.1 kHz
    else
      unit= 1;
    if( bufsize > ULONG_MAX / unit ) {
      fprintf(stderr, "fifo: Buffer size exceeds maximum of %lu bytes.  Setting to maximum.\n", ULONG_MAX );
      bufsize= ULONG_MAX;
    }
    else
      bufsize *= unit;
  }
  else
    bufsize= 1024*1024;
  if( argc == 3 ) {
    chunksize= strtoul(argv[2], &unitstr, 0);
    if( *unitstr=='k' || *unitstr=='K' )
      unit= 1024;
    else if( *unitstr=='m' || *unitstr=='M' )
      unit= 1024*1024;
    else if( *unitstr=='g' || *unitstr=='G' )
      unit= 1024*1024*1024;
    else if( *unitstr=='s' || *unitstr=='S' )
      unit= 4*44100;	// 16 bit stereo at 44.1 kHz
    else
      unit= 1;
    if( chunksize > bufsize ) {
      fprintf(stderr, "fifo: Chunk size exceeds buffer size (%lu bytes).  Setting to buffer size.\n", ULONG_MAX );
      chunksize= bufsize;
    }
    else
      chunksize *= unit;
  }
  else
    chunksize= bufsize/2;
  buf= malloc(bufsize);
  if( !buf ) {
    fprintf(stderr, "fifo: Could not allocate buffer (size %lu bytes).\n", bufsize);
    return -1;
  }
  readptr= writeptr= buf;
  top= buf+bufsize;
  FD_ZERO(&rset);
  FD_ZERO(&wset);
  eoi= 0;
  // first fill the ring buffer
  while( !eoi && writeptr< top-1 ) {
    waitinput();
    xferred= read(0, writeptr, top-writeptr-1);
    if( xferred > 0 )
      writeptr += xferred;
    else
      eoi= 1;		// end of input
  }
  while( 13 )
  {
    space= readptr > writeptr? readptr-1-writeptr : readptr-1+bufsize-writeptr;
    avail= readptr > writeptr? writeptr+bufsize-readptr : writeptr-readptr;
    if( space && !eoi )
      if( avail )
	waitio();
      else
	waitinput();
    else
      if( avail )
	waitoutput();
      else
	break;	// all data transferred
    if( readptr > writeptr )
      avail= top-readptr;	// restrict to contiguous region
    // downstream reads take precedence now (a FIFO read is a write() operation
    // from the FIFO's point of view)
    if( FD_ISSET(1, &wset) ) {
      xferred= write(1, readptr, avail>chunksize? chunksize : avail);
      if( xferred > 0 ) {
	readptr += xferred;
	if( readptr==top )
	  readptr= buf;
	space= readptr > writeptr? readptr-1-writeptr : top-writeptr;
      }
      else
	break;	// downstream disconnect or error
    }
    if( readptr==writeptr )
      readptr= writeptr= buf;	// maximise contiguous space
    if( writeptr >= readptr )
      space= readptr==buf? top-1-writeptr : top-writeptr;	// restrict to contiguous region
    // now for writes to the FIFO, ie reads from upstream
    if( FD_ISSET(0, &rset) ) {
      xferred= read(0, writeptr, space);
      if( xferred > 0 ) {
	writeptr += xferred;
	if( writeptr==top )
	  writeptr= buf;
      }
      else
	eoi= 1;		// end of input
    }
  }
  free(buf);
  return 0;
}


void waitinput()
{
  int result;

  FD_SET(0, &rset);
  result= select(1, &rset, NULL, NULL, NULL);
  if( result==-1 ) {
    perror("fifo: select()");
    exit(-1);
  }
  FD_CLR(1, &wset);	// to simplify main program checks
  return;
}


void waitoutput()
{
  int result;

  FD_SET(1, &wset);
  result= select(2, NULL, &wset, NULL, NULL);
  if( result==-1 ) {
    perror("fifo: select()");
    exit(-1);
  }
  FD_CLR(0, &rset);	// to simplify main program checks
  return;
}


void waitio()
{
  int result;

  FD_SET(0, &rset);
  FD_SET(1, &wset);
  result= select(2, &rset, &wset, NULL, NULL);
  if( result==-1 ) {
    perror("fifo: select()");
    exit(-1);
  }
  return;
}



