/*
 * July 5, 1991
 * Copyright 1991 Lance Norskog And Sundry Contributors
 * This source code is freely redistributable and may be used for
 * any purpose.  This copyright notice must be maintained. 
 * Lance Norskog And Sundry Contributors are not responsible for 
 * the consequences of using this software.
 */

#include "st.h"
#if	defined(unix) || defined(AMIGA) || defined(__OS2__)
#include <sys/types.h>
#include <sys/stat.h>
#endif
#ifdef	__STDC__
#include <stdarg.h>
#else
#include <varargs.h>
#endif
#include <ctype.h>
#include <string.h>
#ifdef VMS
#include <errno.h>
#include <perror.h>
#define LASTCHAR        ']'
#else
#include <errno.h>
#define LASTCHAR        '/'
#endif
/* YOU MAY WANT THIS
#include <getopt.h>
*/

/*
 * SOX main program.
 *
 * Rewrite for new nicer option syntax.  July 13, 1991.
 * Rewrite for separate effects library.  Sep. 15, 1991.
 */

#ifdef AMIGA
/* This is the Amiga version string */
char amiversion[AmiVerSize]=AmiVerChars;      
#endif /* AMIGA */


EXPORT float volume = 1.0;	/* expansion coefficient */
int dovolume = 0;
int clipped = 0;	/* Volume change clipping errors */

EXPORT float amplitude = 1.0;	/* Largest sample so far */

EXPORT int writing = 0;	/* are we writing to a file? */

/* export flags */
EXPORT int verbose = 0;	/* be noisy on stderr */
EXPORT int summary = 0;	/* just print summary of information */

long ibuf[BUFSIZ];	/* Intermediate processing buffer */
long obuf[BUFSIZ];	/* Intermediate processing buffer */


long volumechange();

#ifdef	DOS
char writebuf[BUFSIZ];	/* output write buffer */
#endif

void	gettype(), geteffect(), checkeffect();

EXPORT struct soundstream informat, outformat;

char *myname;

ft_t ft;
struct effect eff;
char *ifile, *ofile, *itype, *otype;
IMPORT char *optarg;
IMPORT int optind;

main(n, args)
int n;
char **args;
{
	myname = args[0];
	init();
	
	ifile = ofile = NULL;

	/* Get input format options */
	ft = &informat;
	doopts(n, args);
	/* Get input file */
	if (optind >= n)
#ifndef	VMS
		usage("No input file?");
#else
		/* I think this is the VMS patch, but am not certain */
		fail("Input file required");	
#endif
	ifile = args[optind];
	if (! strcmp(ifile, "-"))
		ft->fp = stdin;
	else if ((ft->fp = fopen(ifile, READBINARY)) == NULL)
		fail("Can't open input file '%s': %s", 
			ifile, strerror(errno));
	ft->filename = ifile;
	optind++;

	/* Let -e allow no output file, just do an effect */
	if (optind < n) {
	    if (strcmp(args[optind], "-e")) {
		/* Get output format options */
		ft = &outformat;
		doopts(n, args);
		/* Get output file */
		if (optind >= n)
			usage("No output file?");
		ofile = args[optind];
		ft->filename = ofile;
		/*
		 * There are two choices here:
		 *	1) stomp the old file - normal shell "> file" behavior
		 *	2) fail if the old file already exists - csh mode
		 */
		if (! strcmp(ofile, "-"))
			ft->fp = stdout;
		else {
#ifdef	unix
		 	/*	
			 * Remove old file if it's a text file, but 
		 	 * preserve Unix /dev/sound files.  I'm not sure
			 * this needs to be here, but it's not hurting
			 * anything.
			 */
			if ((ft->fp = fopen(ofile, WRITEBINARY)) && 
				   (filetype(fileno(ft->fp)) == S_IFREG)) {
				fclose(ft->fp);
				unlink(ofile);
				creat(ofile, 0666);
				ft->fp = fopen(ofile, WRITEBINARY);
			}
#else
			ft->fp = fopen(ofile, WRITEBINARY);
#endif
			if (ft->fp == NULL)
				fail("Can't open output file '%s': %s", 
					ofile, strerror(errno));
#ifdef	DOS
			if (setvbuf (ft->fp,writebuf,_IOFBF,sizeof(writebuf)))
				fail("Can't set write buffer");
#endif
		}
		writing = 1;
	    }
	    optind++;
	}

	/* ??? */
/*
	if ((optind < n) && !writing && !eff.name)
		fail("Can't do an effect without an output file!");
*/

	/* Get effect name */
	if (optind < n) {
		eff.name = args[optind];
		optind++;
		geteffect(&eff);
		(* eff.h->getopts)(&eff, n - optind, &args[optind]);
	}

	/* 
	 * If we haven't specifically set an output file 
	 * don't write a file; we could be doing a summary
	 * or a format check.
	 */
/*
	if (! ofile)
		usage("Must be given an output file name");
*/
	if (! ofile)
		writing = 0;
	/* Check global arguments */
	if (volume <= 0.0)
		fail("Volume must be greater than 0.0");
#if	defined(unix) || defined(AMIGA)
	informat.seekable  = (filetype(fileno(informat.fp)) == S_IFREG);
	outformat.seekable = (filetype(fileno(outformat.fp)) == S_IFREG); 
#else
#if	defined(DOS) || defined(__OS2__)
	informat.seekable  = 1;
	outformat.seekable = 1;
#else
	informat.seekable  = 0;
	outformat.seekable = 0;
#endif
#endif

	/* If file types have not been set with -t, set from file names. */
	if (! informat.filetype) {
		if (informat.filetype = strrchr(ifile, LASTCHAR))
			informat.filetype++;
		else
			informat.filetype = ifile;
		if (informat.filetype = strrchr(informat.filetype, '.'))
			informat.filetype++;
		else /* Default to "auto" */
			informat.filetype = "auto";
	}
	if (writing && ! outformat.filetype) {
		if (outformat.filetype = strrchr(ofile, LASTCHAR))
			outformat.filetype++;
		else
			outformat.filetype = ofile;
		if (outformat.filetype = strrchr(outformat.filetype, '.'))
			outformat.filetype++;
	}
	/* Default the input comment to the filename. 
	 * The output comment will be assigned when the informat 
	 * structure is copied to the outformat. 
	 */
	informat.comment = informat.filename;

	process();
	statistics();
	exit(0);
}

doopts(n, args)
int n;
char **args;
{
	int c;
	char *str;

	while ((c = getopt(n, args, "r:v:t:c:suUAbwlfdDxSV")) != -1) {
		switch(c) {
		case 't':
			if (! ft) usage("-t");
			ft->filetype = optarg;
			if (ft->filetype[0] == '.')
				ft->filetype++;
			break;

		case 'r':
			if (! ft) usage("-r");
			str = optarg;
			if ((! sscanf(str, "%lu", &ft->info.rate)) ||
					(ft->info.rate <= 0))
				fail("-r must be given a positive integer");
			break;
		case 'v':
			if (! ft) usage("-v");
			str = optarg;
			if ((! sscanf(str, "%e", &volume)) ||
					(volume <= 0))
				fail("Volume value '%s' is not a number",
					optarg);
			dovolume = 1;
			break;

		case 'c':
			if (! ft) usage("-c");
			str = optarg;
			if (! sscanf(str, "%d", &ft->info.channels))
				fail("-c must be given a number");
			break;
		case 'b':
			if (! ft) usage("-b");
			ft->info.size = BYTE;
			break;
		case 'w':
			if (! ft) usage("-w");
			ft->info.size = WORD;
			break;
		case 'l':
			if (! ft) usage("-l");
			ft->info.size = LONG;
			break;
		case 'f':
			if (! ft) usage("-f");
			ft->info.size = FLOAT;
			break;
		case 'd':
			if (! ft) usage("-d");
			ft->info.size = DOUBLE;
			break;
		case 'D':
			if (! ft) usage("-D");
			ft->info.size = IEEE;
			break;

		case 's':
			if (! ft) usage("-s");
			ft->info.style = SIGN2;
			break;
		case 'u':
			if (! ft) usage("-u");
			ft->info.style = UNSIGNED;
			break;
		case 'U':
			if (! ft) usage("-U");
			ft->info.style = ULAW;
			break;
		case 'A':
			if (! ft) usage("-A");
			ft->info.style = ALAW;
			break;
		
		case 'x':
			if (! ft) usage("-x");
			ft->swap = 1;
			break;
		
/*  stat effect does this ?
		case 'S':
			summary = 1;
			break;
*/
		case 'V':
			verbose = 1;
			break;
		}
	}
}

init() {

	/* init files */
	informat.info.rate      = outformat.info.rate  = 0;
	informat.info.size      = outformat.info.size  = -1;
	informat.info.style     = outformat.info.style = -1;
	informat.info.channels  = outformat.info.channels = -1;
	informat.comment   = outformat.comment = NULL;
	informat.swap      = 0;
	informat.filetype  = outformat.filetype  = (char *) 0;
	informat.fp        = stdin;
	outformat.fp       = stdout;
	informat.filename  = "input";
	outformat.filename = "output";
}

/* 
 * Process input file -> effect -> output file
 *	one buffer at a time
 */

process() {
	long isamp, osamp, istart;
	long i, idone, odone;

	gettype(&informat);
	if (writing)
		gettype(&outformat);
	/* Read and write starters can change their formats. */
	(* informat.h->startread)(&informat);
	checkformat(&informat);
	report("Input file: using sample rate %lu\n\tsize %s, style %s, %d %s",
		informat.info.rate, sizes[informat.info.size], 
		styles[informat.info.style], informat.info.channels, 
		(informat.info.channels > 1) ? "channels" : "channel");
	if (informat.comment)
		report("Input file: comment \"%s\"\n",
			informat.comment);
	if (writing) {
		copyformat(&informat, &outformat);
		(* outformat.h->startwrite)(&outformat);
		checkformat(&outformat);
		cmpformats(&informat, &outformat);
	report("Output file: using sample rate %lu\n\tsize %s, style %s, %d %s",
		outformat.info.rate, sizes[outformat.info.size], 
		styles[outformat.info.style], outformat.info.channels, 
		(outformat.info.channels > 1) ? "channels" : "channel");
		if (outformat.comment)
			report("Output file: comment \"%s\"\n",
				outformat.comment);
	}
	/* Very Important: 
	 * Effect fabrication and start is called AFTER files open.
	 * Effect may write out data beforehand, and
	 * some formats don't know their sample rate until now.
	 */
	checkeffect(0);
	/* inform effect about signal information */
	eff.ininfo = informat.info;
	eff.outinfo = outformat.info;
	(* eff.h->start)(&eff);
	istart = 0;
	while((isamp = (*informat.h->read)(&informat,&ibuf[istart],
			(long) BUFSIZ-istart))>0) {
		long *ib = ibuf;

		isamp += istart;
		/* Do volume before effect or after?  idunno */
		if (dovolume) for (i = 0; i < isamp; i++)
			ibuf[i] = volumechange(ibuf[i]);
		osamp = sizeof(obuf) / sizeof(long);
		/* Effect (i.e. rate change) may do different sizes I and O */
		while (isamp) {
			idone = isamp;
			odone = osamp;
			(* eff.h->flow)(&eff, ib, obuf, &idone, &odone);
			/* 
			 * kludge! 	
			 * Effect is stuck.  Start over with new buffer.
			 * This can drop samples at end of file. 
			 * No effects currently do this, but it could happen.
			 */
			if (idone == 0) {
				int i;
				for(i = isamp - 1; i; i--)
					ibuf[i] = ib[i];
				istart = isamp;
				isamp = 0;
				break;
			}
			if (writing && (odone > 0)) 
				(* outformat.h->write)(&outformat, obuf, (long) odone);
			isamp -= idone;
			ib += idone;
		}
	}
	/* Drain effect out */
	if (writing) {
		odone = sizeof(obuf) / sizeof(long);
		(* eff.h->drain)(&eff, obuf, &odone);
		if (odone > 0)
			(* outformat.h->write)(&outformat, obuf, (long) odone);
		/* keep calling it until it returns a partial buffer */
		while (odone == (sizeof(obuf) / sizeof(long))) {
			(* eff.h->drain)(&eff, obuf, &odone);
			if (odone)
			 (* outformat.h->write)(&outformat, obuf, (long) odone);
		}
	}
	/* Very Important: 
	 * Effect stop is called BEFORE files close.
	 * Effect may write out more data after. 
	 */
	(* eff.h->stop)(&eff);
	(* informat.h->stopread)(&informat);
	fclose(informat.fp);
	if (writing)
		(* outformat.h->stopwrite)(&outformat);
	if (writing)
		fclose(outformat.fp);
}

/*
 * Check that we have a known format suffix string.
 */
void
gettype(formp)
ft_t formp;
{
	char **list;
	int i;

	if (! formp->filetype)
fail("Must give file type for %s file, either as suffix or with -t option",
formp->filename);
	for(i = 0; formats[i].names; i++) {
		for(list = formats[i].names; *list; list++) {
			char *s1 = *list, *s2 = formp->filetype;
			if (! strcmpcase(s1, s2))
				break;	/* not a match */
		}
		if (! *list)
			continue;
		/* Found it! */
		formp->h = &formats[i];
		return;
	}
	if (! strcmpcase(formp->filetype, "snd")) {
		verbose = 1;
		report("File type '%s' is used to name several different formats.", formp->filetype);
		report("If the file came from a Macintosh, it is probably");
		report("a .ub file with a sample rate of 11025 (or possibly 5012 or 22050).");
		report("Use the sequence '-t .ub -r 11025 file.snd'");
		report("If it came from a PC, it's probably a Soundtool file.");
		report("Use the sequence '-t .sndt file.snd'");
		report("If it came from a NeXT, it's probably a .au file.");
		fail("Use the sequence '-t .au file.snd'\n");
	}
	fail("File type '%s' of %s file is not known!",
		formp->filetype, formp->filename);
}

copyformat(ft, ft2)
ft_t ft, ft2;
{
	int noise = 0;
	if (ft2->info.rate == 0) {
		ft2->info.rate = ft->info.rate;
		noise = 1;
	}
	if (outformat.info.size == -1) {
		ft2->info.size = ft->info.size;
		noise = 1;
	}
	if (outformat.info.style == -1) {
		ft2->info.style = ft->info.style;
		noise = 1;
	}
	if (outformat.info.channels == -1) {
		ft2->info.channels = ft->info.channels;
		noise = 1;
	}
	if (outformat.comment == NULL) {
		ft2->comment = ft->comment;
		noise = 1;
	}
	return noise;
}

cmpformats(ft, ft2)
ft_t ft, ft2;
{
	int noise = 0;
	float abs;

}

/* check that all settings have been given */
checkformat(ft) 
ft_t ft;
{
	if (ft->info.rate == 0)
		fail("Sampling rate for %s file was not given\n", ft->filename);
	if ((ft->info.rate < 100) || (ft->info.rate > 50000))
		fail("Sampling rate %lu for %s file is bogus\n", 
			ft->info.rate, ft->filename);
	if (ft->info.size == -1)
		fail("Data size was not given for %s file\nUse one of -b/-w/-l/-f/-d/-D", ft->filename);
	if (ft->info.style == -1)
		fail("Data style was not given for %s file\nUse one of -s/-u/-U/-A", ft->filename);
	/* it's so common, might as well default */
	if (ft->info.channels == -1)
		ft->info.channels = 1;
	/*	fail("Number of output channels was not given for %s file",
			ft->filename); */
}

/*
 * If no effect given, decide what it should be.
 */
void
checkeffect(effp)
eff_t effp;
{
	int already = (eff.name != (char *) 0);
	char *rate = 0, *chan = 0;
	int i;

	for (i = 0; effects[i].name; i++) {
		if (!chan && (effects[i].flags & EFF_CHAN))
			chan = effects[i].name;
		if (! rate && (effects[i].flags & EFF_RATE))
			rate = effects[i].name;
	}

	if (eff.name && ! writing)
		return;

	/* 
	 * Require mixdown for channel mismatch.
	 * XXX Doesn't handle channel expansion.  Need an effect for this.
	 * Require one of the rate-changers on a rate change.
	 * Override a rate change by explicitly giving 'copy' command.
	 */
	if (informat.info.channels != outformat.info.channels) {
		if (eff.name && !(eff.h->flags & EFF_CHAN))
			fail("Need to do change number of channels first.  Try the '%s' effect.", chan);
		if (! eff.name) {
			eff.name = chan;
			report(
"Changing %d input channels to %d output channels with '%s' effect\n",
			informat.info.channels, outformat.info.channels, chan);
			geteffect(&eff);
		}
	} 
	/* 
	 * Be liberal on rate difference errors.
	 * Note that the SPARC 8000-8192 problem
	 * comes in just under the wire.  XXX
	 *
 	 * Bogus.  Should just do a percentage.
	 */
	if (abs(informat.info.rate - outformat.info.rate) > 200) {
		if (eff.name && !(eff.h->flags & EFF_RATE))
		    fail("Need to do rate change first.  Try the '%s' effect.", 
			rate);
		if (! eff.name) {
			eff.name = rate;
			report(
"Changing sample rate %lu to rate %lu via noisy 'rate' effect\n",
			informat.info.rate, outformat.info.rate);
			geteffect(&eff);
		}
	}
	/* don't need to change anything */
	if (! eff.name)
		eff.name = "copy";
	if (! already) {
		geteffect(&eff);
		/* give default opts for manufactured effect */
		(* eff.h->getopts)(&eff, 0, (char *) 0);
	}
}

/*
 * Check that we have a known effect name.
 */
void
geteffect(effp)
eff_t effp;
{
	int i;

	for(i = 0; effects[i].name; i++) {
		char *s1 = effects[i].name, *s2 = effp->name;
		while(*s1 && *s2 && (tolower(*s1) == tolower(*s2)))
			s1++, s2++;
		if (*s1 || *s2)
			continue;	/* not a match */
		/* Found it! */
		effp->h = &effects[i];
		return;
	}
	/* Guido Van Rossum fix */
	fprintf(stderr, "Known effects:");
	for (i = 0; effects[i].name; i++)
		fprintf(stderr, "\t%s\n", effects[i].name);
	fail("\nEffect '%s' is not known!", effp->name);
}

/* Guido Van Rossum fix */
statistics() {
	if (dovolume && clipped > 0)
		report("Volume change clipped %d samples", clipped);
}

long volumechange(y)
long y;
{
	double y1;

	y1 = y * volume;
	if (y1 < -2147483647.0) {
		y1 = -2147483647.0;
		clipped++;
	}
	else if (y1 > 2147483647.0) {
		y1 = 2147483647.0;
		clipped++;
	}

	return y1;
}

#if	defined(unix) || defined(AMIGA)
filetype(fd)
int fd;
{
	struct stat st;

	fstat(fd, &st);

	return st.st_mode & S_IFMT;
}
#endif

char *usagestr = 
"[ -V -S ] [ fopts ] ifile [ fopts ] ofile [ effect [ effopts ] ]\nfopts: -r rate -v volume -c channels -s/-u/-U/-A -b/-w/-l/-f/-d/-D -x\neffects and effopts: various";

usage(opt)
char *opt;
{
#ifndef	DOS
	/* single-threaded machines don't really need this */
	fprintf(stderr, "%s: ", myname);
#endif
	fprintf(stderr, "Usage: %s", usagestr);
	if (opt)
		fprintf(stderr, "\nFailed at: %s\n", opt);
	exit(1);
}

void
#ifdef	__STDC__
report(char *fmt, ...)
#else
report(va_alist) 
va_dcl
#endif
{
	va_list args;
#ifndef	__STDC__
	char *fmt;
#endif

	if (! verbose)
		return;
#ifndef	DOS
	/* single-threaded machines don't really need this */
	fprintf(stderr, "%s: ", myname);
#endif
#ifdef	__STDC__
	va_start(args, fmt);
#else
	va_start(args);
	fmt = va_arg(args, char *);
#endif
	vfprintf(stderr, fmt, args);
	va_end(args);
	fprintf(stderr, "\n");
}

void
#ifdef	__STDC__
fail(char *fmt, ...)
#else
fail(va_alist) 
va_dcl
#endif
{
	va_list args;
#ifndef	__STDC__
	char *fmt;
#endif

#ifndef	DOS
	/* single-threaded machines don't really need this */
	fprintf(stderr, "%s: ", myname);
#endif
#ifdef	__STDC__
	va_start(args, fmt);
#else
	va_start(args);
	fmt = va_arg(args, char *);
#endif
	vfprintf(stderr, fmt, args);
	va_end(args);
	fprintf(stderr, "\n");
	/* Close the input file and outputfile before exiting*/
	ft = &informat;
	fclose(ft->fp);
	ft = &outformat;
	fclose(ft->fp);

	/* remove the output file because we failed */
	REMOVE(ft->filename);
	exit(2);
}


strcmpcase(s1, s2)
char *s1, *s2;
{
	while(*s1 && *s2 && (tolower(*s1) == tolower(*s2)))
		s1++, s2++;
	return *s1 - *s2;
}
