/*
 * Copyright (c) 1991-1993 Regents of the University of California.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *	This product includes software developed by the Computer Systems
 *	Engineering Group at Lawrence Berkeley Laboratory.
 * 4. Neither the name of the University nor of the Laboratory may be used
 *    to endorse or promote products derived from this software without
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
static const char rcsid[] =
    "@(#) $Header: audio-sun.cc,v 1.24 96/05/03 01:23:30 van Exp $ (LBL)";

#undef resource_h

#ifdef __svr4__
#include <sys/audioio.h>
#include <sys/filio.h>
#else
#include <sun/audioio.h>
#endif
#include <unistd.h>
#include <stropts.h>
#include <errno.h>
#include <fcntl.h>
#include "audio-sun.h"

class SUNDBRIAudio : public SUNAudio {
    public:
	SUNDBRIAudio(const char* device);
	virtual u_char* Read();
    protected:

	int lastmean_[4];
};

class SUNXAudio : public SUNDBRIAudio {
    public:
	SUNXAudio(const char* device);
	virtual u_char* Read();
	virtual void Write(u_char*);
    protected:
	virtual void setup_device();

	u_char* ubuf;
};

static class SunAudioMatcher : public Matcher {
    public:
	SunAudioMatcher() : Matcher("audio") {}
	TclObject* match(const char* id) {
		if (strcasecmp(id, "sun") == 0)
			return create();
		return (0);
	}
	Audio* create();
} sunaudio_matcher;

Audio* SunAudioMatcher::create()
{
	Tcl& tcl = Tcl::instance();
	const char* device = tcl.attr("audioFileName");
	int i = 0;
	if (strcmp(device, "/dev/audio") == 0 &&
	    (i = open("/dev/audioctl", O_RDONLY, 0)) >= 0) {
		audio_info_t info;
		AUDIO_INITINFO(&info);
		int j = ioctl(i, AUDIO_GETINFO, &info);
		close(i);
		if (j != -1 &&
		    info.play.avail_ports > 3 &&
		    info.play.avail_ports < 32) {
			/*
			 * if the driver is advanced enough to let us set
			 * the blocksize, use the 'sunx' driver which reads
			 * in linear to avoid the ulaw quantization noise
			 * from the dc offset on the mike & line inputs.
			 * If we can't set the blocksize, use the 'dbri'
			 * driver which just reads in ulaw (if we can't
			 * set the blocksize & switch to linear, sun's
			 * braindead driver will supply a weird, huge
			 * blocksize).
			 */
#ifndef __svr4__
			int bs = info.record._xxx[2];
#else
			int bs = info.record.buffer_size;
#endif
			return ((bs != -1 && bs != 0)? 
				  new SUNXAudio(device) :
				  new SUNDBRIAudio(device));
		}
	}
	return (new SUNAudio(device));
}

SUNAudio::SUNAudio(const char* device)
{
	iports = 1;
	oports = 2;

	audio_info_t* i = new audio_info_t;
	state = i;
	AUDIO_INITINFO(i);

	rgain = 0;
	rbalance = 32;
	pgain = 0;
	pbalance = 32;
	mgain = 0;

	bufcur = buf = new u_char[blksize];
	bufend = buf + blksize;
	device_ = new char[strlen(device) + 1];
	strcpy(device_, device);
}

int SUNAudio::command(int argc, const char*const* argv)
{
	Tcl& tcl = Tcl::instance();
	if (argc == 3) {
		if (strcmp(argv[1], "input") == 0) {
			if (strcmp(argv[2], "balance") == 0) {
				sprintf(tcl.result(), "%s %d",
					IPortToStr(iport), rbalance);
				return (TCL_OK);
			}
		} else if (strcmp(argv[1], "output") == 0) {
			if (strcmp(argv[2], "balance") == 0) {
				sprintf(tcl.result(), "%s %d",
					OPortToStr(oport), pbalance);
				return (TCL_OK);
			}
		} else if (strcmp(argv[1], "monitor") == 0) {
			if (strcmp(argv[2], "gain") == 0) {
				sprintf(tcl.result(), "monitor %d", mgain);
				return (TCL_OK);
			}
		}
	} else if (argc == 4) {
		if (strcmp(argv[1], "input") == 0) {
			if (strcmp(argv[2], "balance") == 0) {
				SetRBalance(atoi(argv[3]));
				return (TCL_OK);
			}
		} else if (strcmp(argv[1], "output") == 0) {
			if (strcmp(argv[2], "balance") == 0) {
				SetPBalance(atoi(argv[3]));
				return (TCL_OK);
			}
		} else if (strcmp(argv[1], "monitor") == 0) {
			if (strcmp(argv[2], "gain") == 0) {
				SetMGain(atoi(argv[3]));
				return (TCL_OK);
			}
		}
	}
	return (Audio::command(argc, argv));
}

int SUNAudio::OPort(int p)
{
	switch(p) {
	default:
	case output_speaker:
		return (AUDIO_SPEAKER);
	case output_phones:
		return (AUDIO_HEADPHONE);
	case output_line:
		return (AUDIO_LINE_OUT);
	}
}

int SUNAudio::IPort(int p)
{
	switch(p) {
	default:
	case input_mike:
		return (AUDIO_MICROPHONE);
	case input_line:
		return (AUDIO_LINE_IN);
	}
}

void SUNAudio::Obtain()
{
	if (!HaveAudio()) {
		/*
		 * try to save the current audio state (we have to
		 * do this before opening /dev/audio because the open
		 * will wipe out the sample rate & encoding).
		 */
		int ctl = open("/dev/audioctl", O_RDONLY, 0);
		if (ctl >= 0) {
			audio_info_t* i = (audio_info_t*)state;
			AUDIO_INITINFO(i);
			ioctl(ctl, AUDIO_GETINFO, i);
			close(ctl);
		}
		fd = open(device_, O_RDWR|O_NDELAY);
		if (fd >= 0) {
			setup_device();
			Audio::Obtain();
		}
	}
}

void SUNAudio::Release()
{
	if (HaveAudio()) {
		/* restore the audio state to what it was on entry */
		setinfo((audio_info_t*)state);
		Audio::Release();
	}
}

void SUNAudio::Write(u_char *cp)
{
	register int len = blksize;
	int cc = write(fd, (char *)cp, len);
	if ((len -= cc) != 0) {
		do {
			if (cc < 0) {
				if (errno != EPERM)
					perror("audio write");
				return;
			}
			cp += cc;
			cc = write(fd, (char *)cp, len);
			len -= cc;
		} while (len > 0);
	}
}

int SUNAudio::FrameReady()
{
	register int len = bufend - bufcur;
	while (len > 0) {
		int cc = read(fd, (char *)bufcur, len);
		if (cc < 0) {
			switch (errno) {
			case EINVAL:
				/* probably wrapped file pos. */
				lseek(fd, 0, SEEK_SET);
				cc = 0;
				break;

			case EPERM:
				/* probably lost audio */
				Release();
				Obtain();
				goto out;

#ifndef __svr4__
			case EWOULDBLOCK:
#endif
			case EAGAIN:
				/* should warn about audio_bsize here */
				goto out;

			default:
				perror("audio read");
				Release();
				Obtain();
				return (0);
			}
		}
		bufcur += cc;
		len -= cc;
	}
    out:
	return (bufcur >= bufend);
}

u_char* SUNAudio::Read()
{
	bufcur = buf;
	return (buf);
}

void SUNAudio::setup_device()
{
	audio_info_t info;
	if (fcntl(fd, F_SETFL, O_NONBLOCK|O_NDELAY) < 0)
		perror("audio O_NONBLOCK");
	int on = 1;
	ioctl(fd, FIONBIO, &on);
#ifndef __svr4__
	int bsddrvr = 0;
	AUDIO_INITINFO(&info);
	getinfo(&info);
	if (info._yyy[0] == 0)
		bsddrvr = 1;
#endif
	AUDIO_INITINFO(&info);
	info.play.sample_rate = 8000; /*XXX*/
	info.play.channels = 1; /*XXX*/
	info.play.precision = 8; /*XXX*/
	info.play.encoding = AUDIO_ENCODING_ULAW; /*XXX*/
	info.play.gain = pgain;
	info.play.port = OPort(oport);
	info.play.balance = pbalance;

	info.record.sample_rate = 8000; /*XXX*/
	info.record.channels = 1; /*XXX*/
	info.record.precision = 8; /*XXX*/
	info.record.encoding = AUDIO_ENCODING_ULAW; /*XXX*/
	info.record.gain = rgain;
	info.record.port = IPort(iport);
	info.record.balance = rbalance;
#ifndef __svr4__
	/* solaris 2.2 hack: set buffer size.
	 * NB- this changes to _xxx[3] for os<4.1.3 */
	info.record._xxx[2] = blksize;
	if (bsddrvr) {
		/* bsd driver hack - set buffer size */
		info._yyy[0] = blksize;
	} else {
		/* 4.1.3 bug workaround: pause then resume */
		info.record.pause = 1;
		setinfo(&info);
		info.record.pause = 0;
	}
#else
	info.record.buffer_size = blksize;
#endif
	info.monitor_gain = mgain;
	setinfo(&info);
	/* flush input to get rid of any data fragments */
	ioctl(fd, I_FLUSH, FLUSHR);
	bufcur = buf;
}


int SUNAudio::GainClip(int level)
{
        if (level < AUDIO_MIN_GAIN)
                return AUDIO_MIN_GAIN;
        else if (level > AUDIO_MAX_GAIN)
                return AUDIO_MAX_GAIN;
        else
                return level;
}

int SUNAudio::getinfo(audio_info_t* info)
{
	int sts;
	if (fd < 0)
		sts = 0;
	else
		sts = ioctl(fd, AUDIO_GETINFO, info);
	return (sts);
}

int SUNAudio::setinfo(audio_info_t* info)
{
	int sts;
	if (fd < 0)
		sts = 0;
	else
		sts = ioctl(fd, AUDIO_SETINFO, info);
	return (sts);
}

void SUNAudio::SetRGain(int level)
{
	audio_info_t info;

	rgain = GainClip(level);
	AUDIO_INITINFO(&info);
	info.record.gain = rgain;
	setinfo(&info);
}

void SUNAudio::SetPGain(int level)
{
	audio_info_t info;

	pgain = GainClip(level);
	AUDIO_INITINFO(&info);
	info.play.gain = pgain;
	setinfo(&info);
}

void SUNAudio::SetRBalance(int level)
{
	audio_info_t info;

	rbalance = GainClip(level);
	AUDIO_INITINFO(&info);
	info.record.balance = rbalance;
	setinfo(&info);
}

void SUNAudio::SetMGain(int level)
{
	audio_info_t info;

	mgain = GainClip(level);
	AUDIO_INITINFO(&info);
	info.monitor_gain = mgain;
	setinfo(&info);
}

void SUNAudio::SetPBalance(int level)
{
	audio_info_t info;

	pbalance = GainClip(level);
	AUDIO_INITINFO(&info);
	info.play.balance = pbalance;
	setinfo(&info);
}

void SUNAudio::OutputPort(int p)
{
	audio_info_t info;

	oport = p;
	AUDIO_INITINFO(&info);
	info.play.port = OPort(p);
	setinfo(&info);
}

void SUNAudio::InputPort(int p)
{
	audio_info_t info;

	iport = p;
	AUDIO_INITINFO(&info);
	info.record.port = IPort(p);
	setinfo(&info);
}


extern const short mulawtolin[];
extern const unsigned char lintomulaw[];
extern const unsigned char lintomulawX[];

SUNDBRIAudio::SUNDBRIAudio(const char* device) : SUNAudio(device)
{
	/* XXX - hardwired for dbri speakerbox */
	iports = 2;
	oports = 3;
	lastmean_[0] = 0;
	lastmean_[1] = 0;
	lastmean_[2] = 0;
	lastmean_[3] = 0;
}

u_char* SUNDBRIAudio::Read()
{
	register u_char* cp = buf;

	/*
	 * for some reason, SUN didn't bother to filter out the
	 * mike 'phantom power' DC signal so we end up with a
	 * large DC offset that screws up the lin-to-mu conversion
	 * and the speakerphone power calculations.  So, all the
	 * hair in the following loop is a low-pass filter with ~1Hz
	 * passband to estimate the DC offset.
	 */
	register u_int* ip = (u_int*)cp;
	register u_int* ep = (u_int*)(cp + blksize);
	register int smean = lastmean_[iport];
	register const short* u2l = mulawtolin;
	register const u_char* l2u = lintomulaw;
	if (iport != 0) {
		while (ip < ep) {
			register int mean, dif;
			register u_int res;
			register u_int dat = *ip;
			register int s0 = u2l[dat >> 24];
			register int s1 = u2l[(dat >> 16) & 0xff];
			register int s2 = u2l[(dat >> 8) & 0xff];
			register int s3 = u2l[dat & 0xff];

			mean = smean >> 13;
			dif = s0 - mean;
			smean += dif;
			res = l2u[dif & 0xffff] << 24;

			mean = smean >> 13;
			dif = s1 - mean;
			smean += dif;
			res |= l2u[dif & 0xffff] << 16;

			mean = smean >> 13;
			dif = s2 - mean;
			smean += dif;
			res |= l2u[dif & 0xffff] << 8;

			mean = smean >> 13;
			dif = s3 - mean;
			smean += dif;
			res |= l2u[dif & 0xffff];

			*ip++ = res;
		}
	} else {
		while (ip < ep) {
			register int mean, dif;
			register u_int res;
			register u_int dat = *ip;
			register int s0 = u2l[dat >> 24];
			register int s1 = u2l[(dat >> 16) & 0xff];
			register int s2 = u2l[(dat >> 8) & 0xff];
			register int s3 = u2l[dat & 0xff];

			mean = smean >> 13;
			dif = s0 - mean;
			smean += dif;
			res = l2u[(dif >> 1) & 0xffff] << 24;

			mean = smean >> 13;
			dif = s1 - mean;
			smean += dif;
			res |= l2u[(dif >> 1) & 0xffff] << 16;

			mean = smean >> 13;
			dif = s2 - mean;
			smean += dif;
			res |= l2u[(dif >> 1) & 0xffff] << 8;

			mean = smean >> 13;
			dif = s3 - mean;
			smean += dif;
			res |= l2u[(dif >> 1) & 0xffff];

			*ip++ = res;
		}
	}
	lastmean_[iport] = smean;
	bufcur = cp;
	return (cp);
}


SUNXAudio::SUNXAudio(const char* device) : SUNDBRIAudio(device)
{
	ubuf = bufcur;
	bufcur = buf = new u_char[blksize << 1];
	bufend = buf + (blksize << 1);
}

u_char* SUNXAudio::Read()
{
	/*
	 * for some reason, SUN didn't bother to filter out the
	 * mike 'phantom power' DC signal so we end up with a
	 * large DC offset that screws up the lin-to-mu conversion
	 * and the speakerphone power calculations.  So, all the
	 * hair in the following loop is a low-pass filter with ~1Hz
	 * passband to estimate the DC offset.  To avoid the
	 * quantization noise from unbiasing a mulaw signal, we
	 * have to read & write 8KHz linear.
	 */
	register short* ip = (short*)buf;
	register short* ep = ip + blksize;
	register u_int* op = (u_int*)ubuf;
	register int smean = lastmean_[iport];
	register const u_char* l2u = lintomulawX;
	while (ip < ep) {
		register int mean, dif;
		register u_int res;
		register int s0 = ip[0];
		register int s1 = ip[1];
		register int s2 = ip[2];
		register int s3 = ip[3];

		mean = smean >> 13;
		dif = s0 - mean;
		smean += dif;
		res = l2u[dif & 0x1ffff] << 24;

		mean = smean >> 13;
		dif = s1 - mean;
		smean += dif;
		res |= l2u[dif & 0x1ffff] << 16;

		mean = smean >> 13;
		dif = s2 - mean;
		smean += dif;
		res |= l2u[dif & 0x1ffff] << 8;

		mean = smean >> 13;
		dif = s3 - mean;
		smean += dif;
		res |= l2u[dif & 0x1ffff];

		*op++ = res;
		ip += 4;
	}
	lastmean_[iport] = smean;
	bufcur = buf;
	return (ubuf);
}


void SUNXAudio::Write(u_char *cp)
{
	register u_int len = blksize;
	u_int samps[MAXAUDIOSIZE/2];
	register u_int* sp = samps;
	register u_int* ep = sp + len / 2;
	register const u_short* u2l = (u_short*)mulawtolin;
	register u_int* ip = (u_int*)cp;
	for ( ; sp < ep; sp += 4) {
		register u_int s = *ip++;
		sp[0] = (u2l[(s >> 24) & 0xff] << 16) | u2l[(s >> 16) & 0xff];
		sp[1] = (u2l[(s >> 8) & 0xff] << 16) | u2l[s & 0xff];
		s = *ip++;
		sp[2] = (u2l[(s >> 24) & 0xff] << 16) | u2l[(s >> 16) & 0xff];
		sp[3] = (u2l[(s >> 8) & 0xff] << 16) | u2l[s & 0xff];
	}
	write(fd, (char *)samps, len << 1);
}

void SUNXAudio::setup_device()
{
	SUNAudio::setup_device();

	audio_info_t info;
	AUDIO_INITINFO(&info);
	info.record.precision = 16;
	info.record.encoding  = AUDIO_ENCODING_LINEAR;
#ifndef __svr4__
	info.record._xxx[2]   = blksize << 1;
#else
	info.record.buffer_size = blksize << 1;
#endif
	info.play.precision   = 16;
	info.play.encoding    = AUDIO_ENCODING_LINEAR;
	setinfo(&info);
	/* flush input again to get rid of any data with wrong encoding */
	ioctl(fd, I_FLUSH, FLUSHR);
	bufcur = buf;
}
