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