// Emacs style mode select   -*- C++ -*-
//-----------------------------------------------------------------------------
//
// Copyright (C) 1998-2000 by DooM Legacy Team.
//
// 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.
//-----------------------------------------------------------------------------
/// \file
/// \brief interface level code for sound
///
///	Uses the midiStream* Win32 functions to play MIDI data
///	with low latency and low processor overhead.
#include "../doomdef.h"

#include "win_main.h"
#include <mmsystem.h>
#define DIRECTSOUND_VERSION     0x0600       /* version 6.0 */
#define DIRECTINPUT_VERSION     0x0700
#define DXVERSION_NTCOMPATIBLE  0x0300
#ifdef _MSC_VER
#pragma warning(disable :  4201)
#endif
#include <dsound.h>

#include "../command.h"
#include "../i_sound.h"
#include "../s_sound.h"
#include "../i_system.h"
#include "../m_argv.h"
#include "../w_wad.h"
#include "../z_zone.h"
#include "../doomstat.h"

#include "dx_error.h"

#include "mid2strm.h"

#ifdef HW3SOUND
#include "../hardware/hw3dsdrv.h"
#include "../hardware/hw3sound.h"
#include "win_dll.h"
#endif

#ifndef SURROUND
#define SURROUND // comment out this to disable the SurroundSound code
#endif

#ifdef SURROUND
ATTRNOINLINE static FUNCNOINLINE void CopyAndInvertMemory(void *dest, void *src, int bytes); //Can't inline ASM that haves lables
#endif

#define FMODSOUND // comment out this to disable MOD/IT/MP3/OGG music playback

///////////////////////////////////////////////////////////
#ifdef FMODSOUND
#ifdef __MINGW32__
#include <FMOD/fmod.h>
#else
#include <fmod.h>
#endif
#ifdef FSOUND_INIT_DONTLATENCYADJUST //Alam: why didn't I think about this before? :)
#define FSOUND_Stream_OpenFile(name,mode,length) FSOUND_Stream_Open(name,mode,0,length)
#else
#define FSOUND_INIT_DONTLATENCYADJUST 0
#endif
#ifdef __MINGW32__
#include <FMOD/fmod_errors.h>
#else
#include <fmod_errors.h>	/* optional */
#endif
//#include <conio.h>
#endif
/////////////////////////////////////////////////////////

//#define TESTCODE            // remove this for release version

/* briefly described here for convenience:
typedef struct {
	WORD  wFormatTag;       // WAVE_FORMAT_PCM is the only format accepted for DirectSound:
							// this tag indicates Pulse Code Modulation (PCM), an uncompressed format
							// in which each samples represents the amplitude of the signal at the time
							// of sampling.
	WORD  nChannels;        // either one (mono) or two (stereo)
	DWORD nSamplesPerSec;   // the sampling rate, or frequency, in hertz.
							//  Typical values are 11,025, 22,050, and 44,100
	DWORD nAvgBytesPerSec;  // nAvgBytesPerSec is the product of nBlockAlign and nSamplesPerSec
	WORD  nBlockAlign;      // the number of bytes required for each complete sample, for PCM formats
							// is equal to (wBitsPerSample * nChannels / 8).
	WORD  wBitsPerSample;   // gives the size of each sample, generally 8 or 16 bits
	WORD  cbSize;           // cbSize gives the size of any extra fields required to describe a
							// specialized wave format. This member is always zero for PCM formats.
} WAVEFORMATEX;
*/

// Tails 11-21-2002
#ifdef FMODSOUND
static FMUSIC_MODULE  *mod = NULL;
static int fsoundchannel = -1;
static int fsoundfreq = 0;
static int fmodvol = 127;
static FSOUND_STREAM *fmus = NULL;
#endif

// --------------------------------------------------------------------------
// DirectSound stuff
// --------------------------------------------------------------------------
static LPDIRECTSOUND           DSnd = NULL;
static LPDIRECTSOUNDBUFFER     DSndPrimary = NULL; ;

// Stack sounds means sounds put on top of each other, since DirectSound can't play
// the same sound buffer at different locations at the same time, we need to dupli-
// cate existing buffers to play multiple instances of the same sound in the same
// time frame. A duplicate sound is freed when it is no more used. The priority that
// comes from the s_sound engine, is kept so that the lowest priority sounds are
// stopped to make place for the new sound, unless the new sound has a lower priority
// than all playing sounds, in which case the sound is not started.
#define MAXSTACKSOUNDS      32          // this is the absolute number of sounds that
                                        // can play simultaneously, whatever the value
                                        // of cv_numChannels
typedef struct {
	LPDIRECTSOUNDBUFFER lpSndBuf;
#ifdef SURROUND
		// judgecutor:
		// Need for produce surround sound
	LPDIRECTSOUNDBUFFER lpSurround;
#endif
	int                 priority;
	boolean             duplicate;
} StackSound_t;
static StackSound_t    StackSounds[MAXSTACKSOUNDS];

// --------------------------------------------------------------------------
// Fill the DirectSoundBuffer with data from a sample, made separate so that
// sound data cna be reloaded if a sound buffer was lost.
// --------------------------------------------------------------------------
static boolean CopySoundData (LPDIRECTSOUNDBUFFER dsbuffer, LPBYTE data, DWORD length)
{
	LPVOID  lpvAudio1;              // receives address of lock start
	DWORD   dwBytes1;               // receives number of bytes locked
	LPVOID  lpvAudio2;              // receives address of lock start
	DWORD   dwBytes2;               // receives number of bytes locked
	HRESULT hr;

	// Obtain memory address of write block.
	hr = IDirectSoundBuffer_Lock (dsbuffer, 0, length, &lpvAudio1, &dwBytes1, &lpvAudio2, &dwBytes2, 0);

	// If DSERR_BUFFERLOST is returned, restore and retry lock.
	if (hr == DSERR_BUFFERLOST)
	{
		hr = IDirectSoundBuffer_Restore (dsbuffer);
		if (FAILED (hr))
			I_Error("Restore fail on %x, %s\n",dsbuffer,DXErrorToString(hr));
		hr = IDirectSoundBuffer_Lock (dsbuffer, 0, length, &lpvAudio1, &dwBytes1, &lpvAudio2, &dwBytes2, 0);
		if (FAILED (hr))
			I_Error("Lock fail(2) on %x, %s\n",dsbuffer,DXErrorToString(hr));
	}
	else
		if (FAILED (hr))
			I_Error("Lock fail(1) on %x, %s\n",dsbuffer,DXErrorToString(hr));

	// copy wave data into the buffer (note: dwBytes1 should equal to dsbdesc->dwBufferBytes ...)
	CopyMemory (lpvAudio1, data, dwBytes1);

	if (dwBytes2 && lpvAudio2)
		CopyMemory(lpvAudio2, data+dwBytes1, dwBytes2);

	// finally, unlock the buffer
	hr = IDirectSoundBuffer_Unlock (dsbuffer, lpvAudio1, dwBytes1, lpvAudio2, dwBytes2);

	if (FAILED (hr))
		I_Error("Unlock fail on %x, %s\n",dsbuffer,DXErrorToString(hr));

	return true;
}

#ifdef SURROUND
// judgecutor:
// Hmmm... May be this function is not too good...
static void CopyAndInvertMemory(void *dest, void *src, int bytes)
{
#ifdef __GNUC__
	__asm__("CAIM:;lodsb;neg %%al;stosb;loop CAIM;"::"c"(bytes),"D"(dest),"S"(src): "memory","cc");
#else
	_asm
	{
		push esi
		push edi
		push ecx
		mov ecx,bytes
		mov esi,src
		mov edi,dest
a:
		lodsb
		neg  al
		stosb
		loop a
		pop ecx
		pop edi
		pop esi
	}
#endif
}

// judgecutor:
// Like normal CopySoundData but sound data will be inverted
static boolean CopyAndInvertSoundData(LPDIRECTSOUNDBUFFER dsbuffer, LPBYTE data, DWORD length)
{
	LPVOID  lpvAudio1 = NULL; // receives address of lock start
	DWORD   dwBytes1 = 0;     // receives number of bytes locked
	LPVOID  lpvAudio2 = NULL;
	DWORD   dwBytes2 = 0;
	HRESULT hr;

	// Obtain memory address of write block.
	hr = IDirectSoundBuffer_Lock (dsbuffer, 0, length, &lpvAudio1, &dwBytes1, &lpvAudio2, &dwBytes2, 0);

	// If DSERR_BUFFERLOST is returned, restore and retry lock.
	if (hr == DSERR_BUFFERLOST)
	{
		hr = IDirectSoundBuffer_Restore (dsbuffer);
		if (FAILED (hr))
			I_Error("CopyAndInvert: Restore fail on %x, %s\n",dsbuffer,DXErrorToString(hr));
		hr = IDirectSoundBuffer_Lock (dsbuffer, 0, length, &lpvAudio1, &dwBytes1, &lpvAudio2, &dwBytes2, 0);
		if (FAILED (hr))
			I_Error("CopyAndInvert: Lock fail(2) on %x, %s\n",dsbuffer,DXErrorToString(hr));
	} else if (FAILED (hr))
			  I_Error("CopyAndInvetrt: Lock fail(1) on %x, %s\n",dsbuffer,DXErrorToString(hr));

	// copy wave data into the buffer (note: dwBytes1 should equal to dsbdesc->dwBufferBytes ...)
	CopyAndInvertMemory (lpvAudio1, data, dwBytes1);

	if (dwBytes2 && lpvAudio2)
		CopyAndInvertMemory(lpvAudio2, data+dwBytes1, dwBytes2);

	hr = IDirectSoundBuffer_Unlock (dsbuffer, lpvAudio1, dwBytes1, lpvAudio2, dwBytes2);
	if (FAILED (hr))
		I_Error("CopyAndInvert: Unlock fail on %x, %s\n",dsbuffer,DXErrorToString(hr));

	return false;
}
#endif

static DWORD sound_buffer_flags = DSBCAPS_CTRLPAN |
                                  DSBCAPS_CTRLVOLUME |
                                  DSBCAPS_STICKYFOCUS |
                                //DSBCAPS_LOCSOFTWARE |
                                  DSBCAPS_STATIC;

// --------------------------------------------------------------------------
// raw2DS : convert a raw sound data, returns a LPDIRECTSOUNDBUFFER
// --------------------------------------------------------------------------
//   dsdata points a 4 UINT16 header:
//    +0 : value 3 what does it mean?
//    +2 : sample rate, either 11025 or 22050.
//    +4 : number of samples, each sample is a single byte since it's 8bit
//    +6 : value 0
//
#ifdef SURROUND
// judgecutor:
// We need an another function definition for supporting the surround sound
// Invert just cause to copy an inverted sound data
static LPDIRECTSOUNDBUFFER raw2DS(LPBYTE *dsdata, size_t len, UINT8 invert)
#else
static LPDIRECTSOUNDBUFFER raw2DS(LPBYTE *dsdata, size_t len)
#endif
{
	HRESULT             hr;
	WAVEFORMATEX        wfm;
	DSBUFFERDESC        dsbdesc;
	LPDIRECTSOUNDBUFFER dsbuffer;

	// initialise WAVEFORMATEX structure describing the wave format
	ZeroMemory (&wfm, sizeof (WAVEFORMATEX));
	wfm.wFormatTag = WAVE_FORMAT_PCM;
	wfm.nChannels = 1;
	wfm.nSamplesPerSec = (dsdata[3]<<8)+dsdata[2];      //mostly 11025, but some at 22050.
	wfm.wBitsPerSample = 8;
	wfm.nBlockAlign = (WORD)(wfm.wBitsPerSample / 8 * wfm.nChannels);
	wfm.nAvgBytesPerSec = wfm.nSamplesPerSec * wfm.nBlockAlign;

	// Set up DSBUFFERDESC structure.
	ZeroMemory (&dsbdesc, sizeof (DSBUFFERDESC));
	dsbdesc.dwSize = sizeof (DSBUFFERDESC);
/*    dsbdesc.dwFlags = DSBCAPS_CTRLPAN |
					  DSBCAPS_CTRLVOLUME |
					  DSBCAPS_STICKYFOCUS |
					  //DSBCAPS_LOCSOFTWARE |
					  DSBCAPS_STATIC
					  | DSBCAPS_CTRLFREQUENCY;    // This one for pitching
*/
	dsbdesc.dwFlags = sound_buffer_flags;
	dsbdesc.dwBufferBytes = len-8;
	dsbdesc.lpwfxFormat = &wfm; // pointer to WAVEFORMATEX structure

	// Create the sound buffer
	hr = IDirectSound_CreateSoundBuffer (DSnd, &dsbdesc, &dsbuffer, NULL);

	if (hr == DSERR_CONTROLUNAVAIL)
	{
		CONS_Printf("\tSoundBufferCreate error - a buffer control is not available.\n\tTrying to disable frequency control.\n");

		sound_buffer_flags &= ~DSBCAPS_CTRLFREQUENCY;
		dsbdesc.dwFlags = sound_buffer_flags;

		hr = IDirectSound_CreateSoundBuffer (DSnd, &dsbdesc, &dsbuffer, NULL);
	}

	if (FAILED(hr))
		I_Error("CreateSoundBuffer() FAILED: %s\n", DXErrorToString(hr));

#ifdef SURROUND
	if (invert)
		// just invert a sound data for producing the surround sound
		CopyAndInvertSoundData(dsbuffer, (LPBYTE)dsdata + 8, dsbdesc.dwBufferBytes);
	else
		// Do a normal operation
#endif
	// fill the DirectSoundBuffer waveform data
	CopySoundData (dsbuffer, (LPBYTE)dsdata + 8, dsbdesc.dwBufferBytes);

	return dsbuffer;
}


// --------------------------------------------------------------------------
// This function loads the sound data from the WAD lump, for single sound.
// --------------------------------------------------------------------------
void *I_GetSfx (sfxinfo_t * sfx)
{
	LPBYTE dssfx;

	if (sfx->lumpnum < 0)
		sfx->lumpnum = S_GetSfxLumpNum (sfx);

#ifdef HW3SOUND
	if (hws_mode != HWS_DEFAULT_MODE)
		return HW3S_GetSfx(sfx);
#endif

	sfx->length = W_LumpLength (sfx->lumpnum);

	// PU_CACHE because the data is copied to the DIRECTSOUNDBUFFER, the one here will not be used
	dssfx = (LPBYTE) W_CacheLumpNum (sfx->lumpnum, PU_CACHE);

#ifdef SURROUND
	// Make a normal (not inverted) sound buffer
	return (void *)raw2DS (dssfx, sfx->length, FALSE);
#else
	// return the LPDIRECTSOUNDBUFFER, which will be stored in S_sfx[].data
	return (void *)raw2DS (dssfx, sfx->length);
#endif
}


// --------------------------------------------------------------------------
// Free all allocated resources for a single sound
// --------------------------------------------------------------------------
void I_FreeSfx (sfxinfo_t *sfx)
{
	LPDIRECTSOUNDBUFFER dsbuffer;

	if (sfx->lumpnum < 0)
		return;

#ifdef HW3SOUND
	if (hws_mode != HWS_DEFAULT_MODE)
	{
		HW3S_FreeSfx(sfx);
	}
	else
#endif
	{
		//CONS_Printf("I_FreeSfx(%d)\n", sfx->lumpnum);

		// free DIRECTSOUNDBUFFER
		dsbuffer = (LPDIRECTSOUNDBUFFER) sfx->data;
		if (dsbuffer)
		{
			size_t i;
			for (i = 0; i < MAXSTACKSOUNDS; i++)
			{
				if (StackSounds[i].lpSndBuf == dsbuffer)
				{
					StackSounds[i].lpSndBuf = NULL;
#ifdef SURROUND
					if (StackSounds[i].lpSurround)
					{
						IDirectSoundBuffer_Stop(StackSounds[i].lpSurround);
						IDirectSoundBuffer_Release(StackSounds[i].lpSurround);
					}
					StackSounds[i].lpSurround = NULL;
#endif
				}
			}
			IDirectSoundBuffer_Stop (dsbuffer);
			IDirectSoundBuffer_Release (dsbuffer);
		}
	}
	sfx->data = NULL;
	sfx->lumpnum = -1;
}


// --------------------------------------------------------------------------
// Set the global volume for sound effects
// --------------------------------------------------------------------------
void I_SetSfxVolume(INT32 volume)
{
	int     vol;
	HRESULT hr;

	if (nosound || !sound_started)
		return;

	// use the last quarter of volume range
	if (volume)
		vol = (volume * ((DSBVOLUME_MAX-DSBVOLUME_MIN)/4)) / 31 +
		      (DSBVOLUME_MAX - ((DSBVOLUME_MAX-DSBVOLUME_MIN)/4));
	else
		vol = DSBVOLUME_MIN;    // make sure 0 is silence
	//CONS_Printf("setvolume to %d\n", vol);
	hr = IDirectSoundBuffer_SetVolume (DSndPrimary, vol);
	//if (FAILED(hr))
	//    CONS_Printf("setvolumne failed\n");
}


// --------------------------------------------------------------------------
// Update the volume for a secondary buffer, make sure it was created with
// DSBCAPS_CTRLVOLUME
// --------------------------------------------------------------------------
static void I_UpdateSoundVolume (LPDIRECTSOUNDBUFFER lpSnd, int volume)
{
	HRESULT hr;
	volume = (volume * ((DSBVOLUME_MAX-DSBVOLUME_MIN)/4)) / 256 +
	         (DSBVOLUME_MAX - ((DSBVOLUME_MAX-DSBVOLUME_MIN)/4));
	hr = IDirectSoundBuffer_SetVolume (lpSnd, volume);
	//if (FAILED(hr))
	//    CONS_Printf("\2SetVolume FAILED\n");
}


// --------------------------------------------------------------------------
// Update the panning for a secondary buffer, make sure it was created with
// DSBCAPS_CTRLPAN
// --------------------------------------------------------------------------
#define DSBPAN_RANGE    (DSBPAN_RIGHT-(DSBPAN_LEFT))
#define SEP_RANGE       256     //Doom sounds pan range 0-255 (128 is centre)
static void I_UpdateSoundPanning (LPDIRECTSOUNDBUFFER lpSnd, int sep)
{
	HRESULT hr;
	hr = IDirectSoundBuffer_SetPan (lpSnd, (sep * DSBPAN_RANGE)/SEP_RANGE - DSBPAN_RIGHT);
	//if (FAILED(hr))
	//    CONS_Printf("SetPan FAILED for sep %d pan %d\n", sep, (sep * DSBPAN_RANGE)/SEP_RANGE - DSBPAN_RIGHT);
}

// search a free slot in the stack, free it if needed
static int GetFreeStackNum(int  newpriority)
{
	int  lowestpri = 256,lowestprihandle = 0;
	int  i;
	// DirectSound can't play multiple instances of the same sound buffer
	// unless they are duplicated, so if the sound buffer is in use, make a duplicate
	for (i = 0; i < MAXSTACKSOUNDS; i++)
	{
		// find a free 'playing sound slot' to use
		if (StackSounds[i].lpSndBuf == NULL)
		{
			//CONS_Printf("\t\tfound free slot %d\n", i);
			return i;
		}
		else if (!I_SoundIsPlaying(i)) // check for sounds that finished playing, and can be freed
		{
			//CONS_Printf("\t\tfinished sound in slot %d\n", i);
			//stop sound and free the 'slot'
			I_StopSound (i);
			// we can use this one since it's now freed
			return i;
		}
		else if (StackSounds[i].priority < lowestpri) //remember lowest priority sound
		{
			lowestpri = StackSounds[i].priority;
			lowestprihandle = i;
		}
	}

	// the maximum of sounds playing at the same time is reached, if we have at least
	// one sound playing with a lower priority, stop it and replace it with the new one

	//CONS_Printf("\t\tall slots occupied..\n");
	if (newpriority >= lowestpri)
	{
		I_StopSound (lowestprihandle);
		return lowestprihandle;
		//CONS_Printf(" kicking out lowest priority slot: %d pri: %d, my priority: %d\n",
		//             handle, lowestpri, priority);
	}

	return -1;
}

#ifdef SURROUND
static LPDIRECTSOUNDBUFFER CreateInvertedSound(int id)
{
	lumpnnum_t lumpnum;
	LBYPTE dsdata;

	lumpnum = S_sfx[id].lumpnum;
	if (lumpnum < 0)
		lumpnum = S_GetSfxLumpNum (&S_sfx[id]);
	dsdata = W_CacheLumpNum (lumpnum, PU_CACHE);
	return raw2DS(dsdata, S_sfx[id].length, TRUE);
}
#endif

// Calculate internal pitch from Doom pitch
#if 0
static float recalc_pitch(int doom_pitch)
{
	return doom_pitch < NORMAL_PITCH ?
	       (float)(doom_pitch + NORMAL_PITCH) / (NORMAL_PITCH * 2)
	       :(float)doom_pitch / (float)NORMAL_PITCH;
}
#endif

// --------------------------------------------------------------------------
// Start the given S_sfx[id] sound with given properties (panning, volume..)
// --------------------------------------------------------------------------
INT32 I_StartSound (sfxenum_t      id,
                  INT32          vol,
                  INT32          sep,
                  INT32          pitch,
                  INT32          priority)
{
	HRESULT     hr;
	LPDIRECTSOUNDBUFFER     dsbuffer;
	DWORD       dwStatus;
	int         handle;
	int         i;
	//DWORD       freq;
#ifdef SURROUND
	LPDIRECTSOUNDBUFFER     dssurround;
#endif

	if (nosound)
		return -1;

	//CONS_Printf("I_StartSound:\n\t\tS_sfx[%d]\n", id);
	handle = GetFreeStackNum(priority);
	if (handle < 0)
		return -1;

	//CONS_Printf("\t\tusing handle %d\n", handle);

	// if the original buffer is playing, duplicate it (DirectSound specific)
	// else, use the original buffer
	dsbuffer = (LPDIRECTSOUNDBUFFER) S_sfx[id].data;
	IDirectSoundBuffer_GetStatus (dsbuffer, &dwStatus);
	if (dwStatus & (DSBSTATUS_PLAYING | DSBSTATUS_LOOPING))
	{
		//CONS_Printf("\t\toriginal sound S_sfx[%d] is playing, duplicating.. ", id);
		hr = IDirectSound_DuplicateSoundBuffer(DSnd,  (LPDIRECTSOUNDBUFFER) S_sfx[id].data, &dsbuffer);
		if (FAILED(hr))
		{
			//CONS_Printf("Cound't duplicate sound buffer\n");
			// re-use the original then..
			dsbuffer = (LPDIRECTSOUNDBUFFER) S_sfx[id].data;
			// clean up stacksounds info
			for (i = 0; i < MAXSTACKSOUNDS; i++)
				if (handle != i &&
					StackSounds[i].lpSndBuf == dsbuffer)
				{
					StackSounds[i].lpSndBuf = NULL;
				}
		}
		// stop the duplicate or the re-used original
		IDirectSoundBuffer_Stop (dsbuffer);
	}

	//judgecutor: Sound pitching
#if 0
	if (cv_rndsoundpitch.value)
	{
		// At first reset the buffer back to original frequency
		hr = IDirectSoundBuffer_SetFrequency(dsbuffer, DSBFREQUENCY_ORIGINAL);
		if (SUCCEEDED (hr))
		{

			IDirectSoundBuffer_GetFrequency(dsbuffer, &freq);

			// Now pitch it
			freq *= recalc_pitch(pitch);
			IDirectSoundBuffer_SetFrequency(dsbuffer, freq);
		}
		else
			cv_rndsoundpitch = 0;
	}
#else
	pitch = 0;
#endif
	// store information on the playing sound
	StackSounds[handle].lpSndBuf = dsbuffer;
	StackSounds[handle].priority = priority;
	StackSounds[handle].duplicate = (dsbuffer != (LPDIRECTSOUNDBUFFER)S_sfx[id].data);

	//CONS_Printf("StackSounds[%d].lpSndBuf is %s\n", handle, StackSounds[handle].lpSndBuf == NULL ? "Null":"valid");
	//CONS_Printf("StackSounds[%d].priority is %d\n", handle, StackSounds[handle].priority);
	//CONS_Printf("StackSounds[%d].duplicate is %s\n", handle, StackSounds[handle].duplicate ? "TRUE":"FALSE");

	I_UpdateSoundVolume (dsbuffer, vol);

#ifdef SURROUND
		// Prepare the surround sound buffer
	// Use a normal sound data for the left channel (with pan == 0)
	// and an inverted sound data for the right channel (with pan == 255)

	dssurround = CreateInvertedSound(id);

	// Surround must be pitched too
#if 0
	if (cv_rndsoundpitch.value)
		IDirectSoundBuffer_SetFrequency(dssurround, freq);
#endif
	if (sep == -128)
	{
		I_UpdateSoundPanning(dssurround, 255);
		I_UpdateSoundVolume(dssurround, vol);
		I_UpdateSoundPanning(dsbuffer, 0);
		IDirectSoundBuffer_SetCurrentPosition(dssurround, 0);
	}
		else
	// Perform normal operation
#endif

	I_UpdateSoundPanning (dsbuffer, sep);

	IDirectSoundBuffer_SetCurrentPosition (dsbuffer, 0);

	hr = IDirectSoundBuffer_Play (dsbuffer, 0, 0, 0);
	if (hr == DSERR_BUFFERLOST)
	{
		//CONS_Printf("buffer lost\n");
		// restores the buffer memory and all other settings for the buffer
		hr = IDirectSoundBuffer_Restore (dsbuffer);
		if (SUCCEEDED (hr))
		{
			LPBYTE dsdata;
			// reload sample data here
			int     lumpnum = S_sfx[id].lumpnum;
			if (lumpnum < 0)
				lumpnum = S_GetSfxLumpNum (&S_sfx[id]);
			dsdata = W_CacheLumpNum (lumpnum, PU_CACHE);

			// Well... Data lenght must be -8!!!
			CopySoundData (dsbuffer, dsdata + 8, S_sfx[id].length - 8);

			// play
			hr = IDirectSoundBuffer_Play (dsbuffer, 0, 0, 0);
		}
		else
			I_Error("I_StartSound : ->Restore FAILED, %s",DXErrorToString(hr));
	}

#ifdef SURROUND
	if (sep == -128)
	{
		hr = IDirectSoundBuffer_Play (dssurround, 0, 0, 0);
		//CONS_Printf("Surround playback\n");
		if (hr == DSERR_BUFFERLOST)
		{
			// restores the buffer memory and all other settings for the surround buffer
			hr = IDirectSoundBuffer_Restore (dssurround);
			if (SUCCEEDED (hr))
			{
				LPBYTE dsdata;
				lumpnumt_t lumpnum = S_sfx[id].lumpnum;

				if (lumpnum < 0)
					lumpnum = S_GetSfxLumpNum (&S_sfx[id]);
				dsdata = W_CacheLumpNum (lumpnum, PU_CACHE);
				CopyAndInvertSoundData (dssurround, (LPBYTE)dsdata + 8, S_sfx[id].length - 8);

				hr = IDirectSoundBuffer_Play (dssurround, 0, 0, 0);
			}
			else
				I_Error("I_StartSound : ->Restore FAILED, %s",DXErrorToString(hr));
		}
	}
	StackSounds[handle].lpSurround = dssurround;
#endif

	// Returns a handle
	return handle;
}


// --------------------------------------------------------------------------
// Stop a sound if it is playing,
// free the corresponding 'playing sound slot' in StackSounds[]
// --------------------------------------------------------------------------
void I_StopSound (INT32 handle)
{
	LPDIRECTSOUNDBUFFER dsbuffer;
	HRESULT hr;

	if (nosound || handle < 0)
		return;

	//CONS_Printf("I_StopSound (%d)\n", handle);

	dsbuffer = StackSounds[handle].lpSndBuf;
	hr = IDirectSoundBuffer_Stop (dsbuffer);

	// free duplicates of original sound buffer (DirectSound hassles)
	if (StackSounds[handle].duplicate)
	{
		//CONS_Printf("\t\trelease a duplicate..\n");
		IDirectSoundBuffer_Release (dsbuffer);
	}

#ifdef SURROUND
	// Stop and release the surround sound buffer
	dsbuffer = StackSounds[handle].lpSurround;
	if (dsbuffer != NULL)
	{
		IDirectSoundBuffer_Stop(dsbuffer);
		IDirectSoundBuffer_Release(dsbuffer);
	}
	StackSounds[handle].lpSurround = NULL;
#endif

	StackSounds[handle].lpSndBuf = NULL;
}


// --------------------------------------------------------------------------
// Returns whether the sound is currently playing or not
// --------------------------------------------------------------------------
INT32 I_SoundIsPlaying(INT32 handle)
{
	LPDIRECTSOUNDBUFFER dsbuffer;
	DWORD   dwStatus;

	if (nosound || handle == -1)
		return FALSE;

	dsbuffer = StackSounds[handle].lpSndBuf;
	if (dsbuffer)
	{
		IDirectSoundBuffer_GetStatus (dsbuffer, &dwStatus);
		if (dwStatus & (DSBSTATUS_PLAYING | DSBSTATUS_LOOPING))
			return TRUE;
	}

	return FALSE;
}


// --------------------------------------------------------------------------
// Update properties of a sound currently playing
// --------------------------------------------------------------------------
void I_UpdateSoundParams(INT32    handle,
                         INT32    vol,
                         INT32    sep,
                         INT32    pitch)
{
	LPDIRECTSOUNDBUFFER dsbuffer;
#ifdef SURROUND
	LPDIRECTSOUNDBUFFER dssurround;
	DWORD               dwStatus;
	DWORD               pos;
	boolean             surround_inuse = FALSE;
#endif

	if (nosound)
		return;

	pitch = 0; /// \todo pitch setup
	dsbuffer = StackSounds[handle].lpSndBuf;

#ifdef SURROUND
	if (dsbuffer == NULL)
		return;

	dssurround = StackSounds[handle].lpSurround;
	if (dssurround)
	{
		IDirectSoundBuffer_GetStatus(dssurround, &dwStatus);
		surround_inuse = (dwStatus & (DSBSTATUS_PLAYING | DSBSTATUS_LOOPING));
	}
		// If pan changed to stereo...
	if (sep != -128)
	{
		if (surround_inuse)
		{
			IDirectSoundBuffer_Stop(dssurround);
			surround_inuse = FALSE;
		}
	}
	else  if (!surround_inuse) // Just update volumes and start the surround if need
	{
		I_UpdateSoundVolume(dssurround, vol);
		I_UpdateSoundPanning(dsbuffer, 0);
		IDirectSoundBuffer_GetCurrentPosition(dsbuffer, &pos, NULL);
		IDirectSoundBuffer_SetCurrentPosition(dssurround, pos);
		IDirectSoundBuffer_Play(dssurround, 0, 0, 0);
		surround_inuse = TRUE;
	}
	else
		I_UpdateSoundVolume(dssurround, vol);
	I_UpdateSoundVolume(dsbuffer, vol);

	if (!surround_inuse)
		I_UpdateSoundPanning(dsbuffer, sep);
#else
	if (dsbuffer)
	{
		I_UpdateSoundVolume (dsbuffer, vol);
		I_UpdateSoundPanning (dsbuffer, sep);
	}
#endif
}


//
// Shutdown DirectSound
//
void I_ShutdownSound(void)
{
	int i;

	CONS_Printf("I_ShutdownSound()\n");

#ifdef HW3SOUND
	if (hws_mode != HWS_DEFAULT_MODE)
	{
		HW3S_Shutdown();
		Shutdown3DSDriver();
		return;
	}
#endif
	// release any temporary 'duplicated' secondary buffers
	for (i = 0; i < MAXSTACKSOUNDS; i++)
		if (StackSounds[i].lpSndBuf)
			// stops the sound and release it if it is a duplicate
			I_StopSound (i);

	if (DSnd)
	{
		IDirectSound_Release(DSnd);
		DSnd = NULL;
	}
}


// ==========================================================================
// Startup DirectSound
// ==========================================================================
void I_StartupSound(void)
{
	HRESULT             hr;
	LPDIRECTSOUNDBUFFER lpDsb;
	DSBUFFERDESC        dsbdesc;
	WAVEFORMATEX        wfm;
	int                 cooplevel;
	int                 frequency;

#ifdef HW3SOUND
	const char         *sdrv_name = NULL;
	snddev_t            snddev;
#endif

	sound_started = false;

	if (dedicated)
		return;

	if (nosound)
		return;

	// Secure and configure sound device first.
	CONS_Printf("I_StartupSound: ");

	// frequency of primary buffer may be set at cmd-line
	if (M_CheckParm ("-freq") && M_IsNextParm())
	{
		frequency = atoi(M_GetNextParm());
		CONS_Printf(" requested frequency of %d hz\n", frequency);
		CV_SetValue(&cv_samplerate,frequency);
	}
	else
		frequency = cv_samplerate.value;

	// Set cooperative level
	// Cooperative sound with other applications can be requested at cmd-line
	if (M_CheckParm("-coopsound"))
		cooplevel = DSSCL_PRIORITY;
	else
		cooplevel = DSSCL_EXCLUSIVE;

#ifdef HW3SOUND
	if (M_CheckParm("-ds3d"))
	{
		hws_mode = HWS_DS3D;
		sdrv_name = "s_ds3d.dll";
	}
#if 1
	else if (M_CheckParm("-fmod3d"))
	{
		hws_mode = HWS_FMOD3D;
		sdrv_name = "s_fmod.dll";
	}
	else if (M_CheckParm("-sounddriver") &&  M_IsNextParm())
	{
		hws_mode = HWS_OTHER;
		sdrv_name = M_GetNextParm();
	}
#else
	else if (M_CheckParm("-sounddriver") &&  M_IsNextParm())
	{
		hws_mode = HWS_OTHER;
		sdrv_name = M_GetNextParm();
	}
	else if (!M_CheckParm("-nosd"))
	{
		hws_mode = HWS_FMOD3D;
		sdrv_name = "s_fmod.dll";
	}
#endif

	// There must be further sound drivers (such as A3D and EAX)!!!

	if (hws_mode != HWS_DEFAULT_MODE && sdrv_name != NULL)
	{
		if (Init3DSDriver(sdrv_name))
		{
			//nosound = true;
			snddev.sample_rate = frequency;
			snddev.bps = 16;
			snddev.numsfxs = NUMSFX;
			snddev.cooplevel = cooplevel;
			snddev.hWnd = hWndMain;
			if (HW3S_Init(I_Error, &snddev))
			{
				CONS_Printf("Using external sound driver %s\n", sdrv_name);
				I_AddExitFunc(I_ShutdownSound);
				return;
			}
			// Falls back to default sound system
			CONS_Printf("Not using external sound driver %s\n", sdrv_name);
			HW3S_Shutdown();
			Shutdown3DSDriver();
		}
		hws_mode = HWS_DEFAULT_MODE;
	}
#endif

	// Create DirectSound, use the default sound device
	hr = DirectSoundCreate(NULL, &DSnd, NULL);
	if (FAILED(hr))
	{
		CONS_Printf(" DirectSoundCreate FAILED\n"
					 " there is no sound device or the sound device is under\n"
					 " the control of another application\n");
		nosound = true;
		return;
	}

	// register exit code, now that we have at least DirectSound to close
	I_AddExitFunc(I_ShutdownSound);
	hr = IDirectSound_SetCooperativeLevel (DSnd, hWndMain, cooplevel);
	if (FAILED(hr))
	{
		CONS_Printf(" SetCooperativeLevel FAILED\n");
		nosound = true;
		return;
	}

	// Set up DSBUFFERDESC structure.
	ZeroMemory (&dsbdesc, sizeof (DSBUFFERDESC));
	dsbdesc.dwSize        = sizeof (DSBUFFERDESC);
	dsbdesc.dwFlags = DSBCAPS_PRIMARYBUFFER |
	                  DSBCAPS_CTRLVOLUME;
	dsbdesc.dwBufferBytes = 0;      // Must be 0 for primary buffer
	dsbdesc.lpwfxFormat = NULL;     // Must be NULL for primary buffer

	// Set up structure for the desired format
	ZeroMemory (&wfm, sizeof (WAVEFORMATEX));
	wfm.wFormatTag = WAVE_FORMAT_PCM;
	wfm.nChannels = 2;                              //STEREO SOUND!
	wfm.nSamplesPerSec = frequency;
	wfm.wBitsPerSample = 16;
	wfm.nBlockAlign = (WORD)(wfm.wBitsPerSample / 8 * wfm.nChannels);
	wfm.nAvgBytesPerSec = wfm.nSamplesPerSec * wfm.nBlockAlign;

	// Gain access to the primary buffer
	hr = IDirectSound_CreateSoundBuffer (DSnd, &dsbdesc, &lpDsb, NULL);
	if (FAILED(hr))
	{
		CONS_Printf("CreateSoundBuffer FAILED: %s (ErrNo %d)\n", DXErrorToString(hr), hr);
		nosound = true;
		return;
	}

	// Set the primary buffer to the desired format,
	// but only if we are allowed to do it
	if (cooplevel >= DSSCL_PRIORITY)
	{
		if (SUCCEEDED (hr))
		{
			// Set primary buffer to the desired format. If this fails,
			// we'll just ignore and go with the default.
			hr = IDirectSoundBuffer_SetFormat (lpDsb, &wfm);
			if (FAILED(hr))
				CONS_Printf("I_StartupSound :  couldn't set primary buffer format.\n");
			else
				CV_SetValue(&cv_samplerate,wfm.nSamplesPerSec);
		}
		// move any on-board sound memory into a contiguous block
		// to make the largest portion of free memory available.

		CONS_Printf(" Compacting onboard sound-memory...");
		hr = IDirectSound_Compact (DSnd);
		CONS_Printf(" %s\n", SUCCEEDED(hr) ? "Done\n" : "Failed\n"));
	}

	// set the primary buffer to play continuously, for performance
	// "... this method will ensure that the primary buffer is playing even when no secondary
	// buffers are playing; in that case, silence will be played. This can reduce processing
	// overhead when sounds are started and stopped in sequence, because the primary buffer
	// will be playing continuously rather than stopping and starting between secondary buffers."
	hr = IDirectSoundBuffer_Play (lpDsb, 0, 0, DSBPLAY_LOOPING);
	if (FAILED (hr))
		CONS_Printf(" Primary buffer continuous play FAILED\n");

#ifdef DEBUGSOUND
	{
		DSCAPS              DSCaps;
		DSCaps.dwSize = sizeof (DSCAPS);
		hr = IDirectSound_GetCaps (DSnd, &DSCaps);
		if (SUCCEEDED (hr))
		{
			if (DSCaps.dwFlags & DSCAPS_CERTIFIED)
				CONS_Printf("This driver has been certified by Microsoft\n");
			if (DSCaps.dwFlags & DSCAPS_EMULDRIVER)
				CONS_Printf("No driver with DirectSound support installed (no hardware mixing)\n");
			if (DSCaps.dwFlags & DSCAPS_PRIMARY16BIT)
				CONS_Printf("Supports 16-bit primary buffer\n");
			if (DSCaps.dwFlags & DSCAPS_PRIMARY8BIT)
				CONS_Printf("Supports 8-bit primary buffer\n");
			if (DSCaps.dwFlags & DSCAPS_SECONDARY16BIT)
				CONS_Printf("Supports 16-bit, hardware-mixed secondary buffers\n");
			if (DSCaps.dwFlags & DSCAPS_SECONDARY8BIT)
				CONS_Printf("Supports 8-bit, hardware-mixed secondary buffers\n");

			CONS_Printf("Maximum number of hardware buffers: %d\n", DSCaps.dwMaxHwMixingStaticBuffers);
			CONS_Printf("Size of total hardware memory: %d\n", DSCaps.dwTotalHwMemBytes);
			CONS_Printf("Size of free hardware memory= %d\n", DSCaps.dwFreeHwMemBytes);
			CONS_Printf("Play Cpu Overhead (%% cpu cycles): %d\n", DSCaps.dwPlayCpuOverheadSwBuffers);
		}
		else
			CONS_Printf(" couldn't get sound device caps.\n");
	}
#endif

	// save pointer to the primary DirectSound buffer for volume changes
	DSndPrimary = lpDsb;

	ZeroMemory (StackSounds, sizeof (StackSounds));

	CONS_Printf("sound initialised.\n");
	sound_started = true;
}


// ==========================================================================
//
// MUSIC API using MidiStream
//
// ==========================================================================

#define SPECIAL_HANDLE_CLEANMIDI  -1999 // tell I_StopSong() to do a full (slow) midiOutReset() on exit

static  BOOL        bMusicStarted;

static  UINT        uMIDIDeviceID, uCallbackStatus;
static  HMIDISTRM   hStream;
static  HANDLE      hBufferReturnEvent; // for synch between the callback thread and main program thread
                                        // (we need to synch when we decide to stop/free stream buffers)

static  int         nCurrentBuffer = 0, nEmptyBuffers;

static  BOOL        bBuffersPrepared = FALSE;
static  DWORD       dwVolCache[MAX_MIDI_IN_TRACKS];
        DWORD       dwVolumePercent;    // accessed by win_main.c

        // this is accessed by mid2strm.c conversion code
        BOOL bMidiLooped = FALSE;
static  BOOL bMidiPlaying = FALSE;
static  BOOL bMidiPaused = FALSE;
static  CONVERTINFO ciStreamBuffers[NUM_STREAM_BUFFERS];

#define STATUS_KILLCALLBACK         100     // Signals that the callback should die
#define STATUS_CALLBACKDEAD         200     // Signals callback is done processing
#define STATUS_WAITINGFOREND        300     // Callback's waiting for buffers to play

#define DEBUG_CALLBACK_TIMEOUT 2000         // Wait 2 seconds for callback
                                            // faB: don't freeze the main code if we debug..

#define VOL_CACHE_INIT              127     // for dwVolCache[]

static BOOL bMidiCanSetVolume;          // midi caps

static void Mid2StreamFreeBuffers(void);
static void CALLBACK MidiStreamCallback (HMIDIIN hMidi, UINT uMsg, DWORD dwInstance,
                                         DWORD dwParam1, DWORD dwParam2);
static BOOL StreamBufferSetup(LPBYTE pMidiData, size_t iMidiSize);

// -------------------
// MidiErrorMessageBox
// Calls the midiOutGetErrorText() function and displays the text which
// corresponds to a midi subsystem error code.
// -------------------
static void MidiErrorMessageBox(MMRESULT mmr)
{
	char szTemp[256] = "";

	/*szTemp[0] = '\2';   //white text to stand out*/
	if((MMSYSERR_NOERROR == midiOutGetErrorTextA(mmr, szTemp/*+1*/, sizeof (szTemp))) && *szTemp)
		CONS_Printf("%s\n",szTemp);
	/*MessageBox (GetActiveWindow(), szTemp+1, "LEGACY",
				MB_OK | MB_ICONSTOP);*/
	//wsprintf(szDebug, "Midi subsystem error: %s", szTemp);
}


// ----------------
// I_InitAudioMixer
// ----------------
#ifdef TESTCODE
void I_InitAudioMixer (void)
{
	UINT        cMixerDevs = mixerGetNumDevs();
	CONS_Printf("%d mixer devices available\n", cMixerDevs);
}
#endif

// -----------
// I_InitDigMusic
// Startup Digital device for streaming output
// -----------
void I_InitDigMusic(void)
{
	if (dedicated)
		nodigimusic = true;
	else
		CONS_Printf("I_InitDigMusic()\n");

#ifdef FMODSOUND
	if (!nodigimusic)
	{
		// Tails 11-21-2002
		if (FSOUND_GetVersion() < FMOD_VERSION)
		{
			//I_Error("FMOD Error : You are using the wrong DLL version!\nYou should be using FMOD %s\n", "FMOD_VERSION");
			CONS_Printf("FMOD Error : You are using the wrong DLL version!\nYou should be using FMOD %s\n", "FMOD_VERSION");
			nodigimusic = true;
		}

		/*
			INITIALIZE
		*/
#if 1
		if (!FSOUND_SetHWND(hWndMain))
		{
//			I_Error("FMOD(Init,FSOUND_SetHWND): %s\n", FMOD_ErrorString(FSOUND_GetError()));
			//FSOUND_SetOutput(FSOUND_OUTPUT_DSOUND);
		}
		//else
#endif

		if (!FSOUND_Init(44100, 32, FSOUND_INIT_DONTLATENCYADJUST))
		{
			//I_Error("FMOD(Init,FSOUND_Init): %s\n", FMOD_ErrorString(FSOUND_GetError()));
			CONS_Printf("FMOD(Init,FSOUND_Init): %s\n", FMOD_ErrorString(FSOUND_GetError()));
			nodigimusic = true;
		}
		else
			I_AddExitFunc(I_ShutdownDigMusic);
	}
#endif
}

// -----------
// I_InitMIDIMusic
// Startup Midi device for streaming output
// -----------
void I_InitMIDIMusic(void)
{
	DWORD       idx;
	MMRESULT    mmrRetVal;
	UINT        cMidiDevs;
	MIDIOUTCAPS MidiOutCaps;
	const char *szTechnology;

	bMusicStarted = false;

	if (dedicated)
		nomidimusic = true;
	else
		CONS_Printf("I_InitMIDIMusic()\n");

	if (nomidimusic)
		return;

	// check out number of MIDI devices available
	//
	cMidiDevs = midiOutGetNumDevs();
	if (!cMidiDevs)
	{
		CONS_Printf("No MIDI devices available, music is disabled\n");
		nomidimusic = true;
		return;
	}
#ifdef DEBUGMIDISTREAM
	else
	{
		CONS_Printf("%d MIDI devices available\n", cMidiDevs);
	}
#endif

	if (M_CheckParm("-winmidi") && M_IsNextParm())
		uMIDIDeviceID = atoi(M_GetNextParm());
	else
		uMIDIDeviceID = MIDI_MAPPER;

	// get MIDI device caps
	//
	if ((mmrRetVal = midiOutGetDevCaps (uMIDIDeviceID, &MidiOutCaps, sizeof (MIDIOUTCAPS))) !=
	     MMSYSERR_NOERROR)
	{
		CONS_Printf("midiOutGetCaps FAILED : \n");
		MidiErrorMessageBox (mmrRetVal);
	}
	else
	{
		CONS_Printf("MIDI product name: %s\n", MidiOutCaps.szPname);
		switch (MidiOutCaps.wTechnology)
		{
			case MOD_FMSYNTH:   szTechnology = "FM Synth"; break;
			case MOD_MAPPER:    szTechnology = "Microsoft MIDI Mapper"; break;
			case MOD_MIDIPORT:  szTechnology = "MIDI hardware port"; break;
			case MOD_SQSYNTH:   szTechnology = "Square wave synthesizer"; break;
			case MOD_SYNTH:     szTechnology = "Synthesizer"; break;
			default:            szTechnology = "unknown"; break;
		}
		CONS_Printf("MIDI technology: %s\n", szTechnology);
		CONS_Printf("MIDI caps:\n");
		if (MidiOutCaps.dwSupport & MIDICAPS_CACHE)
			CONS_Printf("-Patch caching\n");
		if (MidiOutCaps.dwSupport & MIDICAPS_LRVOLUME)
			CONS_Printf("-Separate left and right volume control\n");
		if (MidiOutCaps.dwSupport & MIDICAPS_STREAM)
			CONS_Printf("-Direct support for midiStreamOut()\n");
		if (MidiOutCaps.dwSupport & MIDICAPS_VOLUME)
			CONS_Printf("-Volume control\n");
		bMidiCanSetVolume = ((MidiOutCaps.dwSupport & MIDICAPS_VOLUME)!=0);
	}

#ifdef TESTCODE
	I_InitAudioMixer ();
#endif

	// ----------------------------------------------------------------------
	// Midi2Stream initialization
	// ----------------------------------------------------------------------

	// create event for synch'ing the callback thread to main program thread
	// when we will need it
	hBufferReturnEvent = CreateEvent(NULL, FALSE, FALSE,
	                                  TEXT("SRB2 Midi Playback: Wait For Buffer Return"));

	if (!hBufferReturnEvent)
	{
		I_GetLastErrorMsgBox();
		nomidimusic = true;
		return;
	}

	if ((mmrRetVal = midiStreamOpen(&hStream,
	                                &uMIDIDeviceID,
	                                (DWORD)1, (DWORD_PTR)MidiStreamCallback/*NULL*/,
	                                (DWORD)0,
	                                CALLBACK_FUNCTION /*CALLBACK_NULL*/)) != MMSYSERR_NOERROR)
	{
		CONS_Printf("I_RegisterSong: midiStreamOpen FAILED\n");
		MidiErrorMessageBox(mmrRetVal);
		nomidimusic = true;
		return;
	}

	// stream buffers are initially unallocated (set em NULL)
	for (idx = 0; idx < NUM_STREAM_BUFFERS; idx++)
		ZeroMemory (&ciStreamBuffers[idx].mhBuffer, sizeof (MIDIHDR));
	// ----------------------------------------------------------------------

	// register exit code
	I_AddExitFunc(I_ShutdownMIDIMusic);

	bMusicStarted = true;
}

// ---------------
// I_InitMusic
// ---------------
void I_InitMusic(void)
{
	I_InitDigMusic();
	I_InitMIDIMusic();
}

// ---------------
// I_ShutdownDigMusic
// ---------------
void I_ShutdownDigMusic(void)
{
	CONS_Printf("I_ShutdownDigMusic: \n");

#ifdef FMODSOUND
	if (!nodigimusic && FSOUND_GetError() != FMOD_ERR_UNINITIALIZED)
	{
		if (FSOUND_GetError() != FMOD_ERR_NONE && FSOUND_GetError() != FMOD_ERR_CHANNEL_ALLOC && FSOUND_GetError() != FMOD_ERR_MEDIAPLAYER)
			if (devparm) CONS_Printf("FMOD(Shutdown,Unknown): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		if (mod)
		{
			if (FMUSIC_IsPlaying(mod))
				if (!FMUSIC_StopSong(mod))
					if (devparm) CONS_Printf("FMOD(Shutdown,FMUSIC_StopSong): %s\n", FMOD_ErrorString(FSOUND_GetError()));
			if (!FMUSIC_FreeSong(mod))
				if (devparm) CONS_Printf("FMOD(Shutdown,FMUSIC_FreeSong): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		}
		if (fmus)
		{
			if (FSOUND_IsPlaying(fsoundchannel))
				if (!FSOUND_Stream_Stop(fmus))
					if (devparm) CONS_Printf("FMOD(Shutdown,FSOUND_Stream_Stop): %s\n", FMOD_ErrorString(FSOUND_GetError()));
			if (!FSOUND_Stream_Close(fmus))
				if (devparm) CONS_Printf("FMOD(Shutdown,FSOUND_Stream_Close): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		}
		FSOUND_Close();
		remove("fmod.tmp"); // Delete the temp file
		//if (!FSOUND_StopSound(FSOUND_ALL))
			//if (FSOUND_GetError() != FMOD_ERR_MEDIAPLAYER) CONS_Printf("FMOD(Shutdown,FSOUND_StopSound): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		//FMUSIC_StopAllSongs();
			//if (FSOUND_GetError() != FMOD_ERR_NONE && FSOUND_GetError() != FMOD_ERR_MEDIAPLAYER) CONS_Printf("FMOD(Shutdown,FMUSIC_StopAllSongs): %s\n", FMOD_ErrorString(FSOUND_GetError()));
	}
#endif
}

// ---------------
// I_ShutdownMIDIMusic
// ---------------
void I_ShutdownMIDIMusic(void)
{
	DWORD       idx;
	MMRESULT    mmrRetVal;
	HGLOBAL lp = NULL;

	CONS_Printf("I_ShutdownMIDIMusic: \n");

	if (nomidimusic)
		return;

	if (!bMusicStarted)
		return;

	if (hStream)
	{
		I_StopSong (SPECIAL_HANDLE_CLEANMIDI);
	}

	Mid2StreamConverterCleanup();
	Mid2StreamFreeBuffers();

	// Free our stream buffers
	for (idx = 0; idx < NUM_STREAM_BUFFERS; idx++)
	{
		if (ciStreamBuffers[idx].mhBuffer.lpData)
		{
			//GlobalFreePtr(ciStreamBuffers[idx].mhBuffer.lpData);
			lp = GlobalPtrHandle(ciStreamBuffers[idx].mhBuffer.lpData);
			GlobalUnlock(lp);
			GlobalFree(lp);
			ciStreamBuffers[idx].mhBuffer.lpData = NULL;
		}
	}

	if (hStream)
	{
		if ((mmrRetVal = midiStreamClose(hStream)) != MMSYSERR_NOERROR)
			MidiErrorMessageBox(mmrRetVal);
		hStream = NULL;
	}

	CloseHandle(hBufferReturnEvent);

	bMusicStarted = false;
}

// ---------------
// I_ShutdownMusic
// ---------------
void I_ShutdownMusic(void)
{
	if (!nodigimusic)
		I_ShutdownDigMusic();

	if (!nomidimusic)
		I_ShutdownMIDIMusic();
}

// --------------------
// SetAllChannelVolumes
// Given a percent in tenths of a percent, sets volume on all channels to
// reflect the new value.
// --------------------
static void SetAllChannelVolumes(DWORD dwVolumePercent)
{
	DWORD       dwEvent, dwStatus, dwVol, idx;
	MMRESULT    mmrRetVal;

	if (!bMidiPlaying)
		return;

	for (idx = 0, dwStatus = MIDI_CTRLCHANGE; idx < MAX_MIDI_IN_TRACKS; idx++, dwStatus++)
	{
		dwVol = (dwVolCache[idx] * dwVolumePercent) / 1000;
		//CONS_Printf("channel %d vol %d\n", idx, dwVol);
		dwEvent = dwStatus | ((DWORD)MIDICTRL_VOLUME << 8)
			| ((DWORD)dwVol << 16);
		if ((mmrRetVal = midiOutShortMsg((HMIDIOUT)hStream, dwEvent))
			!= MMSYSERR_NOERROR)
		{
			MidiErrorMessageBox(mmrRetVal);
			return;
		}
	}
}


// ----------------
// I_SetMusicVolume
// Set the midi output volume
// ----------------
void I_SetMIDIMusicVolume(INT32 volume)
{
	MMRESULT    mmrRetVal;
	int         iVolume;

	if (nomidimusic)
		return;

	if (bMidiCanSetVolume)
	{
		// method A
		// current volume is 0-31, we need 0-0xFFFF in each word (left/right channel)
		iVolume = (volume << 11) | (volume << 27);
		if ((mmrRetVal = midiOutSetVolume ((HMIDIOUT)(size_t)uMIDIDeviceID, iVolume)) != MMSYSERR_NOERROR)
		{
			CONS_Printf("I_SetMusicVolume: couldn't set volume\n");
			MidiErrorMessageBox(mmrRetVal);
		}
	}
	else
	{
		// method B
		dwVolumePercent = (volume * 1000) / 32;
		SetAllChannelVolumes (dwVolumePercent);
	}
}

void I_SetDigMusicVolume(INT32 volume)
{
#ifdef FMODSOUND
	if (volume != -1)
		fmodvol = (volume<<3)+(volume>>2);
	if (!nodigimusic)
	{
		if (FSOUND_GetError() != FMOD_ERR_NONE && FSOUND_GetError() != FMOD_ERR_CHANNEL_ALLOC && FSOUND_GetError() != FMOD_ERR_MEDIAPLAYER)
			if (devparm) CONS_Printf("FMOD(Volume,Unknown): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		if (mod)
		{
			if (FMUSIC_GetType(mod) != FMUSIC_TYPE_NONE)
			{
				if (!FMUSIC_SetMasterVolume(mod, fmodvol) && devparm)
					CONS_Printf("FMOD(Volume,FMUSIC_SetMasterVolume): %s\n",
						FMOD_ErrorString(FSOUND_GetError()));
			}
			else if (devparm)
				CONS_Printf("FMOD(Volume,FMUSIC_GetType): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		}
		if (fmus)
		{
			if (!FSOUND_SetVolume(fsoundchannel, fmodvol))
				if (devparm) CONS_Printf("FMOD(Volume,FSOUND_SetVolume): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		}
	}
#else
	(void)volume;
#endif
}


// ----------
// I_PlaySong
// Note: doesn't use the handle, would be useful to switch between mid's after
//       some trigger (would do several RegisterSong, then PlaySong the chosen one)
// ----------
boolean I_PlaySong(INT32 handle, INT32 bLooping)
{
	MMRESULT        mmrRetVal;

	if (nomidimusic)
		return false;

#ifdef DEBUGMIDISTREAM
	CONS_Printf("I_PlaySong: looping %d\n", bLooping);
#endif

	// unpause the song first if it was paused
	if (bMidiPaused)
		I_PauseSong(handle);

	// Clear the status of our callback so it will handle
	// MOM_DONE callbacks once more
	uCallbackStatus = 0;
	bMidiPlaying = FALSE;
	if ((mmrRetVal = midiStreamRestart(hStream)) != MMSYSERR_NOERROR)
	{
		MidiErrorMessageBox(mmrRetVal);
		Mid2StreamConverterCleanup();
		Mid2StreamFreeBuffers();
		midiStreamClose(hStream);
		//I_Error("I_PlaySong: midiStreamRestart error");
		midiStreamOpen(&hStream, &uMIDIDeviceID, (DWORD)1,
		               (DWORD_PTR)MidiStreamCallback/*NULL*/,
		               (DWORD)0, CALLBACK_FUNCTION /*CALLBACK_NULL*/);
	}
	else bMidiPlaying = TRUE;
	bMidiLooped = bLooping;
	return bMidiPlaying;
}


// -----------
// I_PauseSong
// calls midiStreamPause() to pause the midi playback
// -----------
void I_PauseSong (INT32 handle)
{
	(void)handle;
#ifdef FMODSOUND
	if (!nodigimusic)
	{
		if (FSOUND_GetError() != FMOD_ERR_NONE && FSOUND_GetError() != FMOD_ERR_CHANNEL_ALLOC && FSOUND_GetError() != FMOD_ERR_MEDIAPLAYER)
			if (devparm) CONS_Printf("FMOD(Pause,Unknown): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		if (mod)
		{
			if (!FMUSIC_GetPaused(mod))
				if (!FMUSIC_SetPaused(mod, true))
					if (devparm) CONS_Printf("FMOD(Pause,FMUSIC_SetPaused): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		}
		if (fmus)
		{
			if (!FSOUND_GetPaused(fsoundchannel))
				if (!FSOUND_SetPaused(fsoundchannel, true))
					if (devparm) CONS_Printf("FMOD(Pause,FSOUND_SetPaused): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		}
	}
#endif

	if (nomidimusic)
		return;

#ifdef DEBUGMIDISTREAM
	CONS_Printf("I_PauseSong: \n");
#endif

	if (!bMidiPaused)
	{
		midiStreamPause(hStream);
		bMidiPaused = true;
	}
}


// ------------
// I_ResumeSong
// un-pause the midi song with midiStreamRestart
// ------------
void I_ResumeSong (INT32 handle)
{
	(void)handle;
#ifdef FMODSOUND
	if (!nodigimusic)
	{
		if (FSOUND_GetError() != FMOD_ERR_NONE && FSOUND_GetError() != FMOD_ERR_CHANNEL_ALLOC && FSOUND_GetError() != FMOD_ERR_MEDIAPLAYER)
			if (devparm) CONS_Printf("FMOD(Resume,Unknown): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		if (mod != NULL)
		{
			if (FMUSIC_GetPaused(mod))
				if (!FMUSIC_SetPaused(mod, false))
					if (devparm) CONS_Printf("FMOD(Resume,FMUSIC_SetPaused): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		}
		if (fmus != NULL)
		{
			if (FSOUND_GetPaused(fsoundchannel))
				if (!FSOUND_SetPaused(fsoundchannel, false))
					if (devparm) CONS_Printf("FMOD(Resume,FSOUND_SetPaused): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		}
	}
#endif

	if (nomidimusic)
		return;

#ifdef DEBUGMIDISTREAM
	CONS_Printf("I_ResumeSong: \n");
#endif

	if (bMidiPaused)
	{
		midiStreamRestart(hStream);
		bMidiPaused = false;
	}
}


// ----------
// I_StopSong
// ----------
// faB: -1999 is a special handle here, it means we stop the midi when exiting
//      Legacy, this will do a midiOutReset() for a more 'sure' midi off.
void I_StopSong(INT32 handle)
{
	MMRESULT        mmrRetVal;

	if (nomidimusic)
		return;

#ifdef DEBUGMIDISTREAM
	CONS_Printf("I_StopSong: \n");
#endif

	if (bMidiPlaying || (uCallbackStatus != STATUS_CALLBACKDEAD))
	{
		bMidiPlaying = bMidiPaused = FALSE;
		if (uCallbackStatus != STATUS_CALLBACKDEAD &&
		    uCallbackStatus != STATUS_WAITINGFOREND)
			uCallbackStatus = STATUS_KILLCALLBACK;

		//CONS_Printf("a: %d\n",I_GetTime());
		if ((mmrRetVal = midiStreamStop(hStream)) != MMSYSERR_NOERROR)
		{
			MidiErrorMessageBox(mmrRetVal);
			return;
		}

		//faB: if we don't call midiOutReset() seems we have to stop the buffers
		//     ourselves (or it doesn't play anymore)
		if (!bMidiPaused && (handle != SPECIAL_HANDLE_CLEANMIDI))
		{
			midiStreamPause(hStream);
		}
		//CONS_Printf("b: %d\n",I_GetTime());
		else
		//faB: this damn call takes 1 second and a half !!! still do it on exit
		//     to be sure everything midi is cleaned as much as possible
		if (handle == SPECIAL_HANDLE_CLEANMIDI)
		{
			if ((mmrRetVal = midiOutReset((HMIDIOUT)hStream)) != MMSYSERR_NOERROR)
			{
				MidiErrorMessageBox(mmrRetVal);
				return;
			}
		}
		//CONS_Printf("c: %d\n",I_GetTime());

		// Wait for the callback thread to release this thread, which it will do by
		// calling SetEvent() once all buffers are returned to it
		if ((devparm)
			&& (WaitForSingleObject(hBufferReturnEvent, DEBUG_CALLBACK_TIMEOUT)
			    == WAIT_TIMEOUT))
		{
			// Note, this is a risky move because the callback may be genuinely busy, but
			// when we're debugging, it's safer and faster than freezing the application,
			// which leaves the MIDI device locked up and forces a system reset...
			CONS_Printf("Timed out waiting for MIDI callback\n");
			uCallbackStatus = STATUS_CALLBACKDEAD;
		}
		//CONS_Printf("d: %d\n",I_GetTime());
	}

	if (uCallbackStatus == STATUS_CALLBACKDEAD)
	{
		uCallbackStatus = 0;
		Mid2StreamConverterCleanup();
		Mid2StreamFreeBuffers();
		//faB: we could close the stream here and re-open later to avoid
		//     a little quirk in mmsystem (see DirectX6 mstream note)
		midiStreamClose(hStream);
		midiStreamOpen(&hStream, &uMIDIDeviceID, (DWORD)1,
		               (DWORD_PTR)MidiStreamCallback/*NULL*/,
		               (DWORD)0, CALLBACK_FUNCTION /*CALLBACK_NULL*/);
	}
}

void I_StopDigSong(void)
{
#ifdef FMODSOUND
	if (!nodigimusic)
	{
		if (FSOUND_GetError() != FMOD_ERR_NONE && FSOUND_GetError() != FMOD_ERR_INVALID_PARAM && FSOUND_GetError() != FMOD_ERR_CHANNEL_ALLOC && FSOUND_GetError() != FMOD_ERR_MEDIAPLAYER)
			if (devparm) CONS_Printf("FMOD(Stop,Unknown): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		if (mod)
		{
			if (FMUSIC_IsPlaying(mod))
			{
				if (!FMUSIC_StopSong(mod))
					if (devparm) CONS_Printf("FMOD(Stop,FMUSIC_StopSong): %s\n", FMOD_ErrorString(FSOUND_GetError()));
			}
		}
		if (fmus)
		{
			if (FSOUND_IsPlaying(fsoundchannel))
			{
				if (!FSOUND_Stream_Stop(fmus))
					if (devparm) CONS_Printf("FMOD(Stop,FSOUND_Stream_Stop): %s\n", FMOD_ErrorString(FSOUND_GetError()));
			}
		}
		//if (!FSOUND_StopSound(FSOUND_ALL))
			//if (FSOUND_GetError() != FMOD_ERR_MEDIAPLAYER) CONS_Printf("FMOD(Stop,FSOUND_StopSound): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		//FMUSIC_StopAllSongs();
			//if (FSOUND_GetError() != FMOD_ERR_NONE && FSOUND_GetError() != FMOD_ERR_MEDIAPLAYER) CONS_Printf("FMOD(Stop,FMUSIC_StopAllSongs): %s\n", FMOD_ErrorString(FSOUND_GetError()));
	}
#endif
}

void I_UnRegisterSong(INT32 handle)
{
	handle = 0;
	if (nomidimusic)
		return;

	//faB: we might free here whatever is allocated per-music
	//     (but we don't cause I hate malloc's)
	Mid2StreamConverterCleanup();

#ifdef DEBUGMIDISTREAM
	CONS_Printf("I_UnregisterSong: \n");
#endif
}

int I_SetSongSpeed(unsigned int speed)
{
#ifdef FMODSOUND
	if (music_disabled || nodigimusic)
		return 0; //there no music or FMOD is not loaded

	if((!fmus || !FSOUND_IsPlaying(fsoundchannel)) && (!mod || !FMUSIC_IsPlaying(mod)))
		return 0; //there no FMOD music playing

	if (speed == 0)
		return 1; //yes, we can set the speed

	if (fmus)
	{
		if (FSOUND_IsPlaying(fsoundchannel)
		&& !FSOUND_SetFrequency(fsoundchannel,(int)(((float)speed*(float)fsoundfreq)/100.0f)))
		{
			if (devparm)
				CONS_Printf("FMOD(ChangeSpeed,FSOUND_SetFrequency): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		}
		else
			return 1;
	}
	else if (mod)
	{
		if (FMUSIC_IsPlaying(mod)
		&& !FMUSIC_SetMasterSpeed(mod,(float)speed/100.0f))
		{
			if (devparm)
				CONS_Printf("FMOD(ChangeSpeed,FMUSIC_SetMasterSpeed): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		}
		else
			return 1;
	}
#else
	(void)speed;
#endif
	return 0;
}

// Special FMOD support Tails 11-21-2002
boolean I_StartDigSong(const char *musicname, INT32 looping)
{
#ifdef FMODSOUND
	char filename[9];
	void *data;
	int lumpnum;

	if (FSOUND_GetError() != FMOD_ERR_NONE && FSOUND_GetError() != FMOD_ERR_CHANNEL_ALLOC &&
	   FSOUND_GetError() != FMOD_ERR_MEDIAPLAYER && FSOUND_GetError() != FMOD_ERR_INVALID_PARAM)
		if (devparm) CONS_Printf("FMOD(Start,Unknown): %s\n", FMOD_ErrorString(FSOUND_GetError()));

	if (fmus)
	{
		if (FSOUND_IsPlaying(fsoundchannel))
			if (!FSOUND_Stream_Stop(fmus))
				if (devparm) CONS_Printf("FMOD(Start,FSOUND_Stream_Stop): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		if (!FSOUND_Stream_Close(fmus))
			if (devparm) CONS_Printf("FMOD(Start,FSOUND_Stream_Close): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		fsoundchannel = -1;
		fmus = NULL;
	}
	if (mod)
	{
		if (FMUSIC_IsPlaying(mod))
			if (!FMUSIC_StopSong(mod))
				if (devparm) CONS_Printf("FMOD(Start,FMUSIC_StopSong): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		if (!FMUSIC_FreeSong(mod))
			if (devparm) CONS_Printf("FMOD(Start,FMUSIC_FreeSong): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		mod = NULL;
	}
	//if (!FSOUND_StopSound(FSOUND_ALL))
		//if (FSOUND_GetError() != FMOD_ERR_MEDIAPLAYER) CONS_Printf("FMOD(Start,FSOUND_StopSound): %s\n", FMOD_ErrorString(FSOUND_GetError()));
	//FMUSIC_StopAllSongs();
		//if (FSOUND_GetError() != FMOD_ERR_NONE && FSOUND_GetError() != FMOD_ERR_MEDIAPLAYER) CONS_Printf("FMOD(Start,FMUSIC_StopAllSongs): %s\n", FMOD_ErrorString(FSOUND_GetError()));

	// Create the filename we need
	sprintf(filename, "o_%s", musicname);

	lumpnum = W_CheckNumForName(filename);

	if (lumpnum == -1)
	{
		// Graue 02-29-2004: don't worry about missing music, there might still be a MIDI
		return false; // No music found. Oh well!
	}

	data = W_CacheLumpName (filename, PU_CACHE);

	I_SaveMemToFile (data, W_LumpLength(lumpnum), "fmod.tmp");

	Z_Free(data);

	mod = FMUSIC_LoadSong("fmod.tmp");

	if (FSOUND_GetError() != FMOD_ERR_NONE)
	{
		if (FSOUND_GetError() != FMOD_ERR_FILE_FORMAT)
			if (devparm) CONS_Printf("FMOD(Start,FMUSIC_LoadSong): %s\n", FMOD_ErrorString(FSOUND_GetError()));

		if (mod)
		{
			if (FMUSIC_IsPlaying(mod))
				if (!FMUSIC_StopSong(mod))
					if (devparm) CONS_Printf("FMOD(Start,FMUSIC_StopSong): %s\n", FMOD_ErrorString(FSOUND_GetError()));
			if (!FMUSIC_FreeSong(mod))
				if (devparm) CONS_Printf("FMOD(Start,FMUSIC_FreeSong): %s\n", FMOD_ErrorString(FSOUND_GetError()));
			mod = NULL;
		}
	}

	if (mod)
	{
		if (!FMUSIC_SetLooping(mod, (signed char)looping))
		{
			if (devparm) CONS_Printf("FMOD(Start,FMUSIC_SetLooping): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		}
//		else if (FMUSIC_GetType(mod) == FMUSIC_TYPE_MOD || FMUSIC_GetType(mod) == FMUSIC_TYPE_S3M)
//		{
//			if (!FMUSIC_SetPanSeperation(mod, 0.85f))		/* 15% crossover */
//				CONS_Printf("FMOD(Start,FMUSIC_SetPanSeperation): %s\n", FMOD_ErrorString(FSOUND_GetError()));
//		}
		else if (!FMUSIC_SetPanSeperation(mod, 0.0f))
		{
			if (devparm) CONS_Printf("FMOD(Start,FMUSIC_SetPanSeperation): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		}
	}
	else
	{
		fmus = FSOUND_Stream_OpenFile("fmod.tmp", ((looping) ? (FSOUND_LOOP_NORMAL) : (0)),0);
		if (fmus == NULL)
		{
			if (devparm) CONS_Printf("FMOD(Start,FSOUND_Stream_Open): %s\n", FMOD_ErrorString(FSOUND_GetError()));
			return false;
		}
	}

	// Scan the Ogg Vorbis file for the COMMENT= field for a custom loop point
	if (fmus && looping)
	{
		int scan;
		char *dataum;
		char looplength[64];
		unsigned int loopstart = 0;
		int newcount = 0;
		const int freq = 44100;
		int lumplength = W_LumpLength(lumpnum);
		int length = FSOUND_Stream_GetLengthMs(fmus);

		if (length == 0)
		{
			if (devparm) CONS_Printf("FMOD(Start,FSOUND_Stream_GetLengthMs): %s\n", FMOD_ErrorString(FSOUND_GetError()));
		}
		else
		{
			//freq = FSOUND_GetFrequency(fsoundchannel);

			data = W_CacheLumpName (filename, PU_CACHE);

			dataum = (char *)data;

			for (scan = 0;scan < lumplength; scan++)
			{
				if (*dataum++ == 'C'){
				if (*dataum++ == 'O'){
				if (*dataum++ == 'M'){
				if (*dataum++ == 'M'){
				if (*dataum++ == 'E'){
				if (*dataum++ == 'N'){
				if (*dataum++ == 'T'){
				if (*dataum++ == '='){
				if (*dataum++ == 'L'){
				if (*dataum++ == 'O'){
				if (*dataum++ == 'O'){
				if (*dataum++ == 'P'){
				if (*dataum++ == 'P'){
				if (*dataum++ == 'O'){
				if (*dataum++ == 'I'){
				if (*dataum++ == 'N'){
				if (*dataum++ == 'T'){
				if (*dataum++ == '=')
				{

					while (*dataum != 1 && newcount != 63)
					{
						looplength[newcount++] = *dataum++;
					}

					looplength[newcount] = '\n';

					loopstart = atoi(looplength);
				}
				else
					dataum--;}
				else
					dataum--;}
				else
					dataum--;}
				else
					dataum--;}
				else
					dataum--;}
				else
					dataum--;}
				else
					dataum--;}
				else
					dataum--;}
				else
					dataum--;}
				else
					dataum--;}
				else
					dataum--;}
				else
					dataum--;}
				else
					dataum--;}
				else
					dataum--;}
				else
					dataum--;}
				else
					dataum--;}
				else
					dataum--;}
			}

			Z_Free(data);
		}

		if (loopstart > 0)
		{
			const unsigned int loopend = (unsigned int)((freq/1000.0f)*length-(freq/1000.0f));
			//const unsigned int loopend = (((freq/2)*length)/500)-8;
			if (!FSOUND_Stream_SetLoopPoints(fmus, loopstart, loopend) && devparm)
				CONS_Printf("FMOD(Start,FSOUND_Stream_SetLoopPoints): %s\n",
					FMOD_ErrorString(FSOUND_GetError()));
		}
	}

	/*
		PLAY SONG
	*/
	if (mod)
	{
		if (FMUSIC_PlaySong(mod))
		{
			fsoundchannel = -1;
			I_SetDigMusicVolume(-1);
			return true;
		}
		else
		{
			if (devparm)
				CONS_Printf("FMOD(Start,FMUSIC_PlaySong): %s\n",
				            FMOD_ErrorString(FSOUND_GetError()));
			return false;
		}
	}
	if (fmus)
	{
		fsoundchannel = FSOUND_Stream_PlayEx(FSOUND_FREE, fmus, NULL, TRUE);

		if (fsoundchannel == -1)
		{
			if (devparm)
				CONS_Printf("FMOD(Start,FSOUND_Stream_PlayEx): %s\n",
				            FMOD_ErrorString(FSOUND_GetError()));
			return false;
		}
		else if (!FSOUND_SetPaused(fsoundchannel, FALSE))
		{
			if (devparm)
				CONS_Printf("FMOD(Start,FSOUND_SetPaused): %s\n",
					FMOD_ErrorString(FSOUND_GetError()));
			return false;
		}

		I_SetDigMusicVolume(-1);
		fsoundfreq = FSOUND_GetFrequency(fsoundchannel);
		return true;
	}
#else
	(void)musicname;
	(void)looping;
#endif
	return false;

/////////////////////////////////////////////////////////////////////////////////
}

// --------------
// I_RegisterSong
// Prepare a song for playback
// - setup midi stream buffers, and activate the callback procedure
//   which will continually fill the buffers with new data
// --------------

INT32 I_RegisterSong(void *data, int len)
{
	char *pMidiFileData = NULL; // MIDI music buffer to be played or NULL

	if (nomidimusic)
		return 1;
	if (!data || !len)
		return 0;

#ifdef DEBUGMIDISTREAM
	CONS_Printf("I_RegisterSong: \n");
#endif
	// check for MID format file
	if (!memcmp(data, "MThd", 4))
		pMidiFileData = data;
	else
	{
		CONS_Printf("Music lump is not MID music format\n");
		return 0;
	}

#ifdef DEBUGMIDISTREAM
	I_SaveMemToFile(pMidiFileData, len, "debug.mid");
#endif

	// setup midi stream buffer
	if (StreamBufferSetup((LPBYTE)pMidiFileData, len))
	{
		Mid2StreamConverterCleanup();
		I_Error("I_RegisterSong: StreamBufferSetup FAILED");
	}

	return 1;
}

// -----------------
// StreamBufferSetup
// This function uses the filename stored in the global character array to
// open a MIDI file. Then it goes about converting at least the first part of
// that file into a midiStream buffer for playback.
// -----------------

// -----------------
// StreamBufferSetup
// - returns TRUE if a problem occurs
// -----------------
static BOOL StreamBufferSetup(LPBYTE pMidiData, size_t iMidiSize)
{
	MMRESULT mmrRetVal;
	MIDIPROPTIMEDIV mptd;
	BOOL bFoundEnd = FALSE;
	int dwConvertFlag, nChkErr, idx;

#ifdef DEBUGMIDISTREAM
	if (hStream == NULL)
		I_Error("StreamBufferSetup: hStream is NULL!");
#endif

	// pause midi stream before manipulating the buffers
	midiStreamPause(hStream);

	// allocate the stream buffers (only once)
	for (idx = 0; idx < NUM_STREAM_BUFFERS; idx++)
	{
		ciStreamBuffers[idx].mhBuffer.dwBufferLength = OUT_BUFFER_SIZE;
		if (!ciStreamBuffers[idx].mhBuffer.lpData)
		{
			ciStreamBuffers[idx].mhBuffer.lpData = GlobalAllocPtr(GHND, OUT_BUFFER_SIZE);
			if (!ciStreamBuffers[idx].mhBuffer.lpData)
				return FALSE;
		}
	}

	// returns TRUE in case of conversion error
	if (Mid2StreamConverterInit(pMidiData, iMidiSize))
		return TRUE;

	// Initialize the volume cache array to some pre-defined value
	for (idx = 0; idx < MAX_MIDI_IN_TRACKS; idx++)
		dwVolCache[idx] = VOL_CACHE_INIT;

	mptd.cbStruct = sizeof (mptd);
	mptd.dwTimeDiv = ifs.dwTimeDivision;
	if ((mmrRetVal = midiStreamProperty(hStream, (LPBYTE)&mptd, MIDIPROP_SET|MIDIPROP_TIMEDIV))
	   != MMSYSERR_NOERROR)
	{
		MidiErrorMessageBox(mmrRetVal);
		return TRUE;
	}

	nEmptyBuffers = 0;
	dwConvertFlag = CONVERTF_RESET;

	for (nCurrentBuffer = 0; nCurrentBuffer < NUM_STREAM_BUFFERS; nCurrentBuffer++)
	{
		// Tell the converter to convert up to one entire buffer's length of output
		// data. Also, set a flag so it knows to reset any saved state variables it
		// may keep from call to call.
		ciStreamBuffers[nCurrentBuffer].dwStartOffset = 0;
		ciStreamBuffers[nCurrentBuffer].dwMaxLength = OUT_BUFFER_SIZE;
		ciStreamBuffers[nCurrentBuffer].tkStart = 0;
		ciStreamBuffers[nCurrentBuffer].bTimesUp = FALSE;

		if ((nChkErr = Mid2StreamConvertToBuffer(dwConvertFlag, &ciStreamBuffers[nCurrentBuffer]))
		   != CONVERTERR_NOERROR)
		{
			if (nChkErr == CONVERTERR_DONE)
				bFoundEnd = TRUE;
			else
			{
				CONS_Printf("StreamBufferSetup: initial conversion pass failed\n");
				return TRUE;
			}
		}
		ciStreamBuffers[nCurrentBuffer].mhBuffer.dwBytesRecorded
			= ciStreamBuffers[nCurrentBuffer].dwBytesRecorded;

		if (!bBuffersPrepared)
		{
			if ((mmrRetVal = midiOutPrepareHeader((HMIDIOUT)hStream,
				&ciStreamBuffers[nCurrentBuffer].mhBuffer, sizeof (MIDIHDR))) != MMSYSERR_NOERROR)
			{
				MidiErrorMessageBox(mmrRetVal);
				return TRUE;
			}
		}
		if ((mmrRetVal = midiStreamOut(hStream, &ciStreamBuffers[nCurrentBuffer].mhBuffer,
			sizeof (MIDIHDR))) != MMSYSERR_NOERROR)
		{
			MidiErrorMessageBox(mmrRetVal);
			break;
		}
		dwConvertFlag = 0;

		if (bFoundEnd)
			break;
	}

	bBuffersPrepared = TRUE;
	nCurrentBuffer = 0;

	// MIDI volume
	dwVolumePercent = (cv_midimusicvolume.value * 1000) / 32;
	if (hStream)
		SetAllChannelVolumes(dwVolumePercent);

	return FALSE;
}

// ----------------
// SetChannelVolume
// Call here delayed by MIDI stream callback, to adapt the volume event of the
// midi stream to our own set volume percentage.
// ----------------
void I_SetMidiChannelVolume(DWORD dwChannel, DWORD dwVolumePercent)
{
	DWORD dwEvent, dwVol;
	MMRESULT mmrRetVal;

	if (!bMidiPlaying)
		return;

	dwVol = (dwVolCache[dwChannel] * dwVolumePercent) / 1000;
	dwEvent = MIDI_CTRLCHANGE|dwChannel|((DWORD)MIDICTRL_VOLUME << 8)|((DWORD)dwVol << 16);
	if ((mmrRetVal = midiOutShortMsg((HMIDIOUT)hStream, dwEvent)) != MMSYSERR_NOERROR)
	{
#ifdef DEBUGMIDISTREAM
		MidiErrorMessageBox(mmrRetVal);
#endif
		return;
	}
}

// ------------------
// MidiStreamCallback
// This is the callback handler which continually refills MIDI data buffers
// as they're returned to us from the audio subsystem.
// ------------------
static void CALLBACK MidiStreamCallback(HMIDIIN hMidi, UINT uMsg, DWORD dwInstance,
	DWORD dwParam1, DWORD dwParam2)
{
	MMRESULT mmrRetVal;
	int nChkErr;
	MIDIEVENT* pme;
	MIDIHDR* pmh;

	hMidi = NULL;
	dwParam1 = dwParam2 = dwInstance = 0;
	switch (uMsg)
	{
		case MOM_DONE:
			// dwParam1 is LPMIDIHDR
			if (uCallbackStatus == STATUS_CALLBACKDEAD)
				return;

			nEmptyBuffers++;

			// we reached end of song, but we wait until all the buffers are returned
			if (uCallbackStatus == STATUS_WAITINGFOREND)
			{
				if (nEmptyBuffers < NUM_STREAM_BUFFERS)
					return;
				else
				{
					// stop the song when end reached (was not looping)
					uCallbackStatus = STATUS_CALLBACKDEAD;
					SetEvent(hBufferReturnEvent);
					I_StopSong(0);
					return;
				}
			}

			// This flag is set whenever the callback is waiting for all buffers to come back.
			if (uCallbackStatus == STATUS_KILLCALLBACK)
			{
				// Count NUM_STREAM_BUFFERS-1 being returned for the last time
				if (nEmptyBuffers < NUM_STREAM_BUFFERS)
					return;
				// Then send a stop message when we get the last buffer back...
				else
				{
					// Change the status to callback dead
					uCallbackStatus = STATUS_CALLBACKDEAD;
					SetEvent(hBufferReturnEvent);
					return;
				}
			}

			dwProgressBytes += ciStreamBuffers[nCurrentBuffer].mhBuffer.dwBytesRecorded;

			// -------------------------------------------------
			// Fill an available buffer with audio data again...
			// -------------------------------------------------

			if (bMidiPlaying && nEmptyBuffers)
			{
				ciStreamBuffers[nCurrentBuffer].dwStartOffset = 0;
				ciStreamBuffers[nCurrentBuffer].dwMaxLength = OUT_BUFFER_SIZE;
				ciStreamBuffers[nCurrentBuffer].tkStart = 0;
				ciStreamBuffers[nCurrentBuffer].dwBytesRecorded = 0;
				ciStreamBuffers[nCurrentBuffer].bTimesUp = FALSE;

				if ((nChkErr = Mid2StreamConvertToBuffer(0, &ciStreamBuffers[nCurrentBuffer]))
				   != CONVERTERR_NOERROR)
				{
					if (nChkErr == CONVERTERR_DONE)
					{
						// Don't include this one in the count
						uCallbackStatus = STATUS_WAITINGFOREND;
						return;
					}
					else
					{
						// We're not in the main thread, so we can't call I_Error() now.
						// Log the error message out, and post exit message.
						CONS_Printf("MidiStreamCallback(): conversion pass failed!\n");
						PostMessage(hWndMain, WM_CLOSE, 0, 0);
						return;
					}
				}

				ciStreamBuffers[nCurrentBuffer].mhBuffer.dwBytesRecorded
					= ciStreamBuffers[nCurrentBuffer].dwBytesRecorded;

				if ((mmrRetVal = midiStreamOut(hStream, &ciStreamBuffers[nCurrentBuffer].mhBuffer,
					sizeof (MIDIHDR))) != MMSYSERR_NOERROR)
				{
					MidiErrorMessageBox(mmrRetVal);
					Mid2StreamConverterCleanup();
					return;
				}

				nCurrentBuffer = (nCurrentBuffer + 1) % NUM_STREAM_BUFFERS;
				nEmptyBuffers--;
			}

			break;
		case MOM_POSITIONCB:
			pmh = (MIDIHDR*)(size_t)dwParam1;
			pme = (MIDIEVENT*)(pmh->lpData + pmh->dwOffset);
			if (MIDIEVENT_TYPE(pme->dwEvent) == MIDI_CTRLCHANGE)
			{
#ifdef DEBUGMIDISTREAM
				if (MIDIEVENT_DATA1(pme->dwEvent) == MIDICTRL_VOLUME_LSB)
				{
					CONS_Printf("Got an LSB volume event\n");
					PostMessage(hWndMain, WM_CLOSE, 0, 0); // can't I_Error() here
					break;
				}
#endif
				// this is meant to respond to our own intention, from mid2strm.c
				if (MIDIEVENT_DATA1(pme->dwEvent) != MIDICTRL_VOLUME)
					break;

				// Mask off the channel number and cache the volume data byte
				dwVolCache[MIDIEVENT_CHANNEL(pme->dwEvent)] = MIDIEVENT_VOLUME(pme->dwEvent);
				// call SetChannelVolume() later to adjust MIDI volume control message to our
				// own current volume level.
				PostMessage(hWndMain, WM_MSTREAM_UPDATEVOLUME,
					MIDIEVENT_CHANNEL(pme->dwEvent), 0L);
			}
			break;
		default:
			break;
	}

	return;
}

// ---------------------
// Mid2StreamFreeBuffers
// This function unprepares and frees all our buffers -- something we must
// do to work around a bug in MMYSYSTEM that prevents a device from playing
// back properly unless it is closed and reopened after each stop.
// ---------------------
static void Mid2StreamFreeBuffers(void)
{
	DWORD idx;
	MMRESULT mmrRetVal;

	if (bBuffersPrepared)
	{
		for (idx = 0; idx < NUM_STREAM_BUFFERS; idx++)
		{
			if ((mmrRetVal = midiOutUnprepareHeader((HMIDIOUT)hStream,
			   &ciStreamBuffers[idx].mhBuffer, sizeof (MIDIHDR))) != MMSYSERR_NOERROR)
			{
				MidiErrorMessageBox(mmrRetVal);
			}
		}
		bBuffersPrepared = FALSE;
	}

	// Don't free the stream buffers here, but rather allocate them once at startup,
	// and free them at shutdown.
}
