/*

  cdr_compress.c - digital audio compression filter
  by "Benjamin C. W. Sittler" <bsittler@nmt.edu>

  This program compresses audio for listening in noisy environments (car, etc.)
  It processes stereo big-endian 16-bit signed linear samples.

*/

#include <limits.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>

/* maximum sample magnitude, expressed as a float */
#define MAX_SAMPLE 32768.0

int verbose = 0;
unsigned long samples, rate = 44100UL;
double max_gain = 50.0, volume = 0.1, seconds = 4.0, cr = 3.6;
struct { char *name; FILE *stream; } infile, outfile;
short *buffer = 0;

/* compress a CDR stream */
int
compress(filename)
     char *filename;
{
  size_t nread;
  unsigned long head, middle, used, samples_read;
  double gain, sum, fsample, rms;

  gain = 1.0;
  sum = 0.0;
  infile.name = filename;
  if ((*infile.name == '-') && ! infile.name[1])
    {
      infile.name = "stdin";
      infile.stream = stdin;
    }
  else
    if (! (infile.stream = fopen (infile.name, "rb")))
      {
	perror (infile.name);
	return 1;
      }
  head = used = samples_read = 0UL;
  middle = samples;
  while (((nread = fread ((void *) (buffer + head),
			  sizeof *buffer, 1, infile.stream)) == 1) ||
	 (used >= samples))
    {
      /* convert the newest sample */
      if (nread)
	{
	  samples_read ++;
	  buffer [head] = ntohs (buffer [head]);
	  fsample = 1.0 * buffer[head] / MAX_SAMPLE;
	  sum += fsample * fsample;
	  /* recalculate gain every other sample */
	  if (head & 1UL)
	    {
	      rms = sqrt (sum / (samples * 2UL));
	      gain = pow (10, (1 - cr) / cr * log10 (rms / volume));
	      if (gain > max_gain)
		gain = max_gain;
	      if (verbose && ! (samples_read % rate))
		{
		  int i;
		  int in_c, out_c;
		  double gain_dB, in_dB, out_dB;
		  
		  gain_dB = 20 * log10 (gain);
		  in_dB = 20 * log10 (rms / volume);
		  out_dB = gain_dB + in_dB;
		  fprintf (stderr, "\r%+6.1fdB %+6.1fdB gain = %+6.1fdB [",
			   in_dB,
			   gain_dB,
			   out_dB);
		  if (in_dB > out_dB)
		    {
		      double q;
		      
		      q = in_dB;
		      in_dB = out_dB;
		      out_dB = q;
		      in_c = '<';
		      out_c = '*';
		    }
		  else
		    {
		      in_c = '*';
		      out_c = '>';
		    }
		  for (i = -33; i < 10; i ++)
		    fputc ((i > (in_dB + 0.5))
			   ? ((i > (out_dB + 0.5))
			      ? (i
				 ? ((abs (i) % 6)
				    ? ' '
				    : ':')
				 : '0')
			      : out_c)
			   : in_c,
			   stderr);
		  fputc (']', stderr);
		  fflush (stderr);
		}
	    }
	  if (used < 2UL * samples)
	    used ++;
	}
      else
	{
	  buffer [head] = 0;
	  if (used)
	    used --;
	}
      /* increment circular buffer pointers */
      head ++;
      head %= 2UL * samples;
      middle ++;
      middle %= 2UL * samples;
      /* head dropping items from the sum */
      if (used == 2UL * samples)
	{
	  fsample = 1.0 * buffer[head] / MAX_SAMPLE;
	  sum -= fsample * fsample;
	  /* we formerly bled a little off our sum every step */
	  /* sum -= volume * sum / (2UL * samples); */
  	  if (sum < 0.0)
  	    sum = 0.0;
	}
      /* write the middle samples, compressed and converted */
      if ((used >= samples) && (middle & 1UL))
	{
	  short converted[2];
	  long scaled;

	  scaled = buffer [middle - 1] * gain;
	  if (scaled >= MAX_SAMPLE)
	    scaled = MAX_SAMPLE - 1;
	  else if (scaled < -MAX_SAMPLE)
	    scaled = -MAX_SAMPLE;
	  converted [0] = htons (scaled);
	  scaled = buffer [middle] * gain;
	  if (scaled >= MAX_SAMPLE)
	    scaled = MAX_SAMPLE - 1;
	  else if (scaled < -MAX_SAMPLE)
	    scaled = -MAX_SAMPLE;
	  converted [1] = htons (scaled);
	  if (! (fwrite ((void *) converted,
			 sizeof *converted,
			 sizeof converted/sizeof *converted,
			 outfile.stream)))
	    {
	      perror (outfile.name);
	      exit (1);
	    }
	}
    }
  if (! feof (infile.stream))
    {
      perror (infile.name);
      return 1;
    }
  if (verbose)
    {
      unsigned long hundredths;

      hundredths = (samples_read / 2UL) * 100.0 / rate;
      fprintf (stderr, "\n%s: compressed [%lu:%2.2lu.%2.2lu]\n",
	       infile.name,
	       hundredths / 6000UL,
	       (hundredths / 100UL) % 60UL,
	       hundredths % 100UL);
    }
  fclose (infile.stream);
  return 0;
}

/* parse command-line arguments */
int
parse(argc, argv)
     char **argv;
{
  int i;

  while ((i = getopt (argc, argv, "c:hm:o:r:s:t:v")) != -1) switch (i)
    {
    case 'c':
      cr = strtod (optarg, &optarg);
      if (*optarg)
	{
	  fprintf (stderr, "%s: option requires a numeric argument -- %c\n",
		   *argv, i);
	  goto usage;
	}
      if (cr <= 0.0)
	{
	  fprintf (stderr, "%s: compression ratio must be positive\n",
		   *argv);
	  goto usage;
	}
      break;
    case 'h':
      printf ("Usage: %s [options] [files]\n", *argv);
      printf ("  -c NUM       set the compression ratio to NUM (default: %g)\n",
	      cr);
      printf ("  -h           print this summary and exit\n");
      printf ("  -m NUM[dB]   set maximum gain to NUM (default: %gdB)\n",
	      20 * log10 (max_gain));
      printf ("               [Add the dB suffix when setting gain in decibels.]\n");
      printf ("  -o FILE      output to FILE (default: %s)\n",
	      outfile.name ? outfile.name : "stdout");
      printf ("  -r NUM       set the sample rate to NUM Hz (default: %lu)\n",
	      rate);
      printf ("  -s NUM       set window size to NUM seconds (default: %g)\n",
	      seconds);
      printf ("  -t NUM       set target RMS volume level to NUM (default: %g)\n",
	      volume);
      printf ("  -v           generate verbose status information\n");
      printf ("If no files are given, input is from stdin.\n");
      exit (0);
    case 'm':
      max_gain = strtod (optarg, &optarg);
      if (! strcasecmp (optarg, "dB"))
	{
	  max_gain = pow (10, max_gain / 20);
	}
      else if (*optarg)
	{
	  fprintf (stderr, "%s: option requires a numeric argument -- %c\n",
		   *argv, i);
	  goto usage;
	}
      break;
    case 'o':
      if ((*optarg == '-') && ! optarg[1])
	outfile.name = 0;
      else
	outfile.name = optarg;
      break;
    case 'r':
      rate = strtoul (optarg, &optarg, 0);
      if (*optarg || (rate < 1UL))
	{
	  fprintf (stderr,
		   "%s: option requires a positive integer argument -- %c\n",
		   *argv, i);
	  goto usage;
	}
      break;
    case 's':
      seconds = strtod (optarg, &optarg);
      if (*optarg)
	{
	  fprintf (stderr, "%s: option requires a numeric argument -- %c\n",
		   *argv, i);
	  goto usage;
	}
      break;
    case 't':
      volume = strtod (optarg, &optarg);
      if (*optarg)
	{
	  fprintf (stderr, "%s: option requires a numeric argument -- %c\n",
		   *argv, i);
	  goto usage;
	}
      break;
    case 'v':
      verbose = 1;
      break;
    default:
      goto usage;
    }
  return 0;
usage:
  printf ("Usage: %s [-h] [options] [files]\n", *argv);
  exit (2);
}

/* central dispatcher */
int
main(argc, argv)
     char **argv;
{
  int retval;

  retval = 0;
  parse (argc, argv);
  samples = seconds * rate;
  if (samples < 3)
    {
      fprintf (stderr, "%s: seconds must be at least %g\n",
	       *argv, 3.0 / rate);
      return 2;
    }
  if (! outfile.name) {
    outfile.name = "stdout";
    outfile.stream = stdout;
  } else
    if (! (outfile.stream = fopen (outfile.name, "wb")))
      {
	perror (outfile.name);
	return 1;
      }
  if (! (buffer = (short *) malloc (samples * 2 * sizeof *buffer)))
    {
      perror ("malloc");
      return 1;
    }
  if (optind < argc)
    while (optind < argc)
      retval += compress (argv[optind ++]);
  else
    retval += compress ("-");
  free ((void *) buffer);
  return retval;
}
