/*
 * Copyright (c) 1996 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 Network Research
 *	Group at Lawrence Berkeley National 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-win32.cc,v 1.10 96/05/14 06:44:53 van Exp $ (LBL)";

#include <assert.h>
#include <winsock.h>
#include <mmsystem.h>
#include <mmreg.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include "audio.h"
#include "ss.h"
#include "Tcl.h"

#define BLKS_PER_READ 4
#define READ_AHEAD 8
#define BLKS_PER_WRITE 4
#define WRITE_AHEAD 4

class audMux {
    public:
	MIXERCONTROLDETAILS select_[8];
	MIXERCONTROLDETAILS vol_[8];
	u_char mcnt_;
	u_char vcnt_;
	char mmap_[8];
	char vmap_[8];
	u_char isOut_;
};

class Win32Audio : public Audio {
    public:
	Win32Audio();
	~Win32Audio();
	void Write(u_char *);
	int FrameReady();
	u_char* Read();
	void SetRGain(int);
	void SetPGain(int);
	void OutputPort(int);
	void InputPort(int);
	void Obtain();
	void Release();
	void RMute();
	void RUnmute();
	int HalfDuplex() const;

    protected:
	int OpenOut();
	void CloseOut();
	int outErr(int) const;
	int OpenIn();
	void CloseIn();
	int inErr(int) const;
	int mapName(audMux& mux, const char* nm);
	int mapMixerPort(audMux& mux, const char* name);
	void setupMux(audMux& mux, DWORD ctype);
	void getMixerDetails(MIXERLINE&, MIXERCONTROL&, audMux&);
	void getMixerCtrls(MIXERLINE&, audMux&);

	HWAVEOUT out_;
	HWAVEIN  in_;

	u_char* rbuf_;
	u_char* rbufStart_;
	u_char* rbufEnd_;
	u_char* zbuf_;
	u_int lastmean_;
	u_int ibindx_;
	u_char* ibufStart_;
	u_char* ibufEnd_;

	u_char* obuf_;
	u_char* obufStart_;
	u_char* obufEnd_;

	u_short iblen_;
	u_short oblen_;

	WAVEHDR iwhdr_[READ_AHEAD];
	WAVEHDR owhdr_[BLKS_PER_WRITE * WRITE_AHEAD];
	const WAVEFORMATEX* iformat_;
	const WAVEFORMATEX* oformat_;

	audMux omux_;
	audMux imux_;
};

static class Win32AudioMatcher : public Matcher {
    public:
	Win32AudioMatcher() : Matcher("audio") {}
	TclObject* match(const char* fmt) {
		if (strcmp(fmt, "pc") == 0)
			return (new Win32Audio);
		return (0);
	}
} win32_audio_matcher;

extern void adios();
extern "C" const u_char lintomulawX[];
extern "C" const short mulawtolin[];

static const WAVEFORMATEX lin16fmt = {
	WAVE_FORMAT_PCM,
	1,
	8000,
	2 * 8000,
	2,
	16,
	0
};
static const WAVEFORMATEX lin8fmt = {
	WAVE_FORMAT_PCM,
	1,
	8000,
	8000,
	1,
	8,
	0
};


Win32Audio::Win32Audio() : 
	out_(0), 
	in_(0), 
	lastmean_(0)
{
	zbuf_ = new u_char[blksize];
	memset(zbuf_, ULAW_ZERO, blksize);

	u_int len = blksize * BLKS_PER_READ;
	rbufStart_ = new u_char[len];
	rbufEnd_ = rbufStart_ + len;
	rbuf_ = rbufEnd_;

	/*
	 * figure out what input format is available.
	 */
	int sts = waveInOpen(0, WAVE_MAPPER, &lin16fmt, 0, 0,
			     WAVE_FORMAT_QUERY);
	if (sts == WAVERR_BADFORMAT) {
		/* can't do 16 bit audio, try 8 bit */
		sts = waveInOpen(0, WAVE_MAPPER, &lin8fmt, 0, 0,
				 WAVE_FORMAT_QUERY);
		if (sts) {
			fprintf(stderr,
    "vat: soundcard supports neither 16 nor 8 bit 8KHz PCM audio input (%d)\n",
				sts);
			adios();
		}
		iformat_ = &lin8fmt;
		iblen_ = len;
		len *= READ_AHEAD;
		ibufStart_ = new u_char[len];
		ibufEnd_ = ibufStart_ + len;
	} else {
		iformat_ = &lin16fmt;
		iblen_ = len * sizeof(short);
		len *= READ_AHEAD;
		ibufStart_ = (u_char*)new short[len];
		ibufEnd_ = ibufStart_ + len * sizeof(short);
	}

	/*
	 * figure out what output format is available.
	 */
	sts = waveOutOpen(0, WAVE_MAPPER, &lin16fmt, 0, 0,
			  WAVE_FORMAT_QUERY);
	if (sts == WAVERR_BADFORMAT) {
		/* can't do 16 bit audio, try 8 bit */
		sts = waveOutOpen(0, WAVE_MAPPER, &lin8fmt, 0, 0,
				  WAVE_FORMAT_QUERY);
		if (sts) {
			fprintf(stderr,
    "vat: soundcard supports neither 16 nor 8 bit 8KHz PCM audio output (%d)\n",
				sts);
			adios();
		}
		oformat_ = &lin8fmt;
		len = blksize * BLKS_PER_WRITE;
		oblen_ = len;
		len *= WRITE_AHEAD;
		obufStart_ = new u_char[len];
		obufEnd_ = obufStart_ + len;
	} else {
		oformat_ = &lin16fmt;
		len = blksize * BLKS_PER_WRITE;
		oblen_ = len * sizeof(short);
		len *= WRITE_AHEAD;
		obufStart_ = (u_char*)new short[len];
		obufEnd_ = obufStart_ + len * sizeof(short);
	}

	if (mixerGetNumDevs()) {
		/* set up the mixer controls for input & out select & gain */
		memset(&imux_, 0, sizeof(imux_));
		memset(&imux_.mmap_, -1, sizeof(imux_.mmap_));
		setupMux(imux_, MIXERLINE_COMPONENTTYPE_DST_WAVEIN);
		int i;
		for (i = 0; i < sizeof(imux_.mmap_); ++i)
			if (iports < imux_.mmap_[i])
				iports = imux_.mmap_[i];

		memset(&omux_, 0, sizeof(omux_));
		memset(&omux_.mmap_, -1, sizeof(omux_.mmap_));
		omux_.isOut_ = 1;
		setupMux(omux_, MIXERLINE_COMPONENTTYPE_DST_SPEAKERS);
		for (i = 0; i < sizeof(omux_.mmap_); ++i)
			if (oports < omux_.mmap_[i])
				oports = omux_.mmap_[i];
	}
}

Win32Audio::~Win32Audio()
{
	CloseIn();
	CloseOut();

	delete zbuf_;
	delete rbufStart_;
	delete ibufStart_;
	delete obufStart_;
}

int Win32Audio::HalfDuplex() const
{
	/*XXX*/
	return 1;
}

int Win32Audio::inErr(int error) const
{
	if (error) {
		char errorText[MAXERRORLENGTH];
		waveInGetErrorText(error, errorText, sizeof(errorText));
		fprintf(stderr, "vat - input error: %s\n", errorText);
	}
	return (error);
}

int Win32Audio::outErr(int error) const
{
	if (error) {
		char errorText[MAXERRORLENGTH];
		waveOutGetErrorText(error, errorText, sizeof(errorText));
		fprintf(stderr, "vat - output error: %s\n", errorText);
	}
	return (error);
}

int Win32Audio::OpenOut()
{
	int error = 0;

	if (out_ == 0) {
		error = waveOutOpen(&out_, WAVE_MAPPER, oformat_,
				    NULL, NULL, CALLBACK_NULL);
				    
		/*
		 * Maybe we failed because someone is playing sound already.
		 * Shut any sound off then try once more before giving up.
		 */
		if (error) {
			sndPlaySound(NULL, 0);
			if (outErr(waveOutOpen(&out_, WAVE_MAPPER, oformat_,
					       NULL, NULL, CALLBACK_NULL))) {	       
				return 1;
			}
		}
		/* restore the gain to what the user set on this vat window */
		SetPGain(pgain);

		/* (re-)initialize the output buffer descriptors */
		memset(owhdr_, 0, sizeof(owhdr_));
		u_char* bp = obufStart_;
		obuf_ = bp;
		u_int len = oblen_;
		int i;
		for (i = 0; i < WRITE_AHEAD; ++i) {
			/*
			 * we mark the last hdr of the group of hdrs
			 * associated with this write block as 'writeable'
			 * (by setting its DONE bit) but make the address
			 * in the hdr indicate the start of the group of
			 * blocks.
			 */
			WAVEHDR* whp = &owhdr_[(i + 1) * BLKS_PER_WRITE - 1];
			whp->dwFlags = 0;
			whp->dwBufferLength = oblen_;
			whp->lpData = (char*)bp;
			outErr(waveOutPrepareHeader(out_, whp, sizeof(*whp)));
			whp->dwFlags |= WHDR_DONE;
			bp += len;
		}
		/*
		 * do initial write to generate a backlog to avoid
		 * dropouts due to scheduling delays.
		 */
		for (i = BLKS_PER_WRITE; --i >= 0; )
			Write(zbuf_);
	}
	return error;
}

int Win32Audio::OpenIn()
{
	if (in_ == 0) {
		if (inErr(waveInOpen(&in_, WAVE_MAPPER, iformat_,
				     NULL, NULL, CALLBACK_NULL)))
			return (1);

		/* restore the gain to what the user set on this vat window */
		SetRGain(rgain);

		/* (re-)initialize the input buffer descriptors */
		memset(iwhdr_, 0, sizeof(iwhdr_));
		ibindx_ = 0;
		rbuf_ = rbufEnd_;
		u_char* bp = ibufStart_;
		u_int len = iblen_;
		memset(bp, 0, len * READ_AHEAD);
		for (int i = 0; i < READ_AHEAD; ++i) {
			WAVEHDR* whp = &iwhdr_[i];
			whp->dwFlags = 0;
			whp->dwBufferLength = len;
			whp->lpData = (char*)bp;
			bp += len;
			waveInPrepareHeader(in_, whp, sizeof(*whp));
			if (inErr(waveInAddBuffer(in_, whp, sizeof(*whp)))) {
				CloseIn();
				return (1);
			}
		}
		waveInStart(in_);
	}
	return 0;
}

void Win32Audio::CloseOut()
{
	if (out_) {
		waveOutReset(out_);
		for (int i = 1; i < WRITE_AHEAD + 1; ++i) {
			WAVEHDR* whp = &owhdr_[i * BLKS_PER_WRITE - 1];
			if (whp->dwFlags & WHDR_PREPARED)
				waveOutUnprepareHeader(out_, whp, sizeof(*whp));
		}
		waveOutClose(out_);
		out_ = 0;
	}
}

void Win32Audio::CloseIn()
{
	if (in_) {
		waveInStop(in_);
		waveInReset(in_);
		for (int i = 0; i < READ_AHEAD; ++i) {
			WAVEHDR* whp = &iwhdr_[i];
			if (whp->dwFlags & WHDR_PREPARED)
				waveInUnprepareHeader(in_, whp, sizeof(*whp));
		}
		waveInClose(in_);
		in_ = 0;
	}
}

void Win32Audio::Obtain()
{
	int error;

	if (HaveAudio())
		abort();

	if (rmute & 1)
		error = OpenOut();
	else
		error = OpenIn();
	if (error)
		fd = -1;
	else {
		fd = 0;
		SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
	}
	notify();
}

void Win32Audio::Release()
{
	CloseOut();
	CloseIn();
	SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_NORMAL);
	Audio::Release();
}

void Win32Audio::Write(u_char *cp)
{
	if (out_) {
		/*
		 * copy the new data into our circular output buffer,
		 * converting from ulaw to linear as we go.
		 */
		WAVEHDR* whp;	
		if (oformat_ == &lin8fmt) {
			u_char* sp = obuf_;
			u_int bindx = (sp - obufStart_) / blksize;
			whp = &owhdr_[bindx];	
			u_char* ep = sp + blksize;
			const short* u2l = mulawtolin;
			while (sp < ep)
				*sp++ = (u2l[*cp++] >> 8) ^ 0x80;
			obuf_ = (ep >= obufEnd_)? obufStart_ : ep;
		} else {
			short* sp = (short*)obuf_;
			u_int bindx = (sp - (short*)obufStart_) / blksize;
			whp = &owhdr_[bindx];	
			short* ep = sp + blksize;
			const short* u2l = mulawtolin;
			while (sp < ep)
				*sp++ = u2l[*cp++];
			obuf_ = ((u_char*)ep >= obufEnd_)? obufStart_ : (u_char*)ep;
		}

		/*
		 * if the buffer descriptor associated with this block
		 * is marked ready, ship it.
		 */
		if (whp->dwFlags & WHDR_DONE) {
			whp->dwFlags &=~ WHDR_DONE;
			outErr(waveOutWrite(out_, whp, sizeof(*whp)));
		}
	}
}

int Win32Audio::FrameReady()
{
	if (in_ && rbuf_ >= rbufEnd_) {
		/* mulaw conversion buffer is empty - see if a read finished */
		u_int i = ibindx_;
		WAVEHDR* whp = iwhdr_ + i;
		if ((whp->dwFlags & WHDR_DONE) == 0)
			return (0);

		/* read finished - move input to ulaw buffer */
		rbuf_ = rbufStart_;
		ibindx_ = (i + 1) % READ_AHEAD;

		const u_char* l2u = lintomulawX;
		u_int* ip = (u_int*)rbuf_;
		int smean = lastmean_;
		if (iformat_ == &lin8fmt) {
			u_char* sp = (u_char*)whp->lpData;
			u_char* ep = sp + whp->dwBytesRecorded;
			for ( ; sp < ep; sp += 4) {
				register int mean, dif;
				register u_int res;
				register int s0 = int(sp[0]) - 0x80 << 8;
				register int s1 = int(sp[1]) - 0x80 << 8;
				register int s2 = int(sp[2]) - 0x80 << 8;
				register int s3 = int(sp[3]) - 0x80 << 8;

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

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

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

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

				*ip++ = res;
			}
		} else {
			short* sp = (short*)whp->lpData;
			short* ep = (short*)((char*)sp + whp->dwBytesRecorded);
			for ( ; sp < ep; sp += 4) {
				register int mean, dif;
				register u_int res;
				register int s0 = sp[0];
				register int s1 = sp[1];
				register int s2 = sp[2];
				register int s3 = sp[3];

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

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

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

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

				*ip++ = res;
			}
		}
		lastmean_ = smean;
		whp->dwFlags &=~ WHDR_DONE;
		inErr(waveInAddBuffer(in_, whp, sizeof(*whp)));
	}
	return (1);
}

u_char* Win32Audio::Read()
{
	u_char* cp;

	if (in_) {
		cp = rbuf_;
		rbuf_ = cp + blksize;
	} else
		cp = zbuf_;
	return (cp);
}

void Win32Audio::SetRGain(int level)
{
	rgain = level;
	if (in_) {
		if (level > 255)
			level = 255;
		level <<= 8;
		MIXERCONTROLDETAILS& mcd = imux_.vol_[imux_.vmap_[iport]];
		for (u_int i = 0; i < mcd.cChannels; ++i)
		   ((MIXERCONTROLDETAILS_UNSIGNED*)mcd.paDetails + i)->dwValue =
			level;
		mixerSetControlDetails(0, &mcd, MIXER_SETCONTROLDETAILSF_VALUE);
	}
}

void Win32Audio::SetPGain(int level)
{
	pgain = level;
	if (out_) {
		if (level > 255)
			level = 255;
		level <<= 8;
		DWORD vol = level | (level << 16);
		outErr(waveOutSetVolume(out_, vol));
	}
}

void Win32Audio::OutputPort(int p)
{
	if (omux_.mmap_[p] >= 0) {
		oport = p;
		if (out_)
			mixerSetControlDetails(0,
				&omux_.select_[omux_.mmap_[p]],
				MIXER_SETCONTROLDETAILSF_VALUE);
	}
}

void Win32Audio::InputPort(int p)
{
	if (imux_.mmap_[p] >= 0) {
		iport = p;
		if (in_)
			mixerSetControlDetails(0,
				&imux_.select_[imux_.mmap_[p]],
				MIXER_SETCONTROLDETAILSF_VALUE);
	}
}

void Win32Audio::RMute()
{
	CloseIn();
	if (OpenOut() == 0)
		rmute |= 1;
}

void Win32Audio::RUnmute()
{
	CloseOut();
	if (OpenIn() == 0)
		rmute &=~ 1;
}

int Win32Audio::mapName(audMux& mux, const char* name)
{
	return (mux.isOut_? StrToOPort(name) : StrToIPort(name));
}

int Win32Audio::mapMixerPort(audMux& mux, const char* name)
{
	int i = mapName(mux, name);
	if (i < 0) {
		char nm[64];
		strcpy(nm, name);
		char* cp;
		while ((cp = strrchr(nm, ' ')) != 0) {
			*cp = 0;
			if ((i = mapName(mux, nm)) >= 0)
				break;
		}
	}
	return (i);
}

void Win32Audio::getMixerDetails(MIXERLINE& ml, MIXERCONTROL& mc, audMux& mux)
{
	MIXERCONTROLDETAILS mcd;

	mcd.cbStruct = sizeof(mcd);
	mcd.dwControlID = mc.dwControlID;
	mcd.cChannels = ml.cChannels;
	mcd.cMultipleItems = mc.cMultipleItems;
	if (mcd.cMultipleItems) {
		MIXERCONTROLDETAILS_LISTTEXT mcdt[16];	
		mcd.cbDetails = sizeof(mcdt[0]);
		mcd.paDetails = mcdt;
		u_int sts = mixerGetControlDetails(0, &mcd,
					     MIXER_GETCONTROLDETAILSF_LISTTEXT);
		if (sts == 0) {
			for (u_int i = 0; i < mc.cMultipleItems; ++i) {
				int port = mapMixerPort(mux, mcdt[i].szName);
				if (port >= 0)
					mux.mmap_[port] = i;

				u_int n = mcd.cMultipleItems * mcd.cChannels;
				MIXERCONTROLDETAILS_BOOLEAN* mcdb =
					new MIXERCONTROLDETAILS_BOOLEAN[n];
				memset(mcdb, 0, n * sizeof(*mcdb));
				for (u_int j = 0; j < mcd.cChannels; ++j)
					mcdb[j*mc.cMultipleItems+i].fValue = 1;
					
				mux.select_[i] = mcd;
				mux.select_[i].cbDetails = sizeof(*mcdb);
				mux.select_[i].paDetails = mcdb;
			}
			mux.mcnt_ = (u_char)mcd.cMultipleItems;
		}
	} else {
		int i = mux.vcnt_++;
		int port = mapMixerPort(mux, ml.szName);
		if (port >= 0)
			mux.vmap_[port] = i;

		MIXERCONTROLDETAILS_UNSIGNED* mcdu =
				new MIXERCONTROLDETAILS_UNSIGNED[mcd.cChannels];
		memset(mcdu, 0, mcd.cChannels * sizeof(*mcdu));
		mux.vol_[i] = mcd;
		mux.vol_[i].cbDetails = sizeof(*mcdu);
		mux.vol_[i].paDetails = mcdu;
	}
}

void Win32Audio::getMixerCtrls(MIXERLINE& ml, audMux& mux)
{
	MIXERLINECONTROLS mlc;
	MIXERCONTROL mc[16];
	u_int i;

	memset(&mlc, 0, sizeof(mlc));
	memset(mc, 0, sizeof(mc));
	mlc.cbStruct = sizeof(mlc);
	mlc.cbmxctrl = sizeof(mc[0]);
	mlc.pamxctrl = &mc[0];
	mlc.dwLineID = ml.dwLineID;
	mlc.cControls = ml.cControls;
	mixerGetLineControls(0, &mlc, MIXER_GETLINECONTROLSF_ALL);
	for (i = 0; i < mlc.cControls; ++i) {
		switch (mc[i].dwControlType) {

		case MIXERCONTROL_CONTROLTYPE_MUX:
		case MIXERCONTROL_CONTROLTYPE_MIXER:
		case MIXERCONTROL_CONTROLTYPE_VOLUME:
			getMixerDetails(ml, mc[i], mux);
			break;
		}
	}
	/*
	 * if there are multiple source lines for this line,
	 * get their controls
	 */
	for (i = 0; i < ml.cConnections; ++i) {
		MIXERLINE src;
		memset(&src, 0, sizeof(src));
		src.cbStruct = sizeof(src);
		src.dwSource = i;
		src.dwDestination = ml.dwDestination;
		if (mixerGetLineInfo(0, &src, MIXER_GETLINEINFOF_SOURCE) == 0)
			getMixerCtrls(src, mux);
	}
}

void Win32Audio::setupMux(audMux& mux, DWORD ctype)
{
	MIXERLINE l;
	memset(&l, 0, sizeof(l));
	l.cbStruct = sizeof(l);
	l.dwComponentType = ctype;
	int s = mixerGetLineInfo(0, &l, MIXER_GETLINEINFOF_COMPONENTTYPE);
	if (s == 0)
		getMixerCtrls(l, mux);
}
