/*

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