//-----------------------------------------------------------------------------
// COPYRIGHT BY OERTEL & ZAHL SOFTWAREENTWICKLUNG GbR, BERLIN, GERMANY
//	file:		audio-ibm.cc
//	written by:	Christian Zahl
//	description:	Audio class for the IBM RS/6000 familiy using the
//			Ultimedia Audio Adapter (or the build-in in the
//			43P)
//	creation date:	1996/03/13
//
// $Id: audio-ibm.cc,v 1.4 1996/03/25 08:30:15 czahl Exp $
//
// Revision 1.4  1996/03/25  08:30:15  czahl
// - forgott to disable the hardware monitor after opening the audio device.
//
// Revision 1.3  1996/03/14  20:01:06  czahl
// - minor bug: the recFd and playFd were not initialized in IBMAudio::Obtain
// - minor cosmetic changes
//
// Revision 1.2  1996/03/14  10:59:58  czahl
// - this is the first alpha version for Mimi.
// - now we also added the other in- and output ports.
//
// Revision 1.1  1996/03/14  08:39:46  czahl
// Initial revision
// - After downloading the merged version of Vat with the NeVoT audio driver
//   support on 1996/03/13, I decided to hack my own real class for the
//   IBM audio decive. At midnight, we had the first stable version running
//   (I listened more than an hour to an Eurythmics CD via the loopback. :-)
//------------------------------------------------------------------------------
static const char rcsid[] =
	"@(#) $Header: audio-ibm.cc,v 1.3 96/05/01 21:04:27 van Exp $";

/*-----	standard includefiles ----------------------------------------------*/
#include <osfcn.h>
#include <stdio.h>
#include <errno.h>
#include <math.h>
#define _BSD_COMPAT 1
#include <sys/file.h>
#include <sys/audio.h>
#include <sys/acpa.h>

/*-----	user includefiles --------------------------------------------------*/
#include "Tcl.h"
#include "audio.h"

/*-----	defines ------------------------------------------------------------*/
#define	WITH_TRACE		1
#if WITH_TRACE
#define	TRACE(xxx)		xxx
#else
#define TRACE(xxx)
#endif

#define AUDIO_MIN_GAIN		0
#define AUDIO_MAX_GAIN		255

/*
 * Because we need some more definitions than normaly found in the "offical"
 * include file (sys/audio.h), we declare them here, if they heven't been
 * declared before.
 */
#ifndef AUDIO_MODIFY_LIMITS
#define AUX1			100
#define AUDIO_SET_GAIN		14
#define INTERNAL_SPEAKER_ON	10
#define INTERNAL_SPEAKER_OFF	11
#define AUDIO_MODIFY_LIMITS	20
typedef struct _audio_set_gain
{
	signed long left_gain;
	signed long right_gain;
} audio_set_gain;
typedef struct _init_buf_req
{
	long buffer_size;
	long play_block_count;
	long play_lower_limit;
	long cap_upper_limit;
	long byte_count;
	long return_code;
} init_buf_req;
#endif /* AUDIO_MODIFY_LIMITS */

/*-----	type definitions ---------------------------------------------------*/

/*-----	local functions ----------------------------------------------------*/

/*-----	global functions ---------------------------------------------------*/

/*-----	local variables ----------------------------------------------------*/

/*-----	global variables ---------------------------------------------------*/
extern const unsigned char	lintomulawX[];
extern const unsigned char	lintomulaw[];
extern const short		mulawtolin[];

/*------------------------------------------------------------------------*/
class IBMAudio : public Audio {
public:
				IBMAudio();
  virtual u_char*		Read();
  virtual void			Write(u_char *);
  virtual void			SetRGain(int);
  virtual void			SetPGain(int);
  virtual void			Obtain();
  virtual void			Release();
  virtual int			FrameReady ();
  virtual void			InputPort (int);
  virtual void			OutputPort (int);
  virtual void			SetMonitor (int);
protected:
	void			InitAudioChange ();
	void			InitAudioControl ();
	void			InitAudioDevice ();
	int			GainClip(int);
	int			recFd;
	int			playFd;
        audio_buffer            bufStat;
	audio_init		audioInit;
	audio_change		audioChange;
	audio_control		audioControl;
        init_buf_req            bufReq;
	u_char			*playBuf;			/* needed for delay reduction */
	short			*recBuf;			/* buffer for holding captured data */
	int			recBufSize;			/* # of bytes in the buffer */
	u_char			*readBuf;			/* buffer for delivering the "read" data */
	int			lastmean;
	int			contHighWaters;			/* # on continous hiwater marks reached */
};
/*------------------------------------------------------------------------*/
static class IBMAudioMatcher : public Matcher {
public:
	IBMAudioMatcher() : Matcher ("audio") {}
	TclObject* match (const char* id) {
		if (strcasecmp (id, "ibm") == 0)
			return (new IBMAudio);
		return 0;
	}
} ibmaudio_matcher;
/*------------------------------------------------------------------------*/
IBMAudio::IBMAudio()
{
	TRACE (fprintf (stderr, "IBMAudio\n");)
/* open (or create) the lock file */
	openlock();
	iports = 4;
	oports = 4;
	fd = -1;
	recFd = -1;
	playFd = -1;
	playBuf = new u_char[blksize];
	readBuf = new u_char[blksize];
	recBuf = new short[blksize];
} /* IBMAudio::IBMAudio */
/*------------------------------------------------------------------------*/
void IBMAudio::Obtain()
{
static	char			*recFn[] = {"/dev/baud0/1", "/dev/paud0/1", NULL};
static	char			*playFn[] = {"/dev/baud0/2", "/dev/paud0/2", NULL};
	int			i;

	TRACE (fprintf (stderr, "IBMAudio::Obtain\n");)
	if (HaveAudio())
		abort();
	if (lock() != 0)
		return;
	recBufSize = 0;
	contHighWaters = 0;
/*** search an valid audio device and open it ***/
	for (i=0; recFn[i] != NULL; i++)
		if ((recFd = open (recFn[i], O_RDONLY | O_NDELAY)) >= 0)
			break;
	if (recFn[i] == NULL) {
		fprintf(stderr, "vat: couldn't open any record port.\n");
		return;
		}
	TRACE (fprintf (stderr, "IBMAudio::Obtain %s %s\n", recFn[i], playFn[i]);)
	if ((playFd = open (playFn[i], O_WRONLY | O_NDELAY)) < 0) {
		close (recFd);
		recFd = -1;
		fprintf (stderr, "vat: cannot open playback port!\n");
		return;
		}
/*** initialize the device ***/
	InitAudioDevice ();
	fd = recFd;
	Audio::Obtain();
} /* IBMAudio::Obtain */
/*------------------------------------------------------------------------*/
void IBMAudio::Release()
{
	TRACE (fprintf (stderr, "IBMAudio::Release\n");)
	if (HaveAudio()) {
		unlock ();
		unlink ();
		close (recFd);
		close (playFd);
		recFd = -1;
		playFd = -1;
		fd = -1;
		notify ();
		}
} /* IBMAudio::IBMAudioRelease */
/*------------------------------------------------------------------------*/
int IBMAudio::FrameReady ()
/*
 * Because the IBM audio device does not deliver blocks of 160 samples, but
 * frames of 2^n, we have to buffer the data, so that vat thinks we are
 * reading 160 samples.
 * Keep in mind, that we are recording with 16 linear at 8000 Hz, because we
 * want to reduce the DC offset.
 */
{
	int			n;

	n = blksize -recBufSize;				/* samples missing to 160 */
	if (n <= 0)
		return 1;
	n *= 2;							/* 2 bytes / sample! */
	if ((n = read (recFd, recBuf +recBufSize, n)) <= 0)
		return 0;
	n /= 2;							/* we need the # of samples, not bytes! */
	recBufSize += n;
	if (recBufSize >= blksize)
		return 1;
	else
		return 0;
} /* IBMAudio::FrameRead */
/*------------------------------------------------------------------------*/
u_char* IBMAudio::Read()
/*
 * Because it seems that the AIX Audio Adapter also has a DC offset,
 * we use the same method to kill the DC level as in the SGI driver.
 */
{
register int			smean = lastmean;
register int			mean, dif, res;
register int			r;
	int			i;

	for (i=0; i<blksize; i++) {
		r = (int(recBuf[i])) << 1;
		mean = smean >> 13;
		dif = r - mean;
		smean += dif;
		readBuf[i] = lintomulawX[dif & 0x1ffff];
		} /* for */
	lastmean = smean;
	recBufSize -= blksize;					/* should be = 0 */
	return readBuf;						/* return the data */
} /* IBMAudio::Read */
/*------------------------------------------------------------------------*/
void IBMAudio::Write (u_char *cp)
/*
 * Write the audio samples the the audio device.
 * Because of the nature of the IBM audio device (don't know how it is on
 * other platforms) it can be possible that the dalay increases to an
 * very bad amount of audio data (up a second), specially on slow machines
 * like my M20. The reason is, that if the process will be descheduled
 * for a time longer than data are available for playback, the buffer
 * gets empty. In this state, the audio device STOPS to play any data.
 * When the process get rescheduled again, it receives a bulk of captured
 * audio data (capturing is NOT stoped). Because this acts as a trigger
 * for writing the audio data, the same bulk of data will be written to
 * the audio device, resulting in a high delay.
 *
 * What I do is the same as I have done in the NeVoT audio driver for AIX.
 * I reduce the delay by ommiting some samples, so that the delay will
 * be reduced by time. Of course, this results in a short frequency shift,
 * but you don't hear it very hard. But the result is still acceptable,
 * not for continous tones, but for voice and music :-)
 *
 * In fact, now I have implemented a variation of the algorithm. Now we not
 * only ommit some samples, but perform a linear aproximation for the new
 * samples by combining several samples (something like dithering). By doing
 * so, the quality is much better, for voice, music and for tones also!
 */
{
#define	HIGHWATER		(150*8)				/* 150ms */
register int			i;
register int			n;
register int			v;

	if (ioctl (playFd, AUDIO_BUFFER, &bufStat) == -1)
		perror ("IBMAudio::Write AUDIO_BUFFER");
	if (bufStat.write_buf_size >= HIGHWATER)
		contHighWaters++;
	else
		contHighWaters = 0;
	if (contHighWaters > 3) {				/* after 3 continous "overflows" */
/*
		fprintf (stderr, "%d ", bufStat.write_buf_size >> 3);
		fflush (stderr);
*/
#ifdef OLD
		for (i=0, n=0; i<blksize; i++)
			if (i % 32)				/* ommit every 20th sample (2.5ms) */
				playBuf[n++] = cp[i];
#else
		for (n=0, i=0;i <(blksize-1)*1000; i+=1025) {	/* shorten the samples by 2.5% */
			v = (int(mulawtolin[cp[i /1000]])) *(1000 -(i %1000)) /1000;
			v+= (int(mulawtolin[cp[i /1000 +1]])) *(i %1000) /1000;
			playBuf[n++] = lintomulaw[(unsigned short)v];
			} /* for */
#endif
		write (playFd, playBuf, n);
		} /* if */
	else
		write (playFd, cp, blksize);
} /* IBMAudio::Write */
/*------------------------------------------------------------------------*/
int IBMAudio::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;
} /* IBMAudio::GainClip */
/*------------------------------------------------------------------------*/
void IBMAudio::SetRGain (int level)
{
        audio_set_gain          audioSetGain;

	rgain = GainClip (level);
	if (recFd < 0)
		return;
	audioSetGain.left_gain = rgain *100 /AUDIO_MAX_GAIN;
	audioSetGain.right_gain = rgain *100 /AUDIO_MAX_GAIN;
        if (ioctl (recFd, AUDIO_SET_GAIN, &audioSetGain) == -1)
		perror ("IBMAudio::SetRGain");
} /* IBMAudio::SetRGain */
/*------------------------------------------------------------------------*/
void IBMAudio::SetPGain (int level)
{
	pgain = GainClip(level);
	if (playFd < 0)
		return;
        InitAudioChange ();
        InitAudioControl ();
        audioChange.volume = 0x7fff0000 /AUDIO_MAX_GAIN *pgain;
        audioChange.volume_delay = 0;
        audioControl.request_info = &audioChange;
        audioControl.ioctl_request = AUDIO_CHANGE;
        if (ioctl (playFd, AUDIO_CONTROL, &audioControl) == -1)
		perror ("IBMAudio::SetPGain");
} /* IBMAudio::SetPGain */
/*--------------------------------------------------------------------*/
void IBMAudio::InputPort (int p)
{
	iport = p;
	if (recFd < 0)
		return;
/*** select the input port ***/
	InitAudioChange ();
	InitAudioControl ();
	switch (iport) {
		case input_mike:	audioChange.input = LOW_GAIN_MIKE; break;
		case input_line:	audioChange.input = LINE_1; break;
		case input_line2:	audioChange.input = LINE_2; break;
		case input_line3:	audioChange.input = HIGH_GAIN_MIKE; break;
	  } /* switch */
	audioControl.request_info = &audioChange;
	audioControl.ioctl_request = AUDIO_CHANGE;
	if (ioctl (recFd, AUDIO_CONTROL, &audioControl) == -1)
		perror ("IBMAudio::InputPort");
} /* IBMAudio::InputPort */
/*--------------------------------------------------------------------*/
void IBMAudio::OutputPort (int p)
{
	oport = p;
	if (playFd < 0)
		return;
/*** select the output port ***/
	InitAudioChange ();
	InitAudioControl ();
	switch (oport) {
		case output_speaker:	audioChange.output = EXTERNAL_SPEAKER; break;
		case output_phones:	audioChange.output = EXTERNAL_SPEAKER; break;
		case output_line:	audioChange.output = OUTPUT_1; break;
		case output_line2:	audioChange.output = AUX1; break;
	  } /* switch */
	audioControl.request_info = &audioChange;
	audioControl.ioctl_request = AUDIO_CHANGE;
	if (ioctl (recFd, AUDIO_CONTROL, &audioControl) == -1)
		perror ("IBMAudio::OutputPort");
/*** internal speaker can only be toggled ***/
	InitAudioChange ();
	InitAudioControl ();
	audioControl.request_info = &audioChange;
	audioControl.ioctl_request = AUDIO_CHANGE;
	if (oport == output_speaker)
		audioChange.output = INTERNAL_SPEAKER_ON;
        else
		audioChange.output = INTERNAL_SPEAKER_OFF;
	if (ioctl (recFd, AUDIO_CONTROL, &audioControl) == -1)
		perror ("IBMAudio::OutputPort internal speaker");
} /* IBMAudio::OutputPort */
/*--------------------------------------------------------------------*/
void IBMAudio::InitAudioDevice ()
{
/*** initialize the recording part ***/
	audioInit.srate =		8000;
	audioInit.bits_per_sample =	8 *2;
	audioInit.bsize =		256 *2;
	audioInit.mode =		PCM;
	audioInit.channels =		1;
	audioInit.position_resolution = AUDIO_IGNORE;
	audioInit.flags =		BIG_ENDIAN;
	audioInit.operation =		RECORD;
	audioInit.reserved =		NULL;
	if (ioctl (recFd, AUDIO_INIT, &audioInit) == -1)
		perror ("IBMAudio::InitAudioDevice record");
	InputPort (iport);
	SetRGain (rgain);
	SetMonitor (0);
/*** select the kernel buffersize for recording ***/
	bufReq.buffer_size =		8000 *2 *2;		/* for 2 seconds */
	bufReq.play_block_count =	AUDIO_IGNORE;
	bufReq.play_lower_limit =	AUDIO_IGNORE;
	bufReq.cap_upper_limit =	AUDIO_IGNORE;
	bufReq.byte_count =		AUDIO_IGNORE;
	bufReq.return_code =		0xDeadBeaf;
	if (ioctl (recFd, AUDIO_MODIFY_LIMITS, &bufReq) == -1)
		perror ("IBMAudio::InitAudioDevice record buffer");
/*** initialize the playback part ***/
	audioInit.srate =		8000;
	audioInit.bits_per_sample =	8;
	audioInit.bsize =		512;
	audioInit.mode =		MU_LAW;
	audioInit.channels =		1;
	audioInit.position_resolution = AUDIO_IGNORE;
	audioInit.flags =		FIXED;
	audioInit.operation =		PLAY;
	audioInit.reserved =		NULL;
	if (ioctl (playFd, AUDIO_INIT, &audioInit) == -1)
		perror ("IBMAudio::InitAudioDevice playback");
	OutputPort (oport);
	SetPGain (pgain);
/*** start the both device parts ***/
	InitAudioControl ();
	audioControl.ioctl_request = AUDIO_START;
	if (ioctl (recFd, AUDIO_CONTROL, &audioControl) != 0)
		perror ("IBMAudio::InitAudioDevice start record");
	InitAudioControl ();
	audioControl.ioctl_request = AUDIO_START;
	if (ioctl (playFd, AUDIO_CONTROL, &audioControl) != 0)
		perror ("IBMAudio::InitAudioDevice start playback");
} /* IBMAudio::InitAudioDevice */
/*------------------------------------------------------------------------*/
void IBMAudio::SetMonitor (int on)
{
	IBMAudio::InitAudioChange ();
	IBMAudio::InitAudioControl ();
	if (on)
		audioChange.monitor = MONITOR_UNCOMPRESSED;
	else
		audioChange.monitor = MONITOR_OFF;
	audioControl.request_info = &audioChange;
	audioControl.ioctl_request = AUDIO_CHANGE;
	if (ioctl (recFd, AUDIO_CONTROL, &audioControl) < 0)
		perror ("IBMAudio::SetMonitor ioctl");
} /* IBMAudio::SetMonitor */
/*--------------------------------------------------------------------*/
void IBMAudio::InitAudioChange ()
{
	audioChange.dev_info =		NULL;
	audioChange.input =		AUDIO_IGNORE;
	audioChange.output =		AUDIO_IGNORE;
	audioChange.monitor =		AUDIO_IGNORE;
	audioChange.volume =		AUDIO_IGNORE;
	audioChange.volume_delay =	AUDIO_IGNORE;
	audioChange.balance =		AUDIO_IGNORE;
	audioChange.balance_delay =	AUDIO_IGNORE;
	audioChange.treble =		AUDIO_IGNORE;
	audioChange.bass =		AUDIO_IGNORE;
	audioChange.pitch =		AUDIO_IGNORE;
} /* IBMAudio::InitAudioChange */
/*--------------------------------------------------------------------*/
void IBMAudio::InitAudioControl ()
{
	audioControl.ioctl_request =	AUDIO_IGNORE;
	audioControl.request_info =	NULL;
	audioControl.position =		0;
	audioControl.return_code =	0xDeadBeaf;
} /* IBMAudio::InitAudioControl */
/*------------------------------------------------------------------------*/
