/*

Name:
DRV_SB.C

Description:
Mikmod driver for output on Creative Labs Soundblasters & compatibles
(through DSP)

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 "mikmod.h"
#include "mdma.h"
#include "mirq.h"

/***************************************************************************
>>>>>>>>>>>>>>>>>>>>>>>>> Lowlevel SB stuff <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
***************************************************************************/

static UWORD sb_port;          /* sb base port */

/*
	Define some important SB i/o ports:
*/

#define MIXER_ADDRESS 		(sb_port+0x4)
#define MIXER_DATA			(sb_port+0x5)
#define DSP_RESET			(sb_port+0x6)
#define DSP_READ_DATA		(sb_port+0xa)
#define DSP_WRITE_DATA		(sb_port+0xc)
#define DSP_WRITE_STATUS	(sb_port+0xc)
#define DSP_DATA_AVAIL		(sb_port+0xe)


static void SB_MixerStereo(void)
/*
	Enables stereo output for DSP versions 3.00 >= ver < 4.00
*/
{
	outportb(MIXER_ADDRESS,0xe);
	outportb(MIXER_DATA,inportb(MIXER_DATA)|2);
}



static void SB_MixerMono(void)
/*
	Disables stereo output for DSP versions 3.00 >= ver < 4.00
*/
{
	outportb(MIXER_ADDRESS,0xe);
	outportb(MIXER_DATA,inportb(MIXER_DATA)&0xfd);
}



static BOOL SB_WaitDSPWrite(void)
/*
	Waits until the DSP is ready to be written to.

	returns FALSE on timeout
*/
{
	UWORD timeout=32767;

	while(timeout--){
		if(!(inportb(DSP_WRITE_STATUS)&0x80)) return 1;
	}
	return 0;
}



static BOOL SB_WaitDSPRead(void)
/*
	Waits until the DSP is ready to read from.

	returns FALSE on timeout
*/
{
	UWORD timeout=32767;

	while(timeout--){
		if(inportb(DSP_DATA_AVAIL)&0x80) return 1;
	}
	return 0;
}



static BOOL SB_WriteDSP(UBYTE data)
/*
	Writes byte 'data' to the DSP.

	returns FALSE on timeout.
*/
{
	if(!SB_WaitDSPWrite()) return 0;
	outportb(DSP_WRITE_DATA,data);
	return 1;
}



static UWORD SB_ReadDSP(void)
/*
	Reads a byte from the DSP.

	returns 0xffff on timeout.
*/
{
	if(!SB_WaitDSPRead()) return 0xffff;
	return(inportb(DSP_READ_DATA));
}



static void SB_SpeakerOn(void)
/*
	Enables DAC speaker output.
*/
{
	SB_WriteDSP(0xd1);
}



static void SB_SpeakerOff(void)
/*
	Disables DAC speaker output
*/
{
	SB_WriteDSP(0xd3);
}



static void SB_ResetDSP(void)
/*
	Resets the DSP.
*/
{
	int t;
	/* reset the DSP by sending 1, (delay), then 0 */
	outportb(DSP_RESET,1);
	for(t=0;t<8;t++) inportb(DSP_RESET);
	outportb(DSP_RESET,0);
}



static BOOL SB_Ping(void)
/*
	Checks if a SB is present at the current baseport by
	resetting the DSP and checking if it returned the value 0xaa.

	returns: TRUE   => SB is present
			 FALSE  => No SB detected
*/
{
	SB_ResetDSP();
	return(SB_ReadDSP()==0xaa);
}



static UWORD SB_GetDSPVersion(void)
/*
	Gets SB-dsp version. returns 0xffff if dsp didn't respond.
*/
{
	UWORD hi,lo;

	if(!SB_WriteDSP(0xe1)) return 0xffff;

	hi=SB_ReadDSP();
	lo=SB_ReadDSP();

	return((hi<<8)|lo);
}



/***************************************************************************
>>>>>>>>>>>>>>>>>>>>>>>>> The actual SB driver <<<<<<<<<<<<<<<<<<<<<<<<<<<<<
***************************************************************************/

static DMAMEM *SB_DMAMEM;
static char *SB_DMABUF;

static UBYTE SB_TIMECONSTANT;

static UBYTE PIC1MSK;
static UBYTE PIC2MSK;

static UWORD sb_int;           /* interrupt vector that belongs to sb_irq */
static UWORD sb_ver;           /* DSP version number */
static UBYTE sb_irq;           /* sb irq */
static UBYTE sb_lodma;         /* 8 bit dma channel (1.0/2.0/pro) */
static UBYTE sb_hidma;         /* 16 bit dma channel (16/16asp) */
static UBYTE sb_dma;           /* current dma channel */


static BOOL SB_IsThere(void)
{
	char *envptr,c;
	static char *endptr;

	sb_port =0xffff;
	sb_irq  =0xff;
	sb_lodma=0xff;
	sb_hidma=0xff;

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

	while(1){

		/* skip whitespace */

		do c=*(envptr++); while(c==' ' || c=='\t');

		/* reached end of string? -> exit */

		if(c==0) break;

		switch(c){

			case 'a':
			case 'A':
				sb_port=strtol(envptr,&endptr,16);
				break;

			case 'i':
			case 'I':
				sb_irq=strtol(envptr,&endptr,10);
				break;

			case 'd':
			case 'D':
				sb_lodma=strtol(envptr,&endptr,10);
				break;

			case 'h':
			case 'H':
				sb_hidma=strtol(envptr,&endptr,10);
				break;

			default:
				strtol(envptr,&endptr,16);
				break;
		}
		envptr=endptr;
	}

	if(sb_port==0xffff || sb_irq==0xff || sb_lodma==0xff) return 0;

	/* determine interrupt vector */

	sb_int = (sb_irq>7) ? sb_irq+104 : sb_irq+8;

	if(!SB_Ping()) return 0;

	/* get dsp version. */

	if((sb_ver=SB_GetDSPVersion())==0xffff) return 0;

	return 1;
}


static void interrupt newhandler(MIRQARGS)
{
	if(sb_ver<0x200){
		SB_WriteDSP(0x14);
		SB_WriteDSP(0xff);
		SB_WriteDSP(0xfe);
	}

	if(md_mode & DMODE_16BITS)
		inportb(sb_port+0xf);
	else
		inportb(DSP_DATA_AVAIL);

	MIrq_EOI(sb_irq);
}


static PVI oldhandler;


static BOOL SB_Init(void)
{
	ULONG t;

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

/*      printf("SB version %x\n",sb_ver); */
/*      if(sb_ver>0x200) sb_ver=0x200; */

	if(sb_ver>=0x400 && sb_hidma==0xff){
		myerr="High-dma setting in 'BLASTER' variable is required for SB-16";
		return 0;
	}

	if(sb_ver<0x400 && md_mode&DMODE_16BITS){
		/* DSP versions below 4.00 can't do 16 bit sound. */
		md_mode&=~DMODE_16BITS;
	}

	if(sb_ver<0x300 && md_mode&DMODE_STEREO){
		/* DSP versions below 3.00 can't do stereo sound. */
		md_mode&=~DMODE_STEREO;
	}

	/* Use low dma channel for 8 bit, high dma for 16 bit */

	sb_dma=(md_mode & DMODE_16BITS) ? sb_hidma : sb_lodma;

	if(sb_ver<0x400){

		t=md_mixfreq;
		if(md_mode & DMODE_STEREO) t<<=1;

		SB_TIMECONSTANT=256-(1000000L/t);

		if(sb_ver<0x201){
			if(SB_TIMECONSTANT>210) SB_TIMECONSTANT=210;
		}
		else{
			if(SB_TIMECONSTANT>233) SB_TIMECONSTANT=233;
		}

		md_mixfreq=1000000L/(256-SB_TIMECONSTANT);
		if(md_mode & DMODE_STEREO) md_mixfreq>>=1;
	}

	if(!VC_Init()) return 0;

	SB_DMAMEM=MDma_AllocMem(md_dmabufsize);

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

	SB_DMABUF=(char *)MDma_GetPtr(SB_DMAMEM);

	oldhandler=MIrq_SetHandler(sb_irq,newhandler);
	return 1;
}



static void SB_Exit(void)
{
	MIrq_SetHandler(sb_irq,oldhandler);
	MDma_FreeMem(SB_DMAMEM);
	VC_Exit();
}


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


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

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

	if(curr==last) return;

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




static void SB_PlayStart(void)
{
	VC_PlayStart();

	MIrq_OnOff(sb_irq,1);

	if(sb_ver>=0x300 && sb_ver<0x400){
		if(md_mode & DMODE_STEREO){
			SB_MixerStereo();
		}
		else{
			SB_MixerMono();
		}
	}

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

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

	MDma_Commit(SB_DMAMEM,0,md_dmabufsize);

	if(!MDma_Start(sb_dma,SB_DMAMEM,md_dmabufsize,INDEF_WRITE)){
		return;
	}

	if(sb_ver<0x400){
		SB_SpeakerOn();

		SB_WriteDSP(0x40);
		SB_WriteDSP(SB_TIMECONSTANT);

		if(sb_ver<0x200){
			SB_WriteDSP(0x14);
			SB_WriteDSP(0xff);
			SB_WriteDSP(0xfe);
		}
		else if(sb_ver==0x200){
			SB_WriteDSP(0x48);
			SB_WriteDSP(0xff);
			SB_WriteDSP(0xfe);
			SB_WriteDSP(0x1c);
		}
		else{
			SB_WriteDSP(0x48);
			SB_WriteDSP(0xff);
			SB_WriteDSP(0xfe);
			SB_WriteDSP(0x90);
		}
	}
	else{
		SB_WriteDSP(0x41);

		SB_WriteDSP(md_mixfreq>>8);
		SB_WriteDSP(md_mixfreq&0xff);

		if(md_mode & DMODE_16BITS){
			SB_WriteDSP(0xb6);
			SB_WriteDSP((md_mode & DMODE_STEREO) ? 0x30 : 0x10);
		}
		else{
			SB_WriteDSP(0xc6);
			SB_WriteDSP((md_mode & DMODE_STEREO) ? 0x20 : 0x00);
		}

		SB_WriteDSP(0xff);
		SB_WriteDSP(0xef);
	}
}


static void SB_PlayStop(void)
{
	VC_PlayStop();
	SB_SpeakerOff();
	SB_ResetDSP();
	SB_ResetDSP();
	MDma_Stop(sb_dma);
	MIrq_OnOff(sb_irq,0);
}


DRIVER drv_sb={
	NULL,
	"Soundblaster & compatibles",
	"MikMod Soundblaster Driver v2.1 for 1.0 / 2.0 / Pro / 16",
	SB_IsThere,
	VC_SampleLoad,
	VC_SampleUnload,
	SB_Init,
	SB_Exit,
	SB_PlayStart,
	SB_PlayStop,
	SB_Update,
	VC_VoiceSetVolume,
	VC_VoiceSetFrequency,
	VC_VoiceSetPanning,
	VC_VoicePlay
};
