You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
560 lines
16 KiB
560 lines
16 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: Implements an entity that measures sound volume at a point in a map. |
|
// |
|
// This entity listens as though it is an NPC, meaning it will only |
|
// hear sounds that were emitted using the CSound::InsertSound function. |
|
// |
|
// It does not hear danger sounds since they are not technically sounds. |
|
// |
|
//============================================================================= |
|
|
|
#include "cbase.h" |
|
#include "entityinput.h" |
|
#include "entityoutput.h" |
|
#include "eventqueue.h" |
|
#include "mathlib/mathlib.h" |
|
#include "soundent.h" |
|
#include "envmicrophone.h" |
|
#include "soundflags.h" |
|
#include "engine/IEngineSound.h" |
|
#include "filters.h" |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
//#define DEBUG_MICROPHONE |
|
|
|
const float MICROPHONE_SETTLE_EPSILON = 0.005; |
|
|
|
// List of env_microphones who want to be told whenever a sound is started |
|
static CUtlVector< CHandle<CEnvMicrophone> > s_Microphones; |
|
|
|
|
|
LINK_ENTITY_TO_CLASS(env_microphone, CEnvMicrophone); |
|
|
|
BEGIN_DATADESC( CEnvMicrophone ) |
|
|
|
DEFINE_KEYFIELD(m_bDisabled, FIELD_BOOLEAN, "StartDisabled"), |
|
DEFINE_FIELD(m_hMeasureTarget, FIELD_EHANDLE), |
|
DEFINE_KEYFIELD(m_nSoundMask, FIELD_INTEGER, "SoundMask"), |
|
DEFINE_KEYFIELD(m_flSensitivity, FIELD_FLOAT, "Sensitivity"), |
|
DEFINE_KEYFIELD(m_flSmoothFactor, FIELD_FLOAT, "SmoothFactor"), |
|
DEFINE_KEYFIELD(m_iszSpeakerName, FIELD_STRING, "SpeakerName"), |
|
DEFINE_KEYFIELD(m_iszListenFilter, FIELD_STRING, "ListenFilter"), |
|
DEFINE_FIELD(m_hListenFilter, FIELD_EHANDLE), |
|
DEFINE_FIELD(m_hSpeaker, FIELD_EHANDLE), |
|
// DEFINE_FIELD(m_bAvoidFeedback, FIELD_BOOLEAN), // DONT SAVE |
|
DEFINE_KEYFIELD(m_iSpeakerDSPPreset, FIELD_INTEGER, "speaker_dsp_preset" ), |
|
DEFINE_KEYFIELD(m_flMaxRange, FIELD_FLOAT, "MaxRange"), |
|
DEFINE_AUTO_ARRAY(m_szLastSound, FIELD_CHARACTER), |
|
|
|
DEFINE_INPUTFUNC(FIELD_VOID, "Enable", InputEnable), |
|
DEFINE_INPUTFUNC(FIELD_VOID, "Disable", InputDisable), |
|
DEFINE_INPUTFUNC(FIELD_STRING, "SetSpeakerName", InputSetSpeakerName), |
|
|
|
DEFINE_OUTPUT(m_SoundLevel, "SoundLevel"), |
|
DEFINE_OUTPUT(m_OnRoutedSound, "OnRoutedSound" ), |
|
DEFINE_OUTPUT(m_OnHeardSound, "OnHeardSound" ), |
|
|
|
END_DATADESC() |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
CEnvMicrophone::~CEnvMicrophone( void ) |
|
{ |
|
s_Microphones.FindAndRemove( this ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Called before spawning, after keyvalues have been handled. |
|
//----------------------------------------------------------------------------- |
|
void CEnvMicrophone::Spawn(void) |
|
{ |
|
// |
|
// Build our sound type mask from our spawnflags. |
|
// |
|
static int nFlags[][2] = |
|
{ |
|
{ SF_MICROPHONE_SOUND_COMBAT, SOUND_COMBAT }, |
|
{ SF_MICROPHONE_SOUND_WORLD, SOUND_WORLD }, |
|
{ SF_MICROPHONE_SOUND_PLAYER, SOUND_PLAYER }, |
|
{ SF_MICROPHONE_SOUND_BULLET_IMPACT, SOUND_BULLET_IMPACT }, |
|
{ SF_MICROPHONE_SOUND_EXPLOSION, SOUND_CONTEXT_EXPLOSION }, |
|
}; |
|
|
|
for (int i = 0; i < sizeof(nFlags) / sizeof(nFlags[0]); i++) |
|
{ |
|
if (m_spawnflags & nFlags[i][0]) |
|
{ |
|
m_nSoundMask |= nFlags[i][1]; |
|
} |
|
} |
|
|
|
if (m_flSensitivity == 0) |
|
{ |
|
// |
|
// Avoid a divide by zero in CanHearSound. |
|
// |
|
m_flSensitivity = 1; |
|
} |
|
else if (m_flSensitivity > 10) |
|
{ |
|
m_flSensitivity = 10; |
|
} |
|
|
|
m_flSmoothFactor = clamp(m_flSmoothFactor, 0.f, 0.9f); |
|
|
|
if (!m_bDisabled) |
|
{ |
|
SetNextThink( gpGlobals->curtime + 0.1f ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Called after all entities have spawned and after a load game. |
|
// Finds the reference point at which to measure sound level. |
|
//----------------------------------------------------------------------------- |
|
void CEnvMicrophone::Activate(void) |
|
{ |
|
BaseClass::Activate(); |
|
|
|
// Get a handle to my filter entity if there is one |
|
if (m_iszListenFilter != NULL_STRING) |
|
{ |
|
m_hListenFilter = dynamic_cast<CBaseFilter *>(gEntList.FindEntityByName( NULL, m_iszListenFilter )); |
|
} |
|
|
|
if (m_target != NULL_STRING) |
|
{ |
|
m_hMeasureTarget = gEntList.FindEntityByName(NULL, STRING(m_target) ); |
|
|
|
// |
|
// If we were given a bad measure target, just measure sound where we are. |
|
// |
|
if ((m_hMeasureTarget == NULL) || (m_hMeasureTarget->edict() == NULL)) |
|
{ |
|
// We've decided to disable this warning since this seems to be the 90% case. |
|
//Warning( "EnvMicrophone - Measure target not found or measure target with no origin. Using Self.!\n"); |
|
m_hMeasureTarget = this; |
|
} |
|
} |
|
else |
|
{ |
|
m_hMeasureTarget = this; |
|
} |
|
|
|
ActivateSpeaker(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CEnvMicrophone::OnRestore( void ) |
|
{ |
|
BaseClass::OnRestore(); |
|
|
|
ActivateSpeaker(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: If we've got a speaker, add ourselves to the list of microphones that want to listen |
|
//----------------------------------------------------------------------------- |
|
void CEnvMicrophone::ActivateSpeaker( void ) |
|
{ |
|
// If we're enabled, set the dsp_speaker preset to my specified one |
|
if ( !m_bDisabled ) |
|
{ |
|
ConVarRef dsp_speaker( "dsp_speaker" ); |
|
if ( dsp_speaker.IsValid() ) |
|
{ |
|
int iDSPPreset = m_iSpeakerDSPPreset; |
|
if ( !iDSPPreset ) |
|
{ |
|
// Reset it to the default |
|
iDSPPreset = atoi( dsp_speaker.GetDefault() ); |
|
} |
|
DevMsg( 2, "Microphone %s set dsp_speaker to %d.\n", STRING(GetEntityName()), iDSPPreset); |
|
dsp_speaker.SetValue( m_iSpeakerDSPPreset ); |
|
} |
|
} |
|
|
|
if ( m_iszSpeakerName != NULL_STRING ) |
|
{ |
|
// We've got a speaker to play heard sounds through. To do this, we need to add ourselves |
|
// to the list of microphones who want to be told whenever a sound is played. |
|
if ( s_Microphones.Find(this) == -1 ) |
|
{ |
|
s_Microphones.AddToTail( this ); |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Stops the microphone from sampling the sound level and firing the |
|
// SoundLevel output. |
|
//----------------------------------------------------------------------------- |
|
void CEnvMicrophone::InputEnable( inputdata_t &inputdata ) |
|
{ |
|
if (m_bDisabled) |
|
{ |
|
m_bDisabled = false; |
|
SetNextThink( gpGlobals->curtime + 0.1f ); |
|
|
|
ActivateSpeaker(); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Resumes sampling the sound level and firing the SoundLevel output. |
|
//----------------------------------------------------------------------------- |
|
void CEnvMicrophone::InputDisable( inputdata_t &inputdata ) |
|
{ |
|
m_bDisabled = true; |
|
if ( m_hSpeaker ) |
|
{ |
|
CBaseEntity::StopSound( m_hSpeaker->entindex(), CHAN_STATIC, m_szLastSound ); |
|
m_szLastSound[0] = 0; |
|
|
|
// Remove ourselves from the list of active mics |
|
s_Microphones.FindAndRemove( this ); |
|
} |
|
SetNextThink( TICK_NEVER_THINK ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : &inputdata - |
|
//----------------------------------------------------------------------------- |
|
void CEnvMicrophone::InputSetSpeakerName( inputdata_t &inputdata ) |
|
{ |
|
SetSpeakerName( inputdata.value.StringID() ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Checks whether this microphone can hear a given sound, and at what |
|
// relative volume level. |
|
// Input : pSound - Sound to test. |
|
// flVolume - Returns with the relative sound volume from 0 - 1. |
|
// Output : Returns true if the sound could be heard at the sample point, false if not. |
|
//----------------------------------------------------------------------------- |
|
bool CEnvMicrophone::CanHearSound(CSound *pSound, float &flVolume) |
|
{ |
|
flVolume = 0; |
|
|
|
if ( m_bDisabled ) |
|
{ |
|
return false; |
|
} |
|
|
|
// Cull out sounds except from specific entities |
|
CBaseFilter *pFilter = m_hListenFilter.Get(); |
|
if ( pFilter ) |
|
{ |
|
CBaseEntity *pSoundOwner = pSound->m_hOwner.Get(); |
|
if ( !pSoundOwner || !pFilter->PassesFilter( this, pSoundOwner ) ) |
|
{ |
|
return false; |
|
} |
|
} |
|
|
|
float flDistance = (pSound->GetSoundOrigin() - m_hMeasureTarget->GetAbsOrigin()).Length(); |
|
|
|
if (flDistance == 0) |
|
{ |
|
flVolume = 1.0; |
|
return true; |
|
} |
|
|
|
// Over our max range? |
|
if ( m_flMaxRange && flDistance > m_flMaxRange ) |
|
{ |
|
return false; |
|
} |
|
|
|
if (flDistance <= pSound->Volume() * m_flSensitivity) |
|
{ |
|
flVolume = 1 - (flDistance / (pSound->Volume() * m_flSensitivity)); |
|
flVolume = clamp(flVolume, 0.f, 1.f); |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Return true if the microphone can hear the specified sound |
|
//----------------------------------------------------------------------------- |
|
bool CEnvMicrophone::CanHearSound( int entindex, soundlevel_t soundlevel, float &flVolume, const Vector *pOrigin ) |
|
{ |
|
if ( m_bDisabled ) |
|
{ |
|
flVolume = 0; |
|
return false; |
|
} |
|
|
|
if ( ( m_spawnflags & SF_MICROPHONE_IGNORE_NONATTENUATED ) && soundlevel == SNDLVL_NONE ) |
|
{ |
|
return false; |
|
} |
|
|
|
// Sound might be coming from an origin or from an entity. |
|
CBaseEntity *pEntity = NULL; |
|
if ( entindex ) |
|
{ |
|
pEntity = CBaseEntity::Instance( engine->PEntityOfEntIndex(entindex) ); |
|
} |
|
|
|
// Cull out sounds except from specific entities |
|
CBaseFilter *pFilter = m_hListenFilter.Get(); |
|
if ( pFilter ) |
|
{ |
|
if ( !pEntity || !pFilter->PassesFilter( this, pEntity ) ) |
|
{ |
|
flVolume = 0; |
|
return false; |
|
} |
|
} |
|
|
|
float flDistance = 0; |
|
if ( pOrigin ) |
|
{ |
|
flDistance = pOrigin->DistTo( m_hMeasureTarget->GetAbsOrigin() ); |
|
} |
|
else if ( pEntity ) |
|
{ |
|
flDistance = pEntity->WorldSpaceCenter().DistTo( m_hMeasureTarget->GetAbsOrigin() ); |
|
} |
|
|
|
// Over our max range? |
|
if ( m_flMaxRange && flDistance > m_flMaxRange ) |
|
{ |
|
#ifdef DEBUG_MICROPHONE |
|
Msg("OUT OF RANGE.\n" ); |
|
#endif |
|
return false; |
|
} |
|
|
|
#ifdef DEBUG_MICROPHONE |
|
Msg(" flVolume %f ", flVolume ); |
|
#endif |
|
|
|
// Reduce the volume by the amount it fell to get to the microphone |
|
float gain = enginesound->GetDistGainFromSoundLevel( soundlevel, flDistance ); |
|
flVolume *= gain; |
|
|
|
#ifdef DEBUG_MICROPHONE |
|
Msg("dist %2f, soundlevel %d: gain %f", flDistance, (int)soundlevel, gain ); |
|
if ( !flVolume ) |
|
{ |
|
Msg(" : REJECTED\n" ); |
|
} |
|
else |
|
{ |
|
Msg(" : SENT\n" ); |
|
} |
|
#endif |
|
|
|
return ( flVolume > 0 ); |
|
} |
|
|
|
void CEnvMicrophone::SetSensitivity( float flSensitivity ) |
|
{ |
|
m_flSensitivity = flSensitivity; |
|
} |
|
|
|
void CEnvMicrophone::SetSpeakerName( string_t iszSpeakerName ) |
|
{ |
|
m_iszSpeakerName = iszSpeakerName; |
|
|
|
// Set the speaker to null. This will force it to find the speaker next time a sound is routed. |
|
m_hSpeaker = NULL; |
|
ActivateSpeaker(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Listens for sounds and updates the value of the SoundLevel output. |
|
//----------------------------------------------------------------------------- |
|
void CEnvMicrophone::Think(void) |
|
{ |
|
int nSound = CSoundEnt::ActiveList(); |
|
bool fHearSound = false; |
|
|
|
float flMaxVolume = 0; |
|
|
|
// |
|
// Find the loudest sound that this microphone cares about. |
|
// |
|
while (nSound != SOUNDLIST_EMPTY) |
|
{ |
|
CSound *pCurrentSound = CSoundEnt::SoundPointerForIndex(nSound); |
|
|
|
if (pCurrentSound) |
|
{ |
|
if (m_nSoundMask & pCurrentSound->SoundType()) |
|
{ |
|
float flVolume = 0; |
|
if (CanHearSound(pCurrentSound, flVolume) && (flVolume > flMaxVolume)) |
|
{ |
|
flMaxVolume = flVolume; |
|
fHearSound = true; |
|
} |
|
} |
|
} |
|
|
|
nSound = pCurrentSound->NextSound(); |
|
} |
|
|
|
if( fHearSound ) |
|
{ |
|
m_OnHeardSound.FireOutput( this, this ); |
|
} |
|
|
|
if (flMaxVolume != m_SoundLevel.Get()) |
|
{ |
|
// |
|
// Don't smooth if we are within an epsilon. This allows the output to stop firing |
|
// much more quickly. |
|
// |
|
if (fabs(flMaxVolume - m_SoundLevel.Get()) < MICROPHONE_SETTLE_EPSILON) |
|
{ |
|
m_SoundLevel.Set(flMaxVolume, this, this); |
|
} |
|
else |
|
{ |
|
m_SoundLevel.Set(flMaxVolume * (1 - m_flSmoothFactor) + m_SoundLevel.Get() * m_flSmoothFactor, this, this); |
|
} |
|
} |
|
|
|
SetNextThink( gpGlobals->curtime + 0.1f ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Hook for the sound system to tell us when a sound's been played |
|
//----------------------------------------------------------------------------- |
|
MicrophoneResult_t CEnvMicrophone::SoundPlayed( int entindex, const char *soundname, soundlevel_t soundlevel, float flVolume, int iFlags, int iPitch, const Vector *pOrigin, float soundtime, CUtlVector< Vector >& soundorigins ) |
|
{ |
|
if ( m_bAvoidFeedback ) |
|
return MicrophoneResult_Ok; |
|
|
|
// Don't hear sounds that have already been heard by a microphone to avoid feedback! |
|
if ( iFlags & SND_SPEAKER ) |
|
return MicrophoneResult_Ok; |
|
|
|
#ifdef DEBUG_MICROPHONE |
|
Msg("%s heard %s: ", STRING(GetEntityName()), soundname ); |
|
#endif |
|
|
|
if ( !CanHearSound( entindex, soundlevel, flVolume, pOrigin ) ) |
|
return MicrophoneResult_Ok; |
|
|
|
// We've heard it. Play it out our speaker. If our speaker's gone away, we're done. |
|
if ( !m_hSpeaker ) |
|
{ |
|
// First time, find our speaker. Done here, because finding it in Activate() wouldn't |
|
// find players, and we need to be able to specify !player for a speaker. |
|
if ( m_iszSpeakerName != NULL_STRING ) |
|
{ |
|
m_hSpeaker = gEntList.FindEntityByName(NULL, STRING(m_iszSpeakerName) ); |
|
|
|
if ( !m_hSpeaker ) |
|
{ |
|
Warning( "EnvMicrophone %s specifies a non-existent speaker name: %s\n", STRING(GetEntityName()), STRING(m_iszSpeakerName) ); |
|
m_iszSpeakerName = NULL_STRING; |
|
} |
|
} |
|
|
|
if ( !m_hSpeaker ) |
|
{ |
|
return MicrophoneResult_Remove; |
|
} |
|
} |
|
|
|
m_bAvoidFeedback = true; |
|
|
|
// Add the speaker flag. Detected at playback and applies the speaker filter. |
|
iFlags |= SND_SPEAKER; |
|
CPASAttenuationFilter filter( m_hSpeaker ); |
|
|
|
EmitSound_t ep; |
|
ep.m_nChannel = CHAN_STATIC; |
|
ep.m_pSoundName = soundname; |
|
ep.m_flVolume = flVolume; |
|
ep.m_SoundLevel = soundlevel; |
|
ep.m_nFlags = iFlags; |
|
ep.m_nPitch = iPitch; |
|
ep.m_pOrigin = &m_hSpeaker->GetAbsOrigin(); |
|
ep.m_flSoundTime = soundtime; |
|
ep.m_nSpeakerEntity = entindex; |
|
|
|
CBaseEntity::EmitSound( filter, m_hSpeaker->entindex(), ep ); |
|
|
|
Q_strncpy( m_szLastSound, soundname, sizeof(m_szLastSound) ); |
|
m_OnRoutedSound.FireOutput( this, this, 0 ); |
|
|
|
m_bAvoidFeedback = false; |
|
|
|
// Copy emitted origin to soundorigins array |
|
for ( int i = 0; i < ep.m_UtlVecSoundOrigin.Count(); ++i ) |
|
{ |
|
soundorigins.AddToTail( ep.m_UtlVecSoundOrigin[ i ] ); |
|
} |
|
|
|
// Do we want to allow the original sound to play? |
|
if ( m_spawnflags & SF_MICROPHONE_SWALLOW_ROUTED_SOUNDS ) |
|
{ |
|
return MicrophoneResult_Swallow; |
|
} |
|
|
|
return MicrophoneResult_Ok; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Called by the sound system whenever a sound is played so that |
|
// active microphones can have a chance to pick up the sound. |
|
// Output : Returns whether or not the sound was swallowed by the microphone. |
|
// Swallowed sounds should not be played by the sound system. |
|
//----------------------------------------------------------------------------- |
|
bool CEnvMicrophone::OnSoundPlayed( int entindex, const char *soundname, soundlevel_t soundlevel, float flVolume, int iFlags, int iPitch, const Vector *pOrigin, float soundtime, CUtlVector< Vector >& soundorigins ) |
|
{ |
|
bool bSwallowed = false; |
|
|
|
// Loop through all registered microphones and tell them the sound was just played |
|
int iCount = s_Microphones.Count(); |
|
if ( iCount > 0 ) |
|
{ |
|
// Iterate backwards because we might be deleting microphones. |
|
for ( int i = iCount - 1; i >= 0; i-- ) |
|
{ |
|
if ( s_Microphones[i] ) |
|
{ |
|
MicrophoneResult_t eResult = s_Microphones[i]->SoundPlayed( |
|
entindex, |
|
soundname, |
|
soundlevel, |
|
flVolume, |
|
iFlags, |
|
iPitch, |
|
pOrigin, |
|
soundtime, |
|
soundorigins ); |
|
|
|
if ( eResult == MicrophoneResult_Swallow ) |
|
{ |
|
// Microphone told us to swallow it |
|
bSwallowed = true; |
|
} |
|
else if ( eResult == MicrophoneResult_Remove ) |
|
{ |
|
s_Microphones.FastRemove( i ); |
|
} |
|
} |
|
} |
|
} |
|
|
|
return bSwallowed; |
|
}
|
|
|