/*

   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
};
