/*******************************************************************************
	FILE:			SHPLAY.CPP

	DESCRIPTION:	Sonic Holography 3D sound player

	AUTHOR:			Peter M. Freese
	CREATED:		11-09-95
	COPYRIGHT:		Copyright (c) 1995 Q Studios Corporation
*******************************************************************************/
#include <conio.h>
#include <dos.h>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <io.h>
#include <fcntl.h>

#include "debug4g.h"
#include "error.h"
#include "fx_man.h"
#include "getopt.h"
#include "inifile.h"
#include "key.h"
#include "misc.h"
#include "music.h"
#include "resource.h"
#include "task_man.h"
#include "textio.h"
#include "typedefs.h"
#include "usrhooks.h"
#include "helix.h"
#include "gfx.h"
#include "pcx.h"


#define kAspectAdjustX	1.2

#define kPi				3.141592654
#define kPi2			(kPi * 2)

#define kAngle360		kPi2
#define kAngle180		(kAngle360 / 2)
#define kAngle90		(kAngle360 / 4)
#define kAngle60		(kAngle360 / 6)
#define kAngle45		(kAngle360 / 8)
#define kAngle30		(kAngle360 / 12)
#define kAngle20		(kAngle360 / 18)
#define kAngle15		(kAngle360 / 24)
#define kAngle10		(kAngle360 / 36)
#define kAngle5			(kAngle360 / 72)

#define kTimerRate		30
#define kSampleRate		11110
#define kIntraAuralTime	0.0006
#define kVolScale		10

#define kMaxPlaySounds	255


struct MENU_LINE
{
	int id;
	char *name;
	char *hint;
};

Resource gResource;

IniFile config("SHPLAY.INI");

MENU_LINE SoundCardMenu[] =
{
	{ -1,				"None",					"No sound card" },
	{ SoundBlaster,		"SoundBlaster",			"Sound Blaster, SB Pro, SB 16, or compatible" },
	{ ProAudioSpectrum,	"Pro Audio Spectrum",	"Media Vision Pro Audio Sprectrum" },
	{ SoundMan16,		"Sound Man 16",			"Logitech Sound Man 16" },
	{ Awe32,			"Awe 32",				"Sound Blaster Awe 32" },
	{ SoundScape,		"SoundScape",			"Ensoniq Sound Scape" },
	{ UltraSound,		"Gravis UltraSound",	"Advanced Gravis UltraSound" },
	{ SoundSource,		"Disney Sound Source",	"Disney Sound Source" },
	{ TandySoundSource,	"Tandy Sound Source",	"Tandy Sound Source" },
	{ 0, NULL, NULL },
};

MENU_LINE BlasterTypeMenu[] =
{
	{ fx_SB,		"Sound Blaster or compatible",	"Vanilla Sound Blaster Card" },
	{ fx_SB20,		"Sound Blaster 2.0",			"Sound Blaster 2.0 Card" },
	{ fx_SBPro,		"Sound Blaster Pro (old)",		"Original Sound Blaster Pro" },
	{ fx_SBPro2,	"Sound Blaster Pro (new)",		"Newer Sound Blaster Pro" },
	{ fx_SB16,		"Sound Blaster 16 or AWE32",	"Sound Blaster 16 or AWE 32" },
	{ 0, NULL, NULL },
};

MENU_LINE BlasterAddressMenu[] =
{
	{ 0x210,	"210h",				"Sound Blaster Port Address at 210h" },
	{ 0x220,	"220h (default)",	"Sound Blaster Port Address at 220h" },
	{ 0x230,	"230h",				"Sound Blaster Port Address at 230h" },
	{ 0x240,	"240h",				"Sound Blaster Port Address at 240h" },
	{ 0x250,	"250h",				"Sound Blaster Port Address at 250h" },
	{ 0x260,	"260h",				"Sound Blaster Port Address at 260h" },
	{ 0x270,	"270h",				"Sound Blaster Port Address at 270h" },
	{ 0x280,	"280h",				"Sound Blaster Port Address at 280h" },
	{ 0, NULL, NULL },
};

MENU_LINE BlasterIRQMenu[] =
{
	{ 2,	"IRQ 2",			"Sound Blaster Set to IRQ 2" },
	{ 3,	"IRQ 3",			"Sound Blaster Set to IRQ 3" },
	{ 5,	"IRQ 5 (default",	"Sound Blaster Set to IRQ 5" },
	{ 7,	"IRQ 7",			"Sound Blaster Set to IRQ 7" },
	{ 10,	"IRQ 10",			"Sound Blaster Set to IRQ 10" },
	{ 11,	"IRQ 11",			"Sound Blaster Set to IRQ 11" },
	{ 12,	"IRQ 12",			"Sound Blaster Set to IRQ 12" },
	{ 15,	"IRQ 15",			"Sound Blaster Set to IRQ 15" },
	{ 0, NULL, NULL },
};

MENU_LINE BlasterDma8Menu[] =
{
	{ 0,	"Channel 0",			"8 bit DMA on channel 0" },
	{ 1,	"Channel 1 (default)",	"8 bit DMA on channel 1" },
	{ 3,	"Channel 3",			"8 bit DMA on channel 3" },
	{ 0, NULL, NULL },
};

MENU_LINE BlasterDma16Menu[] =
{
	{ 5,	"Channel 5 (default)",	"16 bit DMA on channel 5" },
	{ 6,	"Channel 6",			"16 bit DMA on channel 6" },
	{ 7,	"Channel 7",			"16 bit DMA on channel 7" },
	{ 0, NULL, NULL },
};

MENU_LINE BitMenu[] =
{
	{ 8,	"8-bit",	"8-bit low quality mixing" },
	{ 16,	"16-bit",	"16-bit high quality mixing" },
	{ 0, NULL, NULL },
};

MENU_LINE ChannelMenu[] =
{
	{ 1,	"Mono",		"Mono sound, no stereo imaging" },
	{ 2,	"Stereo",	"Stereo sound, full stereo imaging" },
	{ 0, NULL, NULL },
};

MENU_LINE VoiceMenu[] =
{
	{ 8,	"8",		"8 voice mixing" },
	{ 16,	"16",		"16 voice mixing" },
	{ 24,	"24",		"24 voice mixing" },
	{ 32,	"32",		"32 voice mixing" },
	{ 0, NULL, NULL },
};

MENU_LINE MixRateMenu[] =
{
	{ 11110,	"11 KHz",	"Mix at 11 KHz" },
	{ 22220,	"22 KHz",	"Mix at 22 KHz" },
	{ 44440,	"44 KHz",	"Mix at 44 KHz" },
	{ 0, NULL, NULL },
};

BOOL		bSoundWildcard = FALSE;
RESHANDLE	hSound[kMaxPlaySounds];
char		*soundName[kMaxPlaySounds];
int			soundCount = 0;
int			soundCursor = 0;

BYTE *background = NULL, *stage = NULL, *video = (BYTE *)0xA0000;

#define kNumBuffers 34
BYTE *buffers[kNumBuffers];

int gFXDevice;
int gMixBits;
int gChannels;
int gMixVoices;
int gMixRate;
int gFXVolume;
fx_blaster_config gSBConfig;

int fHolography = TRUE;

double gPinnaAngle = kAngle45;
double nSat = 0.8;

static int lVoice, rVoice;
static int lPhase, rPhase, lVel, rVel, lVol, rVol;
int x, y, xold, yold;

volatile short gMouseX, gMouseY;
BOOL gMouseDragging = FALSE;
int gMouseShown = FALSE;

EHF prevErrorHandler;

/*---------------------------------------------------------------------
** Function Prototypes
---------------------------------------------------------------------*/
void ProcessArgument(char *s);


/*---------------------------------------------------------------------
   Function: USRHOOKS_GetMem

   Allocates the requested amount of memory and returns a pointer to
   its location, or NULL if an error occurs.  NOTE: pointer is assumed
   to be dword aligned.
---------------------------------------------------------------------*/

int USRHOOKS_GetMem( void **ptr, unsigned long size )
{
	void *memory;

	memory = Resource::Alloc( size );
	if ( memory == NULL )
	{
		return( USRHOOKS_Error );
	}

	*ptr = memory;
	return( USRHOOKS_Ok );
}


/*---------------------------------------------------------------------
   Function: USRHOOKS_FreeMem

   Deallocates the memory associated with the specified pointer.
---------------------------------------------------------------------*/

int USRHOOKS_FreeMem( void *ptr )
{
	if ( ptr == NULL )
    {
		return( USRHOOKS_Error );
    }

	Resource::Free( ptr );
	return( USRHOOKS_Ok );
}


void mouGetPos( void );
#pragma aux mouseGetPos =\
	"mov	ax,03h",\
	"int 	33h",\
	"shr	cx,1",\
	"mov	[gMouseX],cx",\
	"mov	[gMouseY],dx",\
	modify [eax ecx edx ebx]\

char mouseGetButtons( void );
#pragma aux mouseGetButtons =\
	"mov ax,03h",\
	"int 33h",\
	"xor eax,eax",\
	"mov al,bl",\
	modify [eax ebx ecx edx]


inline void pixel( BYTE *buffer, int x, int y, char c )
{
   	*(char *)(buffer + x + gYLookup[y]) = c;
}


/*---------------------------------------------------------------------
   Function: plot

   XORs a 4x4 block onto the screen.
---------------------------------------------------------------------*/

void plot( BYTE *buffer, int x, int y, char c )
{
   	int i;
	long d = (c << 24) | (c << 16) | (c << 8) | c;
	long *v = (long *)(buffer + x + y * 320);

	for( i = 0; i < 4; i++)
	{
		*v = d;
		v += 80;
	}
}


double Vol3d(double angle, double vol)
{
	if (fHolography)
		return vol * (1.0 - (1.0 - cos(angle)) * nSat / 2.0);
	else
		return vol * (cos(angle) * 0.5 + 0.5);
}


int Dist3d(int x, int y)
{
	return sqrt(x * x + y * y);
}


double CalcAngle( int x, int y )
{
	if (y < 0)
		return atan((double)x / (double)y);

	if (y > 0)
		return kPi + atan((double)x / (double)y);

	if (x < 0)
		return kPi / 2;

	return -kPi / 2;
}

/*******************************************************************************

Here is the equation for determining the frequency shift of a sound:

	            Ú        ¿
	            ³ v + vL ³
	    fL = fS ³ ÄÄÄÄÄÄ ³
	            ³ v - vS ³
	            À        Ù

Where:

	fL	= frequency as heard by the listener
	fS	= frequency of the source
	v	= velocity of sound
	vL	= velocity of listener
	vS	= velocity of source

Speed of sound = 343 m/s = 1126 ft/s.

*******************************************************************************/


#define kSoundVelocity	((double)kSampleRate / kTimerRate)
int Freq( int velocity )
{
	return kSampleRate * kSoundVelocity / (ClipLow(kSoundVelocity - velocity, 1));
}


// ear distance (in samples)
#define kEarLX (int)(-kIntraAuralTime / 2 * kSampleRate)
#define kEarRX (int)(+kIntraAuralTime / 2 * kSampleRate)

void MoveSource( void )
{
	int lDist, rDist;
	double angle, vol;

	xold = x;
	yold = y;
	x = gMouseX - 160;
	y = gMouseY - 100;

	angle = CalcAngle(x, y);

	if (fHolography)
	{
		lDist = Dist3d(x - kEarLX, y);
		lVel = Dist3d(xold - kEarLX, yold) - lDist;
		vol = kVolScale * 255 / ClipLow(lDist, 1);
		lVol = Vol3d(angle - gPinnaAngle, vol);
		lPhase = lDist;

		rDist = Dist3d(x - kEarRX, y);
		rVel = Dist3d(xold - kEarRX, yold) - rDist;
		vol = kVolScale * 255 / ClipLow(rDist, 1);
		rVol = Vol3d(angle + gPinnaAngle, vol);
		rPhase = rDist;
	}
	else
	{
		vol = kVolScale * 255 / ClipLow(Dist3d(x, y), 1);
		if (vol > 255) vol = 255;

		lVol = Vol3d(angle - kAngle90, vol);
		rVol = Vol3d(angle + kAngle90, vol);

		lVel = rVel = 0;
		lPhase = rPhase = 0;
	}
}


/*---------------------------------------------------------------------
   Function: PlaySound

   Starts playback of a sound.
---------------------------------------------------------------------*/

void PlaySound( int num )
{
	int nLength;
	BYTE *vl, *vr;
	int id;

	BYTE *soundData = (BYTE *)gResource.Lock(hSound[num]);
	dassert(soundData != NULL);

	if ( mouseGetButtons() )
		gMouseDragging = TRUE;

	nLength = gResource.Size(hSound[num]);

	vl = (BYTE *)Resource::Alloc(nLength + lPhase);
	dassert (vl != NULL);

	memset(vl, 128, lPhase);
	memcpy(&vl[lPhase], soundData, nLength);

	vr = (BYTE *)Resource::Alloc(nLength + rPhase);
	dassert(vr != NULL);

	memset(vr, 128, rPhase);
	memcpy(&vr[rPhase], soundData, nLength);

	gResource.Unlock(hSound[num]);

	_disable();	// make sure the samples start in the same interval

	int nPriority = 1;	// ASS seems to choke if this is less than 1, e.g., 0
	if ( lVol > nPriority )
		nPriority = lVol;
	if ( rVol > nPriority )
		nPriority = rVol;

	for (id = 0; buffers[id] != 0; id++);
	lVoice = FX_PlayRaw( vl, nLength, Freq(lVel), 0, lVol, lVol, 0, nPriority, id);

	if ( lVoice > FX_Ok )
		buffers[id] = vl;
	else
		Resource::Free(vl);

	for (id = 0; buffers[id] != 0; id++);
	rVoice = FX_PlayRaw( vr, nLength, Freq(rVel), 0, rVol, 0, rVol, nPriority, id);

	if ( rVoice > FX_Ok )
		buffers[id] = vr;
	else
		Resource::Free(vr);

	_enable();	// allow them to start
}


void UpdateSound( void )
{
	if (lVoice > FX_Ok)
	{
		FX_SetPan(lVoice, lVol, lVol, 0);
		FX_SetFrequency(lVoice, Freq(lVel));
	}

	if (rVoice > FX_Ok)
	{
		FX_SetPan(rVoice, rVol, 0, rVol);
		FX_SetFrequency(rVoice, Freq(rVel));
	}
}


/*---------------------------------------------------------------------
   Function: DrawMark

   Displays the position the voice will appear in relation to the
   listener.
---------------------------------------------------------------------*/

void DrawMark( int fErase )
{
	static int  lastx = 0;
	static int  lasty = 0;
	static char c = 16;
	int newx, newy;
	BYTE *v;
	int i, j;

	newx = gMouseX - 1;
	if (newx < 0) newx = 0;
	if (newx > 317) newx = 317;

	newy = gMouseY - 1;
	if (newy < 0) newy = 0;
	if (newy > 197) newy = 197;

	// clear the old one
	if (fErase)
	{
		v = video + lastx + lasty * 320;
		for( i = 0; i < 3; i++)
		{
			for (j = 0; j < 3; j++)
				*v++ ^= c;
			v += 320 - 3;
		}
	}

	if (++c == 32)
		c = 16;

	// draw the new one
	v = video + newx + newy * 320;
	for( i = 0; i < 3; i++)
	{
		for (j = 0; j < 3; j++)
			*v++ ^= c;
		v += 320 - 3;
	}

	lastx = newx;
	lasty = newy;
}



/*---------------------------------------------------------------------
   Function: DrawDemoScreen

   Sets up our information screen for the user.  Displays last error,
   what sounds are playing, and the volume levels.
---------------------------------------------------------------------*/

void DrawDemoScreen( void )
{
	int x;
	double a;
	double lSat, rSat, nCos, nSin;

	// make sure the mouse doesn't get drawn while we're setting up
	gMouseShown = FALSE;

	// clear screen
	memcpy(stage, background, 320 * 200);

	for ( a = 0.0; a < kAngle360; a += kAngle5 )
	{
		if (fHolography)
		{
			lSat = Vol3d(a - gPinnaAngle, 25);
			rSat = Vol3d(a + gPinnaAngle, 25);
		}
		else
		{
			lSat = Vol3d(a - kAngle90, 25);
			rSat = Vol3d(a + kAngle90, 25);
		}

		nSin = sin(a);
		nCos = cos(a);

		pixel(stage, 160 - nSin * lSat, 100 - nCos * lSat, 8);
		pixel(stage, 160 - nSin * rSat, 100 - nCos * rSat, 8);
		pixel(stage, 160 - nSin * (lSat + rSat) * kAspectAdjustX, 100 - nCos * (lSat + rSat), 14);
	}

	// draw the holography toggle
	plot( stage, 135, 6, fHolography ? 14 : 8 );

	// Draw volume display
   	for( x = 0; x < 10; x++ )
	{
    	plot( stage, 240 + x * 7, 6, gFXVolume > x ? 10 : 8 );
	}

	// draw the stage
	memcpy(video, stage, 320 * 200);

	// draw the initial cursor
	DrawMark(0);

	// okay, we're ready for the mouse
	gMouseShown = TRUE;
}



/*---------------------------------------------------------------------
   Function: TimerFunc
---------------------------------------------------------------------*/

void TimerFunc( TASK * /* task */ )
{
	mouseGetPos();
	if ( gMouseDragging && !mouseGetButtons() )
		gMouseDragging = FALSE;

	if (gMouseShown)
	{
		DrawMark(1);
		MoveSource();

		if ( gMouseDragging )
		{
			UpdateSound();
		}
	}
}


/*---------------------------------------------------------------------
   Function: SoundCallback

   When a sound fx stops playing, this function is called.
---------------------------------------------------------------------*/

void SoundCallback(unsigned long id)
{
	if (buffers[id] != NULL)
	{
		Resource::Free(buffers[id]);
		buffers[id] = NULL;
	}
	else
		dprintf("SoundCallback() called for non-playing sample\n");
}


void ShowSoundName( int n )
{
	char zbuffer[60];
	sprintf(zbuffer,"%-12s : %i of %i", soundName[n], soundCursor + 1, soundCount );

	Video.SetColor(0);
	gfxFillBox(0, 192, 320, 200);
	gfxDrawText(0, 192, 15, zbuffer);
}


/*---------------------------------------------------------------------
   Function: CheckKeys

   Checks which keys are hit and responds appropriately.
---------------------------------------------------------------------*/

BYTE CheckKeys( void )
{
	BYTE key;

	key = keyGet();
	switch( key )
	{
		case KEY_SPACE:
	        PlaySound(soundCursor);
	        break;

		case KEY_1:
		case KEY_2:
		case KEY_3:
		case KEY_4:
		case KEY_5:
		case KEY_6:
		case KEY_7:
		case KEY_8:
		case KEY_9:
		case KEY_0:
		{
			int nSound = soundCursor + key - KEY_1;
			if (nSound < soundCount)
	        	PlaySound(nSound);
	        break;
		}

		case KEY_UP:
		case KEY_PADUP:
			soundCursor = ClipLow(soundCursor - 1, 0);
			ShowSoundName(soundCursor);
			break;

		case KEY_DOWN:
		case KEY_PADDOWN:
			soundCursor = ClipHigh(soundCursor + 1, soundCount - 1);
			ShowSoundName(soundCursor);
			break;

		case KEY_PAGEUP:
		case KEY_PADPAGEUP:
			soundCursor = ClipLow(soundCursor - 10, 0);
			ShowSoundName(soundCursor);
			break;

		case KEY_PAGEDN:
		case KEY_PADPAGEDN:
			soundCursor = ClipHigh(soundCursor + 10, soundCount - 1);
			ShowSoundName(soundCursor);
			break;

		case KEY_MINUS:
			if (gFXVolume > 0)
			{
				gFXVolume--;
				FX_SetVolume(gFXVolume * 255 / 10);
				plot( video, 240 + gFXVolume * 7, 6, 8 );
			}
			break;

		case KEY_EQUALS:
			if (gFXVolume < 10)
			{
				plot( video, 240 + gFXVolume * 7, 6, 10 );
				gFXVolume++;
				FX_SetVolume(gFXVolume * 255 / 10);
			}
			break;

		case KEY_LBRACE:
			if ( gPinnaAngle > 0 )
			{
				gPinnaAngle -= kAngle5;
				DrawDemoScreen();
			}
			break;

		case KEY_RBRACE:
			if ( gPinnaAngle < kAngle90 )
			{
				gPinnaAngle += kAngle5;
				DrawDemoScreen();
			}
			break;

		case KEY_COMMA:
			if ( nSat > 0 )
			{
				nSat -= 0.05;
				DrawDemoScreen();
			}
			break;

		case KEY_PERIOD:
			if ( nSat < 1.0 )
			{
				nSat += 0.05;
				DrawDemoScreen();
			}
			break;

		case KEY_K:
        	 FX_StopAllSounds();
         	break;

      	case KEY_S:
        	fHolography = !fHolography;
         	DrawDemoScreen();
         	break;
	}
	return key;
}


/*---------------------------------------------------------------------
   Function: DoDemo

   Main loop for our demo.  Sets up a timer so that marker moves at
   same rate on any computer.
---------------------------------------------------------------------*/
void DoDemo( void )
{
	TASK *taskPtr = NULL;
	int   Timer;
	int   i;
	int nOldMode;
	int width = 320, height = 200;
	PALETTE palette;

	background = (BYTE *)Resource::Alloc(320 * 200);
	dassert(background != NULL);

	if (PCX_OKAY != ReadPCX("screen.pcx", palette, &width, &height, &background))
		ThrowError("Error reading screen.pcx\n", ES_ERROR);

	stage = (BYTE *)Resource::Alloc(320 * 200);
	dassert(stage != NULL);

	nOldMode = getvmode();

	if (!gFindMode(320, 200, 256, CHAIN256))
		ThrowError("Video driver not linked\n", ES_ERROR);

	Video.SetMode();

	gSetDACRange(0, 256, palette);

	taskPtr = TS_ScheduleTask( TimerFunc, kTimerRate, 1, &Timer );
	TS_Dispatch();
	Timer = 0;

	// set the current volume of the sound fx
	FX_SetVolume( gFXVolume * 255 / 10 );

	// Set up my keyboard handler.
	keyInstall();

	for (i = 0; i < kNumBuffers; i++)
		buffers[i] = NULL;

	// Set up our fx callback so we can display the sounds that are playing
	FX_SetCallBack(SoundCallback);

	soundCursor = 0;
	DrawDemoScreen();
	ShowSoundName(soundCursor);

	while ( CheckKeys() != KEY_ESC );

	// Stop the sound fx engine.
	FX_StopAllSounds();

	// Restore the system keyboard handler
	keyRemove();

	setvmode(nOldMode);

	// Terminate my timer.  Failure to do so could be fatal!!!
	TS_Terminate(taskPtr);
	TS_Shutdown();

	Resource::Free(stage);
}


int SelectMenu( char *title, MENU_LINE list[], int nDefault )
{
	int cols = 0, rows = 0;
	int pos = 0;
	int nListItems;

//	cols = strlen(title);
	(void)title;

	// find widest menu item text
	for ( int i = 0; list[i].name != NULL; i++)
	{
		int n = strlen(list[i].name);
		if (n > cols)
			cols = n;
	}
	nListItems = i;

	cols += 5;
	rows = nListItems + 3;

	char *saveUnder = (char *)Resource::Alloc(rows * cols * 2);
	dassert(saveUnder != NULL);

	int left = (tioScreenCols - cols) / 2;
	int top = (tioScreenRows - rows) / 2;
	tioSaveWindow(saveUnder, top, left, rows, cols);

	tioFrame(top, left, rows - 1, cols - 1, kFrameSingle, kColorBlue * 16 | kColorCyan);
	tioFill(top + 1, left + 1, rows - 3, cols - 3, ' ', kColorBlue * 16 | kColorYellow);
	tioFillShadow(top + rows - 1, left + 1, 1, cols - 1);
	tioFillShadow(top + 1, left + cols - 1, rows - 2, 1);

	for( i = 0; i < nListItems; i++ )
	{
		tioLeftString(top + i + 1, left + 2, list[i].name, kColorBlue * 16 | kColorYellow);

		if ( list[i].id == nDefault )
			pos = i;
	}

	int ch = 0;
	while( ch != 0x0D && ch != 0x20 )
	{
		// highlight current line and display hint
		tioFillAttr(top + pos + 1, left + 1, 1, cols - 3, kColorLightGray * 16 | kColorBlack);
		tioCenterString(tioScreenRows - 1, 0, tioScreenCols - 1,
			list[pos].hint, kColorRed * 16 | kColorYellow);

		ch = getch();
		if ( ch == 0 )
		{
			ch = 0x100 | getch();
		}
		// Up arrow
		if ( ch == 0x148 )
		{
			tioFillAttr(top + pos + 1, left + 1, 1, cols - 3, kColorBlue * 16 | kColorYellow);
			pos = DecRotate(pos, nListItems);
		}
		// Down arrow
		if ( ch == 0x150 )
		{
			tioFillAttr(top + pos + 1, left + 1, 1, cols - 3, kColorBlue * 16 | kColorYellow);
			pos = IncRotate(pos, nListItems);
		}
		// Esc
		if ( ch == 0x1B )
		{
			tioRestoreWindow(saveUnder, top, left, rows, cols);
			Resource::Free(saveUnder);
			return -1;
		}
	}
	tioRestoreWindow(saveUnder, top, left, rows, cols);
	Resource::Free(saveUnder);
	return list[pos].id;
}


/*---------------------------------------------------------------------
   Function: ReadSounds

   Sets up sound resources. Returns number of sounds available.
---------------------------------------------------------------------*/

int ReadSounds( void )
{
	int i;

	for ( i = 0; i < kMaxPlaySounds; i++)
		hSound[i] = NULL;	// safer than a memset(soundHandle,0,sizeof(soundHandle)); ?

	gResource.Init(NULL, "*.RAW");

	if (!bSoundWildcard)
		ProcessArgument("*.RAW");	// default to local .RAW files

	for ( i = 0; i < soundCount; i++)
	{
		char zNameBuffer[_MAX_PATH2];
		char *name, *ext;
		_splitpath2(soundName[i], zNameBuffer, NULL, NULL, &name, &ext);
		hSound[i] = gResource.Lookup(name, ext);
	}

	return soundCount;
}


void GetSoundSettings( void )
{
	FX_GetBlasterSettings(&gSBConfig);

	// Read current setup
	gFXDevice		= config.GetKeyInt("Sound Setup", "FXDevice", SoundBlaster);
	gMixBits		= config.GetKeyInt("Sound Setup", "Bits", 16);
	gChannels		= config.GetKeyInt("Sound Setup", "Channels", 2);
	gMixVoices		= config.GetKeyInt("Sound Setup", "Voices", 16);
	gMixRate		= config.GetKeyInt("Sound Setup", "MixRate", 22050);
	gFXVolume		= config.GetKeyInt("Sound Setup", "FXVolume", 8);

	gSBConfig.Address	= config.GetKeyInt("Sound Setup", "BlasterAddress", gSBConfig.Address);
	gSBConfig.Type		= config.GetKeyInt("Sound Setup", "BlasterType", gSBConfig.Type);
	gSBConfig.Interrupt	= config.GetKeyInt("Sound Setup", "BlasterInterrupt", gSBConfig.Interrupt);
	gSBConfig.Dma8		= config.GetKeyInt("Sound Setup", "BlasterDma8", gSBConfig.Dma8);
	gSBConfig.Dma16		= config.GetKeyInt("Sound Setup", "BlasterDma16", gSBConfig.Dma16);
	gSBConfig.Emu		= config.GetKeyInt("Sound Setup", "BlasterEmu", gSBConfig.Emu);
}


/*---------------------------------------------------------------------
   Function: GetSoundFXCard

   Asks the user which card to use for sound fx and initializes the
   sound fx routines.
---------------------------------------------------------------------*/
BOOL SetupSoundCard( void )
{
	// Draw our config screen
	tioCenterString(0, 0, tioScreenCols - 1, "Q Studios Sonic Holography Setup", kColorBlue * 16 | kColorYellow);
	tioFill( 1, 0, tioScreenRows - 2, tioScreenCols, '°', kColorBlack * 16 | kColorDarkGray);

	// Get Sound fx card
	gFXDevice = SelectMenu("Select a device for digital sound",
		SoundCardMenu, gFXDevice);
	if ( gFXDevice < 0 )
		return FALSE;

	switch ( gFXDevice )
	{
		case SoundBlaster:
		case ProAudioSpectrum:
		case SoundMan16:
		case Awe32:
		case WaveBlaster:
		case SoundScape:
			gMixBits = SelectMenu("Select the number of bits to use for mixing",
				BitMenu, gMixBits);
			if ( gMixBits < 0 )
				return FALSE;

			gChannels = SelectMenu("Select the channels mode",
				ChannelMenu, gChannels);
			if ( gChannels < 0 )
				return FALSE;
			gMixVoices = SelectMenu("Select the number of voices to use for mixing",
				VoiceMenu, gMixVoices);


			break;

		default:
			gMixBits = 8;
			gChannels = 1;
			gMixVoices = 2;
			break;
	}

	if ( gFXDevice == SoundBlaster || gFXDevice == Awe32 )
	{
		gSBConfig.Address = SelectMenu("Base I/O address",
			BlasterAddressMenu, gSBConfig.Address);

		gSBConfig.Type = SelectMenu("Type of Sound Blaster card",
			BlasterTypeMenu, gSBConfig.Type);

		gSBConfig.Interrupt = SelectMenu("Interrupt",
			BlasterIRQMenu, gSBConfig.Interrupt);

		gSBConfig.Dma8 = SelectMenu("8 bit DMA channel",
			BlasterDma8Menu, gSBConfig.Dma8);

		gSBConfig.Dma16 = SelectMenu("16 bit DMA channel",
			BlasterDma16Menu, gSBConfig.Dma16);
	}

	gMixRate = SelectMenu("Select the rate to use for mixing",
		MixRateMenu, gMixRate);
	if ( gMixRate < 0 )
		return FALSE;

	config.PutKeyInt("Sound Setup", "FXDevice", gFXDevice);
	config.PutKeyInt("Sound Setup", "Bits", gMixBits);
	config.PutKeyInt("Sound Setup", "Channels", gChannels);
	config.PutKeyInt("Sound Setup", "Voices", gMixVoices);
	config.PutKeyInt("Sound Setup", "MixRate", gMixRate);

	config.PutKeyHex("Sound Setup", "BlasterAddress", gSBConfig.Address);
	config.PutKeyInt("Sound Setup", "BlasterType", gSBConfig.Type);
	config.PutKeyInt("Sound Setup", "BlasterInterrupt", gSBConfig.Interrupt);
	config.PutKeyInt("Sound Setup", "BlasterDma8", gSBConfig.Dma8);
	config.PutKeyInt("Sound Setup", "BlasterDma16", gSBConfig.Dma16);
	config.PutKeyInt("Sound Setup", "BlasterEmu", gSBConfig.Emu);

	return TRUE;
}


void InitSoundDevice(void)
{
	int status;

	// Do special Sound Blaster, AWE32 stuff
	if ( gFXDevice == SoundBlaster || gFXDevice == Awe32 )
	{
		int MaxVoices;
		int MaxBits;
		int MaxChannels;

		status = FX_SetupSoundBlaster(gSBConfig, &MaxVoices, &MaxBits, &MaxChannels);
	}
	else
	{
		fx_device device;
		status = FX_SetupCard( gFXDevice, &device );
	}

	if ( status != FX_Ok )
		ThrowError(FX_ErrorString(status), ES_ERROR);

	status = FX_Init( gFXDevice, gMixVoices, gChannels, gMixBits, gMixRate );
	if ( status != FX_Ok )
		ThrowError(FX_ErrorString(status), ES_ERROR);

	FX_SetVolume(gFXVolume);
}


/*******************************************************************************
	FUNCTION:		ShowUsage()

	DESCRIPTION:	Display command-line parameter usage, then exit
*******************************************************************************/
void ShowUsage(void)
{
	tioPrint("Syntax: SHPLAY [options] wildcard[.RAW]");
	tioPrint("  /setup    Reconfigure sound card");
	tioPrint("  /?        This help");
	tioPrint("");
	exit(0);
}


int compare(const void *p1, const void *p2 )
{
	const char **pp1 = (const char **)p1;
	const char **pp2 = (const char **)p2;
	return strcmp(*pp1, *pp2);


}
/***********************************************************************
 * Convert wildcard parameter to sound list
 **********************************************************************/
void ProcessArgument(char *s)
{
	char filespec[_MAX_PATH];
	char buffer[_MAX_PATH2];
	char path[_MAX_PATH];

	strcpy(filespec, s);
	AddExtension(filespec, ".RAW");

	char *drive, *dir;

	// separate the path from the filespec
	_splitpath2(s, buffer, &drive, &dir, NULL, NULL);
	_makepath(path, drive, dir, NULL, NULL);

	struct find_t fileinfo;

	unsigned r = _dos_findfirst(s, _A_NORMAL, &fileinfo);
	if (r != 0)
		printf("%s not found\n", s);

	while ( r == 0 && soundCount < kMaxPlaySounds )
	{
		strcpy(filespec, path);
		strcat(filespec, fileinfo.name);

		soundName[soundCount] = strdup(filespec);
		dassert( soundName[soundCount] != NULL );	// low memory condition
		soundCount++;

		r = _dos_findnext( &fileinfo );
	}

	_dos_findclose(&fileinfo);

	qsort(soundName, soundCount, sizeof(soundName[0]), compare);
}


/***********************************************************************
 * Process command line arguments
 **********************************************************************/
void ParseOptions( void )
{
	enum {
		kSwitchHelp,
		kSwitchSetup,
		kSwitchMap,
		kSwitchOmit,
	};
	static SWITCH switches[] = {
		{ "?", kSwitchHelp, FALSE },
		{ "SETUP", kSwitchSetup, FALSE },
		{ NULL, 0, FALSE },
	};
	char buffer[256];
	int r;
	while ( (r = GetOptions(switches)) != GO_EOF )
	{
		switch (r)
		{
			case GO_INVALID:
				sprintf(buffer, "Invalid argument: %s", OptArgument);
				tioPrint(buffer);
				exit(1);

			case GO_FULL:
				ProcessArgument(OptArgument);
				bSoundWildcard = TRUE;
				break;

			case kSwitchSetup:
				if ( !SetupSoundCard() )
					exit(0);
				break;

			case kSwitchHelp:
				ShowUsage();
				break;
		}
	}
}


/*---------------------------------------------------------------------
   Function: GameErrorHandler

   Application error handler
---------------------------------------------------------------------*/
ErrorResult GameErrorHandler( const Error& error )
{
	setvmode(3);
	FX_Shutdown();
	tioTerm();

	// chain to the default error handler
	return prevErrorHandler(error);
};


/*---------------------------------------------------------------------
   Function: main

   Sets up sound cards, calls the demo, and then cleans up.
---------------------------------------------------------------------*/
void main( void )
{
	tioInit(0);

	GetSoundSettings();

	Resource::heap = new QHeap(dpmiDetermineMaxRealAlloc());

	if ( !config.KeyExists("Sound Setup", "FXDevice") )
	{
		if ( !SetupSoundCard() )
		{
			tioClearWindow();
			exit(0);
		}
	}

	ParseOptions();

	if ( !ReadSounds() )
		ThrowError("No sounds available",ES_ERROR);

	InitSoundDevice();

	// install our error handler
	prevErrorHandler = errSetHandler(GameErrorHandler);

	DoDemo();

	FX_Shutdown();

	tioClearWindow();
	tioPrint("Sonic Holography Player Version 2.0   Copyright (c) 1995 Q Studios Corporation");
	tioPrint("Written by Peter M. Freese");
	tioPrint("");
	tioPrint("Pinna angle: %3.0f degrees", gPinnaAngle * 360.0 / kAngle360);
	tioPrint("Saturation:  %0.2f", nSat);

	tioTerm();

	config.PutKeyInt("Setup", "FXVolume", gFXVolume);
	config.Save();
}
