
/*
 * /dev/audio driver for NetBSD Amiga
 *
 * The driver interprets minor numbers as:
 *   0     -  channel 0 and 1 (stereo)
 *   1     -  channel 2 and 3 (stereo)
 *   2 - 5 - channels 0 through 4
 * no channel can be opened twice (so unit 0 excludes units 2 and 5)
 *
 * default sampling rate is 8khz.
 * default format is mulaw.
 * these can be changed via ioctl. 
 *
 * TODO:
 *   write() doesnt return the proper count
 *   multiple write()'s dont blend together smoothly.
 *	maybe have the last buffer play asynchronously as
 *	the write returns.
 *   allocate buffers at open rather than init
 *   Maybe adjust volume upwards when using mu-law.
 *
 *                             Tim N.
 */

#include <errno.h>
#include <sys/types.h>
#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/uio.h>
#include <amiga/amiga/cc.h>
#include <amiga/amiga/custom.h>
#include "bsd_audioio.h"

#define spl_audio()	spl4()	/* belongs in amiga/include/param.h */

#define NAUDIO		6
#define NCHAN		4
#define AUDIO_BUF_SIZE	8 * 1024
#define MINPER		125
#define PER_TO_FREQ(x)  (clock / (x))
#define FREQ_TO_PER(x)  (clock / (x))
#define VOL_TO_GAIN(x)	((x) << 2)	/* 0-63  -> 0-255 */
#define GAIN_TO_VOL(x)	((x) >> 2)	/* 0-255 -> 0-63  */

/*
 * unit -> channel mapping
 */
static int      audio_channel[] = {
	3,			/* channel 0 and 1 */
	12,			/* channel 2 and 3 */
	1,			/* channel 0 */
	2,			/* channel 1 */
	4,			/* channel 2 */
	8,			/* channel 3 */
};

/*
 * per-channel busy status
 *
 * 0 = free.  nonzero = unit number for channel
 */
static int      channel_busy[4] = { 0, 0, 0, 0, };

/*
 * per-unit state
 */
static struct audio_softc {
	int             freq;		/* sampling frequency */
	int             per;		/* sampling period */
	int             volume;		/* volume 0 to 64 */
	int             format;		/* type of encoding */
	void           *buf;		/* IO buffers */
	void           *buf2;
	int             sleeping;	/* state of unit */
} au_soft[NAUDIO];

/*
 * hardware clock rate
 */
static int      clock = 3579545;	/* NTSC */

/* from arch/amiga/amiga/cc.c */
struct audio_channel {
	u_short         play_count;	/* number of times to loop sample */
	handler_func_t  handler;	/* interupt handler for channel */
};
extern struct audio_channel channel[4];

/*
 * u-law to linear-signed format
 * This table was generated by gen.c
 */
char            ulaw_to_lin[] = {
  0x82, 0x86, 0x8a, 0x8e, 0x92, 0x96, 0x9a, 0x9e, 0xa2, 0xa6, 0xaa, 0xae, 0xb2, 0xb6, 0xba, 0xbe,
  0xc1, 0xc3, 0xc5, 0xc7, 0xc9, 0xcb, 0xcd, 0xcf, 0xd1, 0xd3, 0xd5, 0xd7, 0xd9, 0xdb, 0xdd, 0xdf,
  0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0xf0,
  0xf0, 0xf1, 0xf1, 0xf2, 0xf2, 0xf3, 0xf3, 0xf4, 0xf4, 0xf5, 0xf5, 0xf6, 0xf6, 0xf7, 0xf7, 0xf8,
  0xf8, 0xf8, 0xf9, 0xf9, 0xf9, 0xf9, 0xfa, 0xfa, 0xfa, 0xfa, 0xfb, 0xfb, 0xfb, 0xfb, 0xfc, 0xfc,
  0xfc, 0xfc, 0xfc, 0xfc, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfd, 0xfe, 0xfe, 0xfe, 0xfe,
  0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00,
  0x7d, 0x79, 0x75, 0x71, 0x6d, 0x69, 0x65, 0x61, 0x5d, 0x59, 0x55, 0x51, 0x4d, 0x49, 0x45, 0x41,
  0x3e, 0x3c, 0x3a, 0x38, 0x36, 0x34, 0x32, 0x30, 0x2e, 0x2c, 0x2a, 0x28, 0x26, 0x24, 0x22, 0x20,
  0x1e, 0x1d, 0x1c, 0x1b, 0x1a, 0x19, 0x18, 0x17, 0x16, 0x15, 0x14, 0x13, 0x12, 0x11, 0x10, 0x0f,
  0x0f, 0x0e, 0x0e, 0x0d, 0x0d, 0x0c, 0x0c, 0x0b, 0x0b, 0x0a, 0x0a, 0x09, 0x09, 0x08, 0x08, 0x07,
  0x07, 0x07, 0x06, 0x06, 0x06, 0x06, 0x05, 0x05, 0x05, 0x05, 0x04, 0x04, 0x04, 0x04, 0x03, 0x03,
  0x03, 0x03, 0x03, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x01, 0x01, 0x01, 0x01,
  0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

/*
 * initialize audio driver
 */
init_audio()
{
	int             i;

	for (i = 0; i < NAUDIO; i++) {
		set_freq(i, 8000);
		au_soft[i].format = AUDIO_ENCODING_ULAW;
		au_soft[i].sleeping = 0;
		au_soft[i].volume = 40;
		au_soft[i].buf = alloc_chipmem(AUDIO_BUF_SIZE);
		au_soft[i].buf2 = alloc_chipmem(AUDIO_BUF_SIZE);
		if (!au_soft[i].buf || !au_soft[i].buf2) {
			cleanup_audio();
			return (ENOMEM);
		}
	}

	return (0);
}

/*
 * free up any resources used by audio driver
 */
cleanup_audio()
{
	int             i;

	for (i = 0; i < NAUDIO; i++) {
		if (au_soft[i].buf)
			free_chipmem(au_soft[i].buf);
		au_soft[i].buf = 0;

		if (au_soft[i].buf2)
			free_chipmem(au_soft[i].buf2);
		au_soft[i].buf2 = 0;
	}
	return (0);
}

/*
 * set the frequency of a unit.
 * clamps to legal limits.
 */
set_freq(unit, freq)
	int             unit, freq;
{
	au_soft[unit].freq = freq;
	au_soft[unit].per = FREQ_TO_PER(freq);

	/* make sure we have a valid value */
	if (au_soft[unit].per < MINPER) {
		printf("period to small %x. forcing to %x\n", /*XXX*/
			au_soft[unit].per, MINPER);	      /*XXX*/

		au_soft[unit].per = MINPER;
		au_soft[unit].freq = PER_TO_FREQ(au_soft[unit].per);
	}
	return (0);
}

/*
 * Open device.
 * Check for busy, memory, etc.
 */
audio_open(dev, oflags, devtype, p)
	dev_t           dev;
	int             oflags, devtype;
	struct proc    *p;
{
	int             unit, ch;

	unit = minor(dev);
	if (unit < 0 || unit >= NAUDIO)
		return (ENXIO);

	if (!au_soft[unit].buf)
		return (ENOMEM);

	/* check if we can use the channels */
	for (ch = 0; ch < NCHAN; ch++) {
		if ((1 << ch) & audio_channel[unit])
			if (channel_busy[ch])
				return (EBUSY);
	}

	/* mark channels as busy */
	for (ch = 0; ch < NCHAN; ch++) {
		if ((1 << ch) & audio_channel[unit])
			channel_busy[ch] = unit;
	}

	return (0);
}

/*
 * close device.
 * mark channels as free
 */
audio_close(dev, fflag, devtype, p)
	dev_t           dev;
	int             fflag, devtype;
	struct proc    *p;
{
	int             unit, ch;

	unit = minor(dev);
	if (unit < 0 || unit >= NAUDIO)
		return (ENXIO);

	/* mark channels as free */
	for (ch = 0; ch < NCHAN; ch++) {
		if ((1 << ch) & audio_channel[unit])
			channel_busy[ch] = 0;
	}

	return (0);
}

/*
 * convert buffer to native format.
 */
format_buf(format, buf, len)
	int             format, len;
	u_char         *buf;
{
	/*
         * native format
         */
	if (format == AUDIO_ENCODING_SIGNED)
		return;

	while (len--) {
		switch (format) {
		case AUDIO_ENCODING_LINEAR:
			*buf = *buf + 128;
			break;

		case AUDIO_ENCODING_ULAW:
			*buf = ulaw_to_lin[*buf];
			break;
		case AUDIO_ENCODING_ALAW:
			/* shouldnt happen */
			printf("ALAW not supported yet\n");
			return(-1);
			break;
		}
		buf++;
	}
}

/*
 * interupt handler for channel.
 * wake up unit so it can play next segment
 */
audio_wakeup(ch)
	int             ch;
{
	int             unit = channel_busy[ch];

	if (au_soft[unit].sleeping == 1)
		wakeup(&au_soft[unit]);
	au_soft[unit].sleeping = 2;
}

/*
 * write to audio device
 */
audio_write(dev, uio, ioflag)
	dev_t           dev;
	struct uio     *uio;
	int             ioflag;
{
	int             unit, ch;
	int             len, err, firstblk, s;
	void           *t;
	extern int      defchannel_handler();

	unit = minor(dev);
	if (unit < 0 || unit >= NAUDIO)
		return (ENXIO);

	/* stop anything that was playing previously */
	custom.dmacon = audio_channel[unit];

	/* read first buffer */
	len = uio->uio_resid;
	if (len > AUDIO_BUF_SIZE)
		len = AUDIO_BUF_SIZE;
	uiomove(au_soft[unit].buf, len, uio);
	format_buf(au_soft[unit].format, au_soft[unit].buf, len);

	/*
	 * set interupt handler, vol and per
	 */
	for (ch = 0; ch < NCHAN; ch++) {
		if ((1 << ch) & audio_channel[unit]) {
			channel[ch].handler = audio_wakeup;
			custom.aud[ch].per = au_soft[unit].per;
			custom.aud[ch].vol = au_soft[unit].volume;
		}
	}

	err = 0;
	firstblk = 1;
	au_soft[unit].sleeping = 0;
	while (len && !err) {
		/* block interupts until we are sleeping */
		s = spl_audio();

		/* play first buffer */
		for (ch = 0; ch < NCHAN; ch++) {
			if ((1 << ch) & audio_channel[unit]) {
				custom.aud[ch].len = len / 2;
				custom.aud[ch].lc = 
					PREP_DMA_MEM(au_soft[unit].buf);
			}
		}

		/* turn on interupts and dma */
		if (firstblk) {
			custom.intena = INTF_SETCLR | 
				(audio_channel[unit] << 7);
			custom.dmacon = DMAF_SETCLR | audio_channel[unit];
			firstblk = 0;
		}

		/*
		 * wait for interupt indicating that the registers are
		 * buffered
		 */
		if (au_soft[unit].sleeping == 0) {
			au_soft[unit].sleeping = 1;
			err = tsleep(&au_soft[unit], PCATCH | PZERO - 1, 
				"audio sleep", 0);
		}
		au_soft[unit].sleeping = 0;
		splx(s);

		/* read in second buffer */
		len = uio->uio_resid;
		if (len > AUDIO_BUF_SIZE)
			len = AUDIO_BUF_SIZE;
		if (len) {
			uiomove(au_soft[unit].buf2, len, uio);
			format_buf(au_soft[unit].format, au_soft[unit].buf2, 
				len);
		}

		/* swap buffers */
		t = au_soft[unit].buf;
		au_soft[unit].buf = au_soft[unit].buf2;
		au_soft[unit].buf2 = t;
	}

	/* wait for one more interupt while last segment plays */
	if (!err && au_soft[unit].sleeping == 0) {
		/*
	         * make sure we dont get extra noise at end if
	         * we dont turn off the channel quick enough
	         */
		s = spl_audio();
		bzero(au_soft[unit].buf2, 2);
		for (ch = 0; ch < NCHAN; ch++) {
			if ((1 << ch) & audio_channel[unit]) {
				custom.aud[ch].len = 2 / 2;
				custom.aud[ch].lc = 
					PREP_DMA_MEM(au_soft[unit].buf2);
			}
		}

		au_soft[unit].sleeping = 1;
		err = tsleep(&au_soft[unit], PCATCH | PZERO - 1, 
			"audio sleep", 0);
		au_soft[unit].sleeping = 0;
		splx(s);
	}

	/* turn off interupts and dma */
	custom.dmacon = audio_channel[unit];
	custom.intena = audio_channel[unit] << 7;

	/* restore default channel interupt handler */
	for (ch = 0; ch < NCHAN; ch++) {
		if ((1 << ch) & audio_channel[unit])
			channel[ch].handler = defchannel_handler;
	}

	return (err);
}

/*
 * ioctl on audio device
 */
audio_ioctl(dev, cmd, data, fflag, p)
	dev_t           dev;
	int             cmd, fflag;
	caddr_t         data;
	struct proc    *p;
{
	int             unit, format, err;
	audio_info_t   *aip = (audio_info_t *) data;

	unit = minor(dev);
	if (unit < 0 || unit >= NAUDIO)
		return (ENXIO);

	err = 0;
	switch (cmd) {
	case AUDIO_SETINFO:
		/* check validity of these fields */
		if(aip->mode != AUMODE_PLAY)
			return(EINVAL);
		if(aip->play.gain < AUDIO_MIN_GAIN ||
		   aip->play.gain > AUDIO_MAX_GAIN)
			return(EINVAL);
		switch(aip->play.encoding) {
		case AUDIO_ENCODING_ULAW:
		case AUDIO_ENCODING_LINEAR:
		case AUDIO_ENCODING_SIGNED:
			break;
		default:
			return(EINVAL);
		}
		
		au_soft[unit].format = aip->play.encoding;
		au_soft[unit].volume = GAIN_TO_VOL(aip->play.gain);
		set_freq(unit, aip->play.sample_rate);

		/* fall through to return the real info to the user */

	case AUDIO_GETINFO:
		bzero(aip, sizeof(audio_info_t));
		aip->mode = AUMODE_PLAY;
		aip->blocksize = AUDIO_BUF_SIZE;
		aip->play.sample_rate = au_soft[unit].freq;
		aip->play.channels = audio_channel[unit];
		aip->play.encoding = au_soft[unit].format;
		aip->play.gain = VOL_TO_GAIN(au_soft[unit].volume);
		break;

	default:
		err = EINVAL;
		break;
	}

	return (err);
}

