/*

Name:
DRV_SS.C

Description:
Mikmod driver for output on Ensoniq Soundscape / Soundscape ELITE

Portability:

MSDOS:	BC(y)	Watcom(y)	DJGPP(y)
Win95:	n
Os2:	n
Linux:	n

(y) - yes
(n) - no (not possible or not useful)
(?) - may be possible, but not tested

*/
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <malloc.h>
#include <conio.h>
#ifndef __DJGPP__
#include <mem.h>
#endif
#include <string.h>

#include "mikmod.h"
#include "mdma.h"
#include "mirq.h"

/***************************************************************************
>>>>>>>>>>>>>>>>>>>>>>>>> Lowlevel SS stuff <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
***************************************************************************/

/* Ensoniq gate-array chip defines ... */

#define ODIE            0       /* ODIE gate array */
#define OPUS            1       /* OPUS gate array */
#define MMIC            2       /* MiMIC gate array */

/* relevant direct register defines - offsets from base address */
#define GA_HOSTCTL_OFF  2       /* host port ctrl/stat reg */
#define GA_ADDR_OFF     4       /* indirect address reg */
#define GA_DATA_OFF     5       /* indirect data reg */
#define GA_CODEC_OFF    8       /* for some boards CoDec is fixed from base */

/* relevant indirect register defines */
#define GA_DMAB_REG     3       /* DMA chan B assign reg */
#define GA_INTCFG_REG   4       /* interrupt configuration reg */
#define GA_DMACFG_REG   5       /* DMA configuration reg */
#define GA_CDCFG_REG    6       /* CD-ROM/CoDec config reg */
#define GA_HMCTL_REG	9	/* host master control reg */


/* AD-1848 or compatible CoDec defines ... */
/* relevant direct register defines - offsets from base */
#define CD_ADDR_OFF     0       /* indirect address reg */
#define CD_DATA_OFF     1       /* indirect data reg */
#define CD_STATUS_OFF   2       /* status register */

/* relevant indirect register defines */
#define CD_ADCL_REG		0	/* left DAC input control reg */
#define CD_ADCR_REG		1	/* right DAC input control reg */
#define CD_CDAUXL_REG   2       /* left DAC output control reg */
#define CD_CDAUXR_REG   3       /* right DAC output control reg */
#define CD_DACL_REG     6       /* left DAC output control reg */
#define CD_DACR_REG     7       /* right DAC output control reg */
#define CD_FORMAT_REG   8       /* clock and data format reg */
#define CD_CONFIG_REG   9       /* interface config register */
#define CD_PINCTL_REG   10      /* external pin control reg */
#define CD_UCOUNT_REG   14      /* upper count reg */
#define CD_LCOUNT_REG   15      /* lower count reg */
#define CD_XFORMAT_REG	28	/* extended format reg - 1845 record */
#define CD_XUCOUNT_REG	30	/* extended upper count reg - 1845 record */
#define CD_XLCOUNT_REG	31	/* extended lower count reg - 1845 record */

#define CD_MODE_CHANGE	0x40	/* mode change mask for addr reg */

/****************************************************************************
hardware config info ...
****************************************************************************/

static UWORD	BasePort;	/* Gate Array/MPU-401 base port */
static UWORD	MidiIrq;	/* the MPU-401 IRQ */
static UWORD	WavePort;	/* the AD-1848 base port */
static UWORD	WaveIrq;	/* the PCM IRQ */
static UWORD	DmaChan;	/* the PCM DMA channel */

/****************************************************************************
all kinds of stuff ...
****************************************************************************/

static UWORD Windx;		/* Wave IRQ index - for reg writes */
static UWORD Mindx;		/* MIDI IRQ index - for reg writes */

static UBYTE IcType;		/* the Ensoniq chip type */
static UBYTE CdCfgSav;		/* gate array register save area */
static UBYTE DmaCfgSav;		/* gate array register save area */
static UBYTE IntCfgSav;		/* gate array register save area */

static UWORD const SsIrqs[4] = { 9, 5, 7, 10 };  /* Soundscape IRQs */
static UWORD const RsIrqs[4] = { 9, 7, 5, 15 };  /* an older IRQ set */
static UWORD const *Irqs;           /* pointer to one of the IRQ sets */

static UBYTE DacSavL;        /* DAC left volume save */
static UBYTE DacSavR;        /* DAC right volume save */
static UBYTE CdxSavL;        /* CD/Aux left volume save */
static UBYTE CdxSavR;        /* CD/Aux right volume save */
static UBYTE AdcSavL;        /* ADC left volume save */
static UBYTE AdcSavR;        /* ADC right volume save */

static DMAMEM *SS_DMAMEM;
static char   *SS_DMABUF;


/***************************************************************************
>>>>>>>>>>>>>>>>>>>>>>>>> The actual SS driver <<<<<<<<<<<<<<<<<<<<<<<<<<<<<
***************************************************************************/



UBYTE GaRead(UWORD rnum)
/*
  This function is used to read the indirect addressed registers in the
  Ensoniq Soundscape gate array.

  INPUTS:
	rnum  - the numner of the indirect register to be read

  RETURNS:
	the contents of the indirect register are returned
*/
{
	outportb(BasePort + GA_ADDR_OFF, rnum);
	return inportb(BasePort + GA_DATA_OFF);
}



void GaWrite(UWORD rnum,UBYTE value)
/*
  This function is used to write the indirect addressed registers in the
  Ensoniq Soundscape gate array.

  INPUTS:
	rnum   - the numner of the indirect register to be read
	value  - the byte value to be written to the indirect register

  RETURNS:
	Nothing
*/
{
	outportb(BasePort + GA_ADDR_OFF, rnum);
	outportb(BasePort + GA_DATA_OFF, value);
}


UBYTE CdRead(UWORD rnum)
/*
  This function is used to read the indirect addressed registers in the
  AD-1848 or compatible CoDec. It will preserve the special function bits
  in the upper-nibble of the indirect address register.

  INPUTS:
	rnum  - the numner of the indirect register to be read

  RETURNS:
	the contents of the indirect register are returned

*/
{
	outportb(WavePort + CD_ADDR_OFF,
		(inportb(WavePort + CD_ADDR_OFF) & 0xf0) | rnum);
	return inportb(WavePort+CD_DATA_OFF);
}



void CdWrite(UWORD rnum,UBYTE value)
/*
  This function is used to write the indirect addressed registers in the
  Ad-1848 or compatible CoDec. It will preserve the special function bits
  in the upper-nibble of the indirect address register.

  INPUTS:
	rnum   - the numner of the indirect register to be read
	value  - the byte value to be written to the indirect register

  RETURNS:
	Nothing
*/
{
	outportb(WavePort + CD_ADDR_OFF,
		(inportb(WavePort + CD_ADDR_OFF) & 0xf0) | rnum);
	outportb(WavePort + CD_DATA_OFF, value);
}




void SetDacVol(UBYTE lvol,UBYTE rvol)
/*
  This function sets the left and right DAC output level in the CoDec.

  INPUTS:
	lvol  - left volume, 0-127
	rvol  - right volume, 0-127

  RETURNS:
	Nothing

*/
{
	CdWrite(CD_DACL_REG, ~(lvol >> 1) & 0x3f);
	CdWrite(CD_DACR_REG, ~(rvol >> 1) & 0x3f);
}



void SetCdRomVol(UBYTE lvol,UBYTE rvol)
/*
  This function sets the left and right CD-ROM output level in the CoDec.

  INPUTS:
	lvol  - left volume, 0-127
	rvol  - right volume, 0-127

  RETURNS:
	Nothing
*/
{
	CdWrite(CD_CDAUXL_REG, ~(lvol >> 2) & 0x1f);
	CdWrite(CD_CDAUXR_REG, ~(rvol >> 2) & 0x1f);
}


void SetAdcVol(UBYTE lvol,UBYTE rvol)
/*
  This function sets the left and right ADC input level in the CoDec.

  INPUTS:
	lvol  - left volume, 0-127
	rvol  - right volume, 0-127

  RETURNS:
	Nothing

*/
{
	CdWrite(CD_ADCL_REG, (CdRead(CD_ADCL_REG) & 0xf0) | (lvol & 0x7f) >> 3);
	CdWrite(CD_ADCR_REG, (CdRead(CD_ADCR_REG) & 0xf0) | (rvol & 0x7f) >> 3);
}


void StopCoDec(void)
{
	UWORD i;

	CdWrite(CD_CONFIG_REG,CdRead(CD_CONFIG_REG)&0xfc);

	/* Let the CoDec receive its last DACK(s). The DMAC must not be */
	/*  masked while the CoDec has DRQs pending. */
/*	for(i=0; i<256; ++i )
		if(!(inportb(DmacRegP->status) & (0x10 << DmaChan))) break;
*/
}



BOOL GetConfigEntry(char *entry, char *dest, FILE *fp)
/*
  This function parses a file (SNDSCAPE.INI) for a left-hand string and,
  if found, writes its associated right-hand value to a destination buffer.
  This function is case-insensitive.

  INPUTS:
	fp  - a file pointer to the open SNDSCAPE.INI config file
	dst - the destination buffer pointer
	lhp - a pointer to the right-hand string

  RETURNS:
	1   - if successful
	0   - if the right-hand string is not found or has no equate
*/
{
	char static str[83];
	char static tokstr[33];
	char *p;

	/* make a local copy of the entry, upper-case it */
	strcpy(tokstr, entry);
	strupr(tokstr);

	/* rewind the file and try to find it ... */
	rewind(fp);

	for( ;; ) {
		/* get the next string from the file */

		fgets(str, 83, fp);
		if(feof(fp)) return 0;

		/* properly terminate the string */
		for( p = str; *p != '\0'; ++p ) {
			if( *p == ' ' || *p == '\t' || *p == 0x0a || *p == 0x0d ) {
				*p = '\0';
				break;
			}
		}

		/* see if it's an 'equate' string; if so, zero the '=' */
		if( !(p = strchr(str, '=')) ) continue;
		*p = '\0';

		/* upper-case the current string and test it */
		strupr(str);
		if( strcmp(str, tokstr) )
			continue;

		/* it's our string - copy the right-hand value to buffer */
		for( p = str + strlen(str) + 1; (*dest++ = *p++) != '\0'; );
		break;
	}
	return 1;
}


static BOOL SS_IsThere(void)
{
	static char str[78];
	char *envptr;
	FILE *fp;
	UBYTE tmp;

	if((envptr=getenv("SNDSCAPE"))==NULL) return 0;

	strcpy(str, envptr);
	if( str[strlen(str) - 1] == '\\' )
		str[strlen(str) - 1] = '\0';

	strcat(str, "\\SNDSCAPE.INI");

	if(!(fp=fopen(str, "r"))) return 0;

	/* read all of the necessary config info ... */
	if(!GetConfigEntry("Product",str,fp)){
		fclose(fp);
		return 0;
	}

	/* if an old product name is read, set the IRQs accordingly */
	strupr(str);
	if(strstr(str,"SOUNDFX") || strstr(str,"MEDIA_FX"))
		Irqs = RsIrqs;
	else
		Irqs = SsIrqs;

	if(!GetConfigEntry("Port", str, fp)){
		fclose(fp);
		return 0;
	}

	BasePort=strtol(str,NULL,16);

	if(!GetConfigEntry("WavePort",str,fp)){
		fclose(fp);
		return 0;
	}

	WavePort=strtol(str,NULL,16);

	if(!GetConfigEntry("IRQ",str,fp)){
		fclose(fp);
		return 0;
	}

	MidiIrq=strtol(str,NULL,10);
	if(MidiIrq==2) MidiIrq = 9;

	if(!GetConfigEntry("SBIRQ",str,fp)){
		fclose(fp);
		return 0;
	}

	WaveIrq=strtol(str,NULL,10);
	if(WaveIrq==2) WaveIrq=9;

	if(!GetConfigEntry("DMA",str,fp)){
		fclose(fp);
		return 0;
	}

	DmaChan=strtol(str,NULL,10);

	fclose(fp);

	/* see if Soundscape is there by reading HW ... */
	if((inportb(BasePort+GA_HOSTCTL_OFF)&0x78) != 0x00)	return 0;
	if((inportb(BasePort+GA_ADDR_OFF)&0xf0)==0xf0) return 0;

	outportb(BasePort+GA_ADDR_OFF,0xf5);

	tmp=inportb(BasePort+GA_ADDR_OFF);

	if((tmp & 0xf0)==0xf0) return 0;
	if((tmp & 0x0f)!=0x05) return 0;

	/* formulate the chip ID */
	if( (tmp & 0x80) != 0x00 )
		IcType = MMIC;
	else if((tmp & 0x70) != 0x00)
		IcType = OPUS;
	else
		IcType = ODIE;

	/* now do a quick check to make sure the CoDec is there too */
	if((inportb(WavePort)&0x80)!=0x00) return 0;
	return 1;
}


#ifdef NEVER

static void interrupt newhandler(void)
{
	if(WaveIrq==7){
		outportb(0x20,0x0b);
		inportb(0x21);
		if(!(inportb(0x20)&0x80)) return;
	}
	else if(WaveIrq==15){
		outportb(0xa0,0x0b);
		inportb(0xa1);
		if(!(inportb(0xa0)&0x80)) return;
	}

	interruptcount++;

	/* if the CoDec is interrupting clear the AD-1848 interrupt */
	if(inportb(WavePort+CD_STATUS_OFF)&0x01)
		outportb(WavePort+CD_STATUS_OFF,0x00);

	MIrq_EOI(WaveIrq);
}


static PVI oldhandler;

#endif


static UWORD codecfreqs[14]={
	5512, 6615, 8000, 9600,11025,16000,18900,
	22050,27428,32000,33075,37800,44100,48000
};


static UWORD codecformats[14]={
	0x01, 0x0f, 0x00, 0x0e,	0x03, 0x02, 0x05,
	0x07, 0x04,	0x06, 0x0d,	0x09, 0x0b,	0x0c
};


static UBYTE codecformat;


static BOOL SS_Init(void)
{
	int t;

	if(!SS_IsThere()){
		myerr="No such hardware detected, check your 'SNDSCAPE' env. variable";
		return 0;
	}

	printf("Ensoniq Soundscape at port 0x%x, irq %d, dma %d\n",WavePort,WaveIrq,DmaChan);

	/* find closest codec frequency */

	for(t=0;t<14;t++){
		if(t==13 || md_mixfreq<=codecfreqs[t]){
			md_mixfreq=codecfreqs[t];
			break;
		}
	}

	codecformat=codecformats[t];
	if(md_mode & DMODE_STEREO) codecformat|=0x10;
	if(md_mode & DMODE_16BITS) codecformat|=0x40;

	if(!VC_Init()) return 0;

	SS_DMAMEM=MDma_AllocMem(md_dmabufsize);

	if(SS_DMAMEM==NULL){
		VC_Exit();
		myerr="Couldn't allocate page-contiguous dma-buffer";
		return 0;
	}

	SS_DMABUF=(char *)MDma_GetPtr(SS_DMAMEM);

	/* In case the CoDec is running, stop it */
	StopCoDec();

	/* Clear possible CoDec and SoundBlaster emulation interrupts */
	outportb(WavePort+CD_STATUS_OFF,0x00);
	inportb(0x22e);

	/* If necessary, save some regs, do some resource re-routing */
	if( IcType != MMIC) {

		/* derive the MIDI and Wave IRQ indices (0-3) for reg writes */
		for( Mindx = 0; Mindx < 4; ++Mindx )
			if( MidiIrq == *(Irqs + Mindx) )
				break;
		for( Windx = 0; Windx < 4; ++Windx )
			if( WaveIrq == *(Irqs + Windx) )
				break;

		/* setup the CoDec DMA polarity */
		GaWrite(GA_DMACFG_REG, 0x50);

		/* give the CoDec control of the DMA and Wave IRQ resources */
			CdCfgSav = GaRead(GA_CDCFG_REG);
		GaWrite(GA_CDCFG_REG, 0x89 | (DmaChan << 4) | (Windx << 1));

		/* pull the Sound Blaster emulation off of those resources */
		DmaCfgSav = GaRead(GA_DMAB_REG);
		GaWrite(GA_DMAB_REG, 0x20);
		IntCfgSav = GaRead(GA_INTCFG_REG);
		GaWrite(GA_INTCFG_REG, 0xf0 | (Mindx << 2) | Mindx);
	}

	/* Save all volumes that we might use, init some levels */
	DacSavL = CdRead(CD_DACL_REG);
	DacSavR = CdRead(CD_DACR_REG);
	CdxSavL = CdRead(CD_CDAUXL_REG);
	CdxSavR = CdRead(CD_CDAUXL_REG);
	AdcSavL = CdRead(CD_ADCL_REG);
	AdcSavR = CdRead(CD_ADCR_REG);

	SetDacVol(127, 127);
	SetAdcVol(96, 96);

	/* Select the mic/line input to the record mux; */
	/* if not ODIE, set the mic gain bit too */
	CdWrite(CD_ADCL_REG, (CdRead(CD_ADCL_REG) & 0x3f) |
		(IcType == ODIE ? 0x80 : 0xa0));
	CdWrite(CD_ADCR_REG, (CdRead(CD_ADCR_REG) & 0x3f) |
		(IcType == ODIE ? 0x80 : 0xa0));

	/* Put the CoDec into mode change state */
	outportb(WavePort + CD_ADDR_OFF, 0x40);

	/* Setup CoDec mode - single DMA chan, AutoCal on */
	CdWrite(CD_CONFIG_REG, 0x0c);

#ifdef NEVER
	/* enable the CoDec interrupt pin */
	CdWrite(CD_PINCTL_REG, CdRead(CD_PINCTL_REG) | 0x02);
	oldhandler=MIrq_SetHandler(WaveIrq,newhandler);
	MIrq_OnOff(WaveIrq,1);
#else
	/* disable the interrupt for mikmod */
	CdWrite(CD_PINCTL_REG, CdRead(CD_PINCTL_REG) & 0xfd);
#endif
	return 1;
}



static void SS_Exit(void)
{
	/* in case the CoDec is running, stop it */
	StopCoDec();

	/* mask the PC DMA Controller */
/*	outportb(DmacRegP->mask, 0x04 | DmaChan); */

#ifdef NEVER
	/* disable the CoDec interrupt pin */
	CdWrite(CD_PINCTL_REG, CdRead(CD_PINCTL_REG) & 0xfd);
	MIrq_OnOff(WaveIrq,0);
	MIrq_SetHandler(WaveIrq,oldhandler);
#endif

	/* restore all volumes ... */
	CdWrite(CD_DACL_REG, DacSavL);
	CdWrite(CD_DACR_REG, DacSavR);
	CdWrite(CD_CDAUXL_REG, CdxSavL);
	CdWrite(CD_CDAUXL_REG, CdxSavR);
	CdWrite(CD_ADCL_REG, AdcSavL);
	CdWrite(CD_ADCR_REG, AdcSavR);

	/* if necessary, restore gate array resource registers */
	if(IcType!=MMIC){
		GaWrite(GA_INTCFG_REG, IntCfgSav);
		GaWrite(GA_DMAB_REG, DmaCfgSav);
		GaWrite(GA_CDCFG_REG, CdCfgSav);
	}

	MDma_FreeMem(SS_DMAMEM);
	VC_Exit();
}


static UWORD last=0;
static UWORD curr=0;


static void SS_Update(void)
{
	UWORD todo,index;

	curr=(md_dmabufsize-MDma_Todo(DmaChan))&0xfffc;

	if(curr>=md_dmabufsize) return;
	if(curr==last) return;

	if(curr>last){
		todo=curr-last; index=last;
		last+=VC_WriteBytes(&SS_DMABUF[index],todo);
		MDma_Commit(SS_DMAMEM,index,todo);
		if(last>=md_dmabufsize) last=0;
	}
	else{
		todo=md_dmabufsize-last;
		VC_WriteBytes(&SS_DMABUF[last],todo);
		MDma_Commit(SS_DMAMEM,last,todo);
		last=VC_WriteBytes(SS_DMABUF,curr);
		MDma_Commit(SS_DMAMEM,0,curr);
	}
}


static void SS_PlayStart(void)
{
	int direction=0;
	long i;
	UWORD tmp;

	VC_PlayStart();

	/* make sure the the CoDec is in mode change state */
	outportb(WavePort + CD_ADDR_OFF, 0x40);

	/* and write the format register */
	CdWrite(CD_FORMAT_REG, codecformat);

	/* if not using ODIE and recording, setup extended format register */
	if( IcType != ODIE && direction )
		CdWrite(CD_XFORMAT_REG, codecformat & 0x70);

	/* delay for internal re-synch */
	for( i = 0; i < 200000UL; ++i )
		inportb(BasePort + GA_ADDR_OFF);

	/* clear the dma buffer to zero (16 bits
	   signed ) or 0x80 (8 bits unsigned) */

	if(md_mode & DMODE_16BITS)
		memset(SS_DMABUF,0,md_dmabufsize);
	else
		memset(SS_DMABUF,0x80,md_dmabufsize);

	MDma_Commit(SS_DMAMEM,0,md_dmabufsize);

	/* Write the CoDec interrupt count - sample frames per half-buffer. */
	/* If not using ODIE and recording, use extended count regs */
	tmp = md_dmabufsize;
	if(md_mode&DMODE_STEREO) tmp>>=1;
	if(md_mode&DMODE_16BITS) tmp>>=1;
	tmp--;

	if( IcType != ODIE && direction ) {
		CdWrite(CD_XLCOUNT_REG, tmp);
		CdWrite(CD_XUCOUNT_REG, tmp >> 8);
	}
	else {
		CdWrite(CD_LCOUNT_REG, tmp);
		CdWrite(CD_UCOUNT_REG, tmp >> 8);
	}

	if(!MDma_Start(DmaChan,SS_DMAMEM,md_dmabufsize,INDEF_WRITE)){
		return;
	}

	/* disable mode change state and start the CoDec */
	outportb(WavePort + CD_ADDR_OFF, 0x00);
	CdWrite(CD_CONFIG_REG, direction ? 0x02 : 0x01);
}


static void SS_PlayStop(void)
{
	StopCoDec();
	VC_PlayStop();
}


DRIVER drv_ss={
	NULL,
	"Ensoniq Soundscape",
	"MikMod Ensoniq Soundscape Driver v0.0 - Thanks to CyberCerus",
	SS_IsThere,
	VC_SampleLoad,
	VC_SampleUnload,
	SS_Init,
	SS_Exit,
	SS_PlayStart,
	SS_PlayStop,
	SS_Update,
	VC_VoiceSetVolume,
	VC_VoiceSetFrequency,
	VC_VoiceSetPanning,
	VC_VoicePlay
};
