/*==LICENSE==*

CyanWorlds.com Engine - MMOG client, server and tools
Copyright (C) 2011 Cyan Worlds, Inc.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

Additional permissions under GNU GPL version 3 section 7

If you modify this Program, or any covered work, by linking or
combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK,
NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent
JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK
(or a modified version of those libraries),
containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA,
PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG
JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the
licensors of this Program grant you additional
permission to convey the resulting work. Corresponding Source for a
non-source form of such a combination shall include the source code for
the parts of OpenSSL and IJG JPEG Library used as well as that of the covered
work.

You can contact Cyan Worlds, Inc. by email legal@cyan.com
 or by snail mail at:
      Cyan Worlds, Inc.
      14617 N Newport Hwy
      Mead, WA   99021

*==LICENSE==*/
#include "HeadSpin.h"
#include "al.h"
#include "alc.h"
#include "efx.h"
#include <MMREG.H>
#ifdef EAX_SDK_AVAILABLE
#include <eax.h>
#endif

#include "hsTimer.h"
#include "hsGeometry3.h"
#include "plgDispatch.h"
#include "plProfile.h"
#include "../plStatusLog/plStatusLog.h"

#include "plSound.h"
#include "plAudioCaps.h"
#include "plAudioSystem.h"
#include "plDSoundBuffer.h"
#include "plEAXEffects.h"
#include "plEAXListenerMod.h"
#include "plVoiceChat.h"

#include "../pnMessage/plAudioSysMsg.h"
#include "../plMessage/plRenderMsg.h"
#include "../pnMessage/plRefMsg.h"
#include "../plMessage/plAgeLoadedMsg.h"
#include "../pnMessage/plTimeMsg.h"

#include "../pnKeyedObject/plFixedKey.h"
#include "../pnKeyedObject/plKey.h"


#define SAFE_RELEASE(p) if(p){ p->Release(); p = nil; }
#define FADE_TIME	3
#define MAX_NUM_SOURCES 128
#define UPDATE_TIME_MS 100

plProfile_CreateTimer("EAX Update", "Sound", SoundEAXUpdate);
plProfile_CreateTimer("Soft Update", "Sound", SoundSoftUpdate);
plProfile_CreateCounter("Max Sounds", "Sound", SoundMaxNum);
plProfile_CreateTimer("AudioUpdate", "RenderSetup", AudioUpdate);

typedef std::vector<DeviceDescriptor>::iterator DeviceIter;

//// Internal plSoftSoundNode Class Definition ///////////////////////////////
class plSoftSoundNode
{
	public:
		const plKey	fSoundKey;
		hsScalar	fRank;

		plSoftSoundNode	*fNext;
		plSoftSoundNode	**fPrev;

		plSoftSoundNode	*fSortNext;
		plSoftSoundNode	**fSortPrev;

		plSoftSoundNode( const plKey s ) : fSoundKey( s ) { fNext = nil; fPrev = nil; }
		~plSoftSoundNode() { Unlink(); }

		void	Link( plSoftSoundNode **prev )
		{
			fNext = *prev;
			fPrev = prev;
			if( fNext != nil )
				fNext->fPrev = &fNext;
			*prev = this;
		}

		void	Unlink( void )
		{
			if( fPrev != nil )
			{
				*fPrev = fNext;
				if( fNext )
					fNext->fPrev = fPrev;
				fPrev = nil;
				fNext = nil;
			}
		}

		void	SortedLink( plSoftSoundNode **prev, hsScalar rank )
		{
			fRank = rank;

			fSortNext = *prev;
			fSortPrev = prev;
			if( fSortNext != nil )
				fSortNext->fSortPrev = &fSortNext;
			*prev = this;
		}

		// Larger values are first in the list
		void	AddToSortedLink( plSoftSoundNode *toAdd, hsScalar rank )
		{
			if( fRank > rank )
			{
				if( fSortNext != nil )
					fSortNext->AddToSortedLink( toAdd, rank );
				else
				{
					toAdd->SortedLink( &fSortNext, rank );
				}
			}
			else
			{
				// Cute trick here...
				toAdd->SortedLink( fSortPrev, rank );
			}
		}

		void	BootSourceOff( void )
		{
			plSound	*sound = plSound::ConvertNoRef( fSoundKey->ObjectIsLoaded() );
			if( sound != nil )
			{
				sound->ForceUnregisterFromAudioSys();
			}
		}
};

// plAudioSystem //////////////////////////////////////////////////////////////////////////

Int32	plAudioSystem::fMaxNumSounds = 16;
Int32	plAudioSystem::fNumSoundsSlop = 8;

plAudioSystem::plAudioSystem() :
fStartTime(0),
fListenerInit(false),
fSoftRegionSounds(nil),
fActiveSofts(nil),
fCurrDebugSound(nil),
fDebugActiveSoundDisplay(nil),
fUsingEAX(false),
fRestartOnDestruct(false),
fWaitingForShutdown(false),
fActive(false),
fDisplayNumBuffers(false),
fStartFade(0),
fFadeLength(FADE_TIME),
fCaptureDevice(nil),
fLastUpdateTimeMs(0)
{
	fCurrListenerPos.Set( -1.e30, -1.e30, -1.e30 );
	//fCommittedListenerPos.Set( -1.e30, -1.e30, -1.e30 );
	fLastPos.Set(100, 100, 100);
}

plAudioSystem::~plAudioSystem()
{
}

void plAudioSystem::IEnumerateDevices()
{
	fDeviceList.clear();
	plStatusLog::AddLineS("audio.log", "--Audio Devices --" );
	char *devices = (char *)alcGetString(nil, ALC_DEVICE_SPECIFIER);
	int major, minor;

	while(*devices != nil)
	{
		ALCdevice *device = alcOpenDevice(devices);
		if (device) 
		{
			ALCcontext *context = alcCreateContext(device, NULL);
			if (context)
			{
				alcMakeContextCurrent(context);
				// if new actual device name isn't already in the list, then add it...
				bool bNewName = true;
				for (DeviceIter i = fDeviceList.begin(); i != fDeviceList.end(); i++) 
				{
					if (strcmp((*i).GetDeviceName(), devices) == 0) 
					{
						bNewName = false;
					}
				}
				if ((bNewName)) 
				{
					alcGetIntegerv(device, ALC_MAJOR_VERSION, sizeof(int), &major);
					alcGetIntegerv(device, ALC_MINOR_VERSION, sizeof(int), &minor);
					plStatusLog::AddLineS("audio.log", "%s OpenAL ver: %d.%d", devices, major, minor );

					// filter out any devices that aren't openal 1.1 compliant
					if(major > 1 || (major == 1 && minor >= 1))
					{
						hsBool supportsEAX = false;
#ifdef EAX_SDK_AVAILABLE
						if(alIsExtensionPresent((ALchar *)"EAX4.0") || alIsExtensionPresent((ALchar *) "EAX4.0Emulated"))		
						{
							supportsEAX = true;
						}
#endif
						DeviceDescriptor desc(devices, supportsEAX);
						fDeviceList.push_back(desc);
					}
				}
				alcMakeContextCurrent(nil);
				alcDestroyContext(context);
			}
			alcCloseDevice(device);
		}
		devices += strlen(devices) + 1;
	}

	DeviceDescriptor temp("", 0);
	// attempt to order devices
	for(unsigned i = 0; i < fDeviceList.size(); ++i)
	{
		if(strstr(fDeviceList[i].GetDeviceName(), "Software"))
		{
			temp = fDeviceList[i];
			fDeviceList[i] = fDeviceList[0];
			fDeviceList[0] = temp;
		}
		if(strstr(fDeviceList[i].GetDeviceName(), "Hardware"))
		{	
			temp = fDeviceList[i];
			fDeviceList[i] = fDeviceList[1];
			fDeviceList[1] = temp;
		}
	}
}

//// Init ////////////////////////////////////////////////////////////////////
hsBool	plAudioSystem::Init( hsWindowHndl hWnd )
{
	plgAudioSys::fRestarting = false;
	static hsBool firstTimeInit = true;	
	plStatusLog::AddLineS( "audio.log", plStatusLog::kBlue, "ASYS: -- Init --" );
	
	fMaxNumSources = 0;
	plStatusLog::AddLineS( "audio.log", plStatusLog::kGreen, "ASYS: Detecting caps..." );
	plSoundBuffer::Init();

	// Set the maximum number of sounds based on priority cutoff slider
	SetMaxNumberOfActiveSounds();
	const char *deviceName = plgAudioSys::fDeviceName.c_str();
	hsBool useDefaultDevice = true;
	
	if(firstTimeInit)
	{
		IEnumerateDevices();
		firstTimeInit = false;
	}

	if(!fDeviceList.size())
	{
		plStatusLog::AddLineS( "audio.log", plStatusLog::kRed, "ASYS: ERROR Unable to query any devices, is openal installed?" );
		return false;
	}

	// shouldn't ever happen, but just in case
	if(!deviceName)
		plgAudioSys::SetDeviceName(DEFAULT_AUDIO_DEVICE_NAME);

	for(DeviceIter i = fDeviceList.begin(); i != fDeviceList.end(); i++)
	{
		if(!strcmp((*i).GetDeviceName(), deviceName))
		{
			useDefaultDevice = false;
			break;
		}
	}
	
	if(useDefaultDevice)
	{
		// if no device has been specified we will use the "Generic Software" device by default. If "Generic Software" is unavailable(which can happen) we select the first available device
		// We want to use software by default since some audio cards have major problems with hardware + eax.
		const char *defaultDev = fDeviceList.front().GetDeviceName();
		for(DeviceIter i = fDeviceList.begin(); i != fDeviceList.end(); i++)
		{
			if(!strcmp(DEFAULT_AUDIO_DEVICE_NAME, (*i).GetDeviceName()))
			{
				defaultDev = DEFAULT_AUDIO_DEVICE_NAME;
				break;
			}
		}

		plgAudioSys::SetDeviceName(defaultDev, false);
		fDevice = alcOpenDevice(defaultDev);
		plStatusLog::AddLineS( "audio.log", plStatusLog::kRed, "ASYS: %s device selected", defaultDev );
		deviceName = defaultDev;
	}
	else
	{
		plgAudioSys::SetDeviceName(deviceName, false);
		fDevice = alcOpenDevice(deviceName);
		plStatusLog::AddLineS( "audio.log", plStatusLog::kRed, "ASYS: %s device selected", deviceName );
	}
	if(!fDevice)
	{
		plStatusLog::AddLineS( "audio.log", plStatusLog::kRed, "ASYS: ERROR initializing OpenAL" );
		return false;
	}
	
	fContext = alcCreateContext(fDevice, 0);
	alcMakeContextCurrent(fContext);

	ALenum error;
	if(alGetError() != AL_NO_ERROR)
	{
		plStatusLog::AddLineS( "audio.log", plStatusLog::kRed, "ASYS: ERROR alcMakeContextCurrent failed" );
		return false;
	}

	plStatusLog::AddLineS("audio.log", "OpenAL vendor: %s",     alGetString(AL_VENDOR));
	plStatusLog::AddLineS("audio.log", "OpenAL version: %s",    alGetString(AL_VERSION));
	plStatusLog::AddLineS("audio.log", "OpenAL renderer: %s",   alGetString(AL_RENDERER));
	plStatusLog::AddLineS("audio.log", "OpenAL extensions: %s", alGetString(AL_EXTENSIONS));
	plAudioCaps caps = plAudioCapsDetector::Detect();

	if(strcmp(deviceName, DEFAULT_AUDIO_DEVICE_NAME))		
	{
		// we are using a hardware device, set priority based on number of hardware voices
		unsigned int numVoices = caps.GetMaxNumVoices();
		
		if(numVoices < 16)
			plgAudioSys::SetPriorityCutoff(3);
		
		SetMaxNumberOfActiveSounds();
	}

	// setup capture device
	ALCsizei bufferSize = FREQUENCY * 2 * BUFFER_LEN_SECONDS;	// times 2 for 16-bit format
	//const char *dev = alcGetString(fDevice, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER);
	fCaptureDevice = alcCaptureOpenDevice(nil, FREQUENCY, AL_FORMAT_MONO16, bufferSize);
 
	fMaxNumSources = caps.GetMaxNumVoices();

	// attempt to init the EAX listener. 
	if( plgAudioSys::fEnableEAX )
	{
		fUsingEAX = plEAXListener::GetInstance().Init();
		if( fUsingEAX )
		{
			plStatusLog::AddLineS( "audio.log", plStatusLog::kGreen, "ASYS: EAX support detected and enabled." );
		}
		else
		{
			plStatusLog::AddLineS( "audio.log", plStatusLog::kRed, "ASYS: EAX support NOT detected. EAX effects disabled." );
		}
	}
	else
		fUsingEAX = false;
	
	plProfile_Set(SoundMaxNum, fMaxNumSounds);

	alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED);	
	
	error = alGetError();

	fWaitingForShutdown = false;
	plgDispatch::Dispatch()->RegisterForExactType( plAgeLoadedMsg::Index(), GetKey() );
	return true;
}

//// Shutdown ////////////////////////////////////////////////////////////////

void plAudioSystem::Shutdown()
{
	plStatusLog::AddLineS( "audio.log", plStatusLog::kBlue, "ASYS: -- Shutdown --" );

	plSoundBuffer::Shutdown();

	// Delete our active sounds list
	delete fDebugActiveSoundDisplay;
	fDebugActiveSoundDisplay = nil;

	// Unregister soft sounds
	while( fSoftRegionSounds != nil )
	{
		fSoftRegionSounds->BootSourceOff();
		delete fSoftRegionSounds;
	}
	while( fActiveSofts != nil )
	{
		fActiveSofts->BootSourceOff();
		delete fActiveSofts;
	}

	while( fEAXRegions.GetCount() > 0 )
	{
		if( fEAXRegions[ 0 ] != nil && fEAXRegions[ 0 ]->GetKey() != nil )
		{
			GetKey()->Release( fEAXRegions[ 0 ]->GetKey() );
		}
		fEAXRegions.Remove( 0 );
	}
	plEAXListener::GetInstance().ClearProcessCache();

	plSound::SetCurrDebugPlate( nil );
	fCurrDebugSound = nil;

	// Reset this, just in case
	fPendingRegisters.Reset();

	//fListenerInit = false;
	
	if( fUsingEAX )
	{
		plEAXListener::GetInstance().Shutdown();
	}

	alcCaptureStop(fCaptureDevice);
	alcCaptureCloseDevice(fCaptureDevice);
	fCaptureDevice = nil;

	alcMakeContextCurrent(nil);
	alcDestroyContext(fContext);
	alcCloseDevice(fDevice);
	fContext = nil;
	fDevice = nil;

	fStartTime = 0;
	fUsingEAX = false;
	fCurrListenerPos.Set( -1.e30, -1.e30, -1.e30 );
	//fCommittedListenerPos.Set( -1.e30, -1.e30, -1.e30 );
	
	if( fRestartOnDestruct )
	{
		fRestartOnDestruct = false;
		plgAudioSys::Activate( true );
	}

	plgDispatch::Dispatch()->UnRegisterForExactType(plAgeLoadedMsg::Index(), GetKey() );
	fWaitingForShutdown = false;
}

int plAudioSystem::GetNumAudioDevices()
{
	return fDeviceList.size();
}

const char *plAudioSystem::GetAudioDeviceName(int index)
{
	if(index < 0 || index >= fDeviceList.size())
	{
		hsAssert(false, "Invalid index passed to GetAudioDeviceName");
		return nil;
	}
	return fDeviceList[index].GetDeviceName();
}

hsBool plAudioSystem::SupportsEAX(const char *deviceName)
{
	for(DeviceIter i = fDeviceList.begin(); i != fDeviceList.end(); i++)
	{
		if(!strcmp((*i).GetDeviceName(), deviceName))
		{
			return (*i).SupportsEAX();
		}
	}
	return false;
}

void plAudioSystem::SetDistanceModel(int i)
{
	switch(i)
	{
		case 0: alDistanceModel(AL_NONE); break;
		case 1: alDistanceModel(AL_INVERSE_DISTANCE ); break;
		case 2: alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); break;
		case 3: alDistanceModel(AL_LINEAR_DISTANCE ); break;
		case 4: alDistanceModel(AL_LINEAR_DISTANCE_CLAMPED ); break;
		case 5: alDistanceModel(AL_EXPONENT_DISTANCE ); break;
		case 6: alDistanceModel(AL_EXPONENT_DISTANCE_CLAMPED); break;
	}
}

// Set the number of active sounds the audio system is allowed to play, based on the priority cutoff
void plAudioSystem::SetMaxNumberOfActiveSounds()
{
	UInt16 priorityCutoff = plgAudioSys::GetPriorityCutoff();
	int maxNumSounds = 24;
	
	// Keep this to a reasonable amount based on the users hardware, since we want the sounds to be played in hardware
	if(maxNumSounds > fMaxNumSources && fMaxNumSources != 0 )
		maxNumSounds = fMaxNumSources / 2;

	// Allow a certain number of sounds based on a user specified setting.
	// Above that limit, we want 1/2 more sounds (8 for the max of 16) for a slop buffer, 
	// if they fit, so that sounds that shouldn't be playing can still play in case they suddenly pop
	// back into priority range (so we don't incur performance hits when sounds hover on
	// the edge of being high enough priority to play)
	fMaxNumSounds = maxNumSounds;
	fNumSoundsSlop = fMaxNumSounds / 2;

	plStatusLog::AddLineS( "audio.log", "Max Number of Sounds Set to: %d", fMaxNumSounds);
}

void plAudioSystem::SetListenerPos(const hsPoint3 pos)
{
	fCurrListenerPos = pos;
	alListener3f(AL_POSITION, pos.fX, pos.fZ, -pos.fY);		// negate z coord, since openal uses opposite handedness
}

void plAudioSystem::SetListenerVelocity(const hsVector3 vel)
{
	alListener3f(AL_VELOCITY, 0, 0, 0);	// no doppler shift
}

void plAudioSystem::SetListenerOrientation(const hsVector3 view, const hsVector3 up)
{
	ALfloat orientation[] = { view.fX, view.fZ, -view.fY, up.fX, up.fZ, -up.fY };
	alListenerfv(AL_ORIENTATION, orientation);
}

void	plAudioSystem::SetActive( hsBool b ) 
{
	fActive = b;
	if( fActive )
	{
		// Clear to send activate message (if listener not inited yet, delay until then)
		plgDispatch::MsgSend( TRACKED_NEW plAudioSysMsg( plAudioSysMsg::kActivate ) );
	}
}

//// IRegisterSoftSound //////////////////////////////////////////////////////
//	Note: each sound might kick another off the top-n-ranked-sounds list, so
//	we call IUpdateSoftSounds() each time one is added. Ugly as sin, but at
//	least all the calc code is in one place. Possible optimization in the
//	future: when calling IUpdate(), any sounds that are already active don't
//	need to be recalced, just resorted.
void	plAudioSystem::RegisterSoftSound( const plKey soundKey )
{
	plSoftSoundNode	*node = TRACKED_NEW plSoftSoundNode( soundKey );
	node->Link( &fSoftRegionSounds );

	fCurrDebugSound = nil;
	plSound::SetCurrDebugPlate( nil );
}

//// IUnregisterSoftSound ////////////////////////////////////////////////////

void	plAudioSystem::UnregisterSoftSound( const plKey soundKey )
{
	plSoftSoundNode	*node;
	for( node = fActiveSofts; node != nil; node = node->fNext )
	{
		if( node->fSoundKey == soundKey )
		{
			delete node;
			return;
		}
	}

	for( node = fSoftRegionSounds; node != nil; node = node->fNext )
	{
		if( node->fSoundKey == soundKey )
		{
			delete node;
			return;
		}
	}

	// We might have unregistered it ourselves on destruction, so don't bother

	fCurrDebugSound = nil;
	plSound::SetCurrDebugPlate( nil );
}

//// IUpdateSoftSounds ///////////////////////////////////////////////////////
//	OK, so the listener moved. Since our sound soft volumes are all based on 
//	the listener's position, we have to update all sounds with soft volumes 
//	now. This involves some steps:
//		- Determining what volumes the listener has moved out of
//		- Determining what volumes the listener has changed position in
//		- Determining what volumes the listener has entered
//		- Updating the levels of all sounds associated with all of the above 
//			volumes
//	The first two tests are easy, since we'll have kept track of what volumes 
//	the listener was in last time. The last part is the tricky one and the 
//	one that will kill us in performance if we're not careful. However, we 
//	can first check the bounding box of the sounds in question, since outside
//	of them the soft volume won't have any effect. The last part is simply a 
//	matter of querying the sound volumes based on the listener's position and
//	setting the soft volume attenuation on the sound to that level.
//
//	Note: for each sound that is still in range, we call CalcSoftVolume() and
//	use the resulting value to rank each sound. Then, we take the top n sounds
//	and disable the rest. We *could* rank by distance to listener, which would
//	be far faster; however, we could have a sound (or background music) that
//	is technically closest to the listener but completely faded by a soft volume,
//	thus needlessly cutting off another sound that might be more important.
//	This way is slower, but better quality.
//	Also note: to differentiate between two sounds in the same soft volume at
//	full strength, we divide the soft volume ranks by the distSquared, thus
//	giving us a reasonable approximation at a good rank...<sigh>

void	plAudioSystem::IUpdateSoftSounds( const hsPoint3 &newPosition )
{
	plSoftSoundNode	*node, *myNode;
	hsScalar		distSquared, rank;
	plSoftSoundNode	*sortedList = nil;
	Int32			i;
	
	plProfile_BeginTiming(SoundSoftUpdate);
	
	// Check the sounds the listener is already inside of. If we moved out, stop sound, else
	// just change attenuation
	for( node = fActiveSofts; node != nil; )
	{
		plSound	*sound = plSound::ConvertNoRef( node->fSoundKey->ObjectIsLoaded() );

		bool notActive = false;
		if(sound)
			sound->Update();
		
		// Quick checks for not-active
		if( sound == nil )
			notActive = true;
		else if( !sound->IsWithinRange( newPosition, &distSquared )  )
			notActive = true;
		else if(sound->GetPriority() > plgAudioSys::GetPriorityCutoff())
			notActive = true;

		if(plgAudioSys::fMutedStateChange)
		{
			sound->SetMuted(plgAudioSys::fMuted);
		}
		
		if( !notActive )
		{
			/// Our initial guess is that it's enabled...
			sound->CalcSoftVolume( true, distSquared );
			rank = sound->GetVolumeRank();
			if( rank <= 0.f )
				notActive = true;
			else
			{
				/// Queue up in our sorted list...
				if( sortedList == nil )
					node->SortedLink( &sortedList, (10.0f - sound->GetPriority()) * rank );
				else
					sortedList->AddToSortedLink( node, (10.0f - sound->GetPriority()) * rank );
				/// Still in radius, so consider it still "active". 
				node = node->fNext;
			}
		}

		if( notActive )
		{
			/// Moved out of range of the sound--stop the sound entirely and move it to our
			/// yeah-they're-registered-but-not-active list
			myNode = node;
			node = node->fNext;
			myNode->Unlink();
			myNode->Link( &fSoftRegionSounds );

			/// We know this sound won't be enabled, so skip the Calc() call
			if( sound != nil )
				sound->UpdateSoftVolume( false );
		}
	}
	
	// Now check remaining sounds to see if the listener moved into them
	for( node = fSoftRegionSounds; node != nil; )
	{
		if( !fListenerInit )
		{
			node = node->fNext;
			continue;
		}
	
		plSound	*sound = plSound::ConvertNoRef( node->fSoundKey->ObjectIsLoaded() ); 
		if( !sound || sound->GetPriority() > plgAudioSys::GetPriorityCutoff() )
		{	
			node = node->fNext;
			continue;
		}

		sound->Update();
		if(plgAudioSys::fMutedStateChange)
		{
			sound->SetMuted(plgAudioSys::fMuted);
		}
		
		if( sound->IsWithinRange( newPosition, &distSquared ) )
		{
			/// Our initial guess is that it's enabled...
			sound->CalcSoftVolume( true, distSquared );
			rank = sound->GetVolumeRank();

			if( rank > 0.f )
			{			
				/// We just moved into its range, so move it to our active list and start the sucker
				myNode = node;
				node = node->fNext;
				myNode->Unlink();
				myNode->Link( &fActiveSofts );

				/// Queue up in our sorted list...
				if( sortedList == nil )
					myNode->SortedLink( &sortedList, (10.0f - sound->GetPriority()) * rank );
				else
					sortedList->AddToSortedLink( myNode, (10.0f - sound->GetPriority()) * rank );
			}
			else
			{
				/// Do NOT notify sound, since we were outside of its range and still are
				// (but if we're playing, we shouldn't be, so better update)
				if( sound->IsPlaying() )
					sound->UpdateSoftVolume( false );

				node = node->fNext;
			}
		}
		else
		{
			/// Do NOT notify sound, since we were outside of its range and still are
			node = node->fNext;		
			sound->Disable();		// ensure that dist attenuation is set to zero so we don't accidentally play
		}
	}
	
	plgAudioSys::fMutedStateChange = false;
	/// Go through sorted list, enabling only the first n sounds and disabling the rest
	// DEBUG: Create a screen-only statusLog to display which sounds are what
	if( fDebugActiveSoundDisplay == nil )
		fDebugActiveSoundDisplay = plStatusLogMgr::GetInstance().CreateStatusLog( 32, "Active Sounds", plStatusLog::kDontWriteFile | plStatusLog::kDeleteForMe | plStatusLog::kFilledBackground );
	fDebugActiveSoundDisplay->Clear();

	if(fDisplayNumBuffers)
		fDebugActiveSoundDisplay->AddLineF(0xffffffff, "Num Buffers: %d", plDSoundBuffer::GetNumBuffers() );
	fDebugActiveSoundDisplay->AddLine("Not streamed", plStatusLog::kGreen);
	fDebugActiveSoundDisplay->AddLine("Disk streamed", plStatusLog::kYellow);
	fDebugActiveSoundDisplay->AddLine("RAM streamed", plStatusLog::kWhite);
	fDebugActiveSoundDisplay->AddLine("Ogg streamed", plStatusLog::kRed);
	fDebugActiveSoundDisplay->AddLine("Incidentals", 0xff00ffff);
	fDebugActiveSoundDisplay->AddLine("--------------------");
	
	for( i = 0; sortedList != nil && i < fMaxNumSounds; sortedList = sortedList->fSortNext )
	{
		plSound	*sound = plSound::ConvertNoRef( sortedList->fSoundKey->GetObjectPtr() );
		if(!sound) continue;
	
		/// Notify sound that it really is still enabled
		sound->UpdateSoftVolume( true );
		
		UInt32 color = plStatusLog::kGreen;
		switch (sound->GetStreamType())
		{
			case plSound::kStreamFromDisk:		color = plStatusLog::kYellow;	break;
			case plSound::kStreamFromRAM:		color = plStatusLog::kWhite;	break;
			case plSound::kStreamCompressed:	color = plStatusLog::kRed;		break;
		}
		if(sound->GetType() == plgAudioSys::kVoice) color = 0xffff8800;
		if(sound->IsPropertySet(plSound::kPropIncidental)) color = 0xff00ffff;
		
		if( fUsingEAX && sound->GetEAXSettings().IsEnabled() )
		{
			fDebugActiveSoundDisplay->AddLineF(
				color, 
				"%d %1.2f %1.2f (%d occ) %s",
				sound->GetPriority(), 
				sortedList->fRank, 
				sound->GetVolume() ? sound->GetVolumeRank() / sound->GetVolume() : 0, 
				sound->GetEAXSettings().GetCurrSofts().GetOcclusion(), 
				sound->GetKeyName()
			);
		}
		else 
		{
			fDebugActiveSoundDisplay->AddLineF(
				color, 
				"%d %1.2f %1.2f %s", 
				sound->GetPriority(), 
				sortedList->fRank, 
				sound->GetVolume() ? sound->GetVolumeRank() / sound->GetVolume() : 0, 
				sound->GetKeyName()
			);
		}
		i++;
	}

	for( ; sortedList != nil; sortedList = sortedList->fSortNext, i++ )
	{
		plSound	*sound = plSound::ConvertNoRef( sortedList->fSoundKey->GetObjectPtr() );
		if(!sound) continue;

		/// These unlucky sounds don't get to play (yet). Also, be extra mean
		/// and pretend we're updating for "the first time", which will force them to
		/// stop immediately
		// Update: since being extra mean can incur a nasty performance hit when sounds hover back and
		// forth around the fMaxNumSounds mark, we have a "slop" allowance: i.e. sounds that we're going
		// to say shouldn't be playing but we'll let them play for a bit anyway just in case they raise
		// in priority. So only be mean to the sounds outside this slop range
		sound->UpdateSoftVolume( false, ( i < fMaxNumSounds + fNumSoundsSlop ) ? false : true );
		fDebugActiveSoundDisplay->AddLineF(
			0xff808080,
			"%d %1.2f %s", 
			sound->GetPriority(), 
			sound->GetVolume() ? sound->GetVolumeRank() / sound->GetVolume() : 0, 
			sound->GetKeyName()
		);
	}
	
	plProfile_EndTiming(SoundSoftUpdate);
}

void	plAudioSystem::NextDebugSound( void )
{
	plSoftSoundNode		*node;

	if( fCurrDebugSound == nil )
		fCurrDebugSound = ( fSoftRegionSounds == nil ) ? fActiveSofts : fSoftRegionSounds;
	else
	{
		node = fCurrDebugSound;
		fCurrDebugSound = fCurrDebugSound->fNext;
		if( fCurrDebugSound == nil )
		{
			// Trace back to find which list we were in
			for( fCurrDebugSound = fSoftRegionSounds; fCurrDebugSound != nil; fCurrDebugSound = fCurrDebugSound->fNext )
			{
				if( fCurrDebugSound == node )	// Was in first list, move to 2nd
				{
					fCurrDebugSound = fActiveSofts;		
					break;
				}
			}
			// else Must've been in 2nd list, so keep nil
		}
	}

	if( fCurrDebugSound != nil )
		plSound::SetCurrDebugPlate( fCurrDebugSound->fSoundKey );
	else
		plSound::SetCurrDebugPlate( nil );
}

void plAudioSystem::SetFadeLength(float lengthSec)
{
	fFadeLength = lengthSec;
}
	
hsBool plAudioSystem::MsgReceive(plMessage* msg)
{
	if(plTimeMsg *time = plTimeMsg::ConvertNoRef( msg ) )
	{
		if(!plgAudioSys::IsMuted())
		{
			double currTime = hsTimer::GetSeconds();
			if(fStartFade == 0)
			{
				plStatusLog::AddLineS("audio.log", "Starting Fade %f", currTime);
			}
			if((currTime - fStartFade) > fFadeLength) 
			{
				fStartFade = 0;
				plgDispatch::Dispatch()->UnRegisterForExactType( plTimeMsg::Index(), GetKey() );
				plStatusLog::AddLineS("audio.log", "Stopping Fade %f", currTime);
				plgAudioSys::SetGlobalFadeVolume( 1.0 ); 
			}
			else
			{
				plgAudioSys::SetGlobalFadeVolume( (hsScalar)((currTime-fStartFade) / fFadeLength) );
			}
		}
		return true;
	}
	
	if (plAudioSysMsg* pASMsg = plAudioSysMsg::ConvertNoRef( msg ))
	{
		if (pASMsg->GetAudFlag() == plAudioSysMsg::kPing && fListenerInit)
		{
			plAudioSysMsg* pMsg = TRACKED_NEW plAudioSysMsg( plAudioSysMsg::kActivate );
			pMsg->AddReceiver( pASMsg->GetSender() );
			pMsg->SetBCastFlag(plMessage::kBCastByExactType, false);
			plgDispatch::MsgSend( pMsg );
			return true;
		}
		else if (pASMsg->GetAudFlag() == plAudioSysMsg::kSetVol)
		{
			return true;
		}
		else if( pASMsg->GetAudFlag() == plAudioSysMsg::kDestroy )
		{
			if( fWaitingForShutdown )
				Shutdown();
			return true;
		}
		else if( pASMsg->GetAudFlag() == plAudioSysMsg::kUnmuteAll )
		{
			if( !pASMsg->HasBCastFlag( plMessage::kBCastByExactType ) )
				plgAudioSys::SetMuted( false );
			return true;
		}
	}
	
	if(plRenderMsg* pRMsg = plRenderMsg::ConvertNoRef( msg ))
	{
		//if( fListener )
		{
			plProfile_BeginLap(AudioUpdate, this->GetKey()->GetUoid().GetObjectName());
			if(hsTimer::GetMilliSeconds() - fLastUpdateTimeMs > UPDATE_TIME_MS)
			{
				IUpdateSoftSounds( fCurrListenerPos );

				if( fUsingEAX )
				{
					plProfile_BeginTiming(SoundEAXUpdate);
					plEAXListener::GetInstance().ProcessMods( fEAXRegions );
					plProfile_EndTiming(SoundEAXUpdate);
				}
				//fCommittedListenerPos = fCurrListenerPos;
			}
			plProfile_EndLap(AudioUpdate, this->GetKey()->GetUoid().GetObjectName());
		}
		
		return true;
	}

	if(plGenRefMsg *refMsg = plGenRefMsg::ConvertNoRef( msg ))
	{
		if( refMsg->GetContext() & ( plRefMsg::kOnCreate | plRefMsg::kOnRequest | plRefMsg::kOnReplace ) )
		{
			fEAXRegions.Append( plEAXListenerMod::ConvertNoRef( refMsg->GetRef() ) );
			plEAXListener::GetInstance().ClearProcessCache();
		}
		else if( refMsg->GetContext() & ( plRefMsg::kOnRemove | plRefMsg::kOnDestroy ) )
		{
			int idx = fEAXRegions.Find( plEAXListenerMod::ConvertNoRef( refMsg->GetRef() ) );
			if( idx != fEAXRegions.kMissingIndex )
				fEAXRegions.Remove( idx );
			plEAXListener::GetInstance().ClearProcessCache();
		}
		return true;
	}
	
	if(plAgeLoadedMsg *pALMsg = plAgeLoadedMsg::ConvertNoRef(msg))
	{
		if(!pALMsg->fLoaded)
		{
			fLastPos = fCurrListenerPos;
			fListenerInit = false;
		}
		else
		{
			fListenerInit = true;
		}
	}

	return hsKeyedObject::MsgReceive(msg);
}

// plgAudioSystem //////////////////////////////////////////////////////////////////////

plAudioSystem*	plgAudioSys::fSys = nil;
hsBool			plgAudioSys::fInit = false;
hsBool			plgAudioSys::fActive = false;
hsBool			plgAudioSys::fUseHardware = false;
hsBool			plgAudioSys::fMuted = true;
hsBool			plgAudioSys::fDelayedActivate = false;
hsBool			plgAudioSys::fEnableEAX = false;
hsWindowHndl	plgAudioSys::fWnd = nil;
hsScalar		plgAudioSys::fChannelVolumes[ kNumChannels ] = { 1.f, 1.f, 1.f, 1.f, 1.f, 1.f };
hsScalar		plgAudioSys::f2D3DBias = 0.75f;
UInt32			plgAudioSys::fDebugFlags = 0;
hsScalar		plgAudioSys::fStreamingBufferSize = 2.f;
hsScalar		plgAudioSys::fStreamFromRAMCutoff = 10.f;
UInt8			plgAudioSys::fPriorityCutoff = 9;			// We cut off sounds above this priority
hsBool			plgAudioSys::fEnableExtendedLogs = false;
hsScalar		plgAudioSys::fGlobalFadeVolume = 1.f;
hsBool			plgAudioSys::fLogStreamingUpdates = false;
std::string 	plgAudioSys::fDeviceName;
hsBool			plgAudioSys::fRestarting = false;
hsBool			plgAudioSys::fMutedStateChange = false;

void plgAudioSys::Init(hsWindowHndl hWnd)
{
	fSys = TRACKED_NEW plAudioSystem;
	fSys->RegisterAs( kAudioSystem_KEY );
	plgDispatch::Dispatch()->RegisterForExactType( plAudioSysMsg::Index(), fSys->GetKey() );
	plgDispatch::Dispatch()->RegisterForExactType( plRenderMsg::Index(), fSys->GetKey() );

	if(hsPhysicalMemory() <= 380)	
	{
		plStatusLog::AddLineS("audio.log", "StreamFromRam Disabled");
		fStreamFromRAMCutoff = 4;
	}
	fWnd = hWnd;

	if(fMuted)
		SetGlobalFadeVolume(0.0f);

	if( fDelayedActivate )
		Activate( true );
}

void plgAudioSys::SetActive(hsBool b)
{
	fActive = b;
}

void plgAudioSys::SetMuted( hsBool b )
{
	fMuted = b;
	fMutedStateChange = true;

	if(fMuted)
		SetGlobalFadeVolume(0.0f);
	else
		SetGlobalFadeVolume(1.0);
}

void plgAudioSys::SetUseHardware(hsBool b)
{
	fUseHardware = b;
	if( fActive )
		Restart();
}

void plgAudioSys::EnableEAX( hsBool b )
{
	fEnableEAX = b;
	if( fActive )
		Restart();
}

void plgAudioSys::SetAudioMode(AudioMode mode)
{
	if(mode == kDisabled)
	{
		Activate(false);
		return;
	}
	else if(mode == kSoftware)
	{
		fActive = true;
		fUseHardware = false;
		fEnableEAX = false;
	}
	else if(mode == kHardware)
	{
		fActive = true;
		fUseHardware = true;
		fEnableEAX = false;
	}
	else if(mode == kHardwarePlusEAX)
	{
		fActive = true;
		fUseHardware = true;
		fEnableEAX = true;
	}
	Restart();
}

int plgAudioSys::GetAudioMode()
{
	if (fActive)
	{
        if (fUseHardware)
		{
            if (fEnableEAX)
			{
				return kHardwarePlusEAX;
			}
			else
			{
				return kHardware;
			}
		}
		else
		{
			return kSoftware;
		}
	}
	else
	{
		return kDisabled;
	}
}

void plgAudioSys::Restart( void )
{
	if( fSys )
	{
		fSys->fRestartOnDestruct = true;
		Activate( false );
		fRestarting = true;
	}
}

void plgAudioSys::Shutdown()
{
	Activate( false );
	if( fSys )
	{
		fSys->UnRegisterAs( kAudioSystem_KEY );
	}
}

void plgAudioSys::Activate(hsBool b)
{ 
	if( fSys == nil )
	{
		fDelayedActivate = true;
		return;
	}

	if (b == fInit)
		return;
	if (!fActive)
		return;
	if( b )
	{
		plStatusLog::AddLineS( "audio.log", plStatusLog::kBlue, "ASYS: -- Attempting audio system init --" );
		if( !fSys->Init( fWnd ) )
		{
			// Cannot init audio system. Don't activate
			return; 
		}
		fInit = true;
		fSys->SetActive( true );

		if( !IsMuted() )
		{
			SetMuted( true );
			plAudioSysMsg *msg = TRACKED_NEW plAudioSysMsg( plAudioSysMsg::kUnmuteAll );
			msg->SetTimeStamp( hsTimer::GetSysSeconds() );
			msg->AddReceiver( fSys->GetKey() );
			msg->SetBCastFlag( plMessage::kBCastByExactType, false );
			msg->Send();
		}
		return;
	}

	fSys->SetActive( false );
	
	plStatusLog::AddLineS( "audio.log", plStatusLog::kBlue, "ASYS: -- Sending deactivate/destroy messages --" );
	plgDispatch::MsgSend( TRACKED_NEW plAudioSysMsg( plAudioSysMsg::kDeActivate ) );

	// Send ourselves a shutdown message, so that the deactivates get processed first
	fSys->fWaitingForShutdown = true;
	plAudioSysMsg *msg = TRACKED_NEW plAudioSysMsg( plAudioSysMsg::kDestroy );
	msg->SetBCastFlag( plMessage::kBCastByExactType, false );
	msg->Send( fSys->GetKey() );
//	fSys->Shutdown();

	fInit = false;
}

void	plgAudioSys::SetChannelVolume( ASChannel chan, hsScalar vol )
{
	fChannelVolumes[ chan ] = vol;
}

void	plgAudioSys::SetGlobalFadeVolume( hsScalar vol )
{
	if(!fMuted)
		fGlobalFadeVolume = vol;
	else
		fGlobalFadeVolume = 0;
}

hsScalar	plgAudioSys::GetChannelVolume( ASChannel chan )
{
	return fChannelVolumes[ chan ];
}

void	plgAudioSys::NextDebugSound( void )
{
	fSys->NextDebugSound();
}

void plgAudioSys::Set2D3DBias( hsScalar bias )
{
	f2D3DBias = bias;
}

hsScalar plgAudioSys::Get2D3Dbias()
{
	return f2D3DBias;
}

void plgAudioSys::SetDeviceName(const char *device, hsBool restart /* = false */)
{
	fDeviceName = device;
	if(restart)
		Restart();
}

int plgAudioSys::GetNumAudioDevices()
{
	if(fSys)
		return fSys->GetNumAudioDevices();
	return 0;
}

const char *plgAudioSys::GetAudioDeviceName(int index)
{
	if(fSys)
	{
		return fSys->GetAudioDeviceName(index);
	}
	return nil;
}

ALCdevice *plgAudioSys::GetCaptureDevice()
{
	if(fSys)
	{
		return fSys->fCaptureDevice;
	}
	return nil;
}

hsBool plgAudioSys::SupportsEAX(const char *deviceName)
{
	if(fSys)
	{
		return fSys->SupportsEAX(deviceName);
	}
	return nil;
}

void plgAudioSys::RegisterSoftSound( const plKey soundKey )
{
	if(fSys)
	{
		fSys->RegisterSoftSound(soundKey);
	}
}

void plgAudioSys::UnregisterSoftSound( const plKey soundKey )
{
	if(fSys)
	{
		fSys->UnregisterSoftSound(soundKey);
	}
}

void plgAudioSys::SetListenerPos(const hsPoint3 pos)
{
	if(fSys)
	{
		fSys->SetListenerPos(pos);
	}
}

void plgAudioSys::SetListenerVelocity(const hsVector3 vel)
{
	if(fSys)
	{
		fSys->SetListenerVelocity(vel);
	}
}

void plgAudioSys::SetListenerOrientation(const hsVector3 view, const hsVector3 up)
{
	if(fSys)
	{
		fSys->SetListenerOrientation(view, up);
	}
}