/* sod2, a player for polychannel .csf music files.
 * Copyright (C) 1995 Russell Marks.
 *
 * Based on `sodman' by Graham Richards.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *
 * sod2.c - the main player code.
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <math.h>
#ifdef linux
/* #include <sys/ioctl.h>     /* didn't exist in older kernels */
#include <sys/soundcard.h>
#endif
#ifdef _ALLOW_BUFFERING
#include <signal.h>
#endif
#include "sod2.h"
#include "grokfile.h"


#define SOD2_VER	"1.0"

/* remove 'inline' if the compiler doesn't like it */
#ifndef _ALLOW_INLINE
#define inline
#endif

/* djgpp doesn't appear to have optopt yet...? */
#ifdef MSDOS
int optopt='?';
#endif

/* global data */

int bsize=64;			/* pattern length being used */
int tempo=125;
int last_pattern,last_line;
int slotlen;			/* length of a notespace in samples */

char outfile[256];
FILE *out;

/* output environment */
/* output is generated internally at 'vsr', but output at 'psr'. */

int sample_maxval=255;		/* defaults to 8-bit output */
int sample_midval=128;		/* 'middle' value - approx half of above */
int widemag=256;		/* multiplier for 8-bit -> 16-bit */
int vsr=8000;			/* virtual (subsampled) sample rate */
int psr=8000;			/* physical (actual) sample rate */

/* flags */

int oversample=0;		/* if 1, do virtual->physical avg mapping */
int undersample=0;		/* if 1, add avgs between real samples */
#ifdef linux
int ioctlfile=1;
#else
int ioctlfile=0;
#endif
int clickdetect=0;		/* moan about possible clicks */
int buffer=0;			/* invoke buffer process */
int startline=1;

int verbose=0;


int next_sample=0;
int sampletick=0,oldstick=0;
struct timeval oldtv,newtv;
int minhz=(1<<30);
int oldval;
int maxchan;

struct sample samples[MAX_SAMPLES];	/* ptrs to 8-bit unsigned samples */

struct pattern patterns[MAX_PATTERNS];

/* entries in line[x][] are indicies into patterns[]. */
int lines[MAX_LINES][MAX_LINE_DEPTH];

/* active notes array - an entry is NULL if unused, otherwise it points
 * to a struct note in a struct pattern somewhere.
 */
struct note *active[MAX_NOTES];
int last_active;

#ifdef _ALLOW_BUFFERING
unsigned char *buf_mem,*buf_head,*buf_tail;
struct itimerval itvslow,itvfast;
int out_fd;
int buf_in=0,buf_out=0;
int last_report=0;
int bufused=0;
int playdone=0;
#endif



/* function prototypes */
int main(int argc,char **argv);
void parseoptions(int argc,char **argv);
void usage_help(void);
void clear_data(void);
void runtime_fixup(void);
void play(void);
void addnotes(int line,int noteslot);
static inline int gensample(int note);
void output_sample(int count);
#ifdef linux
void ioctl_dsp(int fd);
#endif
#ifdef _ALLOW_BUFFERING
void init_buffer(void);
void kill_buffer(void);
void write_from_buffer(int n);
int write_to_buffer(int c,FILE *out);
#endif

int (*write_sample)(int c,FILE *out);


/* code starts here */

int main(int argc,char **argv)
{
#ifdef linux
strcpy(outfile,"/dev/dsp");
#else
strcpy(outfile,"output");
#endif

write_sample=fputc;		/* default to using stdio sample write */

parseoptions(argc,argv);

/* open output file */
if(strcmp(outfile,"-")==0)
  out=stdout;
else
  if((out=fopen(outfile,"wb"))==NULL)
    {
    fprintf(stderr,"Couldn't open output file '%s'.\n",outfile);
    exit(1);
    }

if(undersample) psr*=2;

#ifdef linux
if(ioctlfile) ioctl_dsp(fileno(out));
#endif

vsr=psr;
if(oversample) vsr*=2;
if(undersample) vsr/=2;

clear_data();			/* zero out arrays and general init */
grokfile(argv[optind]);		/* read in file */

runtime_fixup();	/* fix runtime parts of arrays ready for playing */

#ifdef _ALLOW_BUFFERING
if(buffer) init_buffer();
#endif

play();			/* play the music */

#ifdef _ALLOW_BUFFERING
if(buffer)
  {
  playdone=1;
  while(bufused>0) usleep(50000);
  kill_buffer();
  }
#endif

if(strcmp(outfile,"-")!=0)
  fclose(out);

exit(0);
}


void parseoptions(int argc,char **argv)
{
int done=0,tmp,outchng=0;

done=0;
opterr=0;

do
  switch(getopt(argc,argv,"bchl:o:Os:Uvw"))
    {
    case 'b':	/* use buffer process */
#ifdef _ALLOW_BUFFERING
      buffer=1;
#else
      fprintf(stderr,"Buffering was not enabled at compilation.\n");
      exit(1);
#endif
      break;
    case 'c':	/* click detect */
      clickdetect=1;
      break;
    case 'h':
      usage_help();
      exit(1);
    case 'l':	/* start line number */
      startline=atoi(optarg);
      if(startline==0)
        fprintf(stderr,"Strange start line, ignoring\n");
      break;
    case 'o':	/* output file */
      strcpy(outfile,optarg);
      outchng=1;
      ioctlfile=0;
      break;
    case 'O':	/* oversample */
      oversample=1;
      break;
    case 'U':	/* undersample */
      undersample=1;
      break;
    case 'v':	/* verbose, various extra messages */
      verbose=1;
      break;
    case 's':	/* sample rate */
      tmp=atoi(optarg);
      if(tmp==0)
        fprintf(stderr,"Strange sample rate, ignoring\n");
      else
        psr=tmp;
      break;
    case 'w':	/* wide - 16-bit */
      sample_maxval=65535;
      sample_midval=32768;
      break;
    case '?':
      switch(optopt)
        {
        case 'l':
          fprintf(stderr,"The -l option needs a starting line.\n");
          break;
        case 'o':
          fprintf(stderr,"The -o option takes a filename as an argument.\n");
          break;
        case 's':
          fprintf(stderr,"The -s option takes the output sample rate ");
          fprintf(stderr,"as an argument.\n");
          break;
        default:
          fprintf(stderr,"Option '%c' not recognised.\n",optopt);
        }
      exit(1);
    case -1:
      done=1;
    }
while(!done);

if(optind!=argc-1)	/* if no filename given */
  {
  usage_help();
  exit(1);
  }

if(undersample && oversample)
  {
  fprintf(stderr,"Oversampling and undersampling is stoopid, you eeediot!\n");
  exit(1);
  }

if(outchng) buffer=0;	/* don't ever use buffering if output file changed */
}


void usage_help()
{
printf("Sod2 v%s - (c) 1995 Russell Marks for improbabledesigns.\n",SOD2_VER);
printf(
"
usage: sod2 [-bchOUvw] [-l start_line] [-o output_file] [-s sample_rate]
		[-W mult] [filename.csf | filename.tar]

	-b	use a buffer process.
	-c	try to detect clicks - when a note stops playing and the
        	last sample was more than 10%% away from zero, it is
                reported with 'possible click from note on pattern N'.
                You can usually remove the click by using a tighter envelope.
                Note that this will not necessarily detect all clicks.
	-h	this usage help.
        -o	specify output file for music sample.
        -O	do 2x oversampling - note that this takes as much CPU time
        	as it would to play at *double* the output sample rate!
        -s	specify sample rate to output at.
        -U	do 2x undersampling - play at twice the specified sample
        	rate, and output an average value between every pair of
                output values.
        -v	verbose messages.
        -w	give 'wide' output, i.e. output a 16-bit sample.
        -W	set multiplier used to turn 8-bit -> 16-bit. Usually 256,
        	you may want to set this to smaller values to avoid
                output value clipping for files where the combined output
                is too loud. Not recommended generally, but can be useful.
                This flag is (obviously) ignored in normal (8-bit) mode.

With tar files, the *first* file in the archive must be the csf file,
and all samples used by the csf file must be in the tar archive.
");
}


void clear_data()
{
int f,g;

for(f=0;f<MAX_NOTES;f++)	active[f]=NULL;
for(f=0;f<MAX_SAMPLES;f++)	samples[f].data=NULL;
for(f=0;f<MAX_PATTERNS;f++)	patterns[f].mode=-1;
for(f=0;f<MAX_LINES;f++)
  for(g=0;g<MAX_LINE_DEPTH;g++)
    lines[f][g]=0;
}


void runtime_fixup()
{
int f,g,rate;

/* the general idea here is to put in all the timing information
 * in the note structs.
 */

slotlen=15*vsr/tempo;

for(f=1;f<=last_pattern;f++) if(patterns[f].mode!=-1)
  for(g=0;g<bsize;g++) if(patterns[f].notes[g].vol!=0)
    {
    rate=samples[patterns[f].notes[g].sample].rate;

    if(sample_maxval==65535)		/* if 16-bit */
      patterns[f].notes[g].vol*=widemag;
    
    /* notenum -> next-sample gap */
    /* time to wheel out the magic numbers */
    patterns[f].notes[g].gap=FIX_DBL(((double)rate*
    	pow(1.059463,(double)patterns[f].notes[g].notenum)/(double)vsr));

    /* fix enveloping stuff */
    patterns[f].notes[g].sustain*=slotlen/8;
    patterns[f].notes[g].release*=slotlen/8;
    patterns[f].notes[g].relsus=patterns[f].notes[g].release;
    patterns[f].notes[g].release+=patterns[f].notes[g].sustain;

    patterns[f].notes[g].data=samples[patterns[f].notes[g].sample].data;
    patterns[f].notes[g].len=FIX_UP(samples[patterns[f].notes[g].sample].len);
    }
}


/* finally, play the music.
 *
 * I originally had play(), addnotes(), gensample() and output_sample()
 * as one big function, but it became a bit confusing to work on.
 *
 * Since gensample() is a stunningly major hotspot (tens of millions of
 * calls are likely), you should compile with -finline-functions (assuming
 * gcc is being used).
 */
void play()
{
int line,notesp,noteslot,count,note;
int done=0;

last_active=-1;
oldval=sample_midval;
maxchan=0;

if(verbose)
  {
  gettimeofday(&oldtv,NULL);
  oldstick=sampletick;
  }

if(startline>last_line || startline<0) startline=last_line;
  
for(line=startline;line<=last_line+1 && done==0;line++)
  {
  for(noteslot=0;noteslot<bsize && done==0;noteslot++)
    {
    addnotes(line,noteslot);

    /* generate output for the duration of this notespace */
    /* this also kills off any notes which end during that time */
    for(notesp=0;notesp<slotlen;notesp++)
      {
      for(note=0;note<=last_active;)
        if(active[note]==NULL)
          memmove(active+note,active+note+1,
            sizeof(active[0])*(last_active---note));	/* wahay :-) */
        else
          note++;

      count=0;
      
      for(note=0;note<=last_active;note++)
        count+=gensample(note);

      if(line==last_line+1 && last_active==-1) done=1;
      output_sample(count);
      sampletick++;
      if(last_active>maxchan) maxchan=last_active;
      }
    }
  
  fprintf(stderr,"%swritten block %d\n",verbose?"\n":"",line);
  }

fprintf(stderr,"done; max virtual channels %d\n",maxchan);

if(verbose) fprintf(stderr,"Lowest local sample rate %d Hz\n",minhz);
}


void addnotes(int line,int noteslot)
{
int lp,pat,nf,nf2;
int tmp,tmp2,linetime;

for(lp=0;lp<MAX_LINE_DEPTH;lp++)
  {
  if((pat=lines[line][lp])!=0)
    {
    /* add all new notes to be played at this notespace */
    if(patterns[pat].notes[noteslot].vol!=0)
      {
      if(last_active>=MAX_NOTES)
        fprintf(stderr,"Too many simultaneous notes!\n");
      else
        {
        nf=++last_active;
        active[nf]=&patterns[pat].notes[noteslot];
        active[nf]->offset=0;
        active[nf]->counter=0;

        switch(active[nf]->mode)
          {
          case PIANO_MODE:
            /* turn off prev. notes of this pattern which have = notenum */
            tmp=active[nf]->notenum;
            tmp2=active[nf]->pattern;
            for(nf2=0;nf2<last_active;nf2++)
              if(active[nf2]!=NULL &&
                 active[nf2]->notenum==tmp && active[nf2]->pattern==tmp2)
                active[nf2]=NULL;
            break;
          
          case OFFSET_MODE:
            /* turn off prev. notes of this pattern no matter what,
             * but copy their offset into the new note.
             * if no old notes are found, the offset stays at zero.
             */
            tmp=active[nf]->sample;
            tmp2=active[nf]->pattern;
            for(nf2=0;nf2<last_active;nf2++)
              if(active[nf2]!=NULL &&
                 active[nf2]->sample==tmp && active[nf2]->pattern==tmp2)
                {
                active[nf]->offset=active[nf2]->offset;
                active[nf2]=NULL;
                }
            break;
          
          case RETRIGGER_MODE:
            /* turn off prev. notes of this pattern no matter what */
            tmp2=active[nf]->pattern;
            for(nf2=0;nf2<last_active;nf2++)
              if(active[nf2]!=NULL && active[nf2]->pattern==tmp2)
                active[nf2]=NULL;

          default:
            /* nothing needs doing for multi mode */
          }
        }
      }
    }
  }

if(verbose && noteslot%8==7)
  {
  fprintf(stderr,"%2d:",last_active+1);
  gettimeofday(&newtv,NULL);
  linetime=(newtv.tv_sec-oldtv.tv_sec)*100+
           (newtv.tv_usec-oldtv.tv_usec)/10000;
  fprintf(stderr,"%6d ",linetime=((sampletick-oldstick)*10000/linetime)/100);
  oldtv=newtv;
  oldstick=sampletick;
  if(linetime<minhz) minhz=linetime;
  }
}


/* the big hotspot.
 * suggestions for improving the speed of this routine are most welcome.
 */
static inline int gensample(int note)
{
struct note *nptr;
int val;

nptr=active[note];

val=(*(nptr->data+FIX_DOWN(nptr->offset)))*nptr->vol/100;

if(nptr->counter>=nptr->sustain)
  {
  /* scale to account for the decay */
  val*=1024-FIX_UP(nptr->counter-nptr->sustain)/nptr->relsus;
  val/=1024;
  }

nptr->offset+=nptr->gap;
nptr->counter++;

if(nptr->offset>=nptr->len || nptr->counter>=nptr->release)
  {
  if(clickdetect && (val<-sample_midval/10 || val>sample_midval/10))
    fprintf(stderr,"possible click from note on pattern %d\n",nptr->pattern);
  active[note]=NULL;
  }

return(val);
}


void output_sample(int count)
{
int tmp;

count+=sample_midval;
if(count>sample_maxval) count=sample_maxval;
if(count<0) count=0;
if(sample_maxval==255)
  {
  /* 8-bit output */
  if(oversample)
    {
    if(sampletick%2)
      write_sample((count+oldval)>>1,out);
    else
      oldval=count;
    }
  else
    {
    if(undersample)
      {
      write_sample((count+oldval)>>1,out);
      oldval=count;
      }
    write_sample(count,out);
    }
  }
else
  {
  /* 16-bit output */
  if(oversample)
    {
    if(sampletick%2)
      {
      tmp=((count+oldval)>>1);
      write_sample(tmp&255,out);
      write_sample(tmp>>8,out);
      }
    else
      oldval=count;
    }
  else
    {
    if(undersample)
      {
      tmp=((count+oldval)>>1);
      write_sample(tmp&255,out);
      write_sample(tmp>>8,out);
      oldval=count;
      }
    write_sample(count&255,out);
    write_sample(count>>8,out);
    }
  }      
}


void die(char *str)
{
fprintf(stderr,"Couldn't %s.\n",str);
exit(1);
}


#ifdef linux
void ioctl_dsp(int fd)
{
int tmp;

if((ioctl(fd,SNDCTL_DSP_SPEED,&psr))==-1)
  fprintf(stderr,"Error setting frequency\n");
else
  fprintf(stderr,"Playback frequency: %d Hz\n",psr);

tmp=(sample_maxval==255)?AFMT_U8:AFMT_U16_LE;
if((ioctl(fd,SNDCTL_DSP_SETFMT,&tmp))==-1)
  fprintf(stderr,"Error setting sample width\n");
}
#endif


#ifdef _ALLOW_BUFFERING

/* Initialise buffer. This routine should be called just before
 * playing starts.
 */
void init_buffer()
{
int tmp;

out_fd=fileno(out);

tmp=BUF_FRAG_ARG;	/* two 16k fragments */
ioctl(out_fd,SNDCTL_DSP_SETFRAGMENT,&tmp);

if((buf_mem=malloc(BUFFER_SIZE))==NULL) die("malloc buffer");

buf_head=buf_tail=buf_mem;
bufused=0;

write_sample=write_to_buffer;

signal(SIGALRM,write_from_buffer);

tmp=((BUF_WRITE*2000)/psr)/10;
itvfast.it_value.tv_sec= tmp/1000;
itvfast.it_value.tv_usec=(tmp%1000)*1000;
itvfast.it_interval.tv_sec=itvfast.it_interval.tv_usec=0;

tmp=((BUF_WRITE*9000)/psr)/10;
itvslow.it_value.tv_sec= tmp/1000;
itvslow.it_value.tv_usec=(tmp%1000)*1000;
itvslow.it_interval.tv_sec=itvslow.it_interval.tv_usec=0;

setitimer(ITIMER_REAL,&itvslow,NULL);
}


void kill_buffer()
{
/* kill timer */
itvslow.it_value.tv_sec= 0;
itvslow.it_value.tv_usec=0;
setitimer(ITIMER_REAL,&itvslow,NULL);

free(buf_mem);
}


void write_from_buffer(int n)
{
int f;
struct timeval tv;

signal(SIGALRM,write_from_buffer);

if(bufused>=BUF_WRITE || (playdone && bufused>0))
  {
  f=write(out_fd,buf_head,(bufused<=BUF_WRITE)?bufused:BUF_WRITE);
  if(f!=-1)
    {
    bufused-=f; buf_out+=f;
    buf_head+=f; if(buf_head>=buf_mem+BUFFER_SIZE) buf_head-=BUFFER_SIZE;
    }

  gettimeofday(&tv,NULL);
  if(verbose && last_report+5<=tv.tv_sec)
    {
    fprintf(stderr,"%s[buf %7d:%7d",playdone?"":"\n",buf_in,buf_out);
    if(buf_out!=0)
      fprintf(stderr," = %d.%03d",buf_in/buf_out,(buf_in*1000/buf_out)%1000);
    fprintf(stderr,", %d ahead (%d.%03d sec)]\n",bufused,
    		bufused/psr,(bufused*1000/psr)%1000);
    last_report=tv.tv_sec;
    buf_in=buf_out=0;
    }
  setitimer(ITIMER_REAL,&itvslow,NULL);
  }
else
  {
  if(verbose) fprintf(stderr,"!");
  setitimer(ITIMER_REAL,&itvfast,NULL);
  }
}


int write_to_buffer(int c,FILE *out)
{
while(bufused>=BUFFER_SIZE-BUF_WRITE) usleep(50000);
*buf_tail++=c; bufused++; buf_in++;
if(buf_tail>=buf_mem+BUFFER_SIZE) buf_tail-=BUFFER_SIZE;
return(0);
}

#endif /* _ALLOW_BUFFERING */
