/*
 * Copyright (c) 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.
 */

#ifndef lint
static const char rcsid[] =
    "@(#) $Header: audio-hp.cc,v 1.9 96/03/18 02:54:57 van Exp $ (LBL)";
#endif

#include "config.h"
#include <sys/socket.h>
#include <sys/audio.h>
#include <fcntl.h>
#include "audio.h"

#define ABUFSIZE 4096
#define ABUFLEN (ABUFSIZE/sizeof(short))

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

class HPAudio : public Audio {
    public:
	HPAudio();
	virtual int FrameReady();
	virtual u_char* Read();
	virtual	void Write(u_char *);
	virtual void SetRGain(int);
	virtual void SetPGain(int);
	virtual void InputPort(int);
	virtual void OutputPort(int);
	virtual void Obtain();
    protected:
	friend class HPAudioMatcher;
	int lastmean[4];

	short* readptr;
	short* readbufend;
	short* readbuf;

	u_char* ubufptr;
	u_char* ubufend;
	u_char* ubuf;

	short* writeptr;
	short* writebufend;
	short* writebuf;

	float rgain_scale;
	float rgain_min;
	float pgain_scale;
	float pgain_min;
};

class HP2160Audio : public HPAudio {
    public:
	HP2160Audio();
	virtual int FrameReady();
	virtual	void Write(u_char *);
	virtual void Obtain();
};

static class HPAudioMatcher : public Matcher {
    public:
	HPAudioMatcher() : Matcher("audio") {}
	TclObject* match(const char* fmt);
} hpaudio_matcher;

TclObject* HPAudioMatcher::match(const char* fmt)
{
	if (strcasecmp(fmt, "hp") != 0)
		return (0);

	int cfd = open("/dev/audioCtl", O_RDONLY, 0);
	if (cfd < 0) {
		perror("/dev/audioCtl");
		exit(1);
	}

	/* get gain & channel config */
	audio_describe ad;
	if (ioctl(cfd, AUDIO_DESCRIBE, &ad) < 0) {
		perror("AUDIO_DESCRIBE");
		exit(1);
	}
	close(cfd);
	HPAudio* aud;
	if (ad.audio_id == AUDIO_ID_PSB2160)
		aud = new HP2160Audio;
	else
		aud = new HPAudio;

	aud->rgain_scale = float(ad.max_receive_gain-ad.min_receive_gain)/255.;
	aud->rgain_min = ad.min_receive_gain;
	aud->pgain_scale = float(ad.max_transmit_gain-ad.min_transmit_gain)/255.;
	aud->pgain_min = ad.min_transmit_gain;
	return (aud);
}

HPAudio::HPAudio()
{
	iports = 2;
	oports = 3;

	lastmean[0] = 0;
	lastmean[1] = 0;
	lastmean[2] = 0;
	lastmean[3] = 0;

	readbuf = new short[ABUFLEN];
	readptr = readbufend = readbuf + ABUFLEN;

	writeptr = writebuf = new short[ABUFLEN];
	writebufend = writebuf + ABUFLEN;

	ubufptr = ubuf = new u_char[blksize];
	ubufend = ubuf + blksize;
}

void HPAudio::Obtain()
{
	if (HaveAudio())
		abort();

	fd = open("/dev/audio", O_RDWR, 0);
	if (fd >= 0) {
		/*
		 * To get around the
		 * problem that the hardware can only do 4K read/writes
		 * (half a second for 8KHz ulaw), we run in 16bit linear
		 * stereo mode to get the latency down to `only' 125ms.
		 * After setting up the format (which has to be done
		 * before the driver buffers are allocated),
		 * make sure we do non-blocking reads then allocate
		 * about a second of buffer space.
		 */
		if (ioctl(fd, AUDIO_SET_DATA_FORMAT, AUDIO_FORMAT_LINEAR16BIT) < 0) {
			perror("AUDIO_SET_DATA_FORMAT");
			exit(1);
		}
		if (ioctl(fd, AUDIO_SET_CHANNELS, 2) < 0) {
			perror("AUDIO_SET_CHANNELS");
			exit(1);
		}
		/*
		 * the default sample rate is supposed to be 8khz but
		 * there's a kernel bug that keeps it from being set so
		 * we inherit whatever the last used.  so set it to what
		 * we need.
		 */
		if (ioctl(fd, AUDIO_SET_SAMPLE_RATE, 8000) < 0) {
			perror("AUDIO_SET_SAMPLE_RATE");
			exit(1);
		}
		readptr = readbufend;
		writeptr = writebuf;
		SetRGain(rgain);
		SetPGain(pgain);
		OutputPort(oport);
		InputPort(iport);
		if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0) {
			perror("audio O_NONBLOCK");
			exit(1);
		}
		if (ioctl(fd, AUDIO_SET_RXBUFSIZE, 32768) < 0) {
			perror("AUDIO_SET_RXBUFSIZE");
			exit(1);
		}
		if (ioctl(fd, AUDIO_SET_TXBUFSIZE, 32768) < 0) {
			perror("AUDIO_SET_TXBUFSIZE");
			exit(1);
		}
		Audio::Obtain();
	}
}

void HPAudio::Write(u_char *cp)
{
	if (HaveAudio()) {
		register u_char *cpend = cp + blksize;
		register short *wbuf = writeptr;
		register short *wend = writebufend;
		for ( ; cp < cpend; cp += 4) {
			wbuf[1] = wbuf[0] = mulawtolin[cp[0]];
			wbuf[3] = wbuf[2] = mulawtolin[cp[1]];
			wbuf[5] = wbuf[4] = mulawtolin[cp[2]];
			wbuf[7] = wbuf[6] = mulawtolin[cp[3]];
			wbuf += 8;
			if (wbuf >= wend) {
				wbuf = writebuf;
				if (write(fd, (char*)wbuf, ABUFSIZE) != ABUFSIZE)
					perror("aud write");
			}
		}
		writeptr = wbuf;
	}
}

int HPAudio::FrameReady()
{
	register u_char* cp = ubufptr;
	register u_char* cpend = ubufend;
	register short* rbuf = readptr;
	register short* rend = readbufend;
	register int smean = lastmean[iport];

	if (!HaveAudio())
		return (0);

	for ( ; cp < cpend; cp += 4) {
		if (rbuf >= rend) {
			rbuf = readbuf;
			int cc = read(fd, (char*)rbuf, ABUFSIZE);
			if (cc <= 0) {
				/*
				 * The audio seems to occasionally
				 * lock up.  My guess is that
				 * scheduling delays occasionally
				 * cause us to get behind & the
				 * kernel audio input buffer
				 * overflows so the driver
				 * goes into 'pause' (a really
				 * stupid design decision on hp's
				 * part) and we have to manually
				 * unpause it or we get continuous
				 * EIO errors.
				 */
				ubufptr = cp;
				readbufend = rbuf;
				if (cc == -1 && errno != EAGAIN) {
					if (errno == EIO) {
						struct audio_status as;
						ioctl(fd, AUDIO_GET_STATUS,
							&as);
						if (as.receive_status ==
						    AUDIO_PAUSE) {
							ioctl(fd, AUDIO_RESUME,
								AUDIO_RECEIVE);
						}
					} else {
						Release();
						Obtain();
					}
				}
				return (0);
			}
			readbufend = rend = (short*)((u_char*)rbuf + cc);
		}
		/* 
		 * there's probably a dc offset due to the phantom
		 * power for the mike.  this has to be filtered out
		 * so we can do power calculations so we estimate the
		 * dc bias via an ~1HZ lowpass filter & subtract it out.
		 */
		register int mean, dif, res;

		register int r0 = (int(rbuf[0]) + int(rbuf[1])) >> 1;
		mean = smean >> 13;
		dif = r0 - mean;
		smean += dif;
	        res = lintomulawX[dif & 0x1ffff] << 24;

		register int r1 = (int(rbuf[2]) + int(rbuf[3])) >> 1;
		mean = smean >> 13;
		dif = r1 - mean;
		smean += dif;
	        res |= lintomulawX[dif & 0x1ffff] << 16;

		register int r2 = (int(rbuf[4]) + int(rbuf[5])) >> 1;
		mean = smean >> 13;
		dif = r2 - mean;
		smean += dif;
	        res |= lintomulawX[dif & 0x1ffff] << 8;

		register int r3 = (int(rbuf[6]) + int(rbuf[7])) >> 1;
		mean = smean >> 13;
		dif = r3 - mean;
		smean += dif;
	        res |= lintomulawX[dif & 0x1ffff];

		*(int*)cp = res;
		rbuf += 8;
	}
	lastmean[iport] = smean;
	readptr = rbuf;
	return (1);
}

u_char* HPAudio::Read()
{
	return (ubufptr = ubuf);
}

void HPAudio::SetRGain(int level)
{
	rgain = level;
	if (HaveAudio()) {
		struct audio_gain gain;
		if (ioctl(fd, AUDIO_GET_GAINS, &gain) < 0) {
			perror("AUDIO_GET_GAINS");
			exit(1);
		}
		int g = int(float(rgain) * rgain_scale + rgain_min);
		gain.cgain[0].receive_gain = g;
		gain.cgain[1].receive_gain = g;
		if (ioctl(fd, AUDIO_SET_GAINS, &gain) < 0) {
			perror("AUDIO_SET_GAINS");
			exit(1);
		}
	}
}

void HPAudio::SetPGain(int level)
{

	pgain = level;
	if (HaveAudio()) {
		struct audio_gain gain;
		if (ioctl(fd, AUDIO_GET_GAINS, &gain) < 0) {
			perror("AUDIO_GET_GAINS");
			exit(1);
		}
		int g = int(float(pgain) * pgain_scale + pgain_min);
		gain.cgain[0].transmit_gain = g;
		gain.cgain[1].transmit_gain = g;
		if (ioctl(fd, AUDIO_SET_GAINS, &gain) < 0) {
			perror("AUDIO_SET_GAINS");
			exit(1);
		}
	}
}

void HPAudio::InputPort(int p)
{
	iport = p;
	if (HaveAudio()) {
		ioctl(fd, AUDIO_SET_INPUT,
		      iport? AUDIO_IN_LINE : AUDIO_IN_MIKE);
	}
}

void HPAudio::OutputPort(int p)
{
	oport = p;
	if (HaveAudio()) {
		ioctl(fd, AUDIO_SET_OUTPUT,
		      oport == 0? AUDIO_OUT_SPEAKER :
				  oport == 1? AUDIO_OUT_EXTERNAL :
					      AUDIO_OUT_LINE);
	}
}

HP2160Audio::HP2160Audio()
{
	iports = 1;
	oports = 2;
}

void HP2160Audio::Obtain()
{
	if (HaveAudio())
		return;

	fd = open("/dev/audio", O_RDWR, 0);
	if (fd >= 0) {
		struct audio_select_thresholds selt;
		selt.write_threshold = selt.read_threshold = blksize * 4;
		if (ioctl(fd, AUDIO_SET_SEL_THRESHOLD, &selt) < 0) {
			perror("AUDIO_SET_SEL_THRESHOLD");
			exit(1);
		}
		if (ioctl(fd, AUDIO_SET_DATA_FORMAT, AUDIO_FORMAT_LINEAR16BIT) < 0) {
			perror("AUDIO_SET_DATA_FORMAT");
			exit(1);
		}
		if (ioctl(fd, AUDIO_SET_CHANNELS, 1) < 0) {
			perror("AUDIO_SET_CHANNELS");
			exit(1);
		}
		/*
		 * the default sample rate is supposed to be 8khz but
		 * there's a kernel bug that keeps it from being set so
		 * we inherit whatever the last used.  so set it to what
		 * we need.
		 */
		if (ioctl(fd, AUDIO_SET_SAMPLE_RATE, 8000) < 0) {
			perror("AUDIO_SET_SAMPLE_RATE");
			exit(1);
		}
		readptr = readbufend;
		writeptr = writebuf;
		SetRGain(rgain);
		SetPGain(pgain);
		OutputPort(oport);
		InputPort(iport);
		if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0) {
			perror("audio O_NONBLOCK");
			exit(1);
		}
		if (ioctl(fd, AUDIO_SET_RXBUFSIZE, 32768) < 0) {
			perror("AUDIO_SET_RXBUFSIZE");
			exit(1);
		}
		if (ioctl(fd, AUDIO_SET_TXBUFSIZE, 32768) < 0) {
			perror("AUDIO_SET_TXBUFSIZE");
			exit(1);
		}
		notify();
	}
}

void HP2160Audio::Write(u_char *cp)
{
	if (HaveAudio()) {
		register u_char *cpend = cp + blksize;
		register short *wbuf = writebuf;
		for ( ; cp < cpend; cp += 4) {
			wbuf[0] = mulawtolin[cp[0]];
			wbuf[1] = mulawtolin[cp[1]];
			wbuf[2] = mulawtolin[cp[2]];
			wbuf[3] = mulawtolin[cp[3]];
			wbuf += 4;
		}
		int cc = write(fd, (char *)writebuf, wbuf - writebuf);
		if (cc < 0 && errno != EPERM)
			perror("audio write");
	}
}

int HP2160Audio::FrameReady()
{
	register u_char* cp = ubufptr;
	register u_char* cpend = ubufend;
	register short* rbuf = readptr;
	register short* rend = readbufend;
	register int smean = lastmean[iport];

	for ( ; cp < cpend; cp += 4) {
		if (rbuf >= rend) {
			rbuf = readbuf;
			int cc = read(fd, (char*)rbuf, ABUFSIZE);
			if (cc < 0) {
				if (errno == EINVAL)
					/* probably wrapped file pos. */
					lseek(fd, 0, SEEK_SET);
				ubufptr = cp;
				readbufend = rbuf;
				return (0);
			}
			readbufend = rend = (short*)((u_char*)rbuf + cc);
		}
		/* 
		 * there's probably a dc offset due to the phantom
		 * power for the mike.  this has to be filtered out
		 * so we can do power calculations so we estimate the
		 * dc bias via an ~1HZ lowpass filter & subtract it out.
		 */
		register int mean, dif, res;

		register int r0 = int(rbuf[0]);
		mean = smean >> 13;
		dif = r0 - mean;
		smean += dif;
	        res = lintomulawX[dif & 0x1ffff] << 24;

		register int r1 = int(rbuf[1]);
		mean = smean >> 13;
		dif = r1 - mean;
		smean += dif;
	        res |= lintomulawX[dif & 0x1ffff] << 16;

		register int r2 = int(rbuf[2]);
		mean = smean >> 13;
		dif = r2 - mean;
		smean += dif;
	        res |= lintomulawX[dif & 0x1ffff] << 8;

		register int r3 = int(rbuf[3]);
		mean = smean >> 13;
		dif = r3 - mean;
		smean += dif;
	        res |= lintomulawX[dif & 0x1ffff];

		*(int*)cp = res;
		rbuf += 4;
	}
	lastmean[iport] = smean;
	readptr = rbuf;
	return (1);
}
