/*==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 . 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 "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(); }