/*==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/>.

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