sqwarmed/sdk_src/game/shared/soundenvelope.cpp

1424 lines
43 KiB
C++

//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============//
//
// Purpose:
//
// $Workfile: $
// $Date: $
// $NoKeywords: $
//=============================================================================//
#include "cbase.h"
#include "sharedInterface.h"
#include "soundenvelope.h"
#include "engine/IEngineSound.h"
#include "IEffects.h"
#include "isaverestore.h"
#include "saverestore_utlvector.h"
#include "gamestringpool.h"
#include "igamesystem.h"
#include "utlpriorityqueue.h"
#include "mempool.h"
#include "SoundEmitterSystem/isoundemittersystembase.h"
#include "tier0/vprof.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
static ConVar soundpatch_captionlength( "soundpatch_captionlength", "2.0", FCVAR_REPLICATED, "How long looping soundpatch captions should display for." );
// Envelope
// This is a class that controls a ramp for a sound (pitch / volume / etc)
class CSoundEnvelope
{
public:
DECLARE_SIMPLE_DATADESC();
CSoundEnvelope()
{
m_current = 0.0f;
m_target = 0.0f;
m_rate = 0.0f;
m_forceupdate = false;
}
void SetTarget( float target, float deltaTime );
void SetValue( float value );
bool ShouldUpdate( void );
void Update( float time );
inline float Value( void ) { return m_current; }
private:
float m_current;
float m_target;
float m_rate;
bool m_forceupdate;
};
BEGIN_SIMPLE_DATADESC( CSoundEnvelope )
DEFINE_FIELD( m_current, FIELD_FLOAT ),
DEFINE_FIELD( m_target, FIELD_FLOAT ),
DEFINE_FIELD( m_rate, FIELD_FLOAT ),
DEFINE_FIELD( m_forceupdate, FIELD_BOOLEAN ),
END_DATADESC()
//-----------------------------------------------------------------------------
// Purpose: Set the new target value for this ramp. Reach this target in deltaTime
// seconds from now
// Input : target - new target value
// deltaTime - time to reach target
//-----------------------------------------------------------------------------
void CSoundEnvelope::SetTarget( float target, float deltaTime )
{
float deltaValue = target - m_current;
if ( deltaValue && deltaTime > 0 )
{
m_target = target;
m_rate = MAX( 0.1, fabs(deltaValue / deltaTime) );
}
else
{
if ( target != m_current )
{
m_forceupdate = true;
}
SetValue( target );
}
}
//-----------------------------------------------------------------------------
// Purpose: Instantaneously set the value of this ramp
// Input : value - new value
//-----------------------------------------------------------------------------
void CSoundEnvelope::SetValue( float value )
{
if ( m_target != value )
{
m_forceupdate = true;
}
m_current = m_target = value;
m_rate = 0;
}
//-----------------------------------------------------------------------------
// Purpose: Check to see if I need to update this envelope
// Output : Returns true if this envelope is changing
//-----------------------------------------------------------------------------
bool CSoundEnvelope::ShouldUpdate( void )
{
if ( m_forceupdate )
{
m_forceupdate = false;
return true;
}
if ( m_current != m_target )
{
return true;
}
return false;
}
//-----------------------------------------------------------------------------
// Purpose: Update the envelope for the current frame time
// Input : time - amount of time that has passed
//-----------------------------------------------------------------------------
void CSoundEnvelope::Update( float deltaTime )
{
m_current = Approach( m_target, m_current, m_rate * deltaTime );
}
class CCopyRecipientFilter : public IRecipientFilter
{
public:
DECLARE_SIMPLE_DATADESC();
CCopyRecipientFilter() : m_Flags(0) {}
void Init( IRecipientFilter *pSrc )
{
m_Flags = FLAG_ACTIVE;
if ( pSrc->IsReliable() )
{
m_Flags |= FLAG_RELIABLE;
}
if ( pSrc->IsInitMessage() )
{
m_Flags |= FLAG_INIT_MESSAGE;
}
for ( int i = 0; i < pSrc->GetRecipientCount(); i++ )
{
int index = pSrc->GetRecipientIndex( i );
if ( index >= 0 )
m_Recipients.AddToTail( index );
}
}
bool IsActive() const
{
return (m_Flags & FLAG_ACTIVE) != 0;
}
virtual bool IsReliable( void ) const
{
return (m_Flags & FLAG_RELIABLE) != 0;
}
virtual int GetRecipientCount( void ) const
{
return m_Recipients.Count();
}
virtual int GetRecipientIndex( int slot ) const
{
return m_Recipients[ slot ];
}
virtual bool IsInitMessage( void ) const
{
return (m_Flags & FLAG_INIT_MESSAGE) != 0;
}
virtual bool AddRecipient( CBasePlayer *player )
{
Assert( player );
int index = player->entindex();
if ( index < 0 )
return false;
// Already in list
if ( m_Recipients.Find( index ) != m_Recipients.InvalidIndex() )
return false;
m_Recipients.AddToTail( index );
return true;
}
private:
enum
{
FLAG_ACTIVE = 0x1,
FLAG_RELIABLE = 0x2,
FLAG_INIT_MESSAGE = 0x4,
};
int m_Flags;
CUtlVector< int > m_Recipients;
};
BEGIN_SIMPLE_DATADESC( CCopyRecipientFilter )
DEFINE_FIELD( m_Flags, FIELD_INTEGER ),
DEFINE_UTLVECTOR( m_Recipients, FIELD_INTEGER ),
END_DATADESC()
#include "tier0/memdbgoff.h"
// This is the a basic sound controller, a "patch"
// It has envelopes for pitch and volume and can manage state changes to those
class CSoundPatch
{
public:
DECLARE_SIMPLE_DATADESC();
static int g_SoundPatchCount;
CSoundPatch()
{
g_SoundPatchCount++;
m_iszSoundName = NULL_STRING;
m_iszSoundScriptName = NULL_STRING;
m_flCloseCaptionDuration = soundpatch_captionlength.GetFloat();
m_soundOrigin.Init();
m_soundEntityIndex = -1;
m_guid = -1;
}
~CSoundPatch()
{
g_SoundPatchCount--;
}
void Init( IRecipientFilter *pFilter, CBaseEntity *pEnt, int channel, const char *pSoundName,
soundlevel_t iSoundLevel, const Vector *pSoundOrigin, float scriptVolume = 1.0f );
void ChangePitch( float pitchTarget, float deltaTime );
void ChangeVolume( float volumeTarget, float deltaTime );
void FadeOut( float deltaTime, bool destroyOnFadeout );
float GetPitch( void );
float GetVolume( void );
string_t GetName() { return m_iszSoundName; };
#ifdef CLIENT_DLL
int GetGuid() { return m_guid; };
float GetElapsedTime( void );
bool IsStillPlaying( void );
#endif
string_t GetScriptName() { return m_iszSoundScriptName; }
// UNDONE: Don't call this, use the controller to shut down
void Shutdown( void );
bool Update( float time, float deltaTime );
void Reset( void );
void StartSound( float flStartTime = 0 );
void ResumeSound( void );
int IsPlaying( void ) { return m_isPlaying; }
float GetShutdownTime( void ) const { return m_shutdownTime; } // TERROR: debugging
void AddPlayerPost( CBasePlayer *pPlayer );
void SetCloseCaptionDuration( float flDuration ) { m_flCloseCaptionDuration = flDuration; }
void SetBaseFlags( int iFlags ) { m_baseFlags = iFlags; }
// Returns the ent index
int EntIndex() const;
private:
// SoundPatches take volumes between 0 & 1, and use that to multiply the sounds.txt specified volume.
// This function is an internal method of accessing the real volume passed into the engine (i.e. post multiply)
float GetVolumeForEngine( void );
private:
CSoundEnvelope m_pitch;
CSoundEnvelope m_volume;
int m_guid;
soundlevel_t m_soundlevel;
float m_shutdownTime;
string_t m_iszSoundName;
string_t m_iszSoundScriptName;
EHANDLE m_hEnt;
int m_entityChannel;
int m_soundEntityIndex;
Vector m_soundOrigin;
int m_flags;
int m_baseFlags;
int m_isPlaying;
float m_flScriptVolume; // Volume for this sound in sounds.txt
CCopyRecipientFilter m_Filter;
float m_flCloseCaptionDuration;
#ifdef _DEBUG
// Used to get the classname of the entity associated with the sound
string_t m_iszClassName;
#endif
DECLARE_FIXEDSIZE_ALLOCATOR(CSoundPatch);
};
#include "tier0/memdbgon.h"
int CSoundPatch::g_SoundPatchCount = 0;
#ifdef CLIENT_DLL
CON_COMMAND( cl_report_soundpatch, "reports client-side sound patch count" )
#else
CON_COMMAND( report_soundpatch, "reports sound patch count" )
#endif
{
Msg("Current sound patches: %d\n", CSoundPatch::g_SoundPatchCount );
}
DEFINE_FIXEDSIZE_ALLOCATOR( CSoundPatch, 64, CUtlMemoryPool::GROW_FAST );
BEGIN_SIMPLE_DATADESC( CSoundPatch )
DEFINE_EMBEDDED( m_pitch ),
DEFINE_EMBEDDED( m_volume ),
DEFINE_FIELD( m_soundlevel, FIELD_INTEGER ),
DEFINE_FIELD( m_shutdownTime, FIELD_TIME ),
DEFINE_FIELD( m_iszSoundName, FIELD_STRING ),
DEFINE_FIELD( m_iszSoundScriptName, FIELD_STRING ),
DEFINE_FIELD( m_hEnt, FIELD_EHANDLE ),
DEFINE_FIELD( m_entityChannel, FIELD_INTEGER ),
DEFINE_FIELD( m_flags, FIELD_INTEGER ),
DEFINE_FIELD( m_baseFlags, FIELD_INTEGER ),
DEFINE_FIELD( m_isPlaying, FIELD_INTEGER ),
DEFINE_FIELD( m_flScriptVolume, FIELD_FLOAT ),
DEFINE_EMBEDDED( m_Filter ),
DEFINE_FIELD( m_flCloseCaptionDuration, FIELD_FLOAT ),
// Not saved, it's debug only
// DEFINE_FIELD( m_iszClassName, FIELD_STRING ),
END_DATADESC()
//-----------------------------------------------------------------------------
// Purpose: Setup the patch
// Input : nEntIndex - index of the edict that owns the sound channel
// channel - This is a sound channel (CHAN_ITEM, CHAN_STATIC)
// *pSoundName - sound script string name
// attenuation - attenuation of this sound (not animated)
//-----------------------------------------------------------------------------
void CSoundPatch::Init( IRecipientFilter *pFilter, CBaseEntity *pEnt, int channel, const char *pSoundName,
soundlevel_t soundlevel, const Vector *pSoundOrigin , float scriptVolume)
{
m_hEnt = pEnt;
if ( pEnt )
{
m_soundEntityIndex = pEnt->entindex();
}
m_entityChannel = channel;
// Get the volume from the script
CSoundParameters params;
if ( !Q_stristr( pSoundName, ".wav" ) && !Q_stristr( pSoundName, ".mp3" ) &&
CBaseEntity::GetParametersForSound( pSoundName, params, NULL ) )
{
m_flScriptVolume = params.volume;
// This has to be the actual .wav because rndwave would cause a bunch of new .wavs to play... bad...
// e.g., when you pitch shift it would start a different wav instead.
m_iszSoundScriptName = AllocPooledString( pSoundName );
pSoundName = params.soundname;
m_soundlevel = params.soundlevel;
// TERROR: if we say we want CHAN_USER_BASE + N, we mean it!
if ( m_entityChannel < CHAN_USER_BASE )
{
m_entityChannel = params.channel;
}
}
else
{
m_iszSoundScriptName = AllocPooledString( pSoundName );
m_flScriptVolume = scriptVolume;
m_soundlevel = soundlevel;
}
m_iszSoundName = AllocPooledString( pSoundName );
m_volume.SetValue( 0 );
m_pitch.SetValue( 0 );
m_isPlaying = false;
m_shutdownTime = 0;
m_Filter.Init( pFilter );
m_baseFlags = 0;
if( pSoundOrigin )
{
m_soundOrigin.x = pSoundOrigin->x;
m_soundOrigin.y = pSoundOrigin->y;
m_soundOrigin.z = pSoundOrigin->z;
}
#ifdef _DEBUG
if ( pEnt )
{
m_iszClassName = AllocPooledString( pEnt->GetClassname() );
}
#endif
}
//-----------------------------------------------------------------------------
// Purpose: Ramps the pitch to a new value
// Input : pitchTarget - new value
// deltaTime - seconds to reach the value
//-----------------------------------------------------------------------------
void CSoundPatch::ChangePitch( float pitchTarget, float deltaTime )
{
m_flags |= SND_CHANGE_PITCH;
m_pitch.SetTarget( pitchTarget, deltaTime );
}
//-----------------------------------------------------------------------------
// Purpose: Ramps the volume to a new value
// Input : volumeTarget - new volume
// deltaTime - seconds to reach the new volume
//-----------------------------------------------------------------------------
void CSoundPatch::ChangeVolume( float volumeTarget, float deltaTime )
{
m_flags |= SND_CHANGE_VOL;
if ( volumeTarget > 1.0 )
volumeTarget = 1.0;
m_volume.SetTarget( volumeTarget, deltaTime );
}
//-----------------------------------------------------------------------------
// Purpose: Fade volume to zero AND SHUT DOWN THIS SOUND
// Input : deltaTime - seconds before done/shutdown
//-----------------------------------------------------------------------------
void CSoundPatch::FadeOut( float deltaTime, bool destroyOnFadeout )
{
ChangeVolume( 0, deltaTime );
if ( !destroyOnFadeout )
{
m_shutdownTime = g_pEffects->Time() + deltaTime;
}
}
//-----------------------------------------------------------------------------
// Purpose: Get the sound's current pitch
//-----------------------------------------------------------------------------
float CSoundPatch::GetPitch( void )
{
return m_pitch.Value();
}
//-----------------------------------------------------------------------------
// Purpose: Get the sound's current volume
//-----------------------------------------------------------------------------
float CSoundPatch::GetVolume( void )
{
return m_volume.Value();
}
#ifdef CLIENT_DLL
//-----------------------------------------------------------------------------
// Purpose: Get the playing status of the sound
// Returns: Sounds playing status from the engine
//-----------------------------------------------------------------------------
bool CSoundPatch::IsStillPlaying( void )
{
return enginesound->IsSoundStillPlaying(m_guid);
}
//-----------------------------------------------------------------------------
// Purpose: Get the sound's current elapsed time
// Returns: Time in seconds
//-----------------------------------------------------------------------------
float CSoundPatch::GetElapsedTime( void )
{
// convert to seconds
return enginesound->GetElapsedTimeByGuid(m_guid) * 0.01;
}
#endif
//-----------------------------------------------------------------------------
// Returns the ent index
//-----------------------------------------------------------------------------
inline int CSoundPatch::EntIndex() const
{
Assert( !m_hEnt.IsValid() || m_hEnt.Get() );
return m_hEnt.Get() ? m_hEnt->entindex() : -1;
}
//-----------------------------------------------------------------------------
// Purpose: SoundPatches take volumes between 0 & 1, and use that to multiply the sounds.txt specified volume.
// This function is an internal method of accessing the real volume passed into the engine (i.e. post multiply)
// Output : float
//-----------------------------------------------------------------------------
float CSoundPatch::GetVolumeForEngine( void )
{
return ( m_flScriptVolume * m_volume.Value() );
}
//-----------------------------------------------------------------------------
// Purpose: Stop the sound
//-----------------------------------------------------------------------------
void CSoundPatch::Shutdown( void )
{
// Msg( "Removing sound %s\n", m_pszSoundName );
if ( m_isPlaying )
{
int entIndex = -1;
if ( m_hEnt.Get() )
{
entIndex = EntIndex();
}
else
{
// may have deleted the entity after starting the sound, but before stopping the sound, try the saved index
// this will handle that case so a sound patch doesn't get stuck on
entIndex = m_soundEntityIndex;
}
Assert( entIndex >= 0 );
// BUGBUG: Don't crash in release mode
if ( entIndex >= 0 )
{
CBaseEntity::StopSound( entIndex, m_entityChannel, STRING( m_iszSoundName ) );
}
m_isPlaying = false;
}
}
//-----------------------------------------------------------------------------
// Purpose: Update all envelopes and send appropriate data to the client
// Input : time - new global clock
// deltaTime - amount of time that has passed
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CSoundPatch::Update( float time, float deltaTime )
{
VPROF( "CSoundPatch::Update" );
if ( m_shutdownTime && time > m_shutdownTime )
{
Shutdown();
return false;
}
if ( EntIndex() < 0 )
{
// FIXME: The pointer to this soundpatch is probably leaked since no entity is around to clean it up (ywb)
DevWarning( "CSoundPatch::Update: Removing CSoundPatch (%s) with NULL EHandle\n", STRING(m_iszSoundName) );
return false;
}
if ( m_pitch.ShouldUpdate() )
{
m_pitch.Update( deltaTime );
m_flags |= SND_CHANGE_PITCH;
}
else
{
m_flags &= ~SND_CHANGE_PITCH;
}
if ( m_volume.ShouldUpdate() )
{
m_volume.Update( deltaTime );
m_flags |= SND_CHANGE_VOL;
}
else
{
m_flags &= ~SND_CHANGE_VOL;
}
// if ( m_flags && m_Filter.IsActive() )
if ( m_flags )
{
// SoundPatches take volumes between 0 & 1, and use that to multiply the sounds.txt specified volume.
// Because of this, we need to always set the SND_CHANGE_VOL flag when we emit sound, or it'll use the scriptfile's instead.
m_flags |= SND_CHANGE_VOL;
EmitSound_t ep;
ep.m_nChannel = m_entityChannel;
ep.m_pSoundName = STRING(m_iszSoundName);
ep.m_flVolume = GetVolumeForEngine();
ep.m_SoundLevel = m_soundlevel;
ep.m_nFlags = m_flags;
ep.m_nPitch = (int)m_pitch.Value();
// only pass the position if it's coming from the world
if( EntIndex() == 0 )
ep.m_pOrigin = &m_soundOrigin;
CBaseEntity::EmitSound( m_Filter, EntIndex(), ep );
m_flags = 0;
}
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Sound is going to start playing again, clear any shutdown time
//-----------------------------------------------------------------------------
void CSoundPatch::Reset( void )
{
m_shutdownTime = 0;
}
//-----------------------------------------------------------------------------
// Purpose: Start playing the sound - send updates to the client
//-----------------------------------------------------------------------------
void CSoundPatch::StartSound( float flStartTime )
{
// Msg( "Start sound %s\n", m_pszSoundName );
m_flags = 0;
if ( m_Filter.IsActive() )
{
EmitSound_t ep;
ep.m_nChannel = m_entityChannel;
ep.m_pSoundName = STRING(m_iszSoundName);
ep.m_flVolume = GetVolumeForEngine();
ep.m_SoundLevel = m_soundlevel;
// only pass the position if it's coming from the world
if( EntIndex() == 0 )
ep.m_pOrigin = &m_soundOrigin;
if ( V_stristr( STRING(m_iszSoundName), "music" ) )
{
ep.m_nFlags = m_baseFlags;
}
else
{
ep.m_nFlags = (SND_CHANGE_VOL | m_baseFlags);
}
ep.m_nPitch = (int)m_pitch.Value();
ep.m_bEmitCloseCaption = false;
if ( flStartTime )
{
ep.m_flSoundTime = flStartTime;
}
//#ifdef CLIENT_DLL
#ifdef ___NOT
if ( V_stristr( STRING(m_iszSoundName), "music" ) )
{
// Don't play synchronously - we'll get it with the volume adjustments
//engine->ClientCmd( VarArgs("play %s\n", ep.m_pSoundName) );
}
else
#endif
{
CBaseEntity::EmitSound( m_Filter, EntIndex(), ep );
#ifdef CLIENT_DLL
m_guid = enginesound->GetGuidForLastSoundEmitted();
#endif
}
CBaseEntity::EmitCloseCaption( m_Filter, EntIndex(), STRING( m_iszSoundScriptName ), ep.m_UtlVecSoundOrigin, m_flCloseCaptionDuration, true );
}
m_isPlaying = true;
}
//-----------------------------------------------------------------------------
// Purpose: resumes playing the sound on restore
//-----------------------------------------------------------------------------
void CSoundPatch::ResumeSound( void )
{
if ( IsPlaying() && m_Filter.IsActive() )
{
if ( EntIndex() >= 0 )
{
EmitSound_t ep;
ep.m_nChannel = m_entityChannel;
ep.m_pSoundName = STRING(m_iszSoundName);
ep.m_flVolume = GetVolumeForEngine();
ep.m_SoundLevel = m_soundlevel;
ep.m_nFlags = (SND_CHANGE_VOL | SND_CHANGE_PITCH | m_baseFlags);
ep.m_nPitch = (int)m_pitch.Value();
// only pass the position if it's coming from the world
if( EntIndex() == 0 )
ep.m_pOrigin = &m_soundOrigin;
CBaseEntity::EmitSound( m_Filter, EntIndex(), ep );
}
else
{
// FIXME: Lost the entity on restore. It might have been suppressed by the save/restore system.
// This will probably leak the sound patch since there's no one to delete it, but the next
// call to CSoundPatch::Update should at least remove it from the list of sound patches.
DevWarning( "CSoundPatch::ResumeSound: Lost EHAndle on restore - destroy the sound patch in your entity's StopLoopingSounds! (%s)\n", STRING( m_iszSoundName ) );
}
}
}
//-----------------------------------------------------------------------------
// Purpose: A new player's entered the game. See if we need to restart our sound.
//-----------------------------------------------------------------------------
void CSoundPatch::AddPlayerPost( CBasePlayer *pPlayer )
{
if ( m_Filter.IsActive() && m_Filter.AddRecipient(pPlayer) )
{
// Alrighty, he's new. We need to restart our sound just to him.
// Create a new filter just to him.
CSingleUserRecipientFilter filter( pPlayer );
EmitSound_t ep;
ep.m_nChannel = m_entityChannel;
ep.m_pSoundName = STRING(m_iszSoundName);
ep.m_flVolume = GetVolumeForEngine();
ep.m_SoundLevel = m_soundlevel;
ep.m_nFlags = (SND_CHANGE_VOL | m_baseFlags);
ep.m_nPitch = (int)m_pitch.Value();
// only pass the position if it's coming from the world
if( EntIndex() == 0 )
ep.m_pOrigin = &m_soundOrigin;
CBaseEntity::EmitSound( filter, EntIndex(), ep );
}
}
// This is an entry in the command queue. It's used to queue up various pitch and volume changes
// so you can define an envelope without writing timing code in an entity. Existing queued commands
// can be deleted later if the envelope changes dynamically.
#include "tier0/memdbgoff.h"
struct SoundCommand_t
{
SoundCommand_t( void ) { memset( this, 0, sizeof(*this) ); }
SoundCommand_t( CSoundPatch *pSound, float executeTime, soundcommands_t command, float deltaTime, float value ) : m_pPatch(pSound), m_time(executeTime), m_deltaTime(deltaTime), m_command(command), m_value(value) {}
CSoundPatch *m_pPatch;
float m_time;
float m_deltaTime;
soundcommands_t m_command;
float m_value;
SoundCommand_t *m_pNext;
DECLARE_SIMPLE_DATADESC();
DECLARE_FIXEDSIZE_ALLOCATOR(SoundCommand_t);
};
#include "tier0/memdbgon.h"
DEFINE_FIXEDSIZE_ALLOCATOR( SoundCommand_t, 32, CUtlMemoryPool::GROW_FAST );
BEGIN_SIMPLE_DATADESC( SoundCommand_t )
// NOTE: This doesn't need to be saved, sound commands are saved right after the patch
// they are associated with
// DEFINE_FIELD( m_pPatch, FIELD_????? )
DEFINE_FIELD( m_time, FIELD_TIME ),
DEFINE_FIELD( m_deltaTime, FIELD_FLOAT ),
DEFINE_FIELD( m_command, FIELD_INTEGER ),
DEFINE_FIELD( m_value, FIELD_FLOAT ),
// DEFINE_FIELD( m_pNext, FIELD_????? )
END_DATADESC()
typedef SoundCommand_t *SOUNDCOMMANDPTR;
bool SoundCommandLessFunc( const SOUNDCOMMANDPTR &lhs, const SOUNDCOMMANDPTR &rhs )
{
// NOTE: A greater time means "less" priority
return ( lhs->m_time > rhs->m_time );
}
// This implements the sound controller
class CSoundControllerImp : public CSoundEnvelopeController, public CAutoGameSystemPerFrame
{
//-----------------------------------------------------------------------------
// internal functions, private to this file
//-----------------------------------------------------------------------------
public:
CSoundControllerImp( void ) : CAutoGameSystemPerFrame( "CSoundControllerImp" )
{
m_commandList.SetLessFunc( SoundCommandLessFunc );
}
void ProcessCommand( SoundCommand_t *pCmd );
void RemoveFromList( CSoundPatch *pSound );
void SaveSoundPatch( CSoundPatch *pSound, ISave *pSave );
void RestoreSoundPatch( CSoundPatch **ppSound, IRestore *pRestore );
virtual void OnRestore();
//-----------------------------------------------------------------------------
// external interface functions (from CSoundEnvelopeController)
//-----------------------------------------------------------------------------
public:
// Start this sound playing, or reset if already playing with new volume/pitch
void Play( CSoundPatch *pSound, float volume, float pitch, float flStartTime = 0 );
void CommandAdd( CSoundPatch *pSound, float executeDeltaTime, soundcommands_t command, float commandTime, float commandValue );
void SystemReset( void );
void SystemUpdate( void );
void CommandClear( CSoundPatch *pSound );
void Shutdown( CSoundPatch *pSound );
CSoundPatch *SoundCreate( IRecipientFilter& filter, int nEntIndex, const char *pSoundName );
CSoundPatch *SoundCreate( IRecipientFilter& filter, int nEntIndex, int channel, const char *pSoundName,
float attenuation, float scriptVolume = 1.0f );
CSoundPatch *SoundCreate( IRecipientFilter& filter, int nEntIndex, int channel, const char *pSoundName,
float attenuation, const Vector *pSoundOrigin, float scriptVolume = 1.0f );
CSoundPatch *SoundCreate( IRecipientFilter& filter, int nEntIndex, int channel, const char *pSoundName,
soundlevel_t soundlevel );
CSoundPatch *SoundCreate( IRecipientFilter& filter, int nEntIndex, const EmitSound_t &es );
void SoundDestroy( CSoundPatch *pSound );
void SoundChangePitch( CSoundPatch *pSound, float pitchTarget, float deltaTime );
void SoundChangeVolume( CSoundPatch *pSound, float volumeTarget, float deltaTime );
void SoundFadeOut( CSoundPatch *pSound, float deltaTime, bool destroyOnFadeout );
float SoundGetPitch( CSoundPatch *pSound );
float SoundGetVolume( CSoundPatch *pSound );
#ifdef CLIENT_DLL
float SoundGetElapsedTime( CSoundPatch *pSound );
bool SoundIsStillPlaying( CSoundPatch *pSound );
int SoundGetGuid( CSoundPatch *pSound );
#endif
string_t SoundGetName( CSoundPatch *pSound ) { return pSound->GetName(); }
string_t SoundGetScriptName( CSoundPatch *pSound ) { return pSound->GetScriptName(); }
void SoundSetCloseCaptionDuration( CSoundPatch *pSound, float flDuration ) { pSound->SetCloseCaptionDuration(flDuration); }
float SoundPlayEnvelope( CSoundPatch *pSound, soundcommands_t soundCommand, envelopePoint_t *points, int numPoints );
float SoundPlayEnvelope( CSoundPatch *pSound, soundcommands_t soundCommand, envelopeDescription_t *envelope );
void CheckLoopingSoundsForPlayer( CBasePlayer *pPlayer );
// Inserts the command into the list, sorted by time
void CommandInsert( SoundCommand_t *pCommand );
#ifdef CLIENT_DLL
// CAutoClientSystem
virtual void Update( float frametime )
{
SystemUpdate();
}
#else
virtual void PreClientUpdate()
{
SystemUpdate();
}
#endif
virtual void LevelShutdownPreEntity()
{
SystemReset();
}
private:
CUtlVector<CSoundPatch *> m_soundList;
CUtlPriorityQueue<SoundCommand_t *> m_commandList;
float m_flLastTime;
};
// Execute a command from the list
// currently only 3 commands
// UNDONE: Add start command?
void CSoundControllerImp::ProcessCommand( SoundCommand_t *pCmd )
{
switch( pCmd->m_command )
{
case SOUNDCTRL_CHANGE_VOLUME:
pCmd->m_pPatch->ChangeVolume( pCmd->m_value, pCmd->m_deltaTime );
break;
case SOUNDCTRL_CHANGE_PITCH:
pCmd->m_pPatch->ChangePitch( pCmd->m_value, pCmd->m_deltaTime );
break;
case SOUNDCTRL_STOP:
pCmd->m_pPatch->Shutdown();
break;
case SOUNDCTRL_DESTROY:
RemoveFromList( pCmd->m_pPatch );
delete pCmd->m_pPatch;
pCmd->m_pPatch = NULL;
break;
}
}
//-----------------------------------------------------------------------------
// Purpose: Remove this sound from the sound list & shutdown (not in external interface)
// Input : *pSound - patch to remove
//-----------------------------------------------------------------------------
void CSoundControllerImp::RemoveFromList( CSoundPatch *pSound )
{
m_soundList.FindAndRemove( pSound );
pSound->Shutdown();
}
//-----------------------------------------------------------------------------
// Start this sound playing, or reset if already playing with new volume/pitch
//-----------------------------------------------------------------------------
void CSoundControllerImp::Play( CSoundPatch *pSound, float volume, float pitch, float flStartTime )
{
// reset the vars
pSound->Reset();
pSound->ChangeVolume( volume, 0 );
pSound->ChangePitch( pitch, 0 );
if ( pSound->IsPlaying() )
{
// remove any previous commands in the queue
CommandClear( pSound );
}
else
{
m_soundList.AddToTail( pSound );
pSound->StartSound( flStartTime );
}
}
//-----------------------------------------------------------------------------
// Inserts the command into the list, sorted by time
//-----------------------------------------------------------------------------
void CSoundControllerImp::CommandInsert( SoundCommand_t *pCommand )
{
m_commandList.Insert( pCommand );
}
//-----------------------------------------------------------------------------
// Purpose: puts a command into the queue
// Input : *pSound - patch this command affects
// executeDeltaTime - relative time to execute this command
// command - command to execute (SOUNDCTRL_*)
// commandTime - commands have 2 parameters, a time and a value
// value -
// Output : void
//-----------------------------------------------------------------------------
void CSoundControllerImp::CommandAdd( CSoundPatch *pSound, float executeDeltaTime, soundcommands_t command, float commandTime, float commandValue )
{
SoundCommand_t *pCommand = new SoundCommand_t( pSound, g_pEffects->Time() + executeDeltaTime, command, commandTime, commandValue );
CommandInsert( pCommand );
}
// Reset the whole system (level change, etc.)
void CSoundControllerImp::SystemReset( void )
{
for ( int i = m_soundList.Count()-1; i >=0; i-- )
{
CSoundPatch *pNode = m_soundList[i];
// shutdown all active sounds
pNode->Shutdown();
}
// clear the list
m_soundList.Purge();
// clear the command queue
m_commandList.RemoveAll();
}
//-----------------------------------------------------------------------------
// Purpose: Update the active sounds, dequeue any events and move the ramps
//-----------------------------------------------------------------------------
void CSoundControllerImp::SystemUpdate( void )
{
VPROF( "CSoundControllerImp::SystemUpdate" );
float time = g_pEffects->Time();
float deltaTime = time - m_flLastTime;
// handle clock resets
if ( deltaTime < 0 )
deltaTime = 0;
m_flLastTime = time;
{
VPROF( "CSoundControllerImp::SystemUpdate:processcommandlist" );
while ( m_commandList.Count() )
{
SoundCommand_t *pCmd = m_commandList.ElementAtHead();
// Commands are sorted by time.
// process any that should occur by the current time
if ( time >= pCmd->m_time )
{
m_commandList.RemoveAtHead();
ProcessCommand( pCmd );
delete pCmd;
}
else
{
break;
}
}
}
// NOTE: Because this loop goes from the end to the beginning
// we can fast remove inside it without breaking the indexing
{
VPROF( "CSoundControllerImp::SystemUpdate:removesounds" );
for ( int i = m_soundList.Count()-1; i >=0; i-- )
{
CSoundPatch *pNode = m_soundList[i];
if ( !pNode->Update( time, deltaTime ) )
{
pNode->Reset();
m_soundList.FastRemove( i );
}
}
}
}
// Remove any envelope commands from the list (dynamically changing envelope)
void CSoundControllerImp::CommandClear( CSoundPatch *pSound )
{
for ( int i = m_commandList.Count()-1; i >= 0; i-- )
{
SoundCommand_t *pCmd = m_commandList.Element( i );
if ( pCmd->m_pPatch == pSound )
{
m_commandList.RemoveAt(i);
delete pCmd;
}
}
}
//-----------------------------------------------------------------------------
// Saves the sound patch + associated commands
//-----------------------------------------------------------------------------
void CSoundControllerImp::SaveSoundPatch( CSoundPatch *pSoundPatch, ISave *pSave )
{
int i;
// Write out the sound patch
pSave->StartBlock();
pSave->WriteAll( pSoundPatch );
pSave->EndBlock();
// Count the number of commands that refer to the sound patch
int nCount = 0;
for ( i = m_commandList.Count()-1; i >= 0; i-- )
{
SoundCommand_t *pCmd = m_commandList.Element( i );
if ( pCmd->m_pPatch == pSoundPatch )
{
nCount++;
}
}
// Write out the number of commands, followed by each command itself
pSave->StartBlock();
pSave->WriteInt( &nCount );
for ( i = m_commandList.Count()-1; i >= 0; i-- )
{
SoundCommand_t *pCmd = m_commandList.Element( i );
if ( pCmd->m_pPatch == pSoundPatch )
{
pSave->StartBlock();
pSave->WriteAll( pCmd );
pSave->EndBlock();
}
}
pSave->EndBlock();
}
//-----------------------------------------------------------------------------
// Restores the sound patch + associated commands
//-----------------------------------------------------------------------------
void CSoundControllerImp::RestoreSoundPatch( CSoundPatch **ppSoundPatch, IRestore *pRestore )
{
CSoundPatch *pPatch = new CSoundPatch;
// read the sound patch data from the memory block
pRestore->StartBlock();
bool bOk = ( pRestore->ReadAll( pPatch ) != 0 );
pRestore->EndBlock();
bOk = (bOk && pPatch->IsPlaying()) ? true : false;
if (bOk)
{
m_soundList.AddToTail( pPatch );
}
// Count the number of commands that refer to the sound patch
pRestore->StartBlock();
if ( bOk )
{
int nCount;
pRestore->ReadInt( &nCount );
while ( --nCount >= 0 )
{
SoundCommand_t *pCommand = new SoundCommand_t;
pRestore->StartBlock();
if ( pRestore->ReadAll( pCommand ) )
{
pCommand->m_pPatch = pPatch;
CommandInsert( pCommand );
}
pRestore->EndBlock();
}
}
pRestore->EndBlock();
*ppSoundPatch = pPatch;
}
//-----------------------------------------------------------------------------
// Purpose: immediately stop playing this sound
// Input : *pSound - Patch to shut down
//-----------------------------------------------------------------------------
void CSoundControllerImp::Shutdown( CSoundPatch *pSound )
{
if ( !pSound )
return;
pSound->Shutdown();
CommandClear( pSound );
RemoveFromList( pSound );
}
CSoundPatch *CSoundControllerImp::SoundCreate( IRecipientFilter& filter, int nEntIndex, const char *pSoundName )
{
CSoundPatch *pSound = new CSoundPatch;
// FIXME: This is done so we don't have to futz with the public interface
EHANDLE hEnt = (nEntIndex != -1) ? g_pEntityList->GetNetworkableHandle( nEntIndex ) : NULL;
pSound->Init( &filter, hEnt.Get(), CHAN_AUTO, pSoundName, SNDLVL_NORM, NULL );
return pSound;
}
CSoundPatch *CSoundControllerImp::SoundCreate( IRecipientFilter& filter, int nEntIndex, int channel,
const char *pSoundName, float attenuation, float scriptVolume )
{
CSoundPatch *pSound = new CSoundPatch;
EHANDLE hEnt = (nEntIndex != -1) ? g_pEntityList->GetNetworkableHandle( nEntIndex ) : NULL;
pSound->Init( &filter, hEnt.Get(), channel, pSoundName, ATTN_TO_SNDLVL( attenuation ), NULL, scriptVolume );
return pSound;
}
CSoundPatch *CSoundControllerImp::SoundCreate( IRecipientFilter& filter, int nEntIndex, int channel,
const char *pSoundName, float attenuation, const Vector *pSoundOrigin, float scriptVolume )
{
CSoundPatch *pSound = new CSoundPatch;
EHANDLE hEnt = (nEntIndex != -1) ? g_pEntityList->GetNetworkableHandle( nEntIndex ) : NULL;
pSound->Init( &filter, hEnt.Get(), channel, pSoundName, ATTN_TO_SNDLVL( attenuation ), pSoundOrigin, scriptVolume );
return pSound;
}
CSoundPatch *CSoundControllerImp::SoundCreate( IRecipientFilter& filter, int nEntIndex, int channel,
const char *pSoundName, soundlevel_t soundlevel )
{
CSoundPatch *pSound = new CSoundPatch;
EHANDLE hEnt = (nEntIndex != -1) ? g_pEntityList->GetNetworkableHandle( nEntIndex ) : NULL;
pSound->Init( &filter, hEnt.Get(), channel, pSoundName, soundlevel, NULL );
return pSound;
}
CSoundPatch *CSoundControllerImp::SoundCreate( IRecipientFilter& filter, int nEntIndex, const EmitSound_t &es )
{
CSoundPatch *pSound = new CSoundPatch;
// FIXME: This is done so we don't have to futz with the public interface
EHANDLE hEnt = (nEntIndex != -1) ? g_pEntityList->GetNetworkableHandle( nEntIndex ) : NULL;
pSound->Init( &filter, hEnt.Get(), es.m_nChannel, es.m_pSoundName, es.m_SoundLevel, es.m_pOrigin );
pSound->ChangeVolume( es.m_flVolume, 0 );
pSound->ChangePitch( es.m_nPitch, 0 );
if ( es.m_nFlags & SND_SHOULDPAUSE )
{
pSound->SetBaseFlags( SND_SHOULDPAUSE );
}
return pSound;
}
void CSoundControllerImp::SoundDestroy( CSoundPatch *pSound )
{
if ( !pSound )
return;
Shutdown( pSound );
delete pSound;
}
void CSoundControllerImp::SoundChangePitch( CSoundPatch *pSound, float pitchTarget, float deltaTime )
{
pSound->ChangePitch( pitchTarget, deltaTime );
}
void CSoundControllerImp::SoundChangeVolume( CSoundPatch *pSound, float volumeTarget, float deltaTime )
{
pSound->ChangeVolume( volumeTarget, deltaTime );
}
#ifdef CLIENT_DLL
int CSoundControllerImp::SoundGetGuid( CSoundPatch *pSound )
{
return pSound->GetGuid();
}
float CSoundControllerImp::SoundGetElapsedTime( CSoundPatch *pSound )
{
return pSound->GetElapsedTime();
}
bool CSoundControllerImp::SoundIsStillPlaying( CSoundPatch *pSound )
{
return pSound->IsStillPlaying();
}
#endif
float CSoundControllerImp::SoundGetPitch( CSoundPatch *pSound )
{
return pSound->GetPitch();
}
float CSoundControllerImp::SoundGetVolume( CSoundPatch *pSound )
{
return pSound->GetVolume();
}
void CSoundControllerImp::SoundFadeOut( CSoundPatch *pSound, float deltaTime, bool destroyOnFadeout )
{
if ( destroyOnFadeout && (deltaTime == 0.0f) )
{
SoundDestroy( pSound );
return;
}
pSound->FadeOut( deltaTime, destroyOnFadeout );
if ( destroyOnFadeout )
{
CommandAdd( pSound, deltaTime, SOUNDCTRL_DESTROY, 0.0f, 0.0f );
}
}
//-----------------------------------------------------------------------------
// Purpose: Queue a list of envelope points into a sound patch's event list
// Input : *pSound - The sound patch to be operated on
// soundCommand - Type of operation the envelope describes
// *points - List of enevelope points
// numPoints - Number of points provided
// Output : float - Returns the total duration of the envelope
//-----------------------------------------------------------------------------
float CSoundControllerImp::SoundPlayEnvelope( CSoundPatch *pSound, soundcommands_t soundCommand, envelopePoint_t *points, int numPoints )
{
float amplitude = 0.0f;
float duration = 0.0f;
float totalDuration = 0.0f;
Assert( points );
// Clear out all previously acting commands
CommandClear( pSound );
// Evaluate and queue all points
for ( int i = 0; i < numPoints; i++ )
{
// See if we're keeping our last amplitude for this new point
if ( ( points[i].amplitudeMin != -1.0f ) || ( points[i].amplitudeMax != -1.0f ) )
{
amplitude = random->RandomFloat( points[i].amplitudeMin, points[i].amplitudeMax );
}
else if ( i == 0 )
{
// Can't do this on the first entry
Msg( "Invalid starting amplitude value in envelope! (Cannot be -1)\n" );
}
// See if we're keeping our last duration for this new point
if ( ( points[i].durationMin != -1.0f ) || ( points[i].durationMax != -1.0f ) )
{
duration = random->RandomFloat( points[i].durationMin, points[i].durationMax );
//duration = points[i].durationMin;
}
else if ( i == 0 )
{
// Can't do this on the first entry
Msg( "Invalid starting duration value in envelope! (Cannot be -1)\n" );
}
// Queue the command
CommandAdd( pSound, totalDuration, soundCommand, duration, amplitude );
// Tack this command's duration onto the running duration
totalDuration += duration;
}
return totalDuration;
}
//-----------------------------------------------------------------------------
// Purpose: Queue a list of envelope points into a sound patch's event list
// Input : *pSound - The sound patch to be operated on
// soundCommand - Type of operation the envelope describes
// *envelope - The envelope description to be queued
// Output : float - Returns the total duration of the envelope
//-----------------------------------------------------------------------------
float CSoundControllerImp::SoundPlayEnvelope( CSoundPatch *pSound, soundcommands_t soundCommand, envelopeDescription_t *envelope )
{
return SoundPlayEnvelope( pSound, soundCommand, envelope->pPoints, envelope->nNumPoints );
}
//-----------------------------------------------------------------------------
// Purpose: Looping sounds are often started in entity spawn/activate functions.
// In singleplayer, the player's not ready to receive sounds then, so restart
// and SoundPatches that are active and have no receivers.
//-----------------------------------------------------------------------------
void CSoundControllerImp::CheckLoopingSoundsForPlayer( CBasePlayer *pPlayer )
{
for ( int i = m_soundList.Count()-1; i >=0; i-- )
{
CSoundPatch *pNode = m_soundList[i];
pNode->AddPlayerPost( pPlayer );
}
}
//-----------------------------------------------------------------------------
// Purpose: Resumes saved soundpatches
//-----------------------------------------------------------------------------
void CSoundControllerImp::OnRestore()
{
for ( int i = m_soundList.Count()-1; i >=0; i-- )
{
CSoundPatch *pNode = m_soundList[i];
if ( pNode && pNode->IsPlaying() )
{
pNode->ResumeSound();
}
}
}
//-----------------------------------------------------------------------------
// Singleton accessors
//-----------------------------------------------------------------------------
static CSoundControllerImp g_Controller;
CSoundEnvelopeController &CSoundEnvelopeController::GetController( void )
{
return g_Controller;
}
//-----------------------------------------------------------------------------
// Queues up sound patches to save/load
//-----------------------------------------------------------------------------
class CSoundPatchSaveRestoreOps : public CClassPtrSaveRestoreOps
{
public:
virtual void Save( const SaveRestoreFieldInfo_t &fieldInfo, ISave *pSave )
{
pSave->StartBlock();
int nSoundPatchCount = fieldInfo.pTypeDesc->fieldSize;
CSoundPatch **ppSoundPatch = (CSoundPatch**)fieldInfo.pField;
while ( --nSoundPatchCount >= 0 )
{
// Write out commands associated with this sound patch
g_Controller.SaveSoundPatch( *ppSoundPatch, pSave );
++ppSoundPatch;
}
pSave->EndBlock();
}
virtual void Restore( const SaveRestoreFieldInfo_t &fieldInfo, IRestore *pRestore )
{
pRestore->StartBlock();
int nSoundPatchCount = fieldInfo.pTypeDesc->fieldSize;
CSoundPatch **ppSoundPatch = (CSoundPatch**)fieldInfo.pField;
while ( --nSoundPatchCount >= 0 )
{
// Write out commands associated with this sound patch
g_Controller.RestoreSoundPatch( ppSoundPatch, pRestore );
++ppSoundPatch;
}
pRestore->EndBlock();
}
};
static CSoundPatchSaveRestoreOps s_SoundPatchSaveRestoreOps;
ISaveRestoreOps *GetSoundSaveRestoreOps( )
{
return &s_SoundPatchSaveRestoreOps;
}