/*==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>
#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;
        float    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, float 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, float 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_t   plAudioSystem::fMaxNumSounds = 16;
int32_t   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_t 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( 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 = 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;
    float        distSquared, rank;
    plSoftSoundNode *sortedList = nil;
    int32_t           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_t 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().c_str()
            );
        }
        else 
        {
            fDebugActiveSoundDisplay->AddLineF(
                color, 
                "%d %1.2f %1.2f %s", 
                sound->GetPriority(), 
                sortedList->fRank, 
                sound->GetVolume() ? sound->GetVolumeRank() / sound->GetVolume() : 0, 
                sound->GetKeyName().c_str()
            );
        }
        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().c_str()
        );
    }
    
    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( (float)((currTime-fStartFade) / fFadeLength) );
            }
        }
        return true;
    }
    
    if (plAudioSysMsg* pASMsg = plAudioSysMsg::ConvertNoRef( msg ))
    {
        if (pASMsg->GetAudFlag() == plAudioSysMsg::kPing && fListenerInit)
        {
            plAudioSysMsg* pMsg = 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().c_str());
            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().c_str());
        }
        
        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;
float        plgAudioSys::fChannelVolumes[ kNumChannels ] = { 1.f, 1.f, 1.f, 1.f, 1.f, 1.f };
float        plgAudioSys::f2D3DBias = 0.75f;
uint32_t          plgAudioSys::fDebugFlags = 0;
float        plgAudioSys::fStreamingBufferSize = 2.f;
float        plgAudioSys::fStreamFromRAMCutoff = 10.f;
uint8_t           plgAudioSys::fPriorityCutoff = 9;           // We cut off sounds above this priority
hsBool          plgAudioSys::fEnableExtendedLogs = false;
float        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 = 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 = 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( new plAudioSysMsg( plAudioSysMsg::kDeActivate ) );

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

    fInit = false;
}

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

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

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

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

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

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