/*
 * snd_gus.c -- Routines for GUS support in dosquake
 * Author(s) :  Jayeson Lee-Steere
 * from quake1 source with minor adaptations for uhexen2.
 * $Id: snd_gus.c 4767 2012-06-16 20:48:51Z sezero $
 *
 * Copyright (C) 1996-1997  Id Software, Inc.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "quakedef.h"
#include "snd_sys.h"

#if HAVE_DOS_GUS_SOUND

#include <dos.h>
#include "snd_gus.h"
#include "dosisms.h"


//=============================================================================
// Routines for reading from .INI files
// The read routines are fairly efficient
//=============================================================================

#define	INI_STRING_SIZE		0x100

static FILE *ini_fopen (const char *filename, const char *modes);
static int ini_fclose (FILE *f);
static void ini_fgets (FILE *f, const char *section, const char *field, char *s);

#define	MAX_SECTION_WIDTH	20
#define	MAX_FIELD_WIDTH		20

#define	NUM_SECTION_BUFFERS	10
#define	NUM_FIELD_BUFFERS	20

struct section_buffer
{
	long	offset;
	char	name[MAX_SECTION_WIDTH + 1];
};

struct field_buffer
{
	long	offset;
	int	section;
	char	name[MAX_FIELD_WIDTH + 1];
};

static FILE	*current_file = NULL;
static int	current_section;

static int	current_section_buffer = 0;
static int	current_field_buffer = 0;

static struct section_buffer	section_buffers[NUM_SECTION_BUFFERS];
static struct field_buffer	field_buffers[NUM_FIELD_BUFFERS];


static char my_toupper (char c)
{
	if (c >= 'a' && c <= 'z')
		c -= ('a' - 'A');
	return c;
}

static void reset_buffer (FILE *f)
{
	int		i;

	for (i = 0; i < NUM_SECTION_BUFFERS; i++)
		section_buffers[i].name[0] = 0;
	for (i = 0; i < NUM_FIELD_BUFFERS; i++)
		field_buffers[i].name[0] = 0;

	current_file = f;
}

// Sees if the current string is section "name" (i.e. ["name"]).
// If "name"=="*", sees if the current string is any section
// (i.e. [....]). Returns 1 if true else 0 if false.
static int is_section (const char *s, const char *name)
{
	int	wild = 0;

	// See if wild search
	if (strcmp("*", name) == 0)
		wild = 1;

	// Skip leading spaces
	while (s[0] == ' ')
		s++;
	// Look for leading "["
	if (s[0] != '[')
		return 0;
	s++;
	// Make sure name matches
	while (s[0] != ']' && s[0] != 13 && s[0] != 10 && s[0] != 0 && name[0] != 0)
	{
		if (!wild)
		{
			if (my_toupper(s[0]) != my_toupper(name[0]))
				return 0;
		}
		s++;
		if (!wild)
			name++;
	}
	if (!wild)
	{
		if (name[0] != 0)
			return 0;
	}
	// Skip trailing spaces
	while (s[0] == ' ')
		s++;
	// Make sure we have trailing "]"
	if (s[0] != ']')
		return 0;

	return 1;
}

// Sees if the current string is field "name" (i.e. "name"=...).
// If "name"=="*", sees if the current string is any field
// (i.e. ...=...). Returns 1 if true else 0 if false.
static int is_field (const char *s, const char *name)
{
	int	wild = 0;

	// See if wild search
	if (strcmp("*", name) == 0)
		wild = 1;

	// Skip leading spaces
	while (s[0] == ' ')
		s++;

	// Make sure name matches
	while (s[0] != '=' && s[0] != 13 && s[0] != 10 && s[0] != 0 && name[0] != 0)
	{
		if (!wild)
		{
			if (my_toupper(s[0]) != my_toupper(name[0]))
				return 0;
		}
		s++;
		if (!wild)
			name++;
	}
	if (!wild)
	{
		if (name[0] != 0)
			return 0;
	}
	// Skip trailing spaces
	while (s[0] == ' ')
		s++;
	// Make sure we have an "="
	if (s[0] != '=')
		return 0;

	return 1;
}

// Extracts the section name from a section heading
// e.g. in="[hey man]" gives out="hey man"
static void get_section_name (char *out, char *in)
{
	int	i = 0;

	// Skip spaces before '['
	while (in[0] == ' ')
		in++;
	// Make sure there is a '['
	if (in[0] != '[')
	{
		out[0] = 0;
		return;
	}
	// Skip past '['
	in++;
	// Copy string if any to output string.
	while (in[0] != ']' && in[0] != 13 && in[0] != 10 && in[0] != 0)
	{
		if (i < MAX_SECTION_WIDTH)
		{
			out[i] = in[0];
			i++;
		}
		in++;
	}
	// Make sure string was terminated with ']'
	if (in[0] != ']')
	{
		out[0] = 0;
		return;
	}
	// Remove trailing spaces
	while (i > 0 && out[i - 1] == ' ')
		i--;
	// Null terminate the output string.
	out[i] = 0;
}

// Extracts the field name from a field line
// e.g. in="sooty=life be in it" gives out="sooty"
static void get_field_name (char *out, char *in)
{
	int	i = 0;

	// Skip leading spaces
	while (in[0] == ' ')
		in++;
	// Copy name to output string
	while (in[0] != '=' && in[0] != 13 && in[0] != 10 && in[0] != 0)
	{
		if (i < MAX_FIELD_WIDTH)
		{
			out[i] = in[0];
			i++;
		}
		in++;
	}
	// Make sure we stopped on "="
	if (in[0] != '=')
	{
		out[0] = 0;
		return;
	}
	// Remove trailing spaces
	while (i > 0 && out[i - 1] == ' ')
		i--;
	// Null terminate the output string.
	out[i] = 0;
}

// Returns the field data from string s.
// e.g. in="wally = golly man" gives out="golly man"
static void get_field_string (char *out, char *in)
{
	int	i = 0;

	// Find '=' if it exists
	while (in[0] != '=' && in[0] != 13 && in[0] != 10 && in[0] != 0)
		in++;
	// If there is an '=', skip past it.
	if (in[0] == '=')
		in++;
	// Skip any spaces between the '=' and string.
	while (in[0] == ' ' || in[0] == '[')
		in++;
	// Copy string, if there is one, to the output string.
	while (in[0] != 13 && in[0] != 10 && in[0] != 0 && i < (INI_STRING_SIZE - 1))
	{
		out[i] = in[0];
		in++;
		i++;
	}
	// Null terminate the output string.
	out[i] = 0;
}

// Adds a section to the buffer
static int add_section (char *instring, long offset)
{
	int	i;
	char	section[MAX_SECTION_WIDTH + 1];

	// Extract section name
	get_section_name(section, instring);
	// See if section already exists.
	for (i = 0; i < NUM_SECTION_BUFFERS; i++)
	{
		if (stricmp(section, section_buffers[i].name) == 0)
			return i;
	}
	// Increment current_section_buffer
	current_section_buffer++;
	if (current_section_buffer > NUM_SECTION_BUFFERS)
		current_section_buffer = 0;
	// Delete any field buffers that correspond to this section
	for (i = 0; i < NUM_FIELD_BUFFERS; i++)
	{
		if (field_buffers[i].section == current_section_buffer)
			field_buffers[i].name[0] = 0;
	}
	// Set buffer information
	strcpy(section_buffers[current_section_buffer].name, section);
	section_buffers[current_section_buffer].offset = offset;
	return current_section_buffer;
}

// Adds a field to the buffer
static void add_field (char *instring, int section, long offset)
{
	int	i;
	char	field[MAX_FIELD_WIDTH + 1];

	// Extract field name
	get_field_name(field, instring);
	// See if field already exists
	for (i = 0; i < NUM_FIELD_BUFFERS; i++)
	{
		if (field_buffers[i].section == section)
		{
			if (stricmp(field_buffers[i].name, field) == 0)
				return;
		}
	}
	// Increment current_field_buffer
	current_field_buffer++;
	if (current_field_buffer > NUM_FIELD_BUFFERS)
		current_field_buffer = 0;
	// Set buffer information
	strcpy(field_buffers[current_field_buffer].name, field);
	field_buffers[current_field_buffer].section = section;
	field_buffers[current_field_buffer].offset = offset;
}

// Identical to fgets except the string is trucated at the first ';',
// carriage return or line feed.
static char *stripped_fgets (char *s, int n, FILE *f)
{
	int	i = 0;

	if (fgets(s, n, f) == NULL)
		return NULL;

	while (s[i] != ';' && s[i] != 13 && s[i] != 10 && s[i] != 0)
		i++;
	s[i] = 0;

	return s;
}

// Opens an .INI file. Works like fopen
static FILE *ini_fopen (const char *filename, const char *modes)
{
	return fopen(filename, modes);
}

// Closes a .INI file. Works like fclose
static int ini_fclose (FILE *f)
{
	if (f == current_file)
		reset_buffer (NULL);
	return fclose(f);
}

// Puts "field" from "section" from .ini file "f" into "s".
// If "section" does not exist or "field" does not exist in
// section then s="";
static void ini_fgets (FILE *f, const char *section, const char *field, char *s)
{
	int	i;
	long	start_pos, string_start_pos;
	char	ts[INI_STRING_SIZE * 2];

	if (f != current_file)
		reset_buffer(f);

	// Default to "Not found"
	s[0] = 0;

	// See if section is in buffer
	for (i = 0; i < NUM_SECTION_BUFFERS; i++)
	{
		if (strnicmp(section_buffers[i].name, section, MAX_SECTION_WIDTH) == 0)
			break;
	}

	// If section is in buffer, seek to it if necessary
	if (i < NUM_SECTION_BUFFERS)
	{
		if (i != current_section)
		{
			current_section = i;
			fseek(f, section_buffers[i].offset, SEEK_SET);
		}
	}
	// else look through .ini file for it.
	else
	{
		// Make sure we are not at eof or this will cause trouble.
		if (feof(f))
			rewind(f);
		start_pos = ftell(f);
		while (1)
		{
			stripped_fgets(ts, INI_STRING_SIZE * 2, f);
			// If it is a section, add it to the section buffer
			if (is_section(ts, "*"))
				current_section=add_section(ts, ftell(f));
			// If it is the section we are looking for, break.
			if (is_section(ts, section))
				break;
			// If we reach the end of the file, rewind to the start.
			if (feof(f))
				rewind(f);
			if (ftell(f) == start_pos)
				return;
		}
	}

	// See if field is in buffer
	for (i = 0; i < NUM_FIELD_BUFFERS; i++)
	{
		if (field_buffers[i].section == current_section)
		{
			if (strnicmp(field_buffers[i].name, field, MAX_FIELD_WIDTH) == 0)
				break;
		}
	}

	// If field is in buffer, seek to it and read it
	if (i < NUM_FIELD_BUFFERS)
	{
		fseek(f, field_buffers[i].offset, SEEK_SET);
		stripped_fgets(ts, INI_STRING_SIZE * 2, f);
		get_field_string(s, ts);
	}
	// else search through section for field.
	else
	{
		// Make sure we do not start at eof or this will cause problems.
		if (feof(f))
			fseek(f, section_buffers[current_section].offset, SEEK_SET);
		start_pos = ftell(f);
		while (1)
		{
			string_start_pos = ftell(f);
			stripped_fgets(ts, INI_STRING_SIZE * 2, f);
			// If it is a field, add it to the buffer
			if (is_field(ts, "*"))
				add_field(ts, current_section, string_start_pos);
			// If it is the field we are looking for, save it
			if (is_field(ts, field))
			{
				get_field_string(s, ts);
				break;
			}
			// If we reach the end of the section, start over
			if (feof(f) || is_section(ts, "*"))
				fseek(f, section_buffers[current_section].offset, SEEK_SET);
			if (ftell(f) == start_pos)
				return;
		}
	}
}

//=============================================================================
// GUS support
//=============================================================================

#define	BYTE		unsigned char
#define	WORD		unsigned short
#define	DWORD		unsigned long

#define	BUFFER_SIZE		4096

#define	CODEC_ADC_INPUT_CONTROL_LEFT	0x00
#define	CODEC_ADC_INPUT_CONTROL_RIGHT	0x01
#define	CODEC_AUX1_INPUT_CONTROL_LEFT	0x02
#define	CODEC_AUX1_INPUT_CONTROL_RIGHT	0x03
#define	CODEC_AUX2_INPUT_CONTROL_LEFT	0x04
#define	CODEC_AUX2_INPUT_CONTROL_RIGHT	0x05
#define	CODEC_DAC_OUTPUT_CONTROL_LEFT	0x06
#define	CODEC_DAC_OUTPUT_CONTROL_RIGHT	0x07
#define	CODEC_FS_FORMAT			0x08
#define	CODEC_INTERFACE_CONFIG		0x09
#define	CODEC_PIN_CONTROL		0x0A
#define	CODEC_ERROR_STATUS_AND_INIT	0x0B
#define	CODEC_MODE_AND_ID		0x0C
#define	CODEC_LOOPBACK_CONTROL		0x0D
#define	CODEC_PLAYBACK_UPPER_BASE_COUNT	0x0E
#define	CODEC_PLAYBACK_LOWER_BASE_COUNT	0x0F

#define	SET_CONTROL			0x00
#define	SET_FREQUENCY			0x01
#define	SET_START_HIGH			0x02
#define	SET_START_LOW			0x03
#define	SET_END_HIGH			0x04
#define	SET_END_LOW			0x05
#define	SET_VOLUME_RATE			0x06
#define	SET_VOLUME_START		0x07
#define	SET_VOLUME_END			0x08
#define	SET_CURR_VOLUME			0x09
#define	SET_VOLUME			0x09
#define	SET_ACC_HIGH			0x0A
#define	SET_ACC_LOW			0x0B
#define	SET_BALANCE			0x0C
#define	SET_VOLUME_CONTROL		0x0D
#define	SET_VOICES			0x0E

#define	DMA_CONTROL			0x41
#define	SET_DMA_ADDRESS			0x42
#define	SET_DRAM_LOW			0x43
#define	SET_DRAM_HIGH			0x44
#define	ADLIB_CONTROL			0x45
#define	ADLIB_TIMER1			0x46
#define	ADLIB_TIMER2			0x47
#define	SET_RECORD_RATE			0x48
#define	RECORD_CONTROL			0x49
#define	SET_JOYSTICK			0x4B
#define	MASTER_RESET			0x4C

#define	GET_CONTROL			0x80
#define	GET_FREQUENCY			0x81
#define	GET_START_HIGH			0x82
#define	GET_START_LOW			0x83
#define	GET_END_HIGH			0x84
#define	GET_END_LOW			0x85
#define	GET_VOLUME_RATE			0x86
#define	GET_VOLUME_START		0x87
#define	GET_VOLUME_END			0x88
#define	GET_VOLUME			0x89
#define	GET_ACC_HIGH			0x8A
#define	GET_ACC_LOW			0x8B
#define	GET_BALANCE			0x8C
#define	GET_VOLUME_CONTROL		0x8D
#define	GET_VOICES			0x8E
#define	GET_IRQV			0x8F

struct CodecRateStruct
{
	WORD	Rate;
	BYTE	FSVal;
};

struct Gf1RateStruct
{
	WORD	Rate;
	BYTE	Voices;
};

static void  *dma_dosadr = NULL;	/* as received from dos_getmemory() */
static short *dma_buffer = NULL;

static char s_gus_driver[] = "GUS";

static BYTE HaveCodec = 0;

static WORD CodecRegisterSelect;
static WORD CodecData;
static WORD CodecStatus;
static WORD Gf1TimerControl;
static WORD Gf1PageRegister;
static WORD Gf1RegisterSelect;
static WORD Gf1DataLow;
static WORD Gf1DataHigh;

static BYTE DmaChannel;

static BYTE PageRegs[] = { 0x87, 0x83, 0x81, 0x82, 0x8f, 0x8b, 0x89, 0x8a };
static BYTE AddrRegs[] = { 0, 2, 4, 6, 0xc0, 0xc4, 0xc8, 0xcc };
static BYTE CountRegs[] = { 1, 3, 5, 7, 0xc2, 0xc6, 0xca, 0xce };

static WORD AddrReg;
static WORD CountReg;
static WORD ModeReg;
static WORD DisableReg;
static WORD ClearReg;

static struct CodecRateStruct CodecRates[]=
{
	{  5512, 0x01 },
	{  6620, 0x0F },
	{  8000, 0x00 },
	{  9600, 0x0E },
	{ 11025, 0x03 },
	{ 16000, 0x02 },
	{ 18900, 0x05 },
	{ 22050, 0x07 },
	{ 27420, 0x04 },
	{ 32000, 0x06 },
	{ 33075, 0x0D },
	{ 37800, 0x09 },
	{ 44100, 0x0B },
	{ 48000, 0x0C },
	{     0, 0x00 }	// End marker
};

static struct Gf1RateStruct Gf1Rates[]=
{
	{ 19293, 32 },
	{ 19916, 31 },
	{ 20580, 30 },
	{ 21289, 29 },
	{ 22050, 28 },
	{ 22866, 27 },
	{ 23746, 26 },
	{ 24696, 25 },
	{ 25725, 24 },
	{ 26843, 23 },
	{ 28063, 22 },
	{ 29400, 21 },
	{ 30870, 20 },
	{ 32494, 19 },
	{ 34300, 18 },
	{ 36317, 17 },
	{ 38587, 16 },
	{ 41160, 15 },
	{ 44100, 14 },
	{     0,  0 }
};

//=============================================================================
// Basic GF1 functions
//=============================================================================
static void SetGf18 (BYTE reg, BYTE data)
{
	dos_outportb(Gf1RegisterSelect, reg);
	dos_outportb(Gf1DataHigh, data);
}

static void SetGf116 (BYTE reg, WORD data)
{
	dos_outportb(Gf1RegisterSelect, reg);
	dos_outportw(Gf1DataLow, data);
}

static BYTE GetGf18 (BYTE reg)
{
	dos_outportb(Gf1RegisterSelect, reg);
	return (dos_inportb(Gf1DataHigh));
}

static WORD GetGf116 (BYTE reg)
{
	dos_outportb(Gf1RegisterSelect, reg);
	return (dos_inportw(Gf1DataLow));
}

static void Gf1Delay (void)
{
	int	i;

	for (i = 0; i < 27; i++)
		dos_inportb(Gf1TimerControl);
}

static DWORD ConvertTo16 (DWORD Address)
{
	return (((Address >> 1) & 0x0001FFFF) | (Address & 0x000C0000L));
}

static void ClearGf1Ints (void)
{
	int	i;

	SetGf18(DMA_CONTROL, 0x00);
	SetGf18(ADLIB_CONTROL, 0x00);
	SetGf18(RECORD_CONTROL, 0x00);

	GetGf18(DMA_CONTROL);
	GetGf18(RECORD_CONTROL);
	for (i = 0; i < 32; i++)
		;
	GetGf18(GET_IRQV);
}


//=============================================================================
// Get Interwave (UltraSound PnP) configuration if any
//=============================================================================
static qboolean GUS_GetIWData (void)
{
	char	*Interwave, s[INI_STRING_SIZE];
	FILE	*IwFile;
	int	CodecBase, CodecDma, i;

	Interwave = getenv("INTERWAVE");
	if (Interwave == NULL)
		return false;

	// Open IW.INI
	IwFile = ini_fopen(Interwave, "rt");
	if (IwFile == NULL)
		return false;

	// Read codec base and codec DMA
	ini_fgets(IwFile, "setup 0", "CodecBase", s);
	sscanf(s, "%X", &CodecBase);
	ini_fgets(IwFile, "setup 0", "DMA2", s);
	sscanf(s, "%i", &CodecDma);

	ini_fclose(IwFile);

	// Make sure numbers OK
	if (CodecBase == 0 || CodecDma == 0)
		return false;

	CodecRegisterSelect = CodecBase;
	CodecData = CodecBase + 1;
	CodecStatus = CodecBase + 2;
	DmaChannel = CodecDma;

	// Make sure there is a CODEC at the CODEC base

	// Clear any pending IRQs
	dos_inportb(CodecStatus);
	dos_outportb(CodecStatus, 0);

	// Wait for 'INIT' bit to clear
	for (i = 0; i < 0xFFFF; i++)
	{
		if ((dos_inportb(CodecRegisterSelect) & 0x80) == 0)
			break;
	}
	if (i == 0xFFFF)
		return false;

	// Get chip revision - can not be zero
	dos_outportb(CodecRegisterSelect, CODEC_MODE_AND_ID);
	if ((dos_inportb(CodecRegisterSelect) & 0x7F) != CODEC_MODE_AND_ID)
		return false;
	if ((dos_inportb(CodecData) & 0x0F) == 0)
		return false;

	HaveCodec = 1;
	Con_Printf("Sound Card is UltraSound PnP\n");
	return true;
}

//=============================================================================
// Get UltraSound MAX configuration if any
//=============================================================================
static qboolean GUS_GetMAXData (void)
{
	char	*Ultrasnd, *Ultra16;
	int	i;
	int	GusBase, Dma1, Dma2, Irq1, Irq2;
	int	CodecBase, CodecDma, CodecIrq, CodecType;
	BYTE	MaxVal;

	Ultrasnd = getenv("ULTRASND");
	Ultra16 = getenv("ULTRA16");
	if (Ultrasnd == NULL || Ultra16 == NULL)
		return false;

	sscanf(Ultrasnd, "%x,%i,%i,%i,%i", &GusBase, &Dma1, &Dma2, &Irq1, &Irq2);
	sscanf(Ultra16, "%x,%i,%i,%i", &CodecBase, &CodecDma, &CodecIrq, &CodecType);

	if (CodecType == 0 && CodecDma != 0)
		DmaChannel = CodecDma & 0x07;
	else
		DmaChannel = Dma2 & 0x07;

	// Make sure there is a GUS at GUS base
	dos_outportb(GusBase + 0x08, 0x55);
	if (dos_inportb(GusBase + 0x0A) != 0x55)
		return false;
	dos_outportb(GusBase + 0x08, 0xAA);
	if (dos_inportb(GusBase + 0x0A) != 0xAA)
		return false;

	// Program CODEC control register
	MaxVal = ((CodecBase & 0xF0) >> 4) | 0x40;
	if (Dma1 > 3)
		MaxVal |= 0x10;
	if (Dma2 > 3)
		MaxVal |= 0x20;
	dos_outportb(GusBase + 0x106, MaxVal);

	CodecRegisterSelect = CodecBase;
	CodecData = CodecBase + 1;
	CodecStatus = CodecBase + 2;

	// Make sure there is a CODEC at the CODEC base

	// Clear any pending IRQs
	dos_inportb(CodecStatus);
	dos_outportb(CodecStatus, 0);

	// Wait for 'INIT' bit to clear
	for (i = 0; i < 0xFFFF; i++)
	{
		if ((dos_inportb(CodecRegisterSelect) & 0x80) == 0)
			break;
	}
	if (i == 0xFFFF)
		return false;

	// Get chip revision - can not be zero
	dos_outportb(CodecRegisterSelect, CODEC_MODE_AND_ID);
	if ((dos_inportb(CodecRegisterSelect) & 0x7F) != CODEC_MODE_AND_ID)
		return false;
	if ((dos_inportb(CodecData) & 0x0F) == 0)
		return false;

	HaveCodec = 1;
	Con_Printf("Sound Card is UltraSound MAX\n");
	return true;
}

//=============================================================================
// Get regular UltraSound configuration if any
//=============================================================================
static qboolean GUS_GetGUSData (void)
{
	char	*Ultrasnd;
	int	GusBase, Dma1, Dma2, Irq1, Irq2, i;

	Ultrasnd = getenv("ULTRASND");
	if (Ultrasnd == NULL)
		return false;

	sscanf(Ultrasnd, "%x,%i,%i,%i,%i", &GusBase, &Dma1, &Dma2, &Irq1, &Irq2);

	DmaChannel = Dma1 & 0x07;

	// Make sure there is a GUS at GUS base
	dos_outportb(GusBase + 0x08, 0x55);
	if (dos_inportb(GusBase + 0x0A) != 0x55)
		return false;
	dos_outportb(GusBase + 0x08, 0xAA);
	if (dos_inportb(GusBase + 0x0A) != 0xAA)
		return false;

	Gf1TimerControl	  = GusBase + 0x008;
	Gf1PageRegister	  = GusBase + 0x102;
	Gf1RegisterSelect = GusBase + 0x103;
	Gf1DataLow	  = GusBase + 0x104;
	Gf1DataHigh	  = GusBase + 0x105;

	// Reset the GUS
	SetGf18(MASTER_RESET, 0x00);
	Gf1Delay();
	Gf1Delay();
	SetGf18(MASTER_RESET, 0x01);
	Gf1Delay();
	Gf1Delay();

	// Set to max (32) voices
	SetGf18(SET_VOICES, 0xDF);

	// Clear any pending IRQ's
	ClearGf1Ints();

	// Set all registers to known values
	for (i = 0; i < 32; i++)
	{
		dos_outportb(Gf1PageRegister, i);
		SetGf18(SET_CONTROL, 0x03);
		SetGf18(SET_VOLUME_CONTROL, 0x03);
		Gf1Delay();
		SetGf18(SET_CONTROL, 0x03);
		SetGf18(SET_VOLUME_CONTROL, 0x03);
		SetGf116(SET_START_HIGH, 0);
		SetGf116(SET_START_LOW, 0);
		SetGf116(SET_END_HIGH, 0);
		SetGf116(SET_END_LOW, 0);
		SetGf116(SET_ACC_HIGH, 0);
		SetGf116(SET_ACC_LOW, 0);
		SetGf18(SET_VOLUME_RATE, 63);
		SetGf18(SET_VOLUME_START, 5);
		SetGf18(SET_VOLUME_END, 251);
		SetGf116(SET_VOLUME, 5 << 8);
	}

	// Clear any pending IRQ's
	ClearGf1Ints();

	// Enable DAC etc.
	SetGf18(MASTER_RESET, 0x07);

	// Enable line output so we can hear something
	dos_outportb(GusBase, 0x08);

	HaveCodec = 0;
	Con_Printf("Sound Card is UltraSound\n");
	return true;
}


//=============================================================================
// Programs the DMA controller to start DMAing in Auto-init mode
//=============================================================================
static void GUS_StartDMA (BYTE dmaChannel, short *dma_buff, int count)
{
	int	mode;
	int	RealAddr;

	RealAddr = ptr2real(dma_buff);

	if (dmaChannel <= 3)
	{
		ModeReg = 0x0B;
		DisableReg = 0x0A;
		ClearReg = 0x0E;
	}
	else
	{
		ModeReg = 0xD6;
		DisableReg = 0xD4;
		ClearReg = 0xDC;
	}
	CountReg = CountRegs[dmaChannel];
	AddrReg = AddrRegs[dmaChannel];

	dos_outportb(DisableReg, dmaChannel | 4);	// disable channel

	// set mode- see "undocumented pc", p.876
	mode = (1 << 6)		// single-cycle
		+ (0 << 5)	// address increment
		+ (1 << 4)	// auto-init dma
		+ (2 << 2)	// read
		+ (dmaChannel & 0x03);	// channel #
	dos_outportb(ModeReg, mode);

	// set page
	dos_outportb(PageRegs[dmaChannel], RealAddr >> 16);

	if (dmaChannel <= 3)
	{	// address is in bytes
		dos_outportb(0x0C, 0);		// prepare to send 16-bit value
		dos_outportb(AddrReg, RealAddr & 0xff);
		dos_outportb(AddrReg, (RealAddr >> 8) & 0xff);

		dos_outportb(0x0C, 0);		// prepare to send 16-bit value
		dos_outportb(CountReg, (count - 1) & 0xff);
		dos_outportb(CountReg, (count - 1) >> 8);
	}
	else
	{	// address is in words
		dos_outportb(0xD8, 0);		// prepare to send 16-bit value
		dos_outportb(AddrReg, (RealAddr >> 1) & 0xff);
		dos_outportb(AddrReg, (RealAddr >> 9) & 0xff);

		dos_outportb(0xD8, 0);		// prepare to send 16-bit value
		dos_outportb(CountReg, ((count >> 1) - 1) & 0xff);
		dos_outportb(CountReg, ((count >> 1) - 1) >> 8);
	}

	dos_outportb(ClearReg, 0);		// clear write mask
	dos_outportb(DisableReg, dmaChannel & ~4);
}

//=============================================================================
// Starts the CODEC playing
//=============================================================================
static void GUS_StartCODEC (int count, BYTE FSVal)
{
	int	i, j;

	// Clear any pending IRQs
	dos_inportb(CodecStatus);
	dos_outportb(CodecStatus, 0);

	// Set mode to 2
	dos_outportb(CodecRegisterSelect, CODEC_MODE_AND_ID);
	dos_outportb(CodecData, 0xC0);

	// Stop any playback or capture which may be happening
	dos_outportb(CodecRegisterSelect, CODEC_INTERFACE_CONFIG);
	dos_outportb(CodecData, dos_inportb(CodecData) & 0xFC);

	// Set FS
	dos_outportb(CodecRegisterSelect, CODEC_FS_FORMAT | 0x40);
	dos_outportb(CodecData, FSVal | 0x50);	// Or in stereo and 16 bit bits

	// Wait a bit
	for (i = 0; i < 10; i++)
		dos_inportb(CodecData);

	// Routine 1 to counter CODEC bug - wait for init bit to clear and then a
	// bit longer (i=min loop count, j=timeout
	for (i = 0, j = 0; i < 1000 && j < 0x7FFFF; j++)
	{
		if ((dos_inportb(CodecRegisterSelect) & 0x80) == 0)
			i++;
	}

	// Routine 2 to counter CODEC bug - this is from Forte's code. For me it
	// does not seem to cure the problem, but is added security
	// Waits till we can modify index register
	for (j = 0; j < 0x7FFFF; j++)
	{
		dos_outportb(CodecRegisterSelect, CODEC_INTERFACE_CONFIG | 0x40);
		if (dos_inportb(CodecRegisterSelect) == (CODEC_INTERFACE_CONFIG | 0x40))
			break;
	}

	// Perform ACAL
	dos_outportb(CodecRegisterSelect, CODEC_INTERFACE_CONFIG | 0x40);
	dos_outportb(CodecData, 0x08);

	// Clear MCE bit - this makes ACAL happen
	dos_outportb(CodecRegisterSelect, CODEC_INTERFACE_CONFIG);

	// Wait for ACAL to finish
	for (j = 0; j < 0x7FFFF; j++)
	{
		if ((dos_inportb(CodecRegisterSelect) & 0x80) != 0)
			continue;
		dos_outportb(CodecRegisterSelect, CODEC_ERROR_STATUS_AND_INIT);
		if ((dos_inportb(CodecData) & 0x20) == 0)
			break;
	}

	// Clear ACAL bit
	dos_outportb(CodecRegisterSelect, CODEC_INTERFACE_CONFIG | 0x40);
	dos_outportb(CodecData, 0x00);
	dos_outportb(CodecRegisterSelect, CODEC_INTERFACE_CONFIG);

	// Set some other junk
	dos_outportb(CodecRegisterSelect, CODEC_LOOPBACK_CONTROL);
	dos_outportb(CodecData, 0x00);
	dos_outportb(CodecRegisterSelect, CODEC_PIN_CONTROL);
	dos_outportb(CodecData, 0x08);	// IRQ is disabled in PIN control

	// Set count (it doesn't really matter what value we stuff in here
	dos_outportb(CodecRegisterSelect, CODEC_PLAYBACK_LOWER_BASE_COUNT);
	dos_outportb(CodecData, count & 0xFF);
	dos_outportb(CodecRegisterSelect, CODEC_PLAYBACK_UPPER_BASE_COUNT);
	dos_outportb(CodecData, count >> 8);

	// Start playback
	dos_outportb(CodecRegisterSelect, CODEC_INTERFACE_CONFIG);
	dos_outportb(CodecData, 0x01);
}

//=============================================================================
// Starts the GF1 playing
//=============================================================================
static void GUS_StartGf1 (int count, BYTE Voices)
{
	DWORD	StartAddressL, EndAddressL, StartAddressR, EndAddressR;

	// Set number of voices to give us the sampling rate we want
	SetGf18(SET_VOICES,0xC0 | (Voices - 1));

	// Figure out addresses
	StartAddressL = ConvertTo16(0);
	EndAddressL = ConvertTo16(count - 2 - 2);
	StartAddressR = ConvertTo16(2);
	EndAddressR = ConvertTo16(count - 2);

	// Set left voice addresses
	dos_outportb(Gf1PageRegister, 0);
	SetGf116(SET_START_LOW, StartAddressL << 9);
	SetGf116(SET_START_HIGH, StartAddressL >> 7);
	SetGf116(SET_ACC_LOW, StartAddressL << 9);
	SetGf116(SET_ACC_HIGH, StartAddressL >> 7);
	SetGf116(SET_END_LOW, EndAddressL << 9);
	SetGf116(SET_END_HIGH, EndAddressL >> 7);
	// Set balance to full left
	SetGf18(SET_BALANCE, 0);
	// Set volume to full
	SetGf116(SET_VOLUME, 0xFFF0);
	// Set FC to 2 (so we play every second sample)
	SetGf116(SET_FREQUENCY, 0x0800);

	// Set right voice addresses
	dos_outportb(Gf1PageRegister, 1);
	SetGf116(SET_START_LOW, StartAddressR << 9);
	SetGf116(SET_START_HIGH, StartAddressR >> 7);
	SetGf116(SET_ACC_LOW, StartAddressR << 9);
	SetGf116(SET_ACC_HIGH, StartAddressR >> 7);
	SetGf116(SET_END_LOW, EndAddressR << 9);
	SetGf116(SET_END_HIGH, EndAddressR >> 7);
	// Set balance to full right
	SetGf18(SET_BALANCE, 15);
	// Set volume to full
	SetGf116(SET_VOLUME, 0xFFF0);
	// Set FC to 2 (so we play every second sample)
	SetGf116(SET_FREQUENCY, 0x0800);

	// Start voices
	dos_outportb(Gf1PageRegister, 0);
	SetGf18(SET_CONTROL, 0x0C);
	dos_outportb(Gf1PageRegister, 1);
	SetGf18(SET_CONTROL, 0x0C);
	Gf1Delay();
	dos_outportb(Gf1PageRegister, 0);
	SetGf18(SET_CONTROL, 0x0C);
	dos_outportb(Gf1PageRegister, 1);
	SetGf18(SET_CONTROL, 0x0C);
}


//=============================================================================
// Figures out what kind of UltraSound we have, if any, and starts it playing
//=============================================================================
static qboolean S_GUS_Init (dma_t *dma)
{
	int		rc;
	int		RealAddr;
	BYTE	FSVal, Voices;
	struct CodecRateStruct	*CodecRate;
	struct Gf1RateStruct	*Gf1Rate;

	if (COM_CheckParm("-nogus"))
		return false;

	// See what kind of UltraSound we have, if any
	if (GUS_GetIWData() == false)
	{
		if (GUS_GetMAXData() == false)
		{
			if (GUS_GetGUSData() == false)
				return false;
		}
	}

	memset ((void *) dma, 0, sizeof(dma_t));
	shm = dma;

	if (HaveCodec)
	{
		// do 11khz sampling rate unless command line parameter wants different
		shm->speed = 11025;
		FSVal = 0x03;
		rc = COM_CheckParm("-sndspeed");
		if (rc && rc < com_argc - 1)
		{
			shm->speed = atoi(com_argv[rc+1]);

			// Make sure rate not too high
			if (shm->speed > 48000)
				shm->speed = 48000;

			// Adjust speed to match one of the possible CODEC rates
			for (CodecRate = CodecRates; CodecRate->Rate != 0; CodecRate++)
			{
				if (shm->speed <= CodecRate->Rate)
				{
					shm->speed = CodecRate->Rate;
					FSVal = CodecRate->FSVal;
					break;
				}
			}
		}

		// Always do 16 bit stereo
		shm->channels = 2;
		shm->samplebits = 16;

		// allocate buffer twice the size we need so we can get aligned buffer
		dma_dosadr = dos_getmemory(BUFFER_SIZE * 2);
		if (dma_dosadr == NULL)
		{
			shm = NULL;
			Con_Printf("Couldn't allocate sound dma buffer");
			return false;
		}

		RealAddr = ptr2real(dma_dosadr);
		RealAddr = (RealAddr + BUFFER_SIZE) & ~(BUFFER_SIZE - 1);
		dma_buffer = (short *) real2ptr(RealAddr);

		// Zero off DMA buffer
		memset(dma_buffer, 0, BUFFER_SIZE);

		shm->samplepos = 0;
		shm->submission_chunk = 1;
		shm->buffer = (unsigned char *) dma_buffer;
		shm->samples = BUFFER_SIZE / (shm->samplebits / 8);

		GUS_StartDMA(DmaChannel, dma_buffer, BUFFER_SIZE);
		GUS_StartCODEC(BUFFER_SIZE, FSVal);
	}
	else
	{
		// do 19khz sampling rate unless command line parameter wants different
		shm->speed = 19293;
		Voices = 32;
		rc = COM_CheckParm("-sndspeed");
		if (rc && rc < com_argc - 1)
		{
			shm->speed = atoi(com_argv[rc+1]);

			// Make sure rate not too high
			if (shm->speed > 44100)
				shm->speed = 44100;

			// Adjust speed to match one of the possible GF1 rates
			for (Gf1Rate = Gf1Rates; Gf1Rate->Rate != 0; Gf1Rate++)
			{
				if (shm->speed <= Gf1Rate->Rate)
				{
					shm->speed = Gf1Rate->Rate;
					Voices = Gf1Rate->Voices;
					break;
				}
			}
		}

		// Always do 16 bit stereo
		shm->channels = 2;
		shm->samplebits = 16;

		// allocate buffer twice the size we need so we can get aligned buffer
		dma_dosadr = dos_getmemory(BUFFER_SIZE * 2);
		if (dma_dosadr == NULL)
		{
			shm = NULL;
			Con_Printf("Couldn't allocate sound dma buffer");
			return false;
		}

		RealAddr = ptr2real(dma_dosadr);
		RealAddr = (RealAddr + BUFFER_SIZE) & ~(BUFFER_SIZE - 1);
		dma_buffer = (short *) real2ptr(RealAddr);

		// Zero off DMA buffer
		memset(dma_buffer, 0, BUFFER_SIZE);

		shm->samplepos = 0;
		shm->submission_chunk = 1;
		shm->buffer = (unsigned char *) dma_buffer;
		shm->samples = BUFFER_SIZE / (shm->samplebits / 8);

		GUS_StartDMA(DmaChannel, dma_buffer, BUFFER_SIZE);
		SetGf116(SET_DMA_ADDRESS, 0x0000);
		if (DmaChannel <= 3)
			SetGf18(DMA_CONTROL, 0x41);
		else
			SetGf18(DMA_CONTROL, 0x45);
		GUS_StartGf1(BUFFER_SIZE, Voices);
	}
	return true;
}

//=============================================================================
// Returns the current playback position
//=============================================================================
static int S_GUS_GetDMAPos (void)
{
	int	count;

	if (! dma_buffer)	/* not initialized */
		return 0;

	if (HaveCodec)
	{
		// clear 16-bit reg flip-flop
		// load the current dma count register
		if (DmaChannel < 4)
		{
			dos_outportb(0x0C, 0);
			count = dos_inportb(CountReg);
			count += dos_inportb(CountReg) << 8;
			if (shm->samplebits == 16)
				count /= 2;
			count = shm->samples - (count + 1);
		}
		else
		{
			dos_outportb(0xD8, 0);
			count = dos_inportb(CountReg);
			count += dos_inportb(CountReg) << 8;
			if (shm->samplebits == 8)
				count *= 2;
			count = shm->samples - (count + 1);
		}
	}
	else
	{
		// Read current position from GF1
		dos_outportb(Gf1PageRegister,0);
		count = (GetGf116(GET_ACC_HIGH) << 7) & 0xFFFF;
		// See which half of buffer we are in. Note that since this is 16 bit
		// data we are playing, position is in 16 bit samples
		if (GetGf18(DMA_CONTROL) & 0x40)
		{
			GUS_StartDMA(DmaChannel, dma_buffer, BUFFER_SIZE);
			SetGf116(SET_DMA_ADDRESS,0x0000);
			if (DmaChannel <= 3)
				SetGf18(DMA_CONTROL, 0x41);
			else
				SetGf18(DMA_CONTROL, 0x45);
		}
	}

	shm->samplepos = count & (shm->samples - 1);
	return shm->samplepos;
}

//=============================================================================
// Stops the UltraSound playback
//=============================================================================
static void S_GUS_Shutdown (void)
{
	if (! dma_buffer)	/* not initialized */
		return;

	if (HaveCodec)
	{
		// Stop CODEC
		dos_outportb(CodecRegisterSelect, CODEC_INTERFACE_CONFIG);
		dos_outportb(CodecData, 0x01);
	}
	else
	{
		// Stop Voices
		dos_outportb(Gf1PageRegister, 0);
		SetGf18(SET_CONTROL, 0x03);
		dos_outportb(Gf1PageRegister, 1);
		SetGf18(SET_CONTROL, 0x03);
		Gf1Delay();
		dos_outportb(Gf1PageRegister, 0);
		SetGf18(SET_CONTROL, 0x03);
		dos_outportb(Gf1PageRegister, 1);
		SetGf18(SET_CONTROL, 0x03);

		// Stop any DMA
		SetGf18(DMA_CONTROL, 0x00);
		GetGf18(DMA_CONTROL);
	}

	dos_outportb(DisableReg, DmaChannel | 4);	// disable dma channel
	shm = NULL;

	dos_freememory(dma_dosadr);
	dma_dosadr = NULL;
}

/*
==============
SNDDMA_LockBuffer

Makes sure dma buffer is valid
===============
*/
static void S_GUS_LockBuffer (void)
{
	/* nothing to do here */
}

/*
==============
SNDDMA_Submit

Unlock the dma buffer /
Send sound to the device
===============
*/
static void S_GUS_Submit (void)
{
	/* nothing to do here */
}

static void S_GUS_BlockSound (void)
{
}

static void S_GUS_UnblockSound (void)
{
}

snd_driver_t snddrv_gus =
{
	S_GUS_Init,
	S_GUS_Shutdown,
	S_GUS_GetDMAPos,
	S_GUS_LockBuffer,
	S_GUS_Submit,
	S_GUS_BlockSound,
	S_GUS_UnblockSound,
	s_gus_driver,
	SNDDRV_ID_GUS_DOS,
	false,
	NULL
};

#endif	/* HAVE_DOS_GUS_SOUND */

