/*==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 .
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 "hsTypes.h"
#include "hsUtils.h"
#include "hsResMgr.h"
#include "hsTimer.h"
#include "hsGeometry3.h"
#include "hsColorRGBA.h"
#include "plProfile.h"
#include "plgDispatch.h"
#include "plAudioSystem.h"
#include "plSound.h"
#include "plWin32Sound.h"
#include "plAudioCore/plSoundBuffer.h"
#include "plDrawable/plDrawableGenerator.h"
#include "pnMessage/plRefMsg.h"
#include "pnMessage/plTimeMsg.h"
#include "pnMessage/plAudioSysMsg.h"
#include "pnMessage/plSoundMsg.h"
#include "plMessage/plListenerMsg.h"
#include "plIntersect/plSoftVolume.h"
#include "plStatusLog/plStatusLog.h"
#include "plPipeline/plPlates.h"
#include "pnKeyedObject/plKey.h"
#include "pnNetCommon/plSDLTypes.h"
#include "plAvatar/plScalarChannel.h"
#include "plAvatar/plAGModifier.h"
#include "pnSceneObject/plSceneObject.h"
#include "pnSceneObject/plAudioInterface.h"
plProfile_CreateCounterNoReset( "Loaded", "Sound", SoundNumLoaded );
plProfile_CreateCounterNoReset( "Waiting to Die", "Sound", WaitingToDie );
plProfile_CreateAsynchTimer( "Sound Load Time", "Sound", SoundLoadTime );
plGraphPlate *plSound::fDebugPlate = nil;
plSound *plSound::fCurrDebugPlateSound = nil;
hsBool plSound::fLoadOnDemandFlag = true;
hsBool plSound::fLoadFromDiskOnDemand = true;
unsigned plSound::fIncidentalsPlaying = 0;
plSound::plSound() :
fPlaying(false),
fActive(false),
fTime(0),
fMaxFalloff(0),
fMinFalloff(0),
fCurrVolume(0.f),
fOuterVol(0),
fOuterCone(360),
fInnerCone(360),
fLength(0.0f),
fDesiredVol(0.f),
fFading(false),
fRegisteredForTime(false),
fMuted(true),
fFadedVolume(0.f),
fSoftRegion(nil),
fSoftOcclusionRegion(nil),
fSoftVolume(0.f),
fCurrFadeParams(nil),
fRegistered(false),
fDistAttenuation(0.f),
fProperties(0),
fNotHighEnoughPriority(false),
fVirtualStartTime(0),
fOwningSceneObject(nil),
fPriority(0),
fType(plSound::kSoundFX),
fQueued(false),
fLoading(false),
fSynchedStartTimeSec(0),
fMaxVolume(0),
fFreeData(false)
{
plProfile_Inc( SoundNumLoaded );
f3DPosition.Set( 0.f, 0.f, 0.f );
f3DVelocity.Set( 0.f, 0.f, 0.f );
fDataBuffer = nil;
fDataBufferKey = nil;
fPlayOnReactivate = false;
fDataBufferLoaded = false;
}
plSound::~plSound()
{
IStopFade( true );
plProfile_Dec( SoundNumLoaded );
}
void plSound::IPrintDbgMessage( const char *msg, hsBool isError )
{
static plStatusLog *ourLog = nil;
// Print to our log file (create it if necessary)
if( ourLog == nil )
{
// ourLog = plStatusLogMgr::GetInstance().CreateStatusLog( 15,
// "audio.log", plStatusLog::kFilledBackground | plStatusLog::kDeleteForMe | plStatusLog::kAlignToTop );
}
if( isError )
// ourLog->AddLineF( plStatusLog::kRed, "ERROR: %s (%s)", msg, GetKey() ? GetKeyName() : "unkeyed" );
ourLog->AddLineS( "audio.log", plStatusLog::kRed, "ERROR: %s (%s)", msg, GetKey() ? GetKeyName() : "unkeyed" );
else
// ourLog->AddLineF( "%s (%s)", msg, GetKey() ? GetKeyName() : "unkeyed" );
ourLog->AddLineS( "audio.log", "%s (%s)", msg, GetKey() ? GetKeyName() : "unkeyed" );
}
///////////////////////////////////////////////////////////
// Called to send more values to the debug plate, assuming this is the right
// sound. Should be called every time any of the values change, which means
// the best place is inside ISetActualVolume(). Since that's a pure virtual,
// it makes the placement of the call a bit annoying, but oh well.
void plSound::IUpdateDebugPlate( void )
{
if( this == fCurrDebugPlateSound )
{
if( fDebugPlate == nil )
{
plPlateManager::Instance().CreateGraphPlate( &fDebugPlate );
fDebugPlate->SetSize( 0.50, 0.25 );
fDebugPlate->SetPosition( -0.5, 0 );
fDebugPlate->SetDataRange( 0, 100, 100 );
fDebugPlate->SetColors( 0x80202000 );
fDebugPlate->SetTitle( (char *)GetKeyName() ); // Bleah
fDebugPlate->SetLabelText( "Desired", "Curr", "Soft", "Dist" );
}
fDebugPlate->SetVisible( true );
fDebugPlate->AddData( (Int32)( fDesiredVol * 100.f ),
(Int32)( fCurrVolume * 100.f ),
(Int32)( fSoftVolume * 100.f ),
(Int32)( fDistAttenuation * 100.f ) );
}
}
void plSound::SetCurrDebugPlate( const plKey soundKey )
{
if( soundKey == nil )
{
fCurrDebugPlateSound = nil;
if( fDebugPlate != nil )
fDebugPlate->SetVisible( false );
}
else
{
fCurrDebugPlateSound = plSound::ConvertNoRef( soundKey->GetObjectPtr() );
if( fDebugPlate != nil )
{
fDebugPlate->ClearData();
fDebugPlate->SetVisible( true );
fDebugPlate->SetTitle( (char *)fCurrDebugPlateSound->GetKeyName() ); // Bleah
}
}
}
/////////////////////////////////////////////////////////////////////
// We don't keep track of the current time we should be at, but rather the
// time we started at. Since we're calling SetTime(), we should adjust the
// start time to be accurate for the time we want to be at now. Note: we
// don't actually move the buffer position unless it's loaded, since we
// don't want to force a load on a buffer just from a SetTime() call.
void plSound::SetTime( double t )
{
fVirtualStartTime = hsTimer::GetSysSeconds() - t;
if( IActuallyLoaded() )
ISetActualTime( t );
}
// Support for Fast forward responder
void plSound::FastForwardPlay()
{
if(fProperties & kPropLooping)
{
Play();
}
}
void plSound::FastForwardToggle()
{
if(fPlaying == true)
{
Stop();
return;
}
FastForwardPlay();
}
////////////////////////////////////////////////////////////////////////
// Our basic play function. Marks the sound as playing, and if we're actually
// allowed to play, will actually start the sound playing as well.
void plSound::Play()
{
if(fLoading) // if we are loading there is no reason to do this. Play will be called, by Update(), once the data is loaded and this floag is set to false
return;
if( !fActive )
{
// We're not active, so we can't play, but mark to make sure we'll play once we do get activated
fPlayOnReactivate = true;
return;
}
fPlaying = true;
if(IPreLoadBuffer(true) == plSoundBuffer::kPending)
{
return;
}
fVirtualStartTime = hsTimer::GetSysSeconds(); // When we "started", even if we really don't start
// if the sound system is not active do a fake play so callbacks get sent
if(!plgAudioSys::Active())
{
// Do the (fake) actual play
IActuallyPlay();
}
if( IWillBeAbleToPlay() )
{
IRefreshParams();
if( fFadeInParams.fLengthInSecs > 0 )
{
IStartFade( &fFadeInParams);
}
else
{
// we're NOT fading!!!!
if( fFading )
IStopFade();
SetVolume( fDesiredVol );
}
// Do the actual play
IActuallyPlay();
}
}
void plSound::SynchedPlay(unsigned bytes )
{
if( fFading )
IStopFade();
if(fLoading) // the sound is loading, it will be played when loading is finished
return;
if( !fActive )
{
// We're not active, so we can't play, but mark to make sure we'll play once we do get activated
fPlayOnReactivate = true;
return;
}
// Mark as playing, since we'll be calling ITryPlay() directly
fPlaying = true;
fPlayOnReactivate = false;
if( IWillBeAbleToPlay() )
{
SetStartPos(bytes);
Play();
}
}
/////////////////////////////////////////////////////////////////
// Used for synching play state. The state only knows that we're playing
// and what time we started at, so we use that to compute what time we should
// be at and update. Note that we also set our virtual start time to what
// we're given, NOT the current time, 'cause, well, duh, that should be our
// start time!
// So think of it as "Play() but act as if you started at *this* time"...
void plSound::SynchedPlay( hsScalar virtualStartTime )
{
if( fFading )
IStopFade();
ISynchedPlay( virtualStartTime );
}
////////////////////////////////////////////////////////////////
// Only want to do the fade hack when somebody outside synch()s us.
void plSound::ISynchedPlay( double virtualStartTime )
{
if(fLoading) // the sound is loading, it will be played when loading is finished
return;
// Store our start time
fVirtualStartTime = virtualStartTime;
if( !fActive )
{
// We're not active, so we can't play, but mark to make sure we'll play once we do get activated
fPlayOnReactivate = true;
return;
}
// Mark as playing, since we'll be calling ITryPlay() directly
fPlaying = true;
fPlayOnReactivate = false;
// Do da synch, which will start us playing
if( IWillBeAbleToPlay() )
{
ISynchToStartTime();
}
}
///////////////////////////////////////////////////////////
// Takes the virtual start time and sets us to the real time we should be at,
// then starts us playing via ITryPlay().
void plSound::ISynchToStartTime( void )
{
if( !plgAudioSys::Active() )
return;
// We don't want to do this until we're actually loaded, since that's when we'll know our
// REAL length (thanks to the inaccuracies of WMA compression)
// LoadSound( IsPropertySet( kPropIs3DSound ) );
// Haha, the GetLength() call will do this for us
// Calculate what time we should be at
double deltaTime = hsTimer::GetSysSeconds() - fVirtualStartTime;
double length = GetLength();
if( deltaTime > length || deltaTime < 0 )
{
// Hmm, our time went past the length of sound, so handle that
if( IsPropertySet( kPropLooping ) )
{
if( length <= 0 )
deltaTime = 0; // Error, attempt to recover
else if( deltaTime < 0 )
{
int numWholeParts = (int)( -deltaTime / length );
deltaTime += length * ( numWholeParts + 1 );
}
else
{
int numWholeParts = (int)( deltaTime / length );
deltaTime -= length * (double)numWholeParts;
}
//ISetActualTime( deltaTime );
Play();
}
else
// We already played and stopped virtually, so really mark us as stopped
Stop();
}
else
{
// Easy 'nuf...
//ISetActualTime( deltaTime );
Play();
}
}
void plSound::SetPosition(const hsPoint3 pos)
{
f3DPosition = pos;
}
void plSound::SetVelocity(const hsVector3 vel)
{
f3DVelocity = vel;
}
hsPoint3 plSound::GetPosition( void ) const
{
return f3DPosition;
}
hsVector3 plSound::GetVelocity( void ) const
{
return f3DVelocity;
}
void plSound::SetMin(const int m)
{
fMinFalloff = m;
}
void plSound::SetMax(const int m)
{
fMaxFalloff = m;
}
void plSound::SetOuterVolume(const int v)
{
fOuterVol = v;
}
void plSound::SetConeOrientation( hsScalar x, hsScalar y, hsScalar z )
{
fConeOrientation.Set( x, y, z );
}
void plSound::SetConeAngles( int inner, int outer )
{
fOuterCone = outer;
fInnerCone = inner;
}
int plSound::GetMin() const
{
return fMinFalloff;
}
int plSound::GetMax() const
{
return fMaxFalloff;
}
void plSound::SetVolume(const float v)
{
fDesiredVol = v;
if( !fMuted && !fFading )
fCurrVolume = fDesiredVol;
RefreshVolume();
}
void plSound::RefreshVolume( void )
{
this->ISetActualVolume( fCurrVolume );
}
void plSound::SetMuted( hsBool muted )
{
if( muted != fMuted )
{
fMuted = muted;
if( fMuted )
fCurrVolume = 0.f;
else if( !fFading )
fCurrVolume = fDesiredVol;
RefreshVolume();
}
}
void plSound::IRefreshParams( void )
{
SetMax( fMaxFalloff );
SetMin( fMinFalloff );
SetOuterVolume( fOuterVol );
SetConeAngles( fInnerCone, fOuterCone );
SetConeOrientation( fConeOrientation.fX, fConeOrientation.fY, fConeOrientation.fZ);
SetPosition( f3DPosition );
SetVelocity( f3DVelocity );
}
////////////////////////////////////////////////////////////////////////
// The public interface to stopping, which also synchs the state with the
// server.
void plSound::Stop( void )
{
fPlaying = false;
// if the audio data is loading while stop is called we need to make sure the sounds doesn't play, and the data is unloaded.
fPlayOnReactivate = false;
fFreeData = true;
// Do we have an ending fade?
if( fFadeOutParams.fLengthInSecs > 0 && !plgAudioSys::IsRestarting() )
{
IStartFade( &fFadeOutParams );
}
else
{
if( fFading )
IStopFade();
fCurrVolume = 0.f;
ISetActualVolume( fCurrVolume );
IActuallyStop();
}
if(fPlayWhenLoaded)
{
fPlayWhenLoaded = false;
}
}
void plSound::IActuallyStop( void )
{
if( fLoadOnDemandFlag && !IsPropertySet( kPropDisableLOD ) && !IsPropertySet( kPropLoadOnlyOnCall ) )
{
// If we're loading on demand, we want to unload on stop
IFreeBuffers();
}
}
void plSound::Update()
{
if(fLoading)
{
plSoundBuffer::ELoadReturnVal retVal = IPreLoadBuffer(fPlayWhenLoaded);
if(retVal == plSoundBuffer::kError)
{
fLoading = false;
fPlayWhenLoaded = false;
}
if(retVal == plSoundBuffer::kSuccess)
{
fLoading = false;
if(fPlayWhenLoaded)
Play();
fPlayWhenLoaded = false;
// ensure the sound data is released if the sound object was stopped while the audio data was being loaded.
if(fFreeData)
{
fFreeData = false;
FreeSoundData();
}
}
}
}
hsScalar plSound::IGetChannelVolume( void ) const
{
hsScalar channelVol = plgAudioSys::GetChannelVolume( (plgAudioSys::ASChannel)fType );
// if not using hardware acceleration then apply 2D/3D bias to non 3D sounds
if( !plgAudioSys::Hardware() && !IsPropertySet( kPropIs3DSound ) )
channelVol *= plgAudioSys::Get2D3Dbias();
if( IsPropertySet( kPropDontFade ) )
return channelVol;
return channelVol * plgAudioSys::GetGlobalFadeVolume();
}
void plSound::IStartFade( plFadeParams *params, hsScalar offsetIntoFade )
{
fFading = true;
if( params == &fFadeOutParams )
{
fFadeOutParams.fVolStart = fCurrVolume;
fFadeOutParams.fVolEnd = fFadedVolume;
fCurrFadeParams = &fFadeOutParams;
}
else if( params == &fFadeInParams )
{
fFadeInParams.fVolStart = fCurrVolume; // Hopefully, we got to fFadedVolume, but maybe not
fFadeInParams.fVolEnd = fDesiredVol;
fCurrFadeParams = &fFadeInParams;
plStatusLog::AddLineS("audio.log", "Fading in %s", GetKeyName());
}
else
fCurrFadeParams = params;
fCurrFadeParams->fCurrTime = offsetIntoFade;
ISetActualVolume( fCurrFadeParams->InterpValue() );
if( !fRegisteredForTime )
{
plgDispatch::Dispatch()->RegisterForExactType( plTimeMsg::Index(), GetKey() );
fRegisteredForTime = true;
}
}
void plSound::IStopFade( hsBool shuttingDown, hsBool SetVolEnd)
{
if( fCurrFadeParams != nil )
{
if( fCurrFadeParams == &fCoolSoftVolumeTrickParams )
{
plProfile_Dec( WaitingToDie );
}
// This can cause problems if we've exited a soft region and are doing a soft volume fade.
// If the camera pops back into the region of this particular sound this will cause the soft volume to be zero,
// therefore not allowing the sound to play until the listener moves again(triggering another softsound update).
// So if this function is called from UpdateSoftSounds this will not be performed
if(SetVolEnd)
{
if( fCurrFadeParams->fFadeSoftVol )
fSoftVolume = fCurrFadeParams->fVolEnd;
else
fCurrVolume = fCurrFadeParams->fVolEnd;
}
if( !shuttingDown )
ISetActualVolume( fCurrVolume );
fCurrFadeParams->fCurrTime = -1.f;
}
fFading = false;
fCurrFadeParams = nil;
// Fade done, unregister for time message
if( fRegisteredForTime )
{
plgDispatch::Dispatch()->UnRegisterForExactType( plTimeMsg::Index(), GetKey() );
fRegisteredForTime = false;
}
}
hsBool plSound::MsgReceive( plMessage* pMsg )
{
plTimeMsg *time = plTimeMsg::ConvertNoRef( pMsg );
if( time != nil )
{
/// Time message for handling fade ins/outs
if( fCurrFadeParams == nil )
return true;
fCurrFadeParams->fCurrTime += time->DelSeconds();
if( fCurrFadeParams->fCurrTime >= fCurrFadeParams->fLengthInSecs )
{
if( fCurrFadeParams->fFadeSoftVol )
fSoftVolume = fCurrFadeParams->fVolEnd;
else
fCurrVolume = fCurrFadeParams->fVolEnd;
ISetActualVolume( fCurrVolume );
fCurrFadeParams->fCurrTime = -1.f;
// Fade done, unregister for time message
if( fRegisteredForTime )
{
plgDispatch::Dispatch()->UnRegisterForExactType( plTimeMsg::Index(), GetKey() );
fRegisteredForTime = false;
}
// Note: if we're done, and we were fading out, we need to STOP
if( fCurrFadeParams->fStopWhenDone )
{
// REALLY STOP
IActuallyStop();
}
if( fCurrFadeParams == &fCoolSoftVolumeTrickParams )
{
plProfile_Dec( WaitingToDie );
}
// Done with this one!
fCurrFadeParams = nil;
fFading = false;
}
else
{
// Gotta interp
if( fCurrFadeParams->fFadeSoftVol )
fSoftVolume = fCurrFadeParams->InterpValue();
else
fCurrVolume = fCurrFadeParams->InterpValue();
ISetActualVolume( fCurrVolume );
}
return true;
}
plGenRefMsg* refMsg = plGenRefMsg::ConvertNoRef( pMsg );
if( refMsg )
{
if( refMsg->fType == kRefSoftVolume )
{
if( refMsg->GetContext() & (plRefMsg::kOnCreate|plRefMsg::kOnRequest|plRefMsg::kOnReplace) )
{
ISetSoftRegion( plSoftVolume::ConvertNoRef(refMsg->GetRef()) );
return true;
}
else if( refMsg->GetContext() & (plRefMsg::kOnRemove | plRefMsg::kOnDestroy) )
{
ISetSoftRegion( nil );
return true;
}
}
else if( refMsg->fType == kRefSoftOcclusionRegion )
{
if( refMsg->GetContext() & (plRefMsg::kOnCreate|plRefMsg::kOnRequest|plRefMsg::kOnReplace) )
{
ISetSoftOcclusionRegion( plSoftVolume::ConvertNoRef( refMsg->GetRef() ) );
return true;
}
else if( refMsg->GetContext() & (plRefMsg::kOnRemove | plRefMsg::kOnDestroy) )
{
ISetSoftOcclusionRegion( nil );
return true;
}
}
else if( refMsg->fType == kRefDataBuffer )
{
if( refMsg->GetContext() & (plRefMsg::kOnCreate|plRefMsg::kOnRequest|plRefMsg::kOnReplace) )
{
fDataBuffer = plSoundBuffer::ConvertNoRef( refMsg->GetRef() );
SetLength( fDataBuffer->GetDataLengthInSecs() );
}
else
fDataBuffer = nil;
return true;
}
else if( refMsg->fType == kRefParentSceneObject )
{
if( refMsg->GetContext() & (plRefMsg::kOnCreate|plRefMsg::kOnRequest|plRefMsg::kOnReplace) )
fOwningSceneObject = plSceneObject::ConvertNoRef( refMsg->GetRef() );
else
fOwningSceneObject = nil;
return true;
}
}
plSoundMsg *pSoundMsg = plSoundMsg::ConvertNoRef( pMsg );
if( pSoundMsg != nil )
{
if( pSoundMsg->Cmd( plSoundMsg::kAddCallbacks ) )
{
AddCallbacks( pSoundMsg );
return true;
}
else if( pSoundMsg->Cmd( plSoundMsg::kRemoveCallbacks ) )
{
RemoveCallbacks( pSoundMsg );
return true;
}
return false;
}
plListenerMsg *listenMsg = plListenerMsg::ConvertNoRef( pMsg );
if( listenMsg != nil )
{
if( fSoftOcclusionRegion != nil )
{
// The EAX settings have 0 as the start value and 1 as the end, and since start
// translates to "inside the soft region", it's reversed of what the region gives us
fEAXSettings.SetOcclusionSoftValue( 1.f - fSoftOcclusionRegion->GetListenerStrength() );
IRefreshEAXSettings();
}
return true;
}
return plSynchedObject::MsgReceive( pMsg );
}
void plSound::ForceLoad()
{
if( !IsPropertySet( kPropLoadOnlyOnCall ) )
return;
LoadSound( IsPropertySet( kPropIs3DSound ) );
}
void plSound::ForceUnload( void )
{
if( !IsPropertySet( kPropLoadOnlyOnCall ) )
return;
Stop();
IFreeBuffers();
}
bool plSound::ILoadDataBuffer( void )
{
if(!fDataBufferLoaded)
{
plSoundBuffer *buffer = (plSoundBuffer *)fDataBufferKey->RefObject();
if(!buffer)
{
hsAssert(false, "unable to load sound buffer");
plStatusLog::AddLineS("audio.log", "Unable to load sound buffer: %s", GetKeyName());
return false;
}
SetLength( buffer->GetDataLengthInSecs() );
fDataBufferLoaded = true;
}
return true;
}
void plSound::FreeSoundData()
{
if(!fDataBufferKey) return; // for plugins
plSoundBuffer *buffer = (plSoundBuffer *) fDataBufferKey->ObjectIsLoaded();
if(buffer)
{
buffer->UnLoad();
}
}
void plSound::IUnloadDataBuffer( void )
{
if(fDataBufferLoaded)
{
fDataBufferLoaded = false;
fDataBufferKey->UnRefObject();
}
}
/////////////////////////////////////////////////////////////////////////
// calling preload will cause the sound to play once loaded
plSoundBuffer::ELoadReturnVal plSound::IPreLoadBuffer( hsBool playWhenLoaded, hsBool isIncidental /* = false */ )
{
if(!ILoadDataBuffer())
{
return plSoundBuffer::kError;
}
plSoundBuffer *buffer = (plSoundBuffer *)fDataBufferKey->ObjectIsLoaded();
if(buffer && buffer->IsValid() )
{
plProfile_BeginTiming( SoundLoadTime );
plSoundBuffer::ELoadReturnVal retVal = buffer->AsyncLoad(buffer->HasFlag(plSoundBuffer::kStreamCompressed) ? plAudioFileReader::kStreamNative : plAudioFileReader::kStreamWAV);
if(retVal == plSoundBuffer::kPending)
{
fPlayWhenLoaded = playWhenLoaded;
fLoading = true;
}
plProfile_EndTiming( SoundLoadTime );
return retVal;
}
else
{
return plSoundBuffer::kError;
}
}
const char *plSound::GetFileName( void ) const
{
if(fDataBufferKey->ObjectIsLoaded())
{
return ((plSoundBuffer *)fDataBufferKey->ObjectIsLoaded())->GetFileName();
}
return nil;
}
/////////////////////////////////////////////////////////////////////////
// WARNING: to do this right, we have to load the data buffer in, which could
// force an early load of the sound if you're just calling this on a whim. So
// it's best to call this right before you're going to load it anyway (or
// when it's already loaded).
// Note that if we already set the length (like at export time), we never need
// to load the sound, so the optimization at export time is all ready to plug-
// and-play...
double plSound::GetLength( void )
{
if( ( (double)fLength == 0.f ) )
ILoadDataBuffer();
return fLength;
}
void plSound::ISetSoftRegion( plSoftVolume *region )
{
/// We're either registering or unregistering
if( fSoftRegion == nil && region != nil )
RegisterOnAudioSys();
else if( fSoftRegion != nil && region == nil )
UnregisterOnAudioSys();
fSoftRegion = region;
fSoftVolume = 0.f; // Set to zero until we can get our processing call
}
void plSound::ISetSoftOcclusionRegion( plSoftVolume *region )
{
/// We're either registering or unregistering for listener messages
if( fSoftOcclusionRegion == nil && region != nil )
{
plgDispatch::Dispatch()->RegisterForExactType( plListenerMsg::Index(), GetKey() );
}
else if( fSoftOcclusionRegion != nil && region == nil )
{
plgDispatch::Dispatch()->UnRegisterForExactType( plListenerMsg::Index(), GetKey() );
}
fSoftOcclusionRegion = region;
}
/////////////////////////////////////////////////////////////////////////
// This function calculates our new softVolume value. Used both to update the
// said value and so the audio system can rank us in importance.
hsScalar plSound::CalcSoftVolume( hsBool enable, hsScalar distToListenerSquared )
{
// Do distance-based attenuation ourselves
#if MCN_HACK_OUR_ATTEN
if( IsPropertySet( kPropIs3DSound ) )
{
hsScalar minDist = (hsScalar)GetMin();
if( distToListenerSquared <= minDist * minDist )
{
fDistAttenuation = 1.f;
}
else
{
hsScalar maxDist = (hsScalar)GetMax();
if( distToListenerSquared >= maxDist * maxDist )
{
fDistAttenuation = 0.f;
}
else
{
hsScalar d = (hsScalar)sqrt( distToListenerSquared );
fDistAttenuation = minDist / d;
// The following line ramps it to 0 at the maxDistance. Kinda klunky, but good for now I guess...
// fDistAttenuation *= 1.f - ( 1.f / ( maxDist - minDist ) ) * ( d - minDist );
}
}
}
else
fDistAttenuation = 1.f;
#endif
// At the last 50% of our distance attenuation (squared, so it really is farther than that),
// ramp down to 0 so we don't get annoying popping when we stop stuff
if( IsPropertySet( kPropIs3DSound ) )
{
hsScalar maxDistSquared = (hsScalar)( GetMax() * GetMax() );
hsScalar distToStartSquared = (hsScalar)(maxDistSquared * 0.50);
if( maxDistSquared < 0.f ) // Happens when the max distance is REALLY big
{
maxDistSquared = distToListenerSquared + 1.f; // :)
distToStartSquared = maxDistSquared;
}
if( distToListenerSquared > maxDistSquared )
fDistAttenuation = 0.f;
else if( distToListenerSquared > distToStartSquared )
fDistAttenuation = ( maxDistSquared - distToListenerSquared ) / ( maxDistSquared - distToStartSquared );
else
fDistAttenuation = 1.f;
fDistToListenerSquared = distToListenerSquared;
}
else
{
fDistAttenuation = 1.f;
fDistToListenerSquared = 0.f;
}
// Attenuate based on our soft region, if we have one
if( !enable )
// We apparently don't know jack. Let the audioSystem's decision rule
fSoftVolume = 0.f;
else if( fSoftRegion != nil )
fSoftVolume = fSoftRegion->GetListenerStrength();
else
fSoftVolume = 1.f;
return fSoftVolume;
}
/////////////////////////////////////////////////////////////////////////
// Wee function for the audio system. This basically returns the effective
// current volume of this sound. Useful for doing things like ranking all
// sounds based on volume.
hsScalar plSound::GetVolumeRank( void )
{
if( !IsPlaying() && !this->IActuallyPlaying() )
return 0.f;
hsScalar rank = fSoftVolume * fDesiredVol;
if( IsPropertySet( kPropIs3DSound ) )
{
hsScalar minDistSquared = (hsScalar)( GetMin() * GetMin() );
hsScalar maxDistSquared = (hsScalar) (GetMax() * GetMax());
hsPoint3 listenerPos = plgAudioSys::Sys()->GetCurrListenerPos();
if( fDistToListenerSquared > minDistSquared )
{
hsScalar diff = maxDistSquared - minDistSquared;
rank *= fabs((fDistToListenerSquared - maxDistSquared)) / diff;
}
}
return rank;
}
/////////////////////////////////////////////////////////////////////////
// Tests to see whether, if we try to play this sound now, it'll actually
// be able to play. Takes into account whether the sound is within range
// of the listener and the current soft region value.
hsBool plSound::IWillBeAbleToPlay( void )
{
if( fSoftVolume == 0.f )
return false;
return IsWithinRange( plgAudioSys::Sys()->GetCurrListenerPos(), nil );
}
/////////////////////////////////////////////////////////////////////////
// Tests to see whether this sound is within range of the position given,
// ignoring soft volumes.
hsBool plSound::IsWithinRange( const hsPoint3 &listenerPos, hsScalar *distSquared )
{
if( !IsPropertySet( plSound::kPropIs3DSound ) )
{
if( distSquared != nil )
*distSquared = 1.f;
return true;
}
hsVector3 distance;
hsPoint3 soundPos = GetPosition();
distance.Set( &listenerPos, &soundPos );
if( distSquared != nil )
*distSquared = distance.MagnitudeSquared();
if( GetMax() == 1000000000 )
return true;
hsScalar soundRadius = (hsScalar)( GetMax() * GetMax() );
return ( distance.MagnitudeSquared() <= soundRadius ) ? true : false;
}
//// ////////////////////////////////////////////////////////
// Once the soft volume is calculated and our rank is computed, we can
// decide whether to actually enable or not.
// Note: we might have been "enabled" by our Calc call, but the ranking
// still could have disabled us, so we have to specify again whether
// we're enabled.
// Note: if we KNOW we're disabling this sound (out of range), Calc() doesn't
// have to be called at all, and we can simply call this function with
// enable = false.
void plSound::UpdateSoftVolume( hsBool enable, hsBool firstTime )
{
fNotHighEnoughPriority = !enable;
// Don't do any of this special stuff that follows if we're not supposed to be playing
if( IsPlaying() )
{
if( fSoftVolume * fDistAttenuation > 0.f && !fNotHighEnoughPriority )
{
if( fCurrFadeParams == &fCoolSoftVolumeTrickParams )
{
// Stop the fade, but since we are updating the softvolume with the intention of being audible
// tell StopFade not to set the soft volume to zero.
IStopFade(false, false);
}
if( !IActuallyPlaying() )
{
// Must've been stopped from being out of range. Start up again...
// Synch up to our start time.
// If this sound is auto starting and is background music, get the current time so we don't start
// with the play cursor already into the piece.
if(IsPropertySet(kPropAutoStart) && fType == kBackgroundMusic) fVirtualStartTime = hsTimer::GetSysSeconds();
ISynchedPlay( fVirtualStartTime );
}
}
else if( fCurrFadeParams != &fCoolSoftVolumeTrickParams && IActuallyPlaying() )
{
// Start our special trick, courtesy of Brice. Basically, we don't
// stop the sound immediately, but rather let it get to the end of
// the sound and loop once more. This way, if we go away and come back soon
// enough, it will be continuing as we expect it to, but if we wait long enough,
// it'll stop, saving processing time.
// Note: we just do it as a fade because it makes it easier on us that way!
fCoolSoftVolumeTrickParams.fCurrTime = 0.f;
fCoolSoftVolumeTrickParams.fLengthInSecs = firstTime ? 0.f : (hsScalar)fLength + ( (hsScalar)fLength - (hsScalar)GetTime() );
fCoolSoftVolumeTrickParams.fStopWhenDone = true;
fCoolSoftVolumeTrickParams.fFadeSoftVol = true;
fCoolSoftVolumeTrickParams.fType = plFadeParams::kLinear;
fCoolSoftVolumeTrickParams.fVolStart = fSoftVolume; // Don't actually change the volume this way
fCoolSoftVolumeTrickParams.fVolEnd = 0.f;
IStartFade( &fCoolSoftVolumeTrickParams );
plProfile_Inc( WaitingToDie );
}
}
RefreshVolume();
}
/////////////////////////////////////////////////////////////////////////
// Returns the current volume, attenuated
hsScalar plSound::QueryCurrVolume( void ) const
{
return IAttenuateActualVolume( fCurrVolume ) * IGetChannelVolume();
}
/////////////////////////////////////////////////////////////////////////
// Used by ISetActualVolume(). Does the final attenuation on a volume before
// sending it to the sound processing. Only does soft regions for now.
hsScalar plSound::IAttenuateActualVolume( hsScalar volume ) const
{
if( fNotHighEnoughPriority )
return 0.f;
volume *= fSoftVolume;
if( IsPropertySet( kPropIs3DSound ) )
volume *= fDistAttenuation;
return volume;
}
void plSound::Activate(hsBool forcePlay)
{
// Our actual state...
fActive = true;
// re-create the sound state here:
if( forcePlay || fPlayOnReactivate )
{
ISynchedPlay( hsTimer::GetSysSeconds() );
fPlayOnReactivate = false;
}
RegisterOnAudioSys();
SetMuted(plgAudioSys::IsMuted());
}
void plSound::DeActivate( void )
{
UnregisterOnAudioSys();
if( fActive )
{
if( IsPlaying() )
{
Stop();
fPlayOnReactivate = true;
}
else
fPlayOnReactivate = false;
}
fActive = false;
}
/////////////////////////////////////////////////////////////////////////
// Tell the audio system about ourselves.
void plSound::RegisterOnAudioSys( void )
{
if( !fRegistered )
{
plgAudioSys::RegisterSoftSound(GetKey());
fRegistered = true;
}
}
/////////////////////////////////////////////////////////////////////////
// Tell the audio system to stop caring about us
void plSound::UnregisterOnAudioSys( void )
{
if( fRegistered )
{
plgAudioSys::UnregisterSoftSound(GetKey());
fRegistered = false;
}
}
/////////////////////////////////////////////////////////////////////////
// Called by the audio system when we've been booted off (audio system is
// shutting down). Normally, we should already be shut down, but in case
// we're not, this function makes sure everything is cleaned up before
// the audio system itself shuts down.
void plSound::ForceUnregisterFromAudioSys( void )
{
DeActivate();
fRegistered = false;
}
void plSound::Read(hsStream* s, hsResMgr* mgr)
{
plSynchedObject::Read(s, mgr);
IRead( s, mgr );
// If we're autostart, start playing
if( IsPropertySet( kPropAutoStart ) )
Play();
// Make sure we synch or don't synch
if( IsPropertySet( kPropLocalOnly ) )
SetLocalOnly(true);
// If we're not playing, but we're going to, and we're going to fade in,
// then our current state is faded out, so set fFading
if( fFadeInParams.fLengthInSecs > 0 && !fPlaying )
fFading = true;
else if( fFadeInParams.fLengthInSecs <= 0 && !fPlaying )
fFading = false;
ILoadDataBuffer(); // make sure our sound buffer is loaded
// Force load on read
if( !fLoadOnDemandFlag || IsPropertySet( kPropDisableLOD ) )
{
LoadSound( IsPropertySet( kPropIs3DSound ) );
}
else
{
// Loading on demand, but we still need the length. But that's ok, we'll get it when we get the fDataBuffer ref.
// But we want to preload the data, so go ahead and do that
if( !fLoadFromDiskOnDemand && !IsPropertySet( kPropLoadOnlyOnCall ) && fPriority <= plgAudioSys::GetPriorityCutoff())
{
IPreLoadBuffer(false);
}
}
}
void plSound::Write(hsStream* s, hsResMgr* mgr)
{
plSynchedObject::Write(s, mgr);
IWrite( s, mgr );
}
void plSound::IRead( hsStream *s, hsResMgr *mgr )
{
fPlaying = s->ReadBool();
fVirtualStartTime = hsTimer::GetSysSeconds(); // Need if we're autostart
fTime = s->ReadSwapDouble();
fMaxFalloff = s->ReadSwap32();
fMinFalloff = s->ReadSwap32();
s->ReadSwap( &fCurrVolume );
s->ReadSwap( &fDesiredVol );
/// mcn debugging - Thanks to some of my older sound code, it's possible that a few volumes
/// will come in too large. This will only happen with scenes that were exported with that intermediate
/// code, which should be limited to my test scenes locally. Otherwise, things should be fine. This
/// is to compensate for those bogus files. (The fix is to reset the volume in MAX, since the bogusness
/// is in the range of the volume slider itself).
if( fDesiredVol > 1.f )
fDesiredVol = 1.f;
if( fCurrVolume > 1.f )
fCurrVolume = 1.f;
fMaxVolume = fDesiredVol;
fOuterVol = s->ReadSwap32();
fInnerCone = s->ReadSwap32();
fOuterCone = s->ReadSwap32();
s->ReadSwap( &fFadedVolume );
s->ReadSwap( &fProperties );
fType = s->ReadByte();
fPriority = s->ReadByte();
// HACK FOR OLDER EXPORTERS that thought Auto-start meant set fPlaying true
if( fPlaying )
SetProperty( kPropAutoStart, true );
// Read in fade params
fFadeInParams.Read( s );
fFadeOutParams.Read( s );
// Read in soft volume key
mgr->ReadKeyNotifyMe( s, TRACKED_NEW plGenRefMsg( GetKey(), plRefMsg::kOnCreate, 0, kRefSoftVolume ), plRefFlags::kActiveRef );
// Read in the data buffer key
fDataBufferKey = mgr->ReadKey( s );
// EAX params
fEAXSettings.Read( s );
// EAX soft keys
mgr->ReadKeyNotifyMe( s, TRACKED_NEW plGenRefMsg( GetKey(), plRefMsg::kOnCreate, 0, kRefSoftOcclusionRegion ), plRefFlags::kActiveRef );
}
void plSound::IWrite( hsStream *s, hsResMgr *mgr )
{
s->WriteBool(fPlaying);
s->WriteSwapDouble(fTime);
s->WriteSwap32(fMaxFalloff);
s->WriteSwap32(fMinFalloff);
s->WriteSwap( fCurrVolume );
s->WriteSwap( fDesiredVol );
s->WriteSwap32(fOuterVol);
s->WriteSwap32(fInnerCone);
s->WriteSwap32(fOuterCone);
s->WriteSwap( fFadedVolume );
s->WriteSwap( fProperties );
s->WriteByte( fType );
s->WriteByte( fPriority );
// Write out fade params
fFadeInParams.Write( s );
fFadeOutParams.Write( s );
// Write out soft volume key
mgr->WriteKey( s, fSoftRegion );
// Write out data buffer key
if(fDataBuffer)
mgr->WriteKey( s, fDataBuffer->GetKey() );
else
mgr->WriteKey( s, fDataBufferKey );
// EAX params
fEAXSettings.Write( s );
// EAX Soft keys
mgr->WriteKey( s, fSoftOcclusionRegion );
}
void plSound::plFadeParams::Read( hsStream *s )
{
s->ReadSwap( &fLengthInSecs );
s->ReadSwap( &fVolStart );
s->ReadSwap( &fVolEnd );
s->ReadSwap( &fType );
s->ReadSwap( &fCurrTime );
s->ReadSwap( &fStopWhenDone );
s->ReadSwap( &fFadeSoftVol );
}
void plSound::plFadeParams::Write( hsStream *s )
{
s->WriteSwap( fLengthInSecs );
s->WriteSwap( fVolStart );
s->WriteSwap( fVolEnd );
s->WriteSwap( fType );
s->WriteSwap( fCurrTime );
s->WriteSwap( fStopWhenDone );
s->WriteSwap( fFadeSoftVol );
}
hsScalar plSound::plFadeParams::InterpValue( void )
{
hsScalar val;
switch( fType )
{
case kLinear:
val = ( ( fCurrTime / fLengthInSecs ) * ( fVolEnd - fVolStart ) ) + fVolStart;
break;
case kLogarithmic:
val = fCurrTime / fLengthInSecs;
val = ( ( val * val ) * ( fVolEnd - fVolStart ) ) + fVolStart;
break;
case kExponential:
val = fCurrTime / fLengthInSecs;
val = ( (hsScalar)sqrt( val ) * ( fVolEnd - fVolStart ) ) + fVolStart;
break;
default:
val = 0.f;
}
return val;
}
void plSound::SetFadeInEffect( plSound::plFadeParams::Type type, hsScalar length )
{
fFadeInParams.fLengthInSecs = length;
fFadeInParams.fType = type;
fFadeInParams.fVolStart = 0.f; // Will be set when activated
fFadeInParams.fVolEnd = 1.f; // Will be set when activated
fFadeInParams.fCurrTime = -1.f;
// If we're not playing, but we're going to, and we're going to fade in,
// then our current state is faded out, so set fFading
if( fFadeInParams.fLengthInSecs > 0 && !fPlaying )
fFading = true;
else if( fFadeInParams.fLengthInSecs <= 0 && !fPlaying )
fFading = false;
}
void plSound::SetFadeOutEffect( plSound::plFadeParams::Type type, hsScalar length )
{
fFadeOutParams.fLengthInSecs = length;
fFadeOutParams.fType = type;
fFadeOutParams.fVolStart = 1.f; // Will be set when activated
fFadeOutParams.fVolEnd = 0.f; // Will be set when activated
fFadeOutParams.fCurrTime = -1.f;
fFadeOutParams.fStopWhenDone = true;
}
plDrawableSpans* plSound::CreateProxy(const hsMatrix44& l2w, hsGMaterial* mat, hsTArray& idx, plDrawableSpans* addTo)
{
plDrawableSpans* myDraw = addTo;
if( fOuterCone < 360 )
{
hsScalar len = (hsScalar)GetMax();
hsScalar halfAng = hsScalarDegToRad(hsScalar(fInnerCone) * 0.5f);
hsScalar radius = len * tanf(halfAng);
if( fInnerCone < 180 )
len = -len;
myDraw = plDrawableGenerator::GenerateConicalDrawable(
radius,
len,
mat,
l2w,
true,
&hsColorRGBA().Set(1.f, 0.5f, 0.5f, 1.f),
&idx,
myDraw);
len = (hsScalar)GetMin();
halfAng = hsScalarDegToRad(hsScalar(fOuterCone) * 0.5f);
radius = len * tanf(halfAng);
if( fOuterCone < 180 )
len = -len;
myDraw = plDrawableGenerator::GenerateConicalDrawable(
radius,
len,
mat,
l2w,
true,
&hsColorRGBA().Set(0.25f, 0.25f, 0.5f, 1.f),
&idx,
myDraw);
}
else
{
myDraw = plDrawableGenerator::GenerateSphericalDrawable(
hsPoint3(0,0,0),
(hsScalar)GetMin(),
mat,
l2w,
true,
&hsColorRGBA().Set(1.f, 0.5f, 0.5f, 1.f),
&idx,
myDraw);
myDraw = plDrawableGenerator::GenerateSphericalDrawable(
hsPoint3(0,0,0),
(hsScalar)GetMax(),
mat,
l2w,
true,
&hsColorRGBA().Set(0.25f, 0.25f, 0.5f, 1.f),
&idx,
myDraw);
}
return myDraw;
}
// call when state has changed
hsBool plSound::DirtySynchState(const char* sdlName /* kSDLSound */, UInt32 sendFlags)
{
/*
if( sdlName == nil )
sdlName = kSDLSound;
if( fOwningSceneObject != nil )
return fOwningSceneObject->DirtySynchState(sdlName, sendFlags);
*/
return false;
}
//////////////////////////////////////////////////////////////////////////////
//// plSoundVolumeApplicator /////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
void plSoundVolumeApplicator::IApply( const plAGModifier *mod, double time )
{
plScalarChannel *chan = plScalarChannel::ConvertNoRef( fChannel );
if(chan)
{
hsScalar volume = chan->Value( time );
hsScalar digitalVolume = (float)pow( 10.f, volume / 20.f );
// Find the audio interface and thus the plSound from it
plSceneObject *so = mod->GetTarget( 0 );
if( so != nil )
{
const plAudioInterface *ai = so->GetAudioInterface();
if( ai != nil && fIndex < ai->GetNumSounds() )
{
plSound *sound = ai->GetSound( fIndex );
if( sound != nil )
{
sound->SetVolume( digitalVolume );
return;
}
}
}
}
}
plAGApplicator *plSoundVolumeApplicator::CloneWithChannel( plAGChannel *channel )
{
plSoundVolumeApplicator *clone = (plSoundVolumeApplicator *)plAGApplicator::CloneWithChannel( channel );
clone->fIndex = fIndex;
return clone;
}
void plSoundVolumeApplicator::Write( hsStream *stream, hsResMgr *mgr )
{
plAGApplicator::Write( stream, mgr );
stream->WriteSwap32( fIndex );
}
void plSoundVolumeApplicator::Read( hsStream *s, hsResMgr *mgr )
{
plAGApplicator::Read( s, mgr );
fIndex = s->ReadSwap32();
}