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.
8401 lines
220 KiB
8401 lines
220 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: Main control for any streaming sound output device. |
|
// |
|
//===========================================================================// |
|
|
|
#include "audio_pch.h" |
|
#include "const.h" |
|
#include "cdll_int.h" |
|
#include "client_class.h" |
|
#include "icliententitylist.h" |
|
#include "tier0/vcrmode.h" |
|
#include "con_nprint.h" |
|
#include "tier0/icommandline.h" |
|
#include "vox_private.h" |
|
#include "../../traceinit.h" |
|
#include "../../cmd.h" |
|
#include "toolframework/itoolframework.h" |
|
#include "vstdlib/random.h" |
|
#include "vstdlib/jobthread.h" |
|
#include "vaudio/ivaudio.h" |
|
#include "../../client.h" |
|
#include "../../cl_main.h" |
|
#include "utldict.h" |
|
#include "mempool.h" |
|
#include "../../enginetrace.h" // for traceline |
|
#include "../../public/bspflags.h" // for traceline |
|
#include "../../public/gametrace.h" // for traceline |
|
#include "vphysics_interface.h" // for surface props |
|
#include "../../ispatialpartitioninternal.h" // for entity enumerator |
|
#include "../../debugoverlay.h" |
|
#include "icliententity.h" |
|
#include "../../cmodel_engine.h" |
|
#include "../../staticpropmgr.h" |
|
#include "../../server.h" |
|
#include "edict.h" |
|
#include "../../pure_server.h" |
|
#include "filesystem/IQueuedLoader.h" |
|
#include "voice.h" |
|
#if defined( _X360 ) |
|
#include "xbox/xbox_console.h" |
|
#include "xmp.h" |
|
#endif |
|
|
|
#include "replay/iclientreplaycontext.h" |
|
#include "replay/ireplaymovierenderer.h" |
|
|
|
#include "video/ivideoservices.h" |
|
extern IVideoServices *g_pVideo; |
|
|
|
/* |
|
#include "gl_model_private.h" |
|
#include "world.h" |
|
#include "vphysics_interface.h" |
|
#include "client_class.h" |
|
#include "server_class.h" |
|
*/ |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
/////////////////////////////////// |
|
// DEBUGGING |
|
// |
|
// Turn this on to print channel output msgs. |
|
// |
|
//#define DEBUG_CHANNELS |
|
|
|
#define SNDLVL_TO_DIST_MULT( sndlvl ) ( sndlvl ? ((pow( 10.0f, snd_refdb.GetFloat() / 20 ) / pow( 10.0f, (float)sndlvl / 20 )) / snd_refdist.GetFloat()) : 0 ) |
|
#define DIST_MULT_TO_SNDLVL( dist_mult ) (soundlevel_t)(int)( dist_mult ? ( 20 * log10( pow( 10.0f, snd_refdb.GetFloat() / 20 ) / (dist_mult * snd_refdist.GetFloat()) ) ) : 0 ) |
|
|
|
extern ConVar dsp_spatial; |
|
extern IPhysicsSurfaceProps *physprop; |
|
|
|
extern bool IsReplayRendering(); |
|
|
|
static void S_Play( const CCommand &args ); |
|
static void S_PlayVol( const CCommand &args ); |
|
void S_SoundList(void); |
|
static void S_Say ( const CCommand &args ); |
|
void S_Update_(float); |
|
void S_StopAllSounds(bool clear); |
|
void S_StopAllSoundsC(void); |
|
void S_ShutdownMixThread(); |
|
const char *GetClientClassname( SoundSource soundsource ); |
|
|
|
float SND_GetGainObscured( channel_t *ch, bool fplayersound, bool flooping, bool bAttenuated ); |
|
void DSP_ChangePresetValue( int idsp, int channel, int iproc, float value ); |
|
bool DSP_CheckDspAutoEnabled( void ); |
|
void DSP_SetDspAuto( int dsp_preset ); |
|
float dB_To_Radius ( float db ); |
|
int dsp_room_GetInt ( void ); |
|
|
|
bool MXR_LoadAllSoundMixers( void ); |
|
void MXR_ReleaseMemory( void ); |
|
int MXR_GetMixGroupListFromDirName( const char *pDirname, byte *pList, int listMax ); |
|
void MXR_GetMixGroupFromSoundsource( channel_t *pchan, SoundSource soundsource, soundlevel_t soundlevel); |
|
float MXR_GetVolFromMixGroup( int rgmixgroupid[8], int *plast_mixgroupid ); |
|
char *MXR_GetGroupnameFromId( int mixgroupid ); |
|
int MXR_GetMixgroupFromName( const char *pszgroupname ); |
|
void MXR_DebugShowMixVolumes( void ); |
|
#ifdef _DEBUG |
|
static void MXR_DebugSetMixGroupVolume( const CCommand &args ); |
|
#endif //_DEBUG |
|
void MXR_UpdateAllDuckerVolumes( void ); |
|
|
|
void ChannelSetVolTargets( channel_t *pch, int *pvolumes, int ivol_offset, int cvol ); |
|
void ChannelUpdateVolXfade( channel_t *pch ); |
|
void ChannelClearVolumes( channel_t *pch ); |
|
float VOX_GetChanVol(channel_t *ch); |
|
void ConvertListenerVectorTo2D( Vector *pvforward, Vector *pvright ); |
|
int ChannelGetMaxVol( channel_t *pch ); |
|
|
|
// Forceably ends voice tweak mode (only occurs during snd_restart |
|
void VoiceTweak_EndVoiceTweakMode(); |
|
bool VoiceTweak_IsStillTweaking(); |
|
// Only does anything for voice tweak channel so if view entity changes it doesn't fade out to zero volume |
|
void Voice_Spatialize( channel_t *channel ); |
|
|
|
// ======================================================================= |
|
// Internal sound data & structures |
|
// ======================================================================= |
|
|
|
channel_t channels[MAX_CHANNELS]; |
|
|
|
int total_channels; |
|
CActiveChannels g_ActiveChannels; |
|
static double g_LastSoundFrame = 0.0f; // last full frame of sound |
|
static double g_LastMixTime = 0.0f; // last time we did mixing |
|
static float g_EstFrameTime = 0.1f; // estimated frame time running average |
|
|
|
// x360 override to fade out game music when the user is playing music through the dashboard |
|
static float g_DashboardMusicMixValue = 1.0f; |
|
static float g_DashboardMusicMixTarget = 1.0f; |
|
const float g_DashboardMusicFadeRate = 0.5f; // Fades one half full-scale volume per second (two seconds for complete fadeout) |
|
|
|
// sound mixers |
|
int g_csoundmixers = 0; // total number of soundmixers found |
|
int g_cgrouprules = 0; // total number of group rules found |
|
int g_cgroupclass = 0; |
|
|
|
// this is used to enable/disable music playback on x360 when the user selects his own soundtrack to play |
|
void S_EnableMusic( bool bEnable ) |
|
{ |
|
if ( bEnable ) |
|
{ |
|
g_DashboardMusicMixTarget = 1.0f; |
|
} |
|
else |
|
{ |
|
g_DashboardMusicMixTarget = 0.0f; |
|
} |
|
} |
|
|
|
bool IsSoundSourceLocalPlayer( int soundsource ) |
|
{ |
|
if ( soundsource == SOUND_FROM_UI_PANEL ) |
|
return true; |
|
|
|
return ( soundsource == g_pSoundServices->GetViewEntity() ); |
|
} |
|
|
|
CThreadMutex g_SndMutex; |
|
|
|
#define THREAD_LOCK_SOUND() AUTO_LOCK( g_SndMutex ) |
|
|
|
const int MASK_BLOCK_AUDIO = CONTENTS_SOLID|CONTENTS_MOVEABLE|CONTENTS_WINDOW; |
|
|
|
void CActiveChannels::Add( channel_t *pChannel ) |
|
{ |
|
Assert( pChannel->activeIndex == 0 ); |
|
m_list[m_count] = pChannel - channels; |
|
m_count++; |
|
pChannel->activeIndex = m_count; |
|
} |
|
|
|
void CActiveChannels::Remove( channel_t *pChannel ) |
|
{ |
|
if ( pChannel->activeIndex == 0 ) |
|
return; |
|
int activeIndex = pChannel->activeIndex - 1; |
|
Assert( activeIndex >= 0 && activeIndex < m_count ); |
|
Assert( pChannel == &channels[m_list[activeIndex]] ); |
|
m_count--; |
|
// Not the last one? Swap the last one with this one and fix its index |
|
if ( activeIndex < m_count ) |
|
{ |
|
m_list[activeIndex] = m_list[m_count]; |
|
channels[m_list[activeIndex]].activeIndex = activeIndex+1; |
|
} |
|
pChannel->activeIndex = 0; |
|
} |
|
|
|
|
|
void CActiveChannels::GetActiveChannels( CChannelList &list ) |
|
{ |
|
list.m_count = m_count; |
|
if ( m_count ) |
|
{ |
|
Q_memcpy( list.m_list, m_list, sizeof(m_list[0])*m_count ); |
|
} |
|
|
|
for ( int i = SOUND_BUFFER_SPECIAL_START; i < g_paintBuffers.Count(); ++i ) |
|
{ |
|
paintbuffer_t *pSpecialBuffer = MIX_GetPPaintFromIPaint( i ); |
|
if ( pSpecialBuffer->nSpecialDSP != 0 ) |
|
{ |
|
list.m_nSpecialDSPs.AddToTail( pSpecialBuffer->nSpecialDSP ); |
|
} |
|
} |
|
|
|
list.m_hasSpeakerChannels = true; |
|
list.m_has11kChannels = true; |
|
list.m_has22kChannels = true; |
|
list.m_has44kChannels = true; |
|
list.m_hasDryChannels = true; |
|
} |
|
|
|
void CActiveChannels::Init() |
|
{ |
|
m_count = 0; |
|
} |
|
|
|
bool snd_initialized = false; |
|
|
|
Vector listener_origin; |
|
static Vector listener_forward; |
|
Vector listener_right; |
|
static Vector listener_up; |
|
static bool s_bIsListenerUnderwater; |
|
static vec_t sound_nominal_clip_dist=SOUND_NORMAL_CLIP_DIST; |
|
|
|
// @TODO (toml 05-08-02): put this somewhere more reasonable |
|
vec_t S_GetNominalClipDist() |
|
{ |
|
return sound_nominal_clip_dist; |
|
} |
|
|
|
int g_soundtime = 0; // sample PAIRS output since start |
|
int g_paintedtime = 0; // sample PAIRS mixed since start |
|
|
|
float g_ReplaySoundTimeFracAccumulator = 0.0f; // Used by replay |
|
|
|
float g_ClockSyncArray[NUM_CLOCK_SYNCS] = {0}; |
|
int g_SoundClockPaintTime[NUM_CLOCK_SYNCS] = {0}; |
|
|
|
// default 10ms |
|
ConVar snd_delay_sound_shift("snd_delay_sound_shift","0.01"); |
|
// this forces the clock to resync on the next delayed/sync sound |
|
void S_SyncClockAdjust( clocksync_index_t syncIndex ) |
|
{ |
|
g_ClockSyncArray[syncIndex] = 0; |
|
g_SoundClockPaintTime[syncIndex] = 0; |
|
} |
|
|
|
float S_ComputeDelayForSoundtime( float soundtime, clocksync_index_t syncIndex ) |
|
{ |
|
// reset clock and return 0 |
|
if ( g_ClockSyncArray[syncIndex] == 0 ) |
|
{ |
|
// Put the current time marker one tick back to impose a minimum delay on the first sample |
|
// this shifts the drift over so the sounds are more likely to delay (rather than skip) |
|
// over the burst |
|
// NOTE: The first sound after a sync MUST have a non-zero delay for the delay channel |
|
// detection logic to work (otherwise we keep resetting the clock) |
|
g_ClockSyncArray[syncIndex] = soundtime - host_state.interval_per_tick; |
|
g_SoundClockPaintTime[syncIndex] = g_paintedtime; |
|
} |
|
|
|
// how much time has passed in the game since we did a clock sync? |
|
float gameDeltaTime = soundtime - g_ClockSyncArray[syncIndex]; |
|
|
|
// how many samples have been mixed since we did a clock sync? |
|
int paintedSamples = g_paintedtime - g_SoundClockPaintTime[syncIndex]; |
|
int dmaSpeed = g_AudioDevice->DeviceDmaSpeed(); |
|
int gameSamples = (gameDeltaTime * dmaSpeed); |
|
int delaySamples = gameSamples - paintedSamples; |
|
float delay = delaySamples / float(dmaSpeed); |
|
|
|
if ( gameDeltaTime < 0 || fabs(delay) > 0.500f ) |
|
{ |
|
// Note that the equations assume a correlation between game time and real time |
|
// some kind of clock error. This can happen with large host_timescale or when the |
|
// framerate hitches drastically (game time is a smaller clamped value wrt real time). |
|
// The current sync estimate has probably drifted due to this or some other problem, recompute. |
|
//Msg("Clock ERROR!: %.2f %.2f\n", gameDeltaTime, delay); |
|
S_SyncClockAdjust(syncIndex); |
|
return 0; |
|
} |
|
return delay + snd_delay_sound_shift.GetFloat(); |
|
} |
|
|
|
static int s_buffers = 0; |
|
static int s_oldsampleOutCount = 0; |
|
static float s_lastsoundtime = 0.0f; |
|
|
|
bool s_bOnLoadScreen = false; |
|
|
|
static CClassMemoryPool< CSfxTable > s_SoundPool( MAX_SFX ); |
|
struct SfxDictEntry |
|
{ |
|
CSfxTable *pSfx; |
|
}; |
|
|
|
static CUtlMap< FileNameHandle_t, SfxDictEntry > s_Sounds( 0, 0, DefLessFunc( FileNameHandle_t ) ); |
|
|
|
class CDummySfx : public CSfxTable |
|
{ |
|
public: |
|
virtual const char *getname() |
|
{ |
|
return name; |
|
} |
|
|
|
void setname( const char *pName ) |
|
{ |
|
Q_strncpy( name, pName, sizeof( name ) ); |
|
OnNameChanged(name); |
|
} |
|
|
|
private: |
|
char name[MAX_PATH]; |
|
}; |
|
|
|
static CDummySfx dummySfx; |
|
|
|
// returns true if ok to procede with TraceRay calls |
|
bool SND_IsInGame( void ) |
|
{ |
|
return cl.IsActive(); |
|
} |
|
|
|
|
|
CSfxTable::CSfxTable() |
|
{ |
|
m_namePoolIndex = s_Sounds.InvalidIndex(); |
|
pSource = NULL; |
|
m_bUseErrorFilename = false; |
|
m_bIsUISound = false; |
|
m_bIsLateLoad = false; |
|
m_bMixGroupsCached = false; |
|
m_pDebugName = NULL; |
|
} |
|
|
|
|
|
void CSfxTable::SetNamePoolIndex( int index ) |
|
{ |
|
m_namePoolIndex = index; |
|
if ( m_namePoolIndex != s_Sounds.InvalidIndex() ) |
|
{ |
|
OnNameChanged(getname()); |
|
} |
|
#ifdef _DEBUG |
|
m_pDebugName = strdup( getname() ); |
|
#endif |
|
} |
|
|
|
void CSfxTable::OnNameChanged( const char *pName ) |
|
{ |
|
if ( pName && g_cgrouprules ) |
|
{ |
|
char szString[MAX_PATH]; |
|
Q_strncpy( szString, pName, sizeof(szString) ); |
|
Q_FixSlashes( szString, '/' ); |
|
m_mixGroupCount = MXR_GetMixGroupListFromDirName( szString, m_mixGroupList, ARRAYSIZE(m_mixGroupList) ); |
|
m_bMixGroupsCached = true; |
|
} |
|
} |
|
//----------------------------------------------------------------------------- |
|
// Purpose: Wrapper for sfxtable->getname() |
|
// Output : char const |
|
//----------------------------------------------------------------------------- |
|
const char *CSfxTable::getname() |
|
{ |
|
if ( s_Sounds.InvalidIndex() != m_namePoolIndex ) |
|
{ |
|
char* pString = tmpstr512(); |
|
if ( g_pFileSystem ) |
|
g_pFileSystem->String( s_Sounds.Key( m_namePoolIndex ), pString, 512 ); |
|
else |
|
{ |
|
pString[0] = 0; |
|
} |
|
return pString; |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
FileNameHandle_t CSfxTable::GetFileNameHandle() |
|
{ |
|
if ( s_Sounds.InvalidIndex() != m_namePoolIndex ) |
|
{ |
|
return s_Sounds.Key( m_namePoolIndex ); |
|
} |
|
return NULL; |
|
} |
|
|
|
const char *CSfxTable::GetFileName() |
|
{ |
|
if ( IsX360() && m_bUseErrorFilename ) |
|
{ |
|
// Redirecting error sounds to a valid empty wave, prevents a bad loading retry pattern during gameplay |
|
// which may event sounds skipped by preload, because they don't exist. |
|
return "common/null.wav"; |
|
} |
|
|
|
const char *pName = getname(); |
|
return pName ? PSkipSoundChars( pName ) : NULL; |
|
} |
|
|
|
bool CSfxTable::IsPrecachedSound() |
|
{ |
|
const char *pName = getname(); |
|
|
|
if ( sv.IsActive() ) |
|
{ |
|
// Server uses zero to mark invalid sounds |
|
return sv.LookupSoundIndex( pName ) != 0 ? true : false; |
|
} |
|
|
|
// Client uses -1 |
|
// WE SHOULD FIX THIS!!! |
|
return ( cl.LookupSoundIndex( pName ) != -1 ) ? true : false; |
|
} |
|
|
|
float g_DuckScale = 1.0f; |
|
|
|
// Structure used for fading in and out client sound volume. |
|
typedef struct |
|
{ |
|
float initial_percent; |
|
|
|
// How far to adjust client's volume down by. |
|
float percent; |
|
|
|
// GetHostTime() when we started adjusting volume |
|
float starttime; |
|
|
|
// # of seconds to get to faded out state |
|
float fadeouttime; |
|
// # of seconds to hold |
|
float holdtime; |
|
// # of seconds to restore |
|
float fadeintime; |
|
} soundfade_t; |
|
|
|
static soundfade_t soundfade; // Client sound fading singleton object |
|
|
|
// 0)headphones 2)stereo speakers 4)quad 5)5point1 |
|
// autodetected from windows settings |
|
ConVar snd_surround( "snd_surround_speakers", "-1", FCVAR_INTERNAL_USE ); |
|
ConVar snd_legacy_surround( "snd_legacy_surround", "0", FCVAR_ARCHIVE ); |
|
ConVar snd_noextraupdate( "snd_noextraupdate", "0" ); |
|
ConVar snd_show( "snd_show", "0", FCVAR_CHEAT, "Show sounds info" ); |
|
ConVar snd_visualize ("snd_visualize", "0", FCVAR_CHEAT, "Show sounds location in world" ); |
|
ConVar snd_pitchquality( "snd_pitchquality", "1", FCVAR_ARCHIVE ); // 1) use high quality pitch shifters |
|
|
|
// master volume |
|
static ConVar volume( "volume", "1.0", FCVAR_ARCHIVE | FCVAR_ARCHIVE_XBOX, "Sound volume", true, 0.0f, true, 1.0f ); |
|
// user configurable music volume |
|
ConVar snd_musicvolume( "snd_musicvolume", "1.0", FCVAR_ARCHIVE | FCVAR_ARCHIVE_XBOX, "Music volume", true, 0.0f, true, 1.0f ); |
|
|
|
ConVar snd_mixahead( "snd_mixahead", "0.1", FCVAR_ARCHIVE ); |
|
ConVar snd_mix_async( "snd_mix_async", "0" ); |
|
#ifdef _DEBUG |
|
static ConCommand snd_mixvol("snd_mixvol", MXR_DebugSetMixGroupVolume, "Set named Mixgroup to mix volume."); |
|
#endif |
|
|
|
// vaudio DLL |
|
IVAudio *vaudio = NULL; |
|
CSysModule *g_pVAudioModule = NULL; |
|
|
|
//----------------------------------------------------------------------------- |
|
// Resource loading for sound |
|
//----------------------------------------------------------------------------- |
|
class CResourcePreloadSound : public CResourcePreload |
|
{ |
|
public: |
|
CResourcePreloadSound() : m_SoundNames( 0, 0, true ) |
|
{ |
|
} |
|
|
|
virtual bool CreateResource( const char *pName ) |
|
{ |
|
CSfxTable *pSfx = S_PrecacheSound( pName ); |
|
if ( !pSfx ) |
|
{ |
|
return false; |
|
} |
|
|
|
m_SoundNames.AddString( pSfx->GetFileName() ); |
|
return true; |
|
} |
|
|
|
virtual void PurgeUnreferencedResources() |
|
{ |
|
bool bSpew = ( g_pQueuedLoader->GetSpewDetail() & LOADER_DETAIL_PURGES ) != 0; |
|
|
|
for ( int i = s_Sounds.FirstInorder(); i != s_Sounds.InvalidIndex(); i = s_Sounds.NextInorder( i ) ) |
|
{ |
|
// the master sound table grows forever |
|
// remove sound sources from the master sound table that were not in the preload list |
|
CSfxTable *pSfx = s_Sounds[i].pSfx; |
|
if ( pSfx && pSfx->pSource ) |
|
{ |
|
if ( pSfx->m_bIsUISound ) |
|
{ |
|
// never purge ui |
|
continue; |
|
} |
|
|
|
UtlSymId_t symbol = m_SoundNames.Find( pSfx->GetFileName() ); |
|
if ( symbol == UTL_INVAL_SYMBOL ) |
|
{ |
|
// sound was not part of preload, purge it |
|
if ( bSpew ) |
|
{ |
|
Msg( "CResourcePreloadSound: Purging: %s\n", pSfx->GetFileName() ); |
|
} |
|
|
|
pSfx->pSource->CacheUnload(); |
|
delete pSfx->pSource; |
|
pSfx->pSource = NULL; |
|
} |
|
} |
|
} |
|
|
|
m_SoundNames.RemoveAll(); |
|
|
|
if ( !g_pQueuedLoader->IsSameMapLoading() ) |
|
{ |
|
wavedatacache->Flush(); |
|
} |
|
} |
|
|
|
virtual void PurgeAll() |
|
{ |
|
bool bSpew = ( g_pQueuedLoader->GetSpewDetail() & LOADER_DETAIL_PURGES ) != 0; |
|
|
|
for ( int i = s_Sounds.FirstInorder(); i != s_Sounds.InvalidIndex(); i = s_Sounds.NextInorder( i ) ) |
|
{ |
|
// the master sound table grows forever |
|
// remove sound sources from the master sound table that were not in the preload list |
|
CSfxTable *pSfx = s_Sounds[i].pSfx; |
|
if ( pSfx && pSfx->pSource ) |
|
{ |
|
if ( pSfx->m_bIsUISound ) |
|
{ |
|
// never purge ui |
|
if ( bSpew ) |
|
{ |
|
Msg( "CResourcePreloadSound: Skipping: %s\n", pSfx->GetFileName() ); |
|
} |
|
continue; |
|
} |
|
|
|
// sound was not part of preload, purge it |
|
if ( bSpew ) |
|
{ |
|
Msg( "CResourcePreloadSound: Purging: %s\n", pSfx->GetFileName() ); |
|
} |
|
|
|
pSfx->pSource->CacheUnload(); |
|
delete pSfx->pSource; |
|
pSfx->pSource = NULL; |
|
} |
|
} |
|
|
|
m_SoundNames.RemoveAll(); |
|
|
|
wavedatacache->Flush(); |
|
} |
|
|
|
private: |
|
CUtlSymbolTable m_SoundNames; |
|
}; |
|
static CResourcePreloadSound s_ResourcePreloadSound; |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : float |
|
//----------------------------------------------------------------------------- |
|
float S_GetMasterVolume( void ) |
|
{ |
|
float scale = 1.0f; |
|
if ( soundfade.percent != 0 ) |
|
{ |
|
scale = clamp( (float)soundfade.percent / 100.0f, 0.0f, 1.0f ); |
|
scale = 1.0f - scale; |
|
} |
|
return volume.GetFloat() * scale; |
|
} |
|
|
|
|
|
void S_SoundInfo_f(void) |
|
{ |
|
if ( !g_AudioDevice->IsActive() ) |
|
{ |
|
Msg( "Sound system not started\n" ); |
|
return; |
|
} |
|
|
|
Msg( "Sound Device: %s\n", g_AudioDevice->DeviceName() ); |
|
Msg( " Channels: %d\n", g_AudioDevice->DeviceChannels() ); |
|
Msg( " Samples: %d\n", g_AudioDevice->DeviceSampleCount() ); |
|
Msg( " Bits/Sample: %d\n", g_AudioDevice->DeviceSampleBits() ); |
|
Msg( " Rate: %d\n", g_AudioDevice->DeviceDmaSpeed() ); |
|
Msg( "total_channels: %d\n", total_channels); |
|
|
|
if ( IsX360() ) |
|
{ |
|
// dump a glimpse of the mixing state |
|
CChannelList list; |
|
g_ActiveChannels.GetActiveChannels( list ); |
|
|
|
Msg( "\nActive Channels: (%d)\n", list.Count() ); |
|
for ( int i = 0; i < list.Count(); i++ ) |
|
{ |
|
channel_t *pChannel = list.GetChannel(i); |
|
Msg( "%s (Mixer: 0x%p)\n", pChannel->sfx->GetFileName(), pChannel->pMixer ); |
|
} |
|
} |
|
} |
|
|
|
|
|
/* |
|
================ |
|
S_Startup |
|
================ |
|
*/ |
|
|
|
void S_Startup( void ) |
|
{ |
|
if ( !snd_initialized ) |
|
return; |
|
|
|
if ( !g_AudioDevice || g_AudioDevice == Audio_GetNullDevice() ) |
|
{ |
|
g_AudioDevice = IAudioDevice::AutoDetectInit( CommandLine()->CheckParm( "-wavonly" ) != 0 ); |
|
if ( !g_AudioDevice ) |
|
{ |
|
Error( "Unable to init audio" ); |
|
} |
|
} |
|
} |
|
|
|
static ConCommand play("play", S_Play, "Play a sound.", FCVAR_SERVER_CAN_EXECUTE ); |
|
static ConCommand playflush( "playflush", S_Play, "Play a sound, reloading from disk in case of changes." ); |
|
static ConCommand playvol( "playvol", S_PlayVol, "Play a sound at a specified volume." ); |
|
static ConCommand speak( "speak", S_Say, "Play a constructed sentence." ); |
|
static ConCommand stopsound( "stopsound", S_StopAllSoundsC, 0, FCVAR_CHEAT); // Marked cheat because it gives an advantage to players minimising ambient noise. |
|
static ConCommand soundlist( "soundlist", S_SoundList, "List all known sounds." ); |
|
static ConCommand soundinfo( "soundinfo", S_SoundInfo_f, "Describe the current sound device." ); |
|
|
|
bool IsValidSampleRate( int rate ) |
|
{ |
|
return rate == SOUND_11k || rate == SOUND_22k || rate == SOUND_44k; |
|
} |
|
|
|
void VAudioInit() |
|
{ |
|
if ( IsPC() ) |
|
{ |
|
if ( !IsPosix() ) |
|
{ |
|
// vaudio_miles.dll will load this... |
|
g_pFileSystem->GetLocalCopy( "mss32.dll" ); |
|
} |
|
|
|
g_pVAudioModule = FileSystem_LoadModule( "vaudio_miles" ); |
|
if ( g_pVAudioModule ) |
|
{ |
|
CreateInterfaceFn vaudioFactory = Sys_GetFactory( g_pVAudioModule ); |
|
vaudio = (IVAudio *)vaudioFactory( VAUDIO_INTERFACE_VERSION, NULL ); |
|
} |
|
} |
|
} |
|
|
|
/* |
|
================ |
|
S_Init |
|
================ |
|
*/ |
|
void S_Init( void ) |
|
{ |
|
if ( sv.IsDedicated() && !CommandLine()->CheckParm( "-forcesound" ) ) |
|
return; |
|
|
|
DevMsg( "Sound Initialization: Start\n" ); |
|
|
|
// KDB: init sentence array |
|
TRACEINIT( VOX_Init(), VOX_Shutdown() ); |
|
|
|
VAudioInit(); |
|
|
|
if ( CommandLine()->CheckParm( "-nosound" ) ) |
|
{ |
|
g_AudioDevice = Audio_GetNullDevice(); |
|
TRACEINIT( audiosourcecache->Init( host_parms.memsize >> 2 ), audiosourcecache->Shutdown() ); |
|
return; |
|
} |
|
|
|
snd_initialized = true; |
|
|
|
g_ActiveChannels.Init(); |
|
S_Startup(); |
|
|
|
MIX_InitAllPaintbuffers(); |
|
|
|
SND_InitScaletable(); |
|
|
|
MXR_LoadAllSoundMixers(); |
|
|
|
S_StopAllSounds( true ); |
|
|
|
TRACEINIT( audiosourcecache->Init( host_parms.memsize >> 2 ), audiosourcecache->Shutdown() ); |
|
|
|
AllocDsps( true ); |
|
|
|
if ( IsX360() ) |
|
{ |
|
g_pQueuedLoader->InstallLoader( RESOURCEPRELOAD_SOUND, &s_ResourcePreloadSound ); |
|
} |
|
|
|
DevMsg( "Sound Initialization: Finish, Sampling Rate: %i\n", g_AudioDevice->DeviceDmaSpeed() ); |
|
|
|
#ifdef _X360 |
|
BOOL bPlaybackControl; |
|
// get initial state of the x360 media player |
|
if ( XMPTitleHasPlaybackControl( &bPlaybackControl ) == ERROR_SUCCESS ) |
|
{ |
|
S_EnableMusic(bPlaybackControl!=0); |
|
} |
|
|
|
Assert( g_pVideo != NULL ); |
|
|
|
if ( g_pVideo != NULL ) |
|
{ |
|
if ( g_pVideo->SoundDeviceCommand( VideoSoundDeviceOperation::HOOK_X_AUDIO, NULL ) != VideoResult::SUCCESS ) |
|
{ |
|
Assert( 0 ); |
|
} |
|
} |
|
#endif |
|
} |
|
|
|
|
|
// ======================================================================= |
|
// Shutdown sound engine |
|
// ======================================================================= |
|
void S_Shutdown(void) |
|
{ |
|
#if !defined( _X360 ) |
|
if ( VoiceTweak_IsStillTweaking() ) |
|
{ |
|
VoiceTweak_EndVoiceTweakMode(); |
|
} |
|
#endif |
|
|
|
S_StopAllSounds( true ); |
|
S_ShutdownMixThread(); |
|
|
|
TRACESHUTDOWN( audiosourcecache->Shutdown() ); |
|
|
|
SNDDMA_Shutdown(); |
|
|
|
for ( int i = s_Sounds.FirstInorder(); i != s_Sounds.InvalidIndex(); i = s_Sounds.NextInorder( i ) ) |
|
{ |
|
if ( s_Sounds[i].pSfx ) |
|
{ |
|
delete s_Sounds[i].pSfx->pSource; |
|
s_Sounds[i].pSfx->pSource = NULL; |
|
} |
|
} |
|
s_Sounds.RemoveAll(); |
|
s_SoundPool.Clear(); |
|
|
|
// release DSP resources |
|
FreeDsps( true ); |
|
|
|
MXR_ReleaseMemory(); |
|
|
|
// release sentences resources |
|
TRACESHUTDOWN( VOX_Shutdown() ); |
|
|
|
if ( IsPC() ) |
|
{ |
|
// shutdown vaudio |
|
if ( vaudio ) |
|
delete vaudio; |
|
FileSystem_UnloadModule( g_pVAudioModule ); |
|
g_pVAudioModule = NULL; |
|
vaudio = NULL; |
|
} |
|
|
|
MIX_FreeAllPaintbuffers(); |
|
snd_initialized = false; |
|
g_paintedtime = 0; |
|
g_soundtime = 0; |
|
g_ReplaySoundTimeFracAccumulator = 0.0f; |
|
s_buffers = 0; |
|
s_oldsampleOutCount = 0; |
|
s_lastsoundtime = 0.0f; |
|
#if !defined( _X360 ) |
|
Voice_Deinit(); |
|
#endif |
|
} |
|
|
|
bool S_IsInitted() |
|
{ |
|
return snd_initialized; |
|
} |
|
|
|
// ======================================================================= |
|
// Load a sound |
|
// ======================================================================= |
|
|
|
//----------------------------------------------------------------------------- |
|
// Return sfx and set pfInCache to 1 if |
|
// name is in name cache. Otherwise, alloc |
|
// a new spot in name cache and return 0 |
|
// in pfInCache. |
|
//----------------------------------------------------------------------------- |
|
CSfxTable *S_FindName( const char *szName, int *pfInCache ) |
|
{ |
|
int i; |
|
CSfxTable *sfx = NULL; |
|
char szBuff[MAX_PATH]; |
|
const char *pName; |
|
|
|
if ( !szName ) |
|
{ |
|
Error( "S_FindName: NULL\n" ); |
|
} |
|
|
|
pName = szName; |
|
if ( IsX360() ) |
|
{ |
|
Q_strncpy( szBuff, pName, sizeof( szBuff ) ); |
|
int len = Q_strlen( szBuff )-4; |
|
if ( len > 0 && !Q_strnicmp( szBuff+len, ".mp3", 4 ) ) |
|
{ |
|
// convert unsupported .mp3 to .wav |
|
Q_strcpy( szBuff+len, ".wav" ); |
|
} |
|
pName = szBuff; |
|
|
|
if ( pName[0] == CHAR_STREAM ) |
|
{ |
|
// streaming (or not) is hardcoded to alternate criteria |
|
// prevent the same sound from creating disparate instances |
|
pName++; |
|
} |
|
} |
|
|
|
// see if already loaded |
|
FileNameHandle_t fnHandle = g_pFileSystem->FindOrAddFileName( pName ); |
|
i = s_Sounds.Find( fnHandle ); |
|
if ( i != s_Sounds.InvalidIndex() ) |
|
{ |
|
sfx = s_Sounds[i].pSfx; |
|
Assert( sfx ); |
|
if ( pfInCache ) |
|
{ |
|
// indicate whether or not sound is currently in the cache. |
|
*pfInCache = ( sfx->pSource && sfx->pSource->IsCached() ) ? 1 : 0; |
|
} |
|
return sfx; |
|
} |
|
else |
|
{ |
|
SfxDictEntry entry; |
|
entry.pSfx = ( CSfxTable * )s_SoundPool.Alloc(); |
|
|
|
Assert( entry.pSfx ); |
|
|
|
i = s_Sounds.Insert( fnHandle, entry ); |
|
sfx = s_Sounds[i].pSfx; |
|
|
|
sfx->SetNamePoolIndex( i ); |
|
sfx->pSource = NULL; |
|
|
|
if ( pfInCache ) |
|
{ |
|
*pfInCache = 0; |
|
} |
|
} |
|
return sfx; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// S_LoadSound |
|
// |
|
// Check to see if wave data is in the cache. If so, return pointer to data. |
|
// If not, allocate cache space for wave data, load wave file into temporary heap |
|
// space, and dump/convert file data into cache. |
|
//----------------------------------------------------------------------------- |
|
double g_flAccumulatedSoundLoadTime = 0.0f; |
|
CAudioSource *S_LoadSound( CSfxTable *pSfx, channel_t *ch ) |
|
{ |
|
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ ); |
|
|
|
VPROF("S_LoadSound"); |
|
if ( !pSfx->pSource ) |
|
{ |
|
if ( IsX360() ) |
|
{ |
|
if ( SND_IsInGame() && !g_pQueuedLoader->IsMapLoading() ) |
|
{ |
|
// sound should be present (due to reslists), but NOT allowing a load hitch during gameplay |
|
// loading a sound during gameplay is a bad experience, causes a very expensive sync i/o to fetch the header |
|
// and in the case of a memory wave, the actual audio data |
|
bool bFound = false; |
|
if ( !pSfx->m_bIsLateLoad ) |
|
{ |
|
if ( pSfx->getname() != PSkipSoundChars( pSfx->getname() ) ) |
|
{ |
|
// the sound might already exist as an undecorated audio source |
|
FileNameHandle_t fnHandle = g_pFileSystem->FindOrAddFileName( pSfx->GetFileName() ); |
|
int i = s_Sounds.Find( fnHandle ); |
|
if ( i != s_Sounds.InvalidIndex() ) |
|
{ |
|
CSfxTable *pOtherSfx = s_Sounds[i].pSfx; |
|
Assert( pOtherSfx ); |
|
CAudioSource *pOtherSource = pOtherSfx->pSource; |
|
if ( pOtherSource && pOtherSource->IsCached() ) |
|
{ |
|
// Can safely let the "load" continue because the headers are expected to be in the preload |
|
// that are now persisted and the wave data cache will find an existing audio buffer match, |
|
// so no sync i/o should occur from either. |
|
bFound = true; |
|
} |
|
} |
|
} |
|
|
|
if ( !bFound ) |
|
{ |
|
// warn once |
|
DevWarning( "S_LoadSound: Late load '%s', skipping.\n", pSfx->getname() ); |
|
pSfx->m_bIsLateLoad = true; |
|
} |
|
} |
|
|
|
if ( !bFound ) |
|
{ |
|
return NULL; |
|
} |
|
} |
|
else if ( pSfx->m_bIsLateLoad ) |
|
{ |
|
// outside of gameplay, let the load happen |
|
pSfx->m_bIsLateLoad = false; |
|
} |
|
} |
|
|
|
double st = Plat_FloatTime(); |
|
|
|
bool bStream = false; |
|
bool bUserVox = false; |
|
|
|
// sound chars can explicitly categorize usage |
|
bStream = TestSoundChar( pSfx->getname(), CHAR_STREAM ); |
|
if ( !bStream ) |
|
{ |
|
bUserVox = TestSoundChar( pSfx->getname(), CHAR_USERVOX ); |
|
} |
|
|
|
// override streaming |
|
if ( IsX360() ) |
|
{ |
|
const char *s_CriticalSounds[] = |
|
{ |
|
"common", |
|
"items", |
|
"physics", |
|
"player", |
|
"ui", |
|
"weapons", |
|
}; |
|
|
|
// stream everything but critical sounds |
|
bStream = true; |
|
const char *pFileName = pSfx->GetFileName(); |
|
for ( int i = 0; i<ARRAYSIZE( s_CriticalSounds ); i++ ) |
|
{ |
|
size_t len = strlen( s_CriticalSounds[i] ); |
|
if ( !Q_strnicmp( pFileName, s_CriticalSounds[i], len ) && ( pFileName[len] == '\\' || pFileName[len] == '/' ) ) |
|
{ |
|
// never stream these, regardless of sound chars |
|
bStream = false; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
if ( bStream ) |
|
{ |
|
// setup as a streaming resource |
|
pSfx->pSource = Audio_CreateStreamedWave( pSfx ); |
|
} |
|
else |
|
{ |
|
if ( bUserVox ) |
|
{ |
|
if ( !IsX360() ) |
|
{ |
|
pSfx->pSource = Voice_SetupAudioSource( ch->soundsource, ch->entchannel ); |
|
} |
|
else |
|
{ |
|
// not supporting |
|
Assert( 0 ); |
|
} |
|
} |
|
else |
|
{ |
|
// load all into memory directly |
|
pSfx->pSource = Audio_CreateMemoryWave( pSfx ); |
|
} |
|
} |
|
|
|
double ed = Plat_FloatTime(); |
|
g_flAccumulatedSoundLoadTime += ( ed - st ); |
|
} |
|
else |
|
{ |
|
pSfx->pSource->CheckAudioSourceCache(); |
|
} |
|
|
|
if ( !pSfx->pSource ) |
|
{ |
|
return NULL; |
|
} |
|
|
|
// first time to load? Create the mixer |
|
if ( ch && !ch->pMixer ) |
|
{ |
|
ch->pMixer = pSfx->pSource->CreateMixer( ch->initialStreamPosition ); |
|
if ( !ch->pMixer ) |
|
{ |
|
return NULL; |
|
} |
|
} |
|
|
|
return pSfx->pSource; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// S_PrecacheSound |
|
// |
|
// Reserve space for the name of the sound in a global array. |
|
// Load the data for the non-streaming sound. Streaming sounds |
|
// defer loading of data until just before playback. |
|
//----------------------------------------------------------------------------- |
|
CSfxTable *S_PrecacheSound( const char *name ) |
|
{ |
|
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ ); |
|
|
|
if ( !g_AudioDevice ) |
|
return NULL; |
|
|
|
if ( !g_AudioDevice->IsActive() ) |
|
return NULL; |
|
|
|
CSfxTable *sfx = S_FindName( name, NULL ); |
|
if ( sfx ) |
|
{ |
|
// cache sound |
|
S_LoadSound( sfx, NULL ); |
|
} |
|
else |
|
{ |
|
Assert( !"S_PrecacheSound: Failed to create sfx" ); |
|
} |
|
|
|
return sfx; |
|
} |
|
|
|
|
|
void S_InternalReloadSound( CSfxTable *sfx ) |
|
{ |
|
if ( !sfx || !sfx->pSource ) |
|
return; |
|
|
|
sfx->pSource->CacheUnload(); |
|
|
|
delete sfx->pSource; |
|
sfx->pSource = NULL; |
|
|
|
char pExt[10]; |
|
Q_ExtractFileExtension( sfx->getname(), pExt, sizeof(pExt) ); |
|
int nSource = !Q_stricmp( pExt, "mp3" ) ? CAudioSource::AUDIO_SOURCE_MP3 : CAudioSource::AUDIO_SOURCE_WAV; |
|
// audiosourcecache->RebuildCacheEntry( nSource, sfx->IsPrecachedSound(), sfx ); |
|
audiosourcecache->GetInfo( nSource, sfx->IsPrecachedSound(), sfx ); // Do a size/date check and rebuild the cache entry if necessary. |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Refresh a sound in the cache |
|
//----------------------------------------------------------------------------- |
|
void S_ReloadSound( const char *name ) |
|
{ |
|
if ( IsX360() ) |
|
{ |
|
// not supporting |
|
Assert( 0 ); |
|
return; |
|
} |
|
|
|
if ( !g_AudioDevice ) |
|
return; |
|
|
|
if ( !g_AudioDevice->IsActive() ) |
|
return; |
|
|
|
CSfxTable *sfx = S_FindName( name, NULL ); |
|
#ifdef _DEBUG |
|
if ( sfx ) |
|
{ |
|
Assert( Q_stricmp( sfx->getname(), name ) == 0 ); |
|
} |
|
#endif |
|
|
|
S_InternalReloadSound( sfx ); |
|
} |
|
|
|
|
|
// See comments on CL_HandlePureServerWhitelist for details of what we're doing here. |
|
void S_ReloadFilesInList( IFileList *pFilesToReload ) |
|
{ |
|
if ( !IsPC() ) |
|
return; |
|
|
|
S_StopAllSounds( true ); |
|
wavedatacache->Flush(); |
|
audiosourcecache->ForceRecheckDiskInfo(); // Force all cached audio data to recheck size/date info next time it's accessed. |
|
|
|
|
|
CUtlVector< CSfxTable * > processed; |
|
|
|
int iLast = s_Sounds.LastInorder(); |
|
for ( int i = s_Sounds.FirstInorder(); i != iLast; i = s_Sounds.NextInorder( i ) ) |
|
{ |
|
FileNameHandle_t fnHandle = s_Sounds.Key( i ); |
|
char filename[MAX_PATH * 3]; |
|
if ( !g_pFileSystem->String( fnHandle, filename, sizeof( filename ) ) ) |
|
{ |
|
Assert( !"S_HandlePureServerWhitelist - can't get a filename." ); |
|
continue; |
|
} |
|
|
|
// If the file isn't cached in yet, then the filesystem hasn't touched its file, so don't bother. |
|
CSfxTable *sfx = s_Sounds[i].pSfx; |
|
if ( sfx && ( processed.Find( sfx ) == processed.InvalidIndex() ) ) |
|
{ |
|
char fullFilename[MAX_PATH*2]; |
|
if ( IsSoundChar( filename[0] ) ) |
|
Q_snprintf( fullFilename, sizeof( fullFilename ), "sound/%s", &filename[1] ); |
|
else |
|
Q_snprintf( fullFilename, sizeof( fullFilename ), "sound/%s", filename ); |
|
|
|
|
|
if ( !pFilesToReload->IsFileInList( fullFilename ) ) |
|
continue; |
|
|
|
processed.AddToTail( sfx ); |
|
|
|
S_InternalReloadSound( sfx ); |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Unfortunate confusing terminology. |
|
// Here prefetching means hinting to the audio source (which may be a stream) |
|
// to get its async data in flight. |
|
//----------------------------------------------------------------------------- |
|
void S_PrefetchSound( char const *name, bool bPlayOnce ) |
|
{ |
|
CSfxTable *sfx; |
|
|
|
if ( !g_AudioDevice ) |
|
return; |
|
|
|
if ( !g_AudioDevice->IsActive() ) |
|
return; |
|
|
|
sfx = S_FindName( name, NULL ); |
|
if ( sfx ) |
|
{ |
|
// cache sound |
|
S_LoadSound( sfx, NULL ); |
|
} |
|
|
|
if ( !sfx || !sfx->pSource ) |
|
{ |
|
return; |
|
} |
|
|
|
// hint the sound to start loading |
|
sfx->pSource->Prefetch(); |
|
|
|
if ( bPlayOnce ) |
|
{ |
|
sfx->pSource->SetPlayOnce( true ); |
|
} |
|
} |
|
|
|
void S_MarkUISound( CSfxTable *pSfx ) |
|
{ |
|
pSfx->m_bIsUISound = true; |
|
} |
|
|
|
unsigned int RemainingSamples( channel_t *pChannel ) |
|
{ |
|
if ( !pChannel || !pChannel->sfx || !pChannel->sfx->pSource ) |
|
return 0; |
|
|
|
unsigned int timeleft = pChannel->sfx->pSource->SampleCount(); |
|
|
|
if ( pChannel->sfx->pSource->IsLooped() ) |
|
{ |
|
return pChannel->sfx->pSource->SampleRate(); |
|
} |
|
|
|
if ( pChannel->pMixer ) |
|
{ |
|
timeleft -= pChannel->pMixer->GetSamplePosition(); |
|
} |
|
|
|
return timeleft; |
|
} |
|
|
|
// chooses the voice stealing algorithm |
|
ConVar voice_steal("voice_steal", "2"); |
|
|
|
/* |
|
================= |
|
SND_StealDynamicChannel |
|
Select a channel from the dynamic channel allocation area. For the given entity, |
|
override any other sound playing on the same channel (see code comments below for |
|
exceptions). |
|
================= |
|
*/ |
|
channel_t *SND_StealDynamicChannel(SoundSource soundsource, int entchannel, const Vector &origin, CSfxTable *sfx, float flDelay, bool bDoNotOverwriteExisting) |
|
{ |
|
int canSteal[MAX_DYNAMIC_CHANNELS]; |
|
int canStealCount = 0; |
|
|
|
int sameSoundCount = 0; |
|
unsigned int sameSoundRemaining = 0xFFFFFFFF; |
|
int sameSoundIndex = -1; |
|
int sameVol = 0xFFFF; |
|
int availableChannel = -1; |
|
bool bDelaySame = false; |
|
|
|
int nExactMatch[MAX_DYNAMIC_CHANNELS]; |
|
int nExactCount = 0; |
|
// first pass to replace sounds on same ent/channel, and search for free or stealable channels otherwise |
|
for ( int ch_idx = 0; ch_idx < MAX_DYNAMIC_CHANNELS ; ch_idx++) |
|
{ |
|
channel_t *ch = &channels[ch_idx]; |
|
|
|
if ( ch->activeIndex ) |
|
{ |
|
// channel CHAN_AUTO never overrides sounds on same channel |
|
if ( entchannel != CHAN_AUTO ) |
|
{ |
|
int checkChannel = entchannel; |
|
if ( checkChannel == -1 ) |
|
{ |
|
if ( ch->entchannel != CHAN_STREAM && ch->entchannel != CHAN_VOICE && ch->entchannel != CHAN_VOICE2 ) |
|
{ |
|
checkChannel = ch->entchannel; |
|
} |
|
} |
|
if ( ch->soundsource == soundsource && (soundsource != -1) && ch->entchannel == checkChannel ) |
|
{ |
|
// we found an exact match for this entity and this channel, but the sound we want to play is considered |
|
// low priority so instead of stomping this entry pretend we couldn't find a free slot to play and let |
|
// the existing sound keep going |
|
if ( bDoNotOverwriteExisting ) |
|
return NULL; |
|
|
|
if ( ch->flags.delayed_start ) |
|
{ |
|
nExactMatch[nExactCount] = ch_idx; |
|
nExactCount++; |
|
continue; |
|
} |
|
return ch; // always override sound from same entity |
|
} |
|
} |
|
|
|
// Never steal the channel of a streaming sound that is currently playing or |
|
// voice over IP data that is playing or any sound on CHAN_VOICE( acting ) |
|
if ( ch->entchannel == CHAN_STREAM || ch->entchannel == CHAN_VOICE || ch->entchannel == CHAN_VOICE2 ) |
|
continue; |
|
|
|
// don't let monster sounds override player sounds |
|
if ( g_pSoundServices->IsPlayer( ch->soundsource ) && !g_pSoundServices->IsPlayer(soundsource) ) |
|
continue; |
|
|
|
if ( ch->sfx == sfx ) |
|
{ |
|
bDelaySame = ch->flags.delayed_start ? true : bDelaySame; |
|
sameSoundCount++; |
|
int maxVolume = ChannelGetMaxVol( ch ); |
|
unsigned int remaining = RemainingSamples(ch); |
|
if ( maxVolume < sameVol || (maxVolume == sameVol && remaining < sameSoundRemaining) ) |
|
{ |
|
sameSoundIndex = ch_idx; |
|
sameVol = maxVolume; |
|
sameSoundRemaining = remaining; |
|
} |
|
} |
|
canSteal[canStealCount++] = ch_idx; |
|
} |
|
else |
|
{ |
|
if ( availableChannel < 0 ) |
|
{ |
|
availableChannel = ch_idx; |
|
} |
|
} |
|
} |
|
|
|
|
|
// coalesce the timeline for this channel |
|
if ( nExactCount > 0 ) |
|
{ |
|
uint nFreeSampleTime = g_paintedtime + (flDelay * SOUND_DMA_SPEED); |
|
channel_t *pReturn = &channels[nExactMatch[0]]; |
|
uint nMinRemaining = RemainingSamples( pReturn ); |
|
if ( pReturn->nFreeChannelAtSampleTime == 0 || pReturn->nFreeChannelAtSampleTime > nFreeSampleTime ) |
|
{ |
|
pReturn->nFreeChannelAtSampleTime = nFreeSampleTime; |
|
} |
|
for ( int i = 1; i < nExactCount; i++ ) |
|
{ |
|
channel_t *pChannel = &channels[nExactMatch[i]]; |
|
if ( pChannel->nFreeChannelAtSampleTime == 0 || pChannel->nFreeChannelAtSampleTime > nFreeSampleTime ) |
|
{ |
|
pChannel->nFreeChannelAtSampleTime = nFreeSampleTime; |
|
} |
|
uint nRemain = RemainingSamples( pChannel ); |
|
if ( nRemain < nMinRemaining ) |
|
{ |
|
pReturn = pChannel; |
|
nMinRemaining = nRemain; |
|
} |
|
} |
|
// if there's only one, mark it to be freed but don't reuse it. |
|
// otherwise mark all others to be freed and use the closest one to being done |
|
if ( nExactCount > 1 ) |
|
{ |
|
return pReturn; |
|
} |
|
} |
|
|
|
// Limit the number of times a given sfx/wave can play simultaneously |
|
if ( voice_steal.GetInt() > 1 && sameSoundIndex >= 0 ) |
|
{ |
|
// if sounds of this type are normally delayed, then add an extra slot for stealing |
|
// NOTE: In HL2 these are usually NPC gunshot sounds - and stealing too soon will cut |
|
// them off early. This is a safe heuristic to avoid that problem. There's probably a better |
|
// long-term solution involving only counting channels that are actually going to play (delay included) |
|
// at the same time as this one. |
|
int maxSameSounds = bDelaySame ? 5 : 4; |
|
float distSqr = 0.0f; |
|
if ( sfx->pSource ) |
|
{ |
|
distSqr = origin.DistToSqr(listener_origin); |
|
if ( sfx->pSource->IsLooped() ) |
|
{ |
|
maxSameSounds = 3; |
|
} |
|
} |
|
|
|
// don't play more than N copies of the same sound, steal the quietest & closest one otherwise |
|
if ( sameSoundCount >= maxSameSounds ) |
|
{ |
|
channel_t *ch = &channels[sameSoundIndex]; |
|
// you're already playing a closer version of this sound, don't steal |
|
if ( distSqr > 0.0f && ch->origin.DistToSqr(listener_origin) < distSqr && entchannel != CHAN_WEAPON ) |
|
return NULL; |
|
|
|
//Msg("Sound playing %d copies, stole %s (%d)\n", sameSoundCount, ch->sfx->getname(), sameVol ); |
|
return ch; |
|
} |
|
} |
|
|
|
// if there's a free channel, just take that one - don't steal |
|
if ( availableChannel >= 0 ) |
|
return &channels[availableChannel]; |
|
|
|
// Still haven't found a suitable channel, so choose the one with the least amount of time left to play |
|
float life_left = FLT_MAX; |
|
int first_to_die = -1; |
|
bool bAllowVoiceSteal = voice_steal.GetBool(); |
|
|
|
for ( int i = 0; i < canStealCount; i++ ) |
|
{ |
|
int ch_idx = canSteal[i]; |
|
channel_t *ch = &channels[ch_idx]; |
|
float timeleft = 0; |
|
if ( bAllowVoiceSteal ) |
|
{ |
|
int maxVolume = ChannelGetMaxVol( ch ); |
|
if ( maxVolume < 5 ) |
|
{ |
|
//Msg("Sound quiet, stole %s for %s\n", ch->sfx->getname(), sfx->getname() ); |
|
return ch; |
|
} |
|
|
|
if ( ch->sfx && ch->sfx->pSource ) |
|
{ |
|
unsigned int sampleCount = RemainingSamples( ch ); |
|
timeleft = (float)sampleCount / (float)ch->sfx->pSource->SampleRate(); |
|
} |
|
} |
|
else |
|
{ |
|
// UNDONE: Kill this when voice_steal 0,1,2 has been tested |
|
// UNDONE: This is the old buggy code that we're trying to replace |
|
if ( ch->sfx ) |
|
{ |
|
// basically steals the first one you come to |
|
timeleft = 1; //ch->end - paintedtime |
|
} |
|
} |
|
|
|
if ( timeleft < life_left ) |
|
{ |
|
life_left = timeleft; |
|
first_to_die = ch_idx; |
|
} |
|
} |
|
if ( first_to_die >= 0 ) |
|
{ |
|
//Msg("Stole %s, timeleft %d\n", channels[first_to_die].sfx->getname(), life_left ); |
|
return &channels[first_to_die]; |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
channel_t *SND_PickDynamicChannel(SoundSource soundsource, int entchannel, const Vector &origin, CSfxTable *sfx, float flDelay, bool bDoNotOverwriteExisting) |
|
{ |
|
channel_t *pChannel = SND_StealDynamicChannel( soundsource, entchannel, origin, sfx, flDelay, bDoNotOverwriteExisting ); |
|
if ( !pChannel ) |
|
return NULL; |
|
|
|
if (pChannel->sfx) |
|
{ |
|
// Don't restart looping sounds for the same entity |
|
CAudioSource *pSource = pChannel->sfx->pSource; |
|
if ( pSource ) |
|
{ |
|
if ( pSource->IsLooped() ) |
|
{ |
|
if ( pChannel->soundsource == soundsource && pChannel->entchannel == entchannel && pChannel->sfx == sfx ) |
|
{ |
|
// same looping sound, same ent, same channel, don't restart the sound |
|
return NULL; |
|
} |
|
} |
|
} |
|
// be sure and release previous channel |
|
// if sentence. |
|
// ("Stealing channel from %s\n", channels[first_to_die].sfx->getname() ); |
|
S_FreeChannel(pChannel); |
|
} |
|
|
|
return pChannel; |
|
} |
|
|
|
|
|
|
|
/* |
|
===================== |
|
SND_PickStaticChannel |
|
===================== |
|
Pick an empty channel from the static sound area, or allocate a new |
|
channel. Only fails if we're at max_channels (128!!!) or if |
|
we're trying to allocate a channel for a stream sound that is |
|
already playing. |
|
|
|
*/ |
|
channel_t *SND_PickStaticChannel(int soundsource, CSfxTable *pSfx) |
|
{ |
|
int i; |
|
channel_t *ch = NULL; |
|
|
|
// Check for replacement sound, or find the best one to replace |
|
for (i = MAX_DYNAMIC_CHANNELS; i<total_channels; i++) |
|
if (channels[i].sfx == NULL) |
|
break; |
|
|
|
if (i < total_channels) |
|
{ |
|
// reuse an empty static sound channel |
|
ch = &channels[i]; |
|
} |
|
else |
|
{ |
|
// no empty slots, alloc a new static sound channel |
|
if (total_channels == MAX_CHANNELS) |
|
{ |
|
DevMsg ("total_channels == MAX_CHANNELS\n"); |
|
return NULL; |
|
} |
|
|
|
// get a channel for the static sound |
|
ch = &channels[total_channels]; |
|
total_channels++; |
|
} |
|
|
|
return ch; |
|
} |
|
|
|
|
|
void S_SpatializeChannel( int pVolume[CCHANVOLUMES/2], int master_vol, const Vector *psourceDir, float gain, float mono ) |
|
{ |
|
float lscale, rscale, scale; |
|
vec_t dotRight; |
|
Vector sourceDir = *psourceDir; |
|
|
|
dotRight = DotProduct(listener_right, sourceDir); |
|
|
|
// clear volumes |
|
for (int i = 0; i < CCHANVOLUMES/2; i++) |
|
pVolume[i] = 0; |
|
|
|
if (mono > 0.0) |
|
{ |
|
// sound has radius, within which spatialization becomes mono: |
|
|
|
// mono is 0.0 -> 1.0, from radius 100% to radius 50% |
|
|
|
// at radius * 0.5, dotRight is 0 (ie: sound centered left/right) |
|
// at radius * 1.0, dotRight == dotRight |
|
|
|
dotRight *= (1.0 - mono); |
|
} |
|
|
|
rscale = 1.0 + dotRight; |
|
lscale = 1.0 - dotRight; |
|
|
|
// add in distance effect |
|
scale = gain * rscale / 2; |
|
pVolume[IFRONT_RIGHT] = (int) (master_vol * scale); |
|
|
|
scale = gain * lscale / 2; |
|
pVolume[IFRONT_LEFT] = (int) (master_vol * scale); |
|
|
|
pVolume[IFRONT_RIGHT] = clamp( pVolume[IFRONT_RIGHT], 0, 255 ); |
|
pVolume[IFRONT_LEFT] = clamp( pVolume[IFRONT_LEFT], 0, 255 ); |
|
|
|
} |
|
|
|
bool S_IsMusic( channel_t *pChannel ) |
|
{ |
|
if ( !pChannel->flags.bdry ) |
|
return false; |
|
|
|
CSfxTable *sfx = pChannel->sfx; |
|
if ( !sfx ) |
|
return false; |
|
|
|
CAudioSource *source = sfx->pSource; |
|
if ( !source ) |
|
return false; |
|
|
|
// Don't save restore looping sounds as you can end up with an entity restarting them again and have |
|
// them accumulate, etc. |
|
if ( source->IsLooped() ) |
|
return false; |
|
|
|
CAudioMixer *pMixer = pChannel->pMixer; |
|
if ( !pMixer ) |
|
return false; |
|
|
|
for ( int i = 0; i < 8; i++ ) |
|
{ |
|
if ( pChannel->mixgroups[i] != -1 ) |
|
{ |
|
char *pGroupName = MXR_GetGroupnameFromId( pChannel->mixgroups[i] ); |
|
if ( !Q_strcmp( pGroupName, "Music" ) ) |
|
{ |
|
return true; |
|
} |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: For save/restore of currently playing music |
|
// Input : list - |
|
//----------------------------------------------------------------------------- |
|
void S_GetCurrentlyPlayingMusic( CUtlVector< musicsave_t >& musiclist ) |
|
{ |
|
CChannelList list; |
|
g_ActiveChannels.GetActiveChannels( list ); |
|
for ( int i = 0; i < list.Count(); i++ ) |
|
{ |
|
channel_t *pChannel = &channels[list.GetChannelIndex(i)]; |
|
if ( !S_IsMusic( pChannel ) ) |
|
continue; |
|
|
|
musicsave_t song; |
|
Q_strncpy( song.songname, pChannel->sfx->getname(), sizeof( song.songname ) ); |
|
song.sampleposition = pChannel->pMixer->GetPositionForSave(); |
|
song.master_volume = pChannel->master_vol; |
|
|
|
musiclist.AddToTail( song ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : *song - |
|
//----------------------------------------------------------------------------- |
|
void S_RestartSong( const musicsave_t *song ) |
|
{ |
|
Assert( song ); |
|
|
|
// Start the song |
|
CSfxTable *pSound = S_PrecacheSound( song->songname ); |
|
if ( pSound ) |
|
{ |
|
StartSoundParams_t params; |
|
params.staticsound = true; |
|
params.soundsource = SOUND_FROM_WORLD; |
|
params.entchannel = CHAN_STATIC; |
|
params.pSfx = pSound; |
|
params.origin = vec3_origin; |
|
params.fvol = ( (float)song->master_volume / 255.0f ); |
|
params.soundlevel = SNDLVL_NONE; |
|
params.flags = SND_NOFLAGS; |
|
params.pitch = PITCH_NORM; |
|
params.initialStreamPosition = song->sampleposition; |
|
|
|
S_StartSound( params ); |
|
|
|
if ( IsPC() ) |
|
{ |
|
// Now find the channel this went on and skip ahead in the mixer |
|
for (int i = 0; i < total_channels; i++) |
|
{ |
|
channel_t *ch = &channels[i]; |
|
|
|
if ( !ch->pMixer || |
|
!ch->pMixer->GetSource() ) |
|
{ |
|
continue; |
|
} |
|
|
|
if ( ch->pMixer->GetSource() != pSound->pSource ) |
|
{ |
|
continue; |
|
} |
|
|
|
ch->pMixer->SetPositionFromSaved( song->sampleposition ); |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
soundlevel_t SND_GetSndlvl ( channel_t *pchannel ); |
|
|
|
// calculate ammount of sound to be mixed to dsp, based on distance from listener |
|
|
|
|
|
ConVar dsp_dist_min("dsp_dist_min", "0.0", FCVAR_DEMO|FCVAR_CHEAT); // range at which sounds are mixed at dsp_mix_min |
|
ConVar dsp_dist_max("dsp_dist_max", "1440.0", FCVAR_DEMO|FCVAR_CHEAT); // range at which sounds are mixed at dsp_mix_max |
|
|
|
ConVar dsp_mix_min("dsp_mix_min", "0.2", FCVAR_DEMO ); // dsp mix at dsp_dist_min distance "near" |
|
ConVar dsp_mix_max("dsp_mix_max", "0.8", FCVAR_DEMO ); // dsp mix at dsp_dist_max distance "far" |
|
ConVar dsp_db_min("dsp_db_min", "80", FCVAR_DEMO ); // sounds with sndlvl below this get dsp_db_mixdrop % less dsp mix |
|
ConVar dsp_db_mixdrop("dsp_db_mixdrop", "0.5", FCVAR_DEMO ); // sounds with sndlvl below dsp_db_min get dsp_db_mixdrop % less mix |
|
|
|
float DSP_ROOM_MIX = 1.0; // mix volume of dsp_room sounds when added back to 'dry' sounds |
|
float DSP_NOROOM_MIX = 1.0; // mix volume of facing + facing away sounds. added to dsp_room_mix sounds |
|
|
|
extern ConVar dsp_off; |
|
|
|
// returns 0-1.0 dsp mix value. If sound source is at a range >= DSP_DIST_MAX, return a mix value of |
|
// DSP_MIX_MAX. This mix value is used later to determine wet/dry mix ratio of sounds. |
|
|
|
// This ramp changes with db level of sound source, and is set in the dsp room presets by room size |
|
// empirical data: 0.78 is nominal mix for sound 100% at far end of room, 0.24 is mix for sound 25% into room |
|
|
|
float SND_GetDspMix( channel_t *pchannel, int idist) |
|
{ |
|
float mix; |
|
float dist = (float)idist; |
|
float dist_min = dsp_dist_min.GetFloat(); |
|
float dist_max = dsp_dist_max.GetFloat(); |
|
float mix_min; |
|
float mix_max; |
|
|
|
// only set dsp mix_min & mix_max when sound is first started |
|
|
|
if ( pchannel->dsp_mix_min < 0 && pchannel->dsp_mix_max < 0 ) |
|
{ |
|
mix_min = dsp_mix_min.GetFloat(); // set via dsp_room preset |
|
mix_max = dsp_mix_max.GetFloat(); // set via dsp_room preset |
|
|
|
// set mix_min & mix_max based on db level of sound: |
|
// sounds below dsp_db_min decrease dsp_mix_min & dsp_mix_max by N% |
|
// ie: quiet sounds get less dsp mix than loud sounds |
|
|
|
soundlevel_t sndlvl = SND_GetSndlvl( pchannel ); |
|
soundlevel_t sndlvl_min = (soundlevel_t)(dsp_db_min.GetInt()); |
|
|
|
if (sndlvl <= sndlvl_min) |
|
{ |
|
mix_min *= dsp_db_mixdrop.GetFloat(); |
|
mix_max *= dsp_db_mixdrop.GetFloat(); |
|
} |
|
|
|
pchannel->dsp_mix_min = mix_min; |
|
pchannel->dsp_mix_max = mix_max; |
|
} |
|
else |
|
{ |
|
mix_min = pchannel->dsp_mix_min; |
|
mix_max = pchannel->dsp_mix_max; |
|
} |
|
|
|
// dspmix is 0 (100% mix to facing buffer) if dsp_off |
|
|
|
if ( dsp_off.GetInt() ) |
|
return 0.0; |
|
|
|
// doppler wavs are mixed dry |
|
|
|
if ( pchannel->wavtype == CHAR_DOPPLER ) |
|
return 0.0; |
|
|
|
// linear ramp - get dry mix % |
|
|
|
// dist: 0->(max - min) |
|
|
|
dist = clamp( dist, dist_min, dist_max ) - dist_min; |
|
|
|
// dist: 0->1.0 |
|
|
|
dist = dist / (dist_max - dist_min); |
|
|
|
// mix: min->max |
|
|
|
mix = ((mix_max - mix_min) * dist) + mix_min; |
|
|
|
return mix; |
|
} |
|
|
|
// calculate crossfade between wav left (close sound) and wav right (far sound) based on |
|
// distance fron listener |
|
|
|
#define DVAR_DIST_MIN (20.0 * 12.0) // play full 'near' sound at 20' or less |
|
#define DVAR_DIST_MAX (110.0 * 12.0) // play full 'far' sound at 110' or more |
|
#define DVAR_MIX_MIN 0.0 |
|
#define DVAR_MIX_MAX 1.0 |
|
|
|
// calculate mixing parameter for CHAR_DISTVAR wavs |
|
// returns 0 - 1.0, 1.0 is 100% far sound (wav right) |
|
|
|
float SND_GetDistanceMix( channel_t *pchannel, int idist) |
|
{ |
|
float mix; |
|
float dist = (float)idist; |
|
|
|
// doppler wavs are 100% near - their spatialization is calculated later. |
|
|
|
if ( pchannel->wavtype == CHAR_DOPPLER ) |
|
return 0.0; |
|
|
|
// linear ramp - get dry mix % |
|
|
|
// dist 0->(max - min) |
|
|
|
dist = clamp( dist, (float) DVAR_DIST_MIN, (float) DVAR_DIST_MAX ) - (float) DVAR_DIST_MIN; |
|
|
|
// dist 0->1.0 |
|
|
|
dist = dist / (DVAR_DIST_MAX - DVAR_DIST_MIN); |
|
|
|
// mix min->max |
|
|
|
mix = ((DVAR_MIX_MAX - DVAR_MIX_MIN) * dist) + DVAR_MIX_MIN; |
|
|
|
return mix; |
|
} |
|
|
|
// given facing direction of source, and channel, |
|
// return -1.0 - 1.0, where -1.0 is source facing away from listener |
|
// and 1.0 is source facing listener |
|
|
|
|
|
float SND_GetFacingDirection( channel_t *pChannel, const QAngle &source_angles ) |
|
{ |
|
Vector SF; // sound source forward direction unit vector |
|
Vector SL; // sound -> listener unit vector |
|
float dotSFSL; |
|
|
|
// no facing direction unless wavtyp CHAR_DIRECTIONAL |
|
|
|
if ( pChannel->wavtype != CHAR_DIRECTIONAL ) |
|
return 1.0; |
|
|
|
VectorSubtract(listener_origin, pChannel->origin, SL); |
|
VectorNormalize(SL); |
|
|
|
// compute forward vector for sound entity |
|
|
|
AngleVectors( source_angles, &SF, NULL, NULL ); |
|
|
|
// dot source forward unit vector with source to listener unit vector to get -1.0 - 1.0 facing. |
|
// ie: projection of SF onto SL |
|
|
|
dotSFSL = DotProduct( SF, SL ); |
|
|
|
return dotSFSL; |
|
} |
|
|
|
// calculate point of closest approach - caller must ensure that the |
|
// forward facing vector of the entity playing this sound points in exactly the direction of |
|
// travel of the sound. ie: for bullets or tracers, forward vector must point in traceline direction. |
|
// return true if sound is to be played, false if sound cannot be heard (shot away from player) |
|
|
|
bool SND_GetClosestPoint( channel_t *pChannel, QAngle &source_angles, Vector &vnearpoint ) |
|
{ |
|
// S - sound source origin |
|
// L - listener origin |
|
|
|
Vector SF; // sound source forward direction unit vector |
|
Vector SL; // sound -> listener vector |
|
Vector SD; // sound->closest point vector |
|
vec_t dSLSF; // magnitude of project of SL onto SF |
|
|
|
// P = SF (SF . SL) + S |
|
|
|
// only perform this calculation for doppler wavs |
|
|
|
if ( pChannel->wavtype != CHAR_DOPPLER ) |
|
return false; |
|
|
|
// get vector 'SL' from sound source to listener |
|
|
|
VectorSubtract(listener_origin, pChannel->origin, SL); |
|
|
|
// compute sound->forward vector 'SF' for sound entity |
|
|
|
AngleVectors( source_angles, &SF ); |
|
VectorNormalize( SF ); |
|
|
|
dSLSF = DotProduct( SL, SF ); |
|
|
|
|
|
if ( dSLSF <= 0 && !toolframework->IsToolRecording() ) |
|
{ |
|
// source is pointing away from listener, don't play anything |
|
// unless we're recording in the tool, since we may play back from in front of the source |
|
return false; |
|
} |
|
|
|
// project dSLSF along forward unit vector from sound source |
|
|
|
VectorMultiply( SF, dSLSF, SD ); |
|
|
|
// output vector - add SD to sound source origin |
|
|
|
VectorAdd( SD, pChannel->origin, vnearpoint ); |
|
|
|
return true; |
|
} |
|
|
|
|
|
// given point of nearest approach and sound source facing angles, |
|
// return vector pointing into quadrant in which to play |
|
// doppler left wav (incomming) and doppler right wav (outgoing). |
|
|
|
// doppler left is point in space to play left doppler wav |
|
// doppler right is point in space to play right doppler wav |
|
|
|
// Also modifies channel pitch based on distance to nearest approach point |
|
|
|
#define DOPPLER_DIST_LEFT_TO_RIGHT (4*12) // separate left/right sounds by 4' |
|
|
|
#define DOPPLER_DIST_MAX (20*12) // max distance - causes min pitch |
|
#define DOPPLER_DIST_MIN (1*12) // min distance - causes max pitch |
|
#define DOPPLER_PITCH_MAX 1.5 // max pitch change due to distance |
|
#define DOPPLER_PITCH_MIN 0.25 // min pitch change due to distance |
|
|
|
#define DOPPLER_RANGE_MAX (10*12) // don't play doppler wav unless within this range |
|
// UNDONE: should be set by caller! |
|
|
|
void SND_GetDopplerPoints( channel_t *pChannel, QAngle &source_angles, Vector &vnearpoint, Vector &source_doppler_left, Vector &source_doppler_right) |
|
{ |
|
Vector SF; // direction sound source is facing (forward) |
|
Vector LN; // vector from listener to closest approach point |
|
Vector DL; |
|
Vector DR; |
|
|
|
// nearpoint is closest point of approach, when playing CHAR_DOPPLER sounds |
|
|
|
// SF is normalized vector in direction sound source is facing |
|
|
|
AngleVectors( source_angles, &SF ); |
|
VectorNormalize( SF ); |
|
|
|
// source_doppler_left - location in space to play doppler left wav (incomming) |
|
// source_doppler_right - location in space to play doppler right wav (outgoing) |
|
|
|
VectorMultiply( SF, -1*DOPPLER_DIST_LEFT_TO_RIGHT, DL ); |
|
VectorMultiply( SF, DOPPLER_DIST_LEFT_TO_RIGHT, DR ); |
|
|
|
VectorAdd( vnearpoint, DL, source_doppler_left ); |
|
VectorAdd( vnearpoint, DR, source_doppler_right ); |
|
|
|
// set pitch of channel based on nearest distance to listener |
|
|
|
// LN is vector from listener to closest approach point |
|
|
|
VectorSubtract(vnearpoint, listener_origin, LN); |
|
|
|
float pitch; |
|
float dist = VectorLength( LN ); |
|
|
|
// dist varies 0->1 |
|
|
|
dist = clamp(dist, (float)DOPPLER_DIST_MIN, (float)DOPPLER_DIST_MAX); |
|
dist = (dist - DOPPLER_DIST_MIN) / (DOPPLER_DIST_MAX - DOPPLER_DIST_MIN); |
|
|
|
// pitch varies from max to min |
|
|
|
pitch = DOPPLER_PITCH_MAX - dist * (DOPPLER_PITCH_MAX - DOPPLER_PITCH_MIN); |
|
|
|
pChannel->basePitch = (int)(pitch * 100.0); |
|
} |
|
|
|
// console variables used to construct gain curve - don't change these! |
|
|
|
extern ConVar snd_foliage_db_loss; |
|
extern ConVar snd_gain; |
|
extern ConVar snd_refdb; |
|
extern ConVar snd_refdist; |
|
extern ConVar snd_gain_max; |
|
extern ConVar snd_gain_min; |
|
|
|
ConVar snd_showstart( "snd_showstart", "0", FCVAR_CHEAT ); // showstart always skips info on player footsteps! |
|
// 1 - show sound name, channel, volume, time |
|
// 2 - show dspmix, distmix, dspface, l/r/f/r vols |
|
// 3 - show sound origin coords |
|
// 4 - show gain of dsp_room |
|
// 5 - show dB loss due to obscured sound |
|
// 6 - reserved |
|
// 7 - show 2 and total gain & dist in ft. to sound source |
|
|
|
#define SND_DB_MAX 140.0 // max db of any sound source |
|
#define SND_DB_MED 90.0 // db at which compression curve changes |
|
#define SND_DB_MIN 60.0 // min db of any sound source |
|
|
|
#define SND_GAIN_PLAYER_WEAPON_DB 2.0 // increase player weapon gain by N dB |
|
|
|
// dB = 20 log (amplitude/32768) 0 to -90.3dB |
|
// amplitude = 32768 * 10 ^ (dB/20) 0 to +/- 32768 |
|
// gain = amplitude/32768 0 to 1.0 |
|
|
|
float Gain_To_dB ( float gain ) |
|
{ |
|
float dB = 20 * log ( gain ); |
|
return dB; |
|
} |
|
|
|
float dB_To_Gain ( float dB ) |
|
{ |
|
float gain = powf (10, dB / 20.0); |
|
return gain; |
|
} |
|
|
|
float Gain_To_Amplitude ( float gain ) |
|
{ |
|
return gain * 32768; |
|
} |
|
|
|
float Amplitude_To_Gain ( float amplitude ) |
|
{ |
|
return amplitude / 32768; |
|
} |
|
|
|
soundlevel_t SND_GetSndlvl ( channel_t *pchannel ) |
|
{ |
|
return DIST_MULT_TO_SNDLVL( pchannel->dist_mult ); |
|
} |
|
|
|
|
|
// The complete gain calculation, with SNDLVL given in dB is: |
|
// |
|
// GAIN = 1/dist * snd_refdist * 10 ^ ( ( SNDLVL - snd_refdb - (dist * snd_foliage_db_loss / 1200)) / 20 ) |
|
// |
|
// for gain > SND_GAIN_THRESH, start curve smoothing with |
|
// |
|
// GAIN = 1 - 1 / (Y * GAIN ^ SND_GAIN_POWER) |
|
// |
|
// where Y = -1 / ( (SND_GAIN_THRESH ^ SND_GAIN_POWER) * (SND_GAIN_THRESH - 1) ) |
|
// |
|
|
|
float SND_GetGainFromMult( float gain, float dist_mult, vec_t dist ); |
|
|
|
// gain curve construction |
|
|
|
float SND_GetGain( channel_t *ch, bool fplayersound, bool fmusicsound, bool flooping, vec_t dist, bool bAttenuated ) |
|
{ |
|
VPROF_("SND_GetGain",2,VPROF_BUDGETGROUP_OTHER_SOUND,false,BUDGETFLAG_OTHER); |
|
if ( ch->flags.m_bCompatibilityAttenuation ) |
|
{ |
|
// Convert to the original attenuation value. |
|
soundlevel_t soundlevel = DIST_MULT_TO_SNDLVL( ch->dist_mult ); |
|
float flAttenuation = SNDLVL_TO_ATTN( soundlevel ); |
|
|
|
// Now get the goldsrc dist_mult and use the same calculation it uses in SND_Spatialize. |
|
// Straight outta Goldsrc!!! |
|
vec_t nominal_clip_dist = 1000.0; |
|
float flGoldsrcDistMult = flAttenuation / nominal_clip_dist; |
|
dist *= flGoldsrcDistMult; |
|
float flReturnValue = 1.0f - dist; |
|
flReturnValue = clamp( flReturnValue, 0.f, 1.f ); |
|
return flReturnValue; |
|
} |
|
else |
|
{ |
|
float gain = snd_gain.GetFloat(); |
|
|
|
if ( fmusicsound ) |
|
{ |
|
gain = gain * snd_musicvolume.GetFloat(); |
|
gain = gain * g_DashboardMusicMixValue; |
|
} |
|
|
|
if ( ch->dist_mult ) |
|
{ |
|
gain = SND_GetGainFromMult( gain, ch->dist_mult, dist ); |
|
} |
|
|
|
if ( fplayersound ) |
|
{ |
|
|
|
// player weapon sounds get extra gain - this compensates |
|
// for npc distance effect weapons which mix louder as L+R into L,R |
|
// Hack. |
|
|
|
if ( ch->entchannel == CHAN_WEAPON ) |
|
gain = gain * dB_To_Gain( SND_GAIN_PLAYER_WEAPON_DB ); |
|
} |
|
|
|
// modify gain if sound source not visible to player |
|
|
|
gain = gain * SND_GetGainObscured( ch, fplayersound, flooping, bAttenuated ); |
|
|
|
if (snd_showstart.GetInt() == 6) |
|
{ |
|
DevMsg( "(gain %1.3f : dist ft %1.1f) ", gain, (float)dist/12.0 ); |
|
snd_showstart.SetValue(5); // display once |
|
} |
|
|
|
return gain; |
|
} |
|
} |
|
|
|
// always ramp channel gain changes over time |
|
// returns ramped gain, given new target gain |
|
|
|
#define SND_GAIN_FADE_TIME 0.25 // xfade seconds between obscuring gain changes |
|
|
|
float SND_FadeToNewGain( channel_t *ch, float gain_new ) |
|
{ |
|
|
|
if ( gain_new == -1.0 ) |
|
{ |
|
// if -1 passed in, just keep fading to existing target |
|
|
|
gain_new = ch->ob_gain_target; |
|
} |
|
|
|
// if first time updating, store new gain into gain & target, return |
|
// if gain_new is close to existing gain, store new gain into gain & target, return |
|
|
|
if ( ch->flags.bfirstpass || (fabs (gain_new - ch->ob_gain) < 0.01)) |
|
{ |
|
ch->ob_gain = gain_new; |
|
ch->ob_gain_target = gain_new; |
|
ch->ob_gain_inc = 0.0; |
|
return gain_new; |
|
} |
|
|
|
// set up new increment to new target |
|
|
|
float frametime = g_pSoundServices->GetHostFrametime(); |
|
float speed; |
|
speed = ( frametime / SND_GAIN_FADE_TIME ) * (gain_new - ch->ob_gain); |
|
|
|
ch->ob_gain_inc = fabs(speed); |
|
|
|
// ch->ob_gain_inc = fabs(gain_new - ch->ob_gain) / 10.0; |
|
|
|
ch->ob_gain_target = gain_new; |
|
|
|
// if not hit target, keep approaching |
|
|
|
if ( fabs( ch->ob_gain - ch->ob_gain_target ) > 0.01 ) |
|
{ |
|
ch->ob_gain = Approach( ch->ob_gain_target, ch->ob_gain, ch->ob_gain_inc ); |
|
} |
|
else |
|
{ |
|
// close enough, set gain = target |
|
ch->ob_gain = ch->ob_gain_target; |
|
} |
|
|
|
return ch->ob_gain; |
|
} |
|
|
|
#define SND_TRACE_UPDATE_MAX 2 // max of N channels may be checked for obscured source per frame |
|
|
|
static int g_snd_trace_count = 0; // total tracelines for gain obscuring made this frame |
|
|
|
// All new sounds must traceline once, |
|
// but cap the max number of tracelines performed per frame |
|
// for longer or looping sounds to SND_TRACE_UPDATE_MAX. |
|
|
|
bool SND_ChannelOkToTrace( channel_t *ch ) |
|
{ |
|
// always trace first time sound is spatialized (doesn't update counter) |
|
|
|
if ( ch->flags.bfirstpass ) |
|
{ |
|
ch->flags.bTraced = true; |
|
return true; |
|
} |
|
|
|
// if already traced max channels this frame, return |
|
|
|
if ( g_snd_trace_count >= SND_TRACE_UPDATE_MAX ) |
|
return false; |
|
|
|
// ok to trace if this sound hasn't yet been traced in this round |
|
|
|
if ( ch->flags.bTraced ) |
|
return false; |
|
|
|
// set flag - don't traceline this sound again until all others have |
|
// been traced |
|
|
|
ch->flags.bTraced = true; |
|
|
|
g_snd_trace_count++; // total traces this frame |
|
|
|
return true; |
|
} |
|
|
|
// determine if we need to reset all flags for traceline limiting - |
|
// this happens if we hit a frame whein no tracelines occur ie: all currently |
|
// playing sounds are blocked. |
|
|
|
void SND_ChannelTraceReset( void ) |
|
{ |
|
if ( g_snd_trace_count ) |
|
return; |
|
|
|
// if no tracelines performed this frame, then reset all |
|
// trace flags |
|
|
|
for (int i = 0; i < total_channels; i++) |
|
channels[i].flags.bTraced = false; |
|
} |
|
|
|
bool SND_IsLongWave( channel_t *pChannel ) |
|
{ |
|
CAudioSource *pSource = pChannel->sfx ? pChannel->sfx->pSource : NULL; |
|
if ( pSource ) |
|
{ |
|
if ( pSource->IsStreaming() ) |
|
return true; |
|
|
|
// UNDONE: Do this on long wave files too? |
|
#if 0 |
|
float length = (float)pSource->SampleCount() / (float)pSource->SampleRate(); |
|
if ( length > 0.75f ) |
|
return true; |
|
#endif |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
ConVar snd_obscured_gain_db( "snd_obscured_gain_dB", "-2.70", FCVAR_CHEAT ); // dB loss due to obscured sound source |
|
|
|
// drop gain on channel if sound emitter obscured by |
|
// world, unbroken windows, closed doors, large solid entities etc. |
|
|
|
float SND_GetGainObscured( channel_t *ch, bool fplayersound, bool flooping, bool bAttenuated ) |
|
{ |
|
float gain = 1.0; |
|
int count = 1; |
|
float snd_gain_db; // dB loss due to obscured sound source |
|
|
|
// Unattenuated sounds don't get obscured. |
|
if ( !bAttenuated ) |
|
return 1.0f; |
|
|
|
if ( fplayersound ) |
|
return gain; |
|
|
|
// During signon just apply regular state machine since world hasn't been |
|
// created or settled yet... |
|
|
|
if ( !SND_IsInGame() ) |
|
{ |
|
if ( !toolframework->InToolMode() ) |
|
{ |
|
gain = SND_FadeToNewGain( ch, -1.0 ); |
|
} |
|
|
|
return gain; |
|
} |
|
|
|
// don't do gain obscuring more than once on short one-shot sounds |
|
|
|
if ( !ch->flags.bfirstpass && !ch->flags.isSentence && !flooping && !SND_IsLongWave(ch) ) |
|
{ |
|
gain = SND_FadeToNewGain( ch, -1.0 ); |
|
return gain; |
|
} |
|
|
|
snd_gain_db = snd_obscured_gain_db.GetFloat(); |
|
|
|
// if long or looping sound, process N channels per frame - set 'processed' flag, clear by |
|
// cycling through all channels - this maintains a cap on traces per frame |
|
|
|
if ( !SND_ChannelOkToTrace( ch ) ) |
|
{ |
|
// just keep updating fade to existing target gain - no new trace checking |
|
|
|
gain = SND_FadeToNewGain( ch, -1.0 ); |
|
return gain; |
|
} |
|
// set up traceline from player eyes to sound emitting entity origin |
|
|
|
Vector endpoint = ch->origin; |
|
|
|
trace_t tr; |
|
CTraceFilterWorldOnly filter; // UNDONE: also test for static props? |
|
Ray_t ray; |
|
ray.Init( MainViewOrigin(), endpoint ); |
|
g_pEngineTraceClient->TraceRay( ray, MASK_BLOCK_AUDIO, &filter, &tr ); |
|
|
|
if (tr.DidHit() && tr.fraction < 0.99) |
|
{ |
|
// can't see center of sound source: |
|
// build extents based on dB sndlvl of source, |
|
// test to see how many extents are visible, |
|
// drop gain by snd_gain_db per extent hidden |
|
|
|
Vector endpoints[4]; |
|
soundlevel_t sndlvl = DIST_MULT_TO_SNDLVL( ch->dist_mult ); |
|
float radius; |
|
Vector vsrc_forward; |
|
Vector vsrc_right; |
|
Vector vsrc_up; |
|
Vector vecl; |
|
Vector vecr; |
|
Vector vecl2; |
|
Vector vecr2; |
|
int i; |
|
|
|
// get radius |
|
|
|
if ( ch->radius > 0 ) |
|
radius = ch->radius; |
|
else |
|
radius = dB_To_Radius( sndlvl); // approximate radius from soundlevel |
|
|
|
// set up extent endpoints - on upward or downward diagonals, facing player |
|
|
|
for (i = 0; i < 4; i++) |
|
endpoints[i] = endpoint; |
|
|
|
// vsrc_forward is normalized vector from sound source to listener |
|
|
|
VectorSubtract( listener_origin, endpoint, vsrc_forward ); |
|
VectorNormalize( vsrc_forward ); |
|
VectorVectors( vsrc_forward, vsrc_right, vsrc_up ); |
|
|
|
VectorAdd( vsrc_up, vsrc_right, vecl ); |
|
|
|
// if src above listener, force 'up' vector to point down - create diagonals up & down |
|
|
|
if ( endpoint.z > listener_origin.z + (10 * 12) ) |
|
vsrc_up.z = -vsrc_up.z; |
|
|
|
VectorSubtract( vsrc_up, vsrc_right, vecr ); |
|
VectorNormalize( vecl ); |
|
VectorNormalize( vecr ); |
|
|
|
// get diagonal vectors from sound source |
|
|
|
vecl2 = radius * vecl; |
|
vecr2 = radius * vecr; |
|
vecl = (radius / 2.0) * vecl; |
|
vecr = (radius / 2.0) * vecr; |
|
|
|
// endpoints from diagonal vectors |
|
|
|
endpoints[0] += vecl; |
|
endpoints[1] += vecr; |
|
endpoints[2] += vecl2; |
|
endpoints[3] += vecr2; |
|
|
|
// drop gain for each point on radius diagonal that is obscured |
|
|
|
for (count = 0, i = 0; i < 4; i++) |
|
{ |
|
// UNDONE: some endpoints are in walls - in this case, trace from the wall hit location |
|
|
|
ray.Init( MainViewOrigin(), endpoints[i] ); |
|
g_pEngineTraceClient->TraceRay( ray, MASK_BLOCK_AUDIO, &filter, &tr ); |
|
|
|
if (tr.DidHit() && tr.fraction < 0.99 && !tr.startsolid ) |
|
{ |
|
count++; // skip first obscured point: at least 2 points + center should be obscured to hear db loss |
|
if (count > 1) |
|
gain = gain * dB_To_Gain( snd_gain_db ); |
|
} |
|
} |
|
} |
|
|
|
|
|
if ( flooping && snd_showstart.GetInt() == 7) |
|
{ |
|
static float g_drop_prev = 0; |
|
float drop = (count-1) * snd_gain_db; |
|
|
|
if (drop != g_drop_prev) |
|
{ |
|
DevMsg( "dB drop: %1.4f \n", drop); |
|
g_drop_prev = drop; |
|
} |
|
} |
|
|
|
// crossfade to new gain |
|
|
|
gain = SND_FadeToNewGain( ch, gain ); |
|
|
|
return gain; |
|
} |
|
|
|
// convert sound db level to approximate sound source radius, |
|
// used only for determining how much of sound is obscured by world |
|
|
|
#define SND_RADIUS_MAX (20.0 * 12.0) // max sound source radius |
|
#define SND_RADIUS_MIN (2.0 * 12.0) // min sound source radius |
|
|
|
inline float dB_To_Radius ( float db ) |
|
{ |
|
float radius = SND_RADIUS_MIN + (SND_RADIUS_MAX - SND_RADIUS_MIN) * (db - SND_DB_MIN) / (SND_DB_MAX - SND_DB_MIN); |
|
|
|
return radius; |
|
} |
|
|
|
struct snd_spatial_t |
|
{ |
|
int chan; // 0..4 cycles through up to 5 channels |
|
int cycle; // 0..2 cycles through 3 vectors per channel |
|
int dist[5][3]; // stores last 3 channel distance values [channel][cycle] |
|
|
|
float value_prev[5]; // previous value per channel |
|
|
|
double last_change; |
|
}; |
|
|
|
bool g_ssp_init = false; |
|
snd_spatial_t g_ssp; |
|
|
|
// return 0..1 percent difference between a & b |
|
|
|
float PercentDifference( float a, float b ) |
|
{ |
|
float vp; |
|
|
|
if (!(int)a && !(int)b) |
|
return 0.0; |
|
|
|
if (!(int)a || !(int)b) |
|
return 1.0; |
|
|
|
if (a > b) |
|
vp = b / a; |
|
else |
|
vp = a / b; |
|
|
|
return (1.0 - vp); |
|
} |
|
|
|
// NOTE: Do not change SND_WALL_TRACE_LEN without also changing PRC_MDY6 delay value in snd_dsp.cpp! |
|
|
|
#define SND_WALL_TRACE_LEN (100.0*12.0) // trace max of 100' = max of 100 milliseconds of linear delay |
|
#define SND_SPATIAL_WAIT (0.25) // seconds to wait between traces |
|
|
|
// change mod delay value on chan 0..3 to v (inches) |
|
|
|
void DSP_SetSpatialDelay( int chan, float v ) |
|
{ |
|
// remap delay value 0..1200 to 1.0 to -1.0 for modulation |
|
|
|
float value = ( v / SND_WALL_TRACE_LEN) - 1.0; // -1.0...0 |
|
value = value * 2.0; // -2.0...0 |
|
value += 1.0; // -1.0...1.0 (0...1200) |
|
value *= -1.0; // 1.0...-1.0 (0...1200) |
|
|
|
// assume first processor in dsp_spatial is the modulating delay unit for DSP_ChangePresetValue |
|
|
|
int iproc = 0; |
|
|
|
DSP_ChangePresetValue( idsp_spatial, chan, iproc, value ); |
|
/* |
|
|
|
if (chan & 0x01) |
|
DevMsg("RDly: %3.0f \n", v/12 ); |
|
else |
|
DevMsg("LDly: %3.0f \n", v/12 ); |
|
*/ |
|
} |
|
|
|
// use non-feedback delay to stereoize (or make quad, or quad + center) the mono dsp_room fx, |
|
// This simulates the average sum of delays caused by reflections |
|
// from the left and right walls relative to the player. The average delay |
|
// difference between left & right wall is (l + r)/2. This becomes the average |
|
// delay difference between left & right ear. |
|
// call at most once per frame to update player->wall spatial delays |
|
|
|
void SND_SetSpatialDelays() |
|
{ |
|
VPROF("SoundSpatialDelays"); |
|
float dist, v, vp; |
|
Vector v_dir, v_dir2; |
|
int chan_max = (g_AudioDevice->IsSurround() ? 4 : 2) + (g_AudioDevice->IsSurroundCenter() ? 1 : 0); // 2, 4, 5 channels |
|
|
|
// use listener_forward2d, which doesn't change when player looks up/down. |
|
|
|
Vector listener_forward2d; |
|
|
|
ConvertListenerVectorTo2D( &listener_forward2d, &listener_right ); |
|
|
|
// init struct if 1st time through |
|
|
|
if ( !g_ssp_init ) |
|
{ |
|
Q_memset(&g_ssp, 0, sizeof(snd_spatial_t)); |
|
g_ssp_init = true; |
|
} |
|
|
|
// return if dsp_spatial is 0 |
|
|
|
if ( !dsp_spatial.GetInt() ) |
|
return; |
|
|
|
// if listener has not been updated, do nothing |
|
|
|
if ((listener_origin == vec3_origin) && |
|
(listener_forward == vec3_origin) && |
|
(listener_right == vec3_origin) && |
|
(listener_up == vec3_origin) ) |
|
return; |
|
|
|
if ( !SND_IsInGame() ) |
|
return; |
|
|
|
// get time |
|
|
|
double dtime = g_pSoundServices->GetHostTime(); |
|
|
|
// compare to previous time - if starting new check - don't check for new room until timer expires |
|
|
|
if (!g_ssp.chan && !g_ssp.cycle) |
|
{ |
|
if (fabs(dtime - g_ssp.last_change) < SND_SPATIAL_WAIT) |
|
return; |
|
} |
|
|
|
// cycle through forward, left, rearward vectors, averaging to get left/right delay |
|
// count[chan][cycle] 0,1 0,2 0,3 1,1 1,2 1,3 2,1 2,2 2,3 ... |
|
|
|
g_ssp.cycle++; |
|
|
|
if (g_ssp.cycle == 3) |
|
{ |
|
g_ssp.cycle = 0; |
|
|
|
// cycle through front left, front right, rear left, rear right, front center delays |
|
|
|
g_ssp.chan++; |
|
|
|
if (g_ssp.chan >= chan_max ) |
|
g_ssp.chan = 0; |
|
} |
|
|
|
// set up traceline from player eyes to surrounding walls |
|
|
|
switch( g_ssp.chan ) |
|
{ |
|
default: |
|
case 0: // front left: trace max 100' 'cone' to player's left |
|
if ( g_AudioDevice->IsSurround() ) |
|
{ |
|
// 4-5 speaker case - front left |
|
v_dir = (-listener_right + listener_forward2d) / 2.0; |
|
v_dir = g_ssp.cycle ? (g_ssp.cycle == 1 ? -listener_right * 0.5: listener_forward2d * 0.5) : v_dir; |
|
} |
|
else |
|
{ |
|
// 2 speaker case - left |
|
v_dir = listener_right * -1.0; |
|
v_dir2 = g_ssp.cycle ? (g_ssp.cycle == 1 ? listener_forward2d * 0.5 : -listener_forward2d * 0.5) : v_dir; |
|
v_dir = (v_dir + v_dir2) / 2.0; |
|
} |
|
break; |
|
|
|
case 1: // front right: trace max 100' 'cone' to player's right |
|
if ( g_AudioDevice->IsSurround() ) |
|
{ |
|
// 4-5 speaker case - front right |
|
v_dir = (listener_right + listener_forward2d) / 2.0; |
|
v_dir = g_ssp.cycle ? (g_ssp.cycle == 1 ? listener_right * 0.5: listener_forward2d * 0.5) : v_dir; |
|
} |
|
else |
|
{ |
|
// 2 speaker case - right |
|
v_dir = listener_right; |
|
v_dir2 = g_ssp.cycle ? (g_ssp.cycle == 1 ? listener_forward2d * 0.5 : -listener_forward2d * 0.5) : v_dir; |
|
v_dir = (v_dir + v_dir2) / 2.0; |
|
} |
|
break; |
|
|
|
case 2: // rear left: trace max 100' 'cone' to player's rear left |
|
v_dir = (listener_right + listener_forward2d) / -2.0; |
|
v_dir = g_ssp.cycle ? (g_ssp.cycle == 1 ? -listener_right * 0.5 : -listener_forward2d * 0.5) : v_dir; |
|
break; |
|
|
|
case 3: // rear right: trace max 100' 'cone' to player's rear right |
|
v_dir = (listener_right - listener_forward2d) / 2.0; |
|
v_dir = g_ssp.cycle ? (g_ssp.cycle == 1 ? listener_right * 0.5: -listener_forward2d * 0.5) : v_dir; |
|
break; |
|
|
|
case 4: // front center: trace max 100' 'cone' to player's front |
|
v_dir = listener_forward2d; |
|
v_dir2 = g_ssp.cycle ? (g_ssp.cycle == 1 ? listener_right * 0.15 : -listener_right * 0.15) : v_dir; |
|
v_dir = (v_dir + v_dir2); |
|
break; |
|
} |
|
|
|
Vector endpoint; |
|
trace_t tr; |
|
CTraceFilterWorldOnly filter; |
|
|
|
endpoint = MainViewOrigin() + v_dir * SND_WALL_TRACE_LEN; |
|
Ray_t ray; |
|
ray.Init( MainViewOrigin(), endpoint ); |
|
g_pEngineTraceClient->TraceRay( ray, MASK_BLOCK_AUDIO, &filter, &tr ); |
|
|
|
dist = SND_WALL_TRACE_LEN; |
|
|
|
if ( tr.DidHit() ) |
|
{ |
|
dist = VectorLength( tr.endpos - MainViewOrigin() ); |
|
} |
|
|
|
g_ssp.dist[g_ssp.chan][g_ssp.cycle] = dist; |
|
|
|
// set new result in dsp_spatial delay params when all delay values have been filled in |
|
|
|
if (!g_ssp.cycle && !g_ssp.chan) |
|
{ |
|
// update delay for each channel |
|
|
|
for (int chan = 0; chan < chan_max; chan++) |
|
{ |
|
// compute average of 3 traces per channel |
|
|
|
v = (g_ssp.dist[chan][0] + g_ssp.dist[chan][1] + g_ssp.dist[chan][2]) / 3.0; |
|
vp = g_ssp.value_prev[chan]; |
|
|
|
// only change if 10% difference from previous |
|
|
|
if ((vp != v) && int(v) && (PercentDifference( v, vp ) >= 0.1)) |
|
{ |
|
// update when we have data for all L/R && RL/RR channels... |
|
|
|
if (chan & 0x1) |
|
{ |
|
float vr = fpmin( v, (50*12.0f) ); |
|
float vl = fpmin(g_ssp.value_prev[chan-1], (50*12.0f)); |
|
|
|
/* UNDONE: not needed, now that this applies only to dsp 'room' buffer |
|
|
|
// ensure minimum separation = average distance to walls |
|
|
|
float dmin = (vl + vr) / 2.0; // average distance to walls |
|
float d = vl - vr; // l/r separation |
|
|
|
// if separation is less than average, increase min |
|
|
|
if (abs(d) < dmin/2) |
|
{ |
|
if (vl > vr) |
|
vl += dmin/2 - d; |
|
else |
|
vr += dmin/2 - d; |
|
} |
|
*/ |
|
DSP_SetSpatialDelay(chan-1, vl); |
|
DSP_SetSpatialDelay(chan, vr); |
|
} |
|
|
|
// update center chan |
|
|
|
if (chan == 4) |
|
{ |
|
float vl = fpmin( v, (50*12.0f) ); |
|
DSP_SetSpatialDelay(chan, vl); |
|
} |
|
} |
|
|
|
g_ssp.value_prev[chan] = v; |
|
|
|
} |
|
|
|
// update wait timer now that all values have been checked |
|
|
|
g_ssp.last_change = dtime; |
|
} |
|
} |
|
|
|
// Dsp Automatic Selection: |
|
|
|
// a) enabled by setting dsp_room to DSP_AUTOMATIC. Subsequently, dsp_automatic is the actual dsp value for dsp_room. |
|
// b) disabled by setting dsp_room to anything else |
|
|
|
// c) while enabled, detection nodes are placed as player moves into a new space |
|
// i. at each node, a new dsp setting is calculated and dsp_automatic is set to an appropriate preset |
|
// ii. new nodes are set when player moves out of sight of previous node |
|
// iii. moving into line of sight of a detection node causes closest node to player to set dsp_automatic |
|
|
|
// see void DAS_CheckNewRoomDSP( ) for main entrypoint |
|
|
|
ConVar das_debug( "adsp_debug", "0", FCVAR_ARCHIVE ); |
|
// >0: draw blue dsp detection node location |
|
// >1: draw green room trace height detection bars |
|
// 3: draw yellow horizontal trace bars for room width/depth detection |
|
// 4: draw yellow upward traces for height detection |
|
// 5: draw teal box around all props around player |
|
// 6: draw teal box around room as detected |
|
|
|
#define DAS_CWALLS 20 // # of wall traces to save for calculating room dimensions |
|
#define DAS_ROOM_TRACE_LEN (400.0*12.0) // max size of trace to check for room dimensions |
|
|
|
#define DAS_AUTO_WAIT 0.25 // wait min of n seconds between dsp_room changes and update checks |
|
|
|
#define DAS_WIDTH_MIN 0.4 // min % change in avg width of any wall pair to cause new dsp |
|
#define DAS_REFL_MIN 0.5 // min % change in avg refl of any wall to cause new dsp |
|
#define DAS_SKYHIT_MIN 0.8 // min % change in # of sky hits per wall |
|
|
|
#define DAS_DIST_MIN (4.0 * 12.0) // min distance between room dsp changes |
|
#define DAS_DIST_MAX (40.0 * 12.0) // max distance to preserve room dsp changes |
|
|
|
#define DAS_DIST_MIN_OUTSIDE (6.0 * 12.0) // min distance between room dsp changes outside |
|
#define DAS_DIST_MAX_OUTSIDE (100.0 * 12.0) // max distance to preserve room dsp changes outside |
|
|
|
#define IVEC_DIAG_UP 8 // start of diagonal up vectors |
|
#define IVEC_UP 18 // up vector |
|
#define IVEC_DOWN 19 // down vector |
|
|
|
#define DAS_REFLECTIVITY_NORM 0.5 |
|
#define DAS_REFLECTIVITY_SKY 0.0 |
|
|
|
// auto dsp room struct |
|
|
|
struct das_room_t |
|
{ |
|
int dist[DAS_CWALLS]; // distance in units from player to axis aligned and diagonal walls |
|
float reflect[DAS_CWALLS]; // acoustic reflectivity per wall |
|
float skyhits[DAS_CWALLS]; // every sky hit adds 0.1 |
|
Vector hit[DAS_CWALLS]; // location of trace hit on wall - used for calculating average centers |
|
Vector norm[DAS_CWALLS]; // wall normal at hit location |
|
|
|
Vector vplayer; // 'frozen' location above player's head |
|
|
|
Vector vplayer_eyes; // 'frozen' location player's eyes |
|
|
|
int width_max; // max width |
|
int length_max; // max length |
|
int height_max; // max height |
|
|
|
float refl_avg; // running average of reflectivity of all walls |
|
float refl_walls[6]; // left,right,front,back,ceiling,floor reflectivities |
|
|
|
float sky_pct; // percent of sky hits |
|
|
|
Vector room_mins; // room bounds |
|
Vector room_maxs; |
|
|
|
double last_dsp_change; // time since last dsp change |
|
|
|
float diffusion; // 0..1.0 check radius (avg of width_avg) for # of props - scale diffusion based on # found |
|
short iwall; // cycles through walls 0..5, ensuring only one trace per frame |
|
short ent_count; // count of entities found in radius |
|
bool bskyabove; // true if sky found above player (ie: outside) |
|
bool broomready; // true if all distances are filled in and room is ready to check |
|
short lowceiling; // if non-zero, ceiling directly above player if < 112 units |
|
}; |
|
|
|
// dsp detection node |
|
|
|
struct das_node_t |
|
{ |
|
Vector vplayer; // position |
|
|
|
bool fused; // true if valid node |
|
bool fseesplayer; // true if node sees player on last check |
|
short dsp_preset; // preset |
|
|
|
int range_min; // min,max detection ranges |
|
int range_max; |
|
|
|
int dist; // last distance to player |
|
|
|
// room parameters when node was created: |
|
|
|
das_room_t room; |
|
}; |
|
|
|
#define DAS_CNODES 40 // keep around last n nodes - must be same as DSP_CAUTO_PRESETS!!! |
|
|
|
das_node_t g_das_nodes[DAS_CNODES]; // all dsp detection nodes |
|
das_node_t *g_pdas_last_node = NULL; // last node that saw player |
|
|
|
int g_das_check_next; // next node to check |
|
int g_das_store_next; // next place to store node |
|
bool g_das_all_checked; // true if all nodes checked |
|
int g_das_checked_count; // count of nodes checked in latest pass |
|
|
|
das_room_t g_das_room; // room detector |
|
|
|
bool g_bdas_room_init = 0; |
|
bool g_bdas_init_nodes = 0; |
|
bool g_bdas_create_new_node = 0; |
|
|
|
bool DAS_TraceNodeToPlayer( das_room_t *proom, das_node_t *pnode ); |
|
void DAS_InitAutoRoom( das_room_t *proom); |
|
void DAS_DebugDrawTrace ( trace_t *ptr, int r, int g, int b, float duration, int imax ); |
|
|
|
Vector g_das_vec3[DAS_CWALLS]; // trace vectors to walls, ceiling, floor |
|
|
|
void DAS_InitNodes( void ) |
|
{ |
|
Q_memset(g_das_nodes, 0, sizeof(das_node_t) * DAS_CNODES); |
|
g_das_check_next = 0; |
|
g_das_store_next = 0; |
|
g_das_all_checked = 0; |
|
g_das_checked_count = 0; |
|
|
|
// init all rooms |
|
|
|
for (int i = 0; i < DAS_CNODES; i++) |
|
DAS_InitAutoRoom( &(g_das_nodes[i].room) ); |
|
|
|
// init trace vectors |
|
// set up trace vectors for max, min width |
|
float vl = DAS_ROOM_TRACE_LEN; |
|
float vlu = DAS_ROOM_TRACE_LEN * 0.52; |
|
float vlu2 = DAS_ROOM_TRACE_LEN * 0.48; // don't use 'perfect' diagonals |
|
|
|
g_das_vec3[0].Init(vl, 0.0, 0.0); // x left |
|
g_das_vec3[1].Init(-vl, 0.0, 0.0); // x right |
|
|
|
g_das_vec3[2].Init(0.0, vl, 0.0); // y front |
|
g_das_vec3[3].Init(0.0, -vl, 0.0); // y back |
|
|
|
g_das_vec3[4].Init(-vlu, vlu2, 0.0); // diagonal front left |
|
g_das_vec3[5].Init(vlu, -vlu2, 0.0); // diagonal rear right |
|
|
|
g_das_vec3[6].Init(vlu, vlu2, 0.0); // diagonal front right |
|
g_das_vec3[7].Init(-vlu, -vlu2, 0.0); // diagonal rear left |
|
|
|
// set up trace vectors for max height - on x=y diagonal |
|
|
|
g_das_vec3[8].Init(vlu, vlu2, vlu/2.0); // front right up A x,y,z/2 (IVEC_DIAG_UP) |
|
g_das_vec3[9].Init(vlu, vlu2, vlu); // front right up B x,y,z |
|
g_das_vec3[10].Init(vlu/2.0, vlu2/2.0, vlu); // front right up C x/2,y/2,z |
|
|
|
g_das_vec3[11].Init(-vlu, -vlu2, vlu/2.0); // rear left up A -x,-y,z/2 |
|
g_das_vec3[12].Init(-vlu, -vlu2, vlu); // rear left up B -x,-y,z |
|
g_das_vec3[13].Init(-vlu/2.0, -vlu2/2.0, vlu); // rear left up C -x/2,-y/2,z |
|
|
|
// set up trace vectors for max height - on x axis & y axis |
|
|
|
g_das_vec3[14].Init(-vlu, 0, vlu); // left up B -x,0,z |
|
g_das_vec3[15].Init(0, vlu/2.0, vlu); // front up C -x/2,0,z |
|
|
|
g_das_vec3[16].Init(0, -vlu, vlu); // rear up B x,0,z |
|
g_das_vec3[17].Init(vlu/2.0, 0, vlu); // right up C x/2,0,z |
|
|
|
g_das_vec3[18].Init(0.0, 0.0, vl); // up (IVEC_UP) |
|
g_das_vec3[19].Init(0.0, 0.0, -vl); // down (IVEC_DOWN) |
|
} |
|
|
|
void DAS_InitAutoRoom( das_room_t *proom) |
|
{ |
|
Q_memset(proom, 0, sizeof (das_room_t)); |
|
} |
|
|
|
// reset all nodes for next round of visibility checks between player & nodes |
|
|
|
void DAS_ResetNodes( void ) |
|
{ |
|
for (int i = 0; i < DAS_CNODES; i++) |
|
{ |
|
g_das_nodes[i].fseesplayer = false; |
|
g_das_nodes[i].dist = 0; |
|
} |
|
|
|
g_das_all_checked = false; |
|
g_das_checked_count = 0; |
|
g_bdas_create_new_node = false; |
|
} |
|
|
|
// utility function - return next index, wrap at max |
|
|
|
int DAS_GetNextIndex( int *pindex, int max ) |
|
{ |
|
int i = *pindex; |
|
int j; |
|
|
|
j = i+1; |
|
if ( j >= max ) |
|
j = 0; |
|
|
|
*pindex = j; |
|
|
|
return i; |
|
} |
|
|
|
// returns true if dsp node is within range of player |
|
|
|
bool DAS_NodeInRange( das_room_t *proom, das_node_t *pnode ) |
|
{ |
|
float dist; |
|
|
|
dist = VectorLength( proom->vplayer - pnode->vplayer ); |
|
|
|
// player can still see previous room selection point, and it's less than n feet away, |
|
// then flag this node as visible |
|
|
|
pnode->dist = dist; |
|
|
|
return ( dist <= pnode->range_max ); |
|
} |
|
|
|
// update next valid node - set up internal node state if it can see player |
|
// called once per frame |
|
// returns true if all nodes have been checked |
|
|
|
bool DAS_CheckNextNode( das_room_t *proom ) |
|
{ |
|
int i, j; |
|
|
|
if ( g_das_all_checked ) |
|
return true; |
|
|
|
// find next valid node |
|
|
|
for (j = 0; j < DAS_CNODES; j++) |
|
{ |
|
// track number of nodes checked |
|
|
|
g_das_checked_count++; |
|
|
|
// get next node in range to check |
|
|
|
i = DAS_GetNextIndex( &g_das_check_next, DAS_CNODES ); |
|
|
|
if ( g_das_nodes[i].fused && DAS_NodeInRange( proom, &(g_das_nodes[i]) ) ) |
|
{ |
|
// trace to see if player can still see node, |
|
// if so stop checking |
|
|
|
if ( DAS_TraceNodeToPlayer( proom, &(g_das_nodes[i]) )) |
|
goto checknode_exit; |
|
} |
|
} |
|
|
|
checknode_exit: |
|
|
|
// flag that all nodes have been checked |
|
|
|
if ( g_das_checked_count >= DAS_CNODES ) |
|
g_das_all_checked = true; |
|
|
|
return g_das_all_checked; |
|
} |
|
|
|
|
|
int DAS_GetNextNodeIndex() |
|
{ |
|
return g_das_store_next; |
|
} |
|
// store new node for room |
|
|
|
void DAS_StoreNode( das_room_t *proom, int dsp_preset) |
|
{ |
|
// overwrite node in cyclic list |
|
|
|
int i = DAS_GetNextIndex( &g_das_store_next, DAS_CNODES ); |
|
|
|
g_das_nodes[i].dsp_preset = dsp_preset; |
|
g_das_nodes[i].fused = true; |
|
g_das_nodes[i].vplayer = proom->vplayer; |
|
|
|
// calculate node scanning range_max based on room size |
|
|
|
if ( !proom->bskyabove ) |
|
{ |
|
// inside range - halls & tunnels have nodes every 5*width |
|
g_das_nodes[i].range_max = fpmin((int)DAS_DIST_MAX, min(proom->width_max * 5, proom->length_max) ); |
|
g_das_nodes[i].range_min = DAS_DIST_MIN; |
|
} |
|
else |
|
{ |
|
// outside range |
|
g_das_nodes[i].range_max = DAS_DIST_MAX_OUTSIDE; |
|
g_das_nodes[i].range_min = DAS_DIST_MIN_OUTSIDE; |
|
} |
|
|
|
g_das_nodes[i].fseesplayer = false; |
|
g_das_nodes[i].dist = 0; |
|
|
|
g_das_nodes[i].room = *proom; |
|
|
|
// update last node visible as this node |
|
|
|
g_pdas_last_node = &(g_das_nodes[i]); |
|
} |
|
|
|
// check all updated nodes, |
|
// return dsp_preset of largest node (by area) that can see player |
|
// return -1 if no preset found |
|
|
|
// NOTE: outside nodes can't see player if player is inside and vice versa |
|
// foutside is true if player is outside |
|
|
|
int DAS_GetDspPreset( bool foutside ) |
|
{ |
|
int dsp_preset = -1; |
|
|
|
int i; |
|
// int dist_min = 100000; |
|
int area_max = 0; |
|
int area; |
|
|
|
// find node that represents room with greatest floor area, return its preset. |
|
|
|
for (i = 0; i < DAS_CNODES; i++) |
|
{ |
|
if (g_das_nodes[i].fused && g_das_nodes[i].fseesplayer) |
|
{ |
|
area = (g_das_nodes[i].room.width_max * g_das_nodes[i].room.length_max); |
|
|
|
if ( g_das_nodes[i].room.bskyabove == foutside ) |
|
{ |
|
if (area > area_max) |
|
{ |
|
area_max = area; |
|
dsp_preset = g_das_nodes[i].dsp_preset; |
|
|
|
// save pointer to last node that saw player |
|
|
|
g_pdas_last_node = &(g_das_nodes[i]); |
|
} |
|
} |
|
/* |
|
|
|
// find nearest node, return its preset |
|
|
|
if (g_das_nodes[i].dist < dist_min) |
|
{ |
|
if ( g_das_nodes[i].room.bskyabove == foutside ) |
|
{ |
|
dist_min = g_das_nodes[i].dist; |
|
dsp_preset = g_das_nodes[i].dsp_preset; |
|
|
|
// save pointer to last node that saw player |
|
|
|
g_pdas_last_node = &(g_das_nodes[i]); |
|
|
|
} |
|
} |
|
*/ |
|
} |
|
} |
|
|
|
return dsp_preset; |
|
} |
|
|
|
// custom trace filter: |
|
// a) never hit player or monsters or entities |
|
// b) always hit world, or moveables or static props |
|
|
|
class CTraceFilterDAS : public ITraceFilter |
|
{ |
|
public: |
|
bool ShouldHitEntity( IHandleEntity *pHandleEntity, int contentsMask ) |
|
{ |
|
IClientUnknown *pUnk = static_cast<IClientUnknown*>(pHandleEntity); |
|
IClientEntity *pEntity; |
|
|
|
if ( !pUnk ) |
|
return false; |
|
|
|
// don't hit non-collideable props |
|
|
|
if ( StaticPropMgr()->IsStaticProp( pHandleEntity ) ) |
|
{ |
|
|
|
ICollideable *pCollide = StaticPropMgr()->GetStaticProp( pHandleEntity); |
|
if (!pCollide) |
|
return false; |
|
} |
|
|
|
// don't hit any ents |
|
|
|
pEntity = pUnk->GetIClientEntity(); |
|
|
|
if ( pEntity ) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
virtual TraceType_t GetTraceType() const |
|
{ |
|
return TRACE_EVERYTHING_FILTER_PROPS; |
|
} |
|
}; |
|
|
|
#define DAS_TRACE_MASK (CONTENTS_SOLID|CONTENTS_MOVEABLE|CONTENTS_WINDOW) |
|
|
|
// returns true if clear line exists between node and player |
|
// if node can see player, sets up node distance and flag fseesplayer |
|
|
|
bool DAS_TraceNodeToPlayer( das_room_t *proom, das_node_t *pnode ) |
|
{ |
|
trace_t trP; |
|
CTraceFilterDAS filterP; |
|
bool fseesplayer = false; |
|
float dist; |
|
Ray_t ray; |
|
ray.Init( proom->vplayer, pnode->vplayer ); |
|
|
|
g_pEngineTraceClient->TraceRay( ray, DAS_TRACE_MASK, &filterP, &trP ); |
|
dist = VectorLength( proom->vplayer - pnode->vplayer ); |
|
|
|
// player can still see previous room selection point, and it's less than n feet away, |
|
// then flag this node as visible |
|
|
|
if ( !trP.DidHit() && (dist <= DAS_DIST_MAX) ) |
|
{ |
|
fseesplayer = true; |
|
pnode->dist = dist; |
|
} |
|
|
|
pnode->fseesplayer = fseesplayer; |
|
|
|
return fseesplayer; |
|
} |
|
|
|
// update room boundary maxs, mins |
|
|
|
void DAS_SetRoomBounds( das_room_t *proom, Vector &hit, bool bheight ) |
|
{ |
|
Vector maxs, mins; |
|
|
|
maxs = proom->room_maxs; |
|
mins = proom->room_mins; |
|
|
|
if (!bheight) |
|
{ |
|
if (hit.x > maxs.x) |
|
maxs.x = hit.x; |
|
|
|
if (hit.x < mins.x) |
|
mins.x = hit.x; |
|
|
|
if (hit.z > maxs.z) |
|
maxs.z = hit.z; |
|
|
|
if (hit.z < mins.z) |
|
mins.z = hit.z; |
|
} |
|
|
|
if (bheight) |
|
{ |
|
if (hit.y > maxs.y) |
|
maxs.y = hit.y; |
|
|
|
if (hit.y < mins.y) |
|
mins.y = hit.y; |
|
} |
|
|
|
proom->room_maxs = maxs; |
|
proom->room_mins = mins; |
|
} |
|
|
|
// when all walls are updated, calculate max length, width, height, reflectivity, sky hit%, room center |
|
// returns true if room parameters are in good location to place a node |
|
// returns false if room parameters are not in good location to place a node |
|
// note: false occurs if up vector doesn't hit sky, but one or more up diagonal vectors do hit sky |
|
|
|
bool DAS_CalcRoomProps( das_room_t *proom ) |
|
{ |
|
int length_max = 0; |
|
int width_max = 0; |
|
int height_max = 0; |
|
int dist[4]; |
|
float area1, area2; |
|
int height; |
|
int i; |
|
int j; |
|
int k; |
|
bool b_diaghitsky = false; |
|
|
|
// reject this location if up vector doesn't hit sky, but |
|
// one or more up diagonals do hit sky - |
|
// in this case, player is under a slight overhang, narrow bridge, or |
|
// standing just inside a window or doorway. keep looking for better node location |
|
|
|
for (i = IVEC_DIAG_UP; i < IVEC_UP; i++) |
|
{ |
|
if (proom->skyhits[i] > 0.0) |
|
b_diaghitsky = true; |
|
} |
|
|
|
if (b_diaghitsky && !(proom->skyhits[IVEC_UP] > 0.0)) |
|
return false; |
|
|
|
// get all distance pairs |
|
|
|
for (i = 0; i < IVEC_DIAG_UP; i+=2) |
|
dist[i/2] = proom->dist[i] + proom->dist[i+1]; // 1st pair is width |
|
|
|
// if areas differ by more than 25% |
|
// select the pair with the greater area |
|
|
|
// if areas do not differ by more than 25%, select the pair with the |
|
// longer measured distance. Filters incorrect selection due to diagonals. |
|
|
|
area1 = (float)(dist[0] * dist[1]); |
|
area2 = (float)(dist[2] * dist[3]); |
|
|
|
area1 = (int)area1 == 0 ? 1.0 : area1; |
|
area2 = (int)area2 == 0 ? 1.0 : area2; |
|
|
|
if ( PercentDifference(area1, area2) > 0.25 ) |
|
{ |
|
// areas are more than 25% different - select pair with greater area |
|
|
|
j = area1 > area2 ? 0 : 2; |
|
} |
|
else |
|
{ |
|
// select pair with longer measured distance |
|
|
|
int iMaxDist = 0; // index to max dist |
|
int dmax = 0; |
|
|
|
for (i = 0; i < 4; i++) |
|
{ |
|
if (dist[i] > dmax) |
|
{ |
|
dmax = dist[i]; |
|
iMaxDist = i; |
|
} |
|
} |
|
|
|
j = iMaxDist > 1 ? 2 : 0; |
|
} |
|
|
|
|
|
// width is always the smaller of the dimensions |
|
|
|
width_max = min (dist[j], dist[j+1]); |
|
length_max = max (dist[j], dist[j+1]); |
|
|
|
// get max height |
|
|
|
for (i = IVEC_DIAG_UP; i < IVEC_DOWN; i++) |
|
{ |
|
height = proom->dist[i]; |
|
|
|
if (height > height_max) |
|
height_max = height; |
|
} |
|
|
|
proom->length_max = length_max; |
|
proom->width_max = width_max; |
|
proom->height_max = height_max; |
|
|
|
// get room max,min from chosen width, depth |
|
// 0..3 or 4..7 |
|
|
|
for ( i = j*2; i < 4+(j*2); i++) |
|
DAS_SetRoomBounds( proom, proom->hit[i], false ); |
|
|
|
// get room height min from down trace |
|
|
|
proom->room_mins.z = proom->hit[IVEC_DOWN].z; |
|
|
|
// reset room height max to player trace height |
|
|
|
proom->room_maxs.z = proom->vplayer.z; |
|
|
|
// draw box around room max,min |
|
|
|
if (das_debug.GetInt() == 6) |
|
{ |
|
// draw box around all objects detected |
|
Vector maxs = proom->room_maxs; |
|
Vector mins = proom->room_mins; |
|
Vector orig = (maxs + mins) / 2.0; |
|
Vector absMax = maxs - orig; |
|
Vector absMin = mins - orig; |
|
|
|
CDebugOverlay::AddBoxOverlay( orig, absMax, absMin, vec3_angle, 255, 0, 255, 0, 60.0f ); |
|
} |
|
// calculate average reflectivity |
|
|
|
float refl = 0.0; |
|
|
|
// average reflectivity for walls |
|
|
|
// 0..3 or 4..7 |
|
|
|
for ( k = 0, i = j*2; i < 4+(j*2); i++, k++) |
|
{ |
|
refl += proom->reflect[i]; |
|
proom->refl_walls[k] = proom->reflect[i]; |
|
} |
|
|
|
// assume ceiling is open |
|
|
|
proom->refl_walls[4] = 0.0; |
|
|
|
// get ceiling reflectivity, if any non zero |
|
|
|
for ( i = IVEC_DIAG_UP; i < IVEC_DOWN; i++) |
|
{ |
|
if (proom->reflect[i] == 0.0) |
|
{ |
|
// if any upward trace hit sky, exit; |
|
// ceiling reflectivity is 0.0 |
|
|
|
proom->refl_walls[4] = 0.0; |
|
|
|
i = IVEC_DOWN; // exit loop |
|
} |
|
else |
|
{ |
|
|
|
// upward trace didn't hit sky, keep checking |
|
|
|
proom->refl_walls[4] = proom->reflect[i]; |
|
} |
|
} |
|
|
|
// add in ceiling reflectivity, if any |
|
|
|
refl += proom->refl_walls[4]; |
|
|
|
// get floor reflectivity |
|
|
|
refl += proom->reflect[IVEC_DOWN]; |
|
proom->refl_walls[5] = proom->reflect[IVEC_DOWN]; |
|
|
|
proom->refl_avg = refl / 6.0; |
|
|
|
// calculate sky hit percent for this wall |
|
|
|
float sky_pct = 0.0; |
|
|
|
// 0..3 or 4..7 |
|
|
|
for ( i = j*2; i < 4+(j*2); i++) |
|
sky_pct += proom->skyhits[i]; |
|
|
|
for ( i = IVEC_DIAG_UP; i < IVEC_DOWN; i++) |
|
{ |
|
if (proom->skyhits[i] > 0.0) |
|
{ |
|
// if any upward trace hit sky, exit loop |
|
sky_pct += proom->skyhits[i]; |
|
i = IVEC_DOWN; |
|
} |
|
} |
|
|
|
// get floor skyhit |
|
|
|
sky_pct += proom->skyhits[IVEC_DOWN]; |
|
|
|
proom->sky_pct = sky_pct; |
|
|
|
// check for sky above |
|
proom->bskyabove = false; |
|
|
|
for (i = IVEC_DIAG_UP; i < IVEC_DOWN; i++) |
|
{ |
|
if (proom->skyhits[i] > 0.0) |
|
proom->bskyabove = true; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
// return true if trace hit solid |
|
// return false if trace hit sky or didn't hit anything |
|
|
|
bool DAS_HitSolid( trace_t *ptr ) |
|
{ |
|
// if hit nothing return false |
|
|
|
if (!ptr->DidHit()) |
|
return false; |
|
|
|
// if hit sky, return false (not solid) |
|
if (ptr->surface.flags & SURF_SKY) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
// returns true if trace hit sky |
|
|
|
bool DAS_HitSky( trace_t *ptr ) |
|
{ |
|
if (ptr->DidHit() && (ptr->surface.flags & SURF_SKY)) |
|
return true; |
|
if (!ptr->DidHit() ) |
|
{ |
|
float dz = ptr->endpos.z - ptr->startpos.z; |
|
if ( dz > 200*12.0f ) |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
|
|
bool DAS_ScanningForHeight( das_room_t *proom ) |
|
{ |
|
return (proom->iwall >= IVEC_DIAG_UP); |
|
} |
|
|
|
bool DAS_ScanningForWidth( das_room_t *proom ) |
|
{ |
|
return (proom->iwall < IVEC_DIAG_UP); |
|
} |
|
|
|
bool DAS_ScanningForFloor( das_room_t *proom ) |
|
{ |
|
return (proom->iwall == IVEC_DOWN); |
|
} |
|
|
|
ConVar das_door_height("adsp_door_height", "112"); // standard door height hl2 |
|
ConVar das_wall_height("adsp_wall_height", "128"); // standard wall height hl2 |
|
ConVar das_low_ceiling("adsp_low_ceiling", "108"); // low ceiling height hl2 |
|
|
|
|
|
// set origin for tracing out to walls to point above player's head |
|
// allows calculations over walls and floor obstacles, and above door openings |
|
|
|
// WARNING: the current settings are optimal for skipping floor and ceiling clutter, |
|
// and for detecting rooms without 'looking' through doors or windows. Don't change these cvars for hl2! |
|
|
|
void DAS_SetTraceHeight( das_room_t *proom, trace_t *ptrU, trace_t *ptrD ) |
|
{ |
|
// NOTE: when tracing down through player's box, endpos and startpos are reversed and |
|
// startsolid and allsolid are true. |
|
|
|
int zup = abs(ptrU->endpos.z - ptrU->startpos.z); // height above player's head |
|
int zdown = abs(ptrD->endpos.z - ptrD->startpos.z); // distance to floor from player's head |
|
int h; |
|
h = zup + zdown; |
|
|
|
int door_height = das_door_height.GetInt(); |
|
int wall_height = das_wall_height.GetInt(); |
|
int low_ceiling = das_low_ceiling.GetInt(); |
|
|
|
if (h > low_ceiling && h <= wall_height) |
|
{ |
|
// low ceiling - trace out just above standard door height @ 112 |
|
if (h > door_height) |
|
proom->vplayer.z = fpmin(ptrD->endpos.z, ptrD->startpos.z) + door_height + 1; |
|
else |
|
proom->vplayer.z = fpmin(ptrD->endpos.z, ptrD->startpos.z) + h - 1; |
|
} |
|
else if ( h > wall_height ) |
|
{ |
|
// tall ceiling - trace out over standard walls @ 128 |
|
|
|
proom->vplayer.z = fpmin(ptrD->endpos.z, ptrD->startpos.z) + wall_height + 1; |
|
} |
|
else |
|
{ |
|
// very low ceiling, trace out from just below ceiling |
|
proom->vplayer.z = fpmin(ptrD->endpos.z, ptrD->startpos.z) + h - 1; |
|
proom->lowceiling = h; |
|
} |
|
|
|
Assert (proom->vplayer.z <= ptrU->endpos.z); |
|
|
|
if (das_debug.GetInt() > 1) |
|
{ |
|
// draw line to height, and between floor and ceiling |
|
|
|
CDebugOverlay::AddLineOverlay( ptrD->endpos, ptrU->endpos, 0, 255, 0, 255, false, 20 ); |
|
|
|
Vector mins; |
|
Vector maxs; |
|
mins.Init(-1,-1,-2.0); |
|
maxs.Init(1,1,0); |
|
|
|
CDebugOverlay::AddBoxOverlay( proom->vplayer, mins, maxs, vec3_angle, 255, 0, 0, 0, 20 ); |
|
|
|
CDebugOverlay::AddBoxOverlay( ptrU->endpos, mins, maxs, vec3_angle, 0, 255, 0, 0, 20 ); |
|
CDebugOverlay::AddBoxOverlay( ptrD->endpos, mins, maxs, vec3_angle, 0, 255, 0, 0, 20 ); |
|
|
|
} |
|
} |
|
|
|
// prepare room struct for new round of checks: |
|
// clear out struct, |
|
// init trace height origin by finding space above player's head |
|
// returns true if player is in valid position to begin checks from |
|
|
|
bool DAS_StartTraceChecks( das_room_t *proom ) |
|
{ |
|
// starting new check: store player position, init maxs, mins |
|
|
|
proom->vplayer_eyes = MainViewOrigin(); |
|
proom->vplayer = MainViewOrigin(); |
|
|
|
proom->height_max = 0; |
|
proom->width_max = 0; |
|
proom->length_max = 0; |
|
proom->room_maxs.Init (0.0, 0.0, 0.0); |
|
proom->room_mins.Init (10000.0, 10000.0, 10000.0); |
|
|
|
proom->lowceiling = 0; |
|
|
|
// find point between player's head and ceiling - trace out to walls from here |
|
|
|
trace_t trU, trD; |
|
CTraceFilterDAS filterU, filterD; |
|
|
|
Vector v_dir = g_das_vec3[IVEC_DOWN]; // down - find floor |
|
|
|
Vector endpoint = proom->vplayer + v_dir; |
|
|
|
Ray_t ray; |
|
ray.Init( proom->vplayer, endpoint ); |
|
|
|
g_pEngineTraceClient->TraceRay( ray, DAS_TRACE_MASK, &filterD, &trD ); |
|
|
|
// if player jumping or in air, don't continue |
|
|
|
if (trD.DidHit() && abs(trD.endpos.z - trD.startpos.z) > 72) |
|
return false; |
|
|
|
v_dir = g_das_vec3[IVEC_UP]; // up - find ceiling |
|
|
|
endpoint = proom->vplayer + v_dir; |
|
|
|
ray.Init( proom->vplayer, endpoint ); |
|
|
|
g_pEngineTraceClient->TraceRay( ray, DAS_TRACE_MASK, &filterU, &trU ); |
|
|
|
// if down trace hits floor, set trace height, otherwise default is player eye location |
|
|
|
if ( DAS_HitSolid( &trD) ) |
|
DAS_SetTraceHeight( proom, &trU, &trD ); |
|
|
|
return true; |
|
} |
|
|
|
void DAS_DebugDrawTrace ( trace_t *ptr, int r, int g, int b, float duration, int imax) |
|
{ |
|
|
|
// das_debug == 3: draw horizontal trace bars for room width/depth detection |
|
// das_debug == 4: draw upward traces for height detection |
|
|
|
if (das_debug.GetInt() != imax) |
|
return; |
|
|
|
CDebugOverlay::AddLineOverlay( ptr->startpos, ptr->endpos, r, g, b, 255, false, duration ); |
|
|
|
Vector mins; |
|
Vector maxs; |
|
mins.Init(-1,-1,-2.0); |
|
maxs.Init(1,1,0); |
|
|
|
CDebugOverlay::AddBoxOverlay( ptr->endpos, mins, maxs, vec3_angle, r, g, b, 0, duration ); |
|
|
|
} |
|
|
|
// wall surface data |
|
|
|
struct das_surfdata_t |
|
{ |
|
float dist; // distance to player |
|
float reflectivity; // acoustic reflectivity of material on surface |
|
Vector hit; // trace hit location |
|
Vector norm; // wall normal at hit location |
|
}; |
|
|
|
// trace hit wall surface, get info about surface and store in surfdata struct |
|
// if scanning for height, bounce a second trace off of ceiling and get dist to floor |
|
|
|
void DAS_GetSurfaceData( das_room_t *proom, trace_t *ptr, das_surfdata_t *psurfdata ) |
|
{ |
|
|
|
float dist; // distance to player |
|
float reflectivity; // acoustic reflectivity of material on surface |
|
Vector hit; // trace hit location |
|
Vector norm; // wall normal at hit location |
|
surfacedata_t *psurf; |
|
|
|
psurf = physprop->GetSurfaceData( ptr->surface.surfaceProps ); |
|
|
|
reflectivity = psurf ? psurf->audio.reflectivity : DAS_REFLECTIVITY_NORM; |
|
|
|
// keep wall hit location and normal, to calc room bounds and center |
|
|
|
norm = ptr->plane.normal; |
|
|
|
// get length to hit location |
|
|
|
dist = VectorLength(ptr->endpos - ptr->startpos); |
|
|
|
// if started tracing from within player box, startpos & endpos may be flipped |
|
|
|
if (ptr->endpos.z >= ptr->startpos.z) |
|
hit = ptr->endpos; |
|
else |
|
hit = ptr->startpos; |
|
|
|
// if checking for max height by bouncing several vectors off of ceiling: |
|
// ignore returned normal from 1st bounce, just search straight down from trace hit location |
|
|
|
if ( DAS_ScanningForHeight( proom ) && !DAS_ScanningForFloor( proom ) ) |
|
{ |
|
trace_t tr2; |
|
CTraceFilterDAS filter2; |
|
|
|
norm.Init(0.0, 0.0, -1.0); |
|
|
|
Vector endpoint = hit + ( norm * DAS_ROOM_TRACE_LEN ); |
|
|
|
Ray_t ray; |
|
ray.Init( hit, endpoint ); |
|
|
|
g_pEngineTraceClient->TraceRay( ray, DAS_TRACE_MASK, &filter2, &tr2 ); |
|
|
|
//DAS_DebugDrawTrace( &tr2, 255, 255, 0, 10, 1); |
|
|
|
if (tr2.DidHit()) |
|
{ |
|
// get distance between surfaces |
|
|
|
dist = VectorLength(tr2.endpos - tr2.startpos); |
|
} |
|
} |
|
|
|
// set up surface struct and return |
|
|
|
psurfdata->dist = dist; |
|
psurfdata->hit = hit; |
|
psurfdata->norm = norm; |
|
psurfdata->reflectivity = reflectivity; |
|
|
|
} |
|
|
|
|
|
// algorithm for detecting approximate size of space around player. Handles player in corner & non-axis aligned rooms. |
|
// also handles player on catwalk or player under small bridge/overhang. |
|
// The goal is to only change the dsp room description if the the player moves into |
|
// a space which is SIGNIFICANTLY different from the previously set dsp space. |
|
|
|
// save player position. find a point above player's head and trace out from here. |
|
|
|
// from player position, get max width and max length: |
|
|
|
// from player position, |
|
// a) trace x,-x, y,-y axes |
|
// b) trace xy, -xy, x-y, -x-y diagonals |
|
// c) select largest room size detected from max width, max length |
|
|
|
|
|
// from player position, get height |
|
// a) trace out along front-up (or left-up, back-up, right-up), save hit locations |
|
// b) trace down -z from hit locations |
|
// c) save max height |
|
|
|
// when max width, max length, max height all updated, get new player position |
|
|
|
// get average room size & wall materials: |
|
// update averages with one traceline per frame only |
|
// returns true if room is fully updated and ready to check |
|
|
|
bool DAS_UpdateRoomSize( das_room_t *proom ) |
|
{ |
|
Vector endpoint; |
|
Vector startpoint; |
|
Vector v_dir; |
|
int iwall; |
|
bool bskyhit = false; |
|
das_surfdata_t surfdata; |
|
|
|
// do nothing if room already fully checked |
|
|
|
if ( proom->broomready ) |
|
return true; |
|
|
|
// cycle through all walls, floor, ceiling |
|
// get wall index |
|
|
|
iwall = proom->iwall; |
|
|
|
// get height above player and init proom for new round of checks |
|
|
|
if (iwall == 0) |
|
{ |
|
if (!DAS_StartTraceChecks( proom )) |
|
return false; // bad location to check room - player is jumping etc. |
|
} |
|
|
|
// get trace vector |
|
|
|
v_dir = g_das_vec3[iwall]; |
|
|
|
// trace out from trace origin, in axis-aligned direction or along diagonals |
|
|
|
// if looking for max height, trace from top of player's eyes |
|
|
|
if ( DAS_ScanningForHeight( proom ) ) |
|
{ |
|
startpoint = proom->vplayer_eyes; |
|
endpoint = proom->vplayer_eyes + v_dir; |
|
} |
|
else |
|
{ |
|
startpoint = proom->vplayer; |
|
endpoint = proom->vplayer + v_dir; |
|
} |
|
|
|
// try less expensive world-only trace first (no props, no ents - just try to hit walls) |
|
|
|
trace_t tr; |
|
CTraceFilterWorldOnly filter; |
|
|
|
Ray_t ray; |
|
ray.Init( startpoint, endpoint ); |
|
|
|
g_pEngineTraceClient->TraceRay( ray, CONTENTS_SOLID, &filter, &tr ); |
|
|
|
// if didn't hit world, or we hit sky when looking horizontally, |
|
// retrace, this time including props |
|
|
|
if ( !DAS_HitSolid( &tr ) && DAS_ScanningForWidth( proom ) ) |
|
{ |
|
CTraceFilterDAS filterDas; |
|
|
|
ray.Init( startpoint, endpoint ); |
|
g_pEngineTraceClient->TraceRay( ray, DAS_TRACE_MASK, &filterDas, &tr ); |
|
} |
|
|
|
if (das_debug.GetInt() > 2) |
|
{ |
|
// draw trace lines |
|
|
|
if ( DAS_HitSolid( &tr ) ) |
|
DAS_DebugDrawTrace( &tr, 0, 255, 255, 10, DAS_ScanningForHeight( proom ) + 3); |
|
else |
|
DAS_DebugDrawTrace( &tr, 255, 0, 0, 10, DAS_ScanningForHeight( proom ) + 3); // red lines if sky hit or no hit |
|
} |
|
|
|
// init surface data with defaults, in case we didn't hit world |
|
|
|
surfdata.dist = DAS_ROOM_TRACE_LEN; |
|
surfdata.reflectivity = DAS_REFLECTIVITY_SKY; // assume sky or open area |
|
surfdata.hit = endpoint; // trace hit location |
|
surfdata.norm = -v_dir; |
|
|
|
// check for sky hits |
|
|
|
if ( DAS_HitSky( &tr ) ) |
|
{ |
|
bskyhit = true; |
|
|
|
if ( DAS_ScanningForWidth( proom ) ) |
|
// ignore horizontal sky hits for distance calculations |
|
surfdata.dist = 1.0; |
|
else |
|
surfdata.dist = surfdata.dist; // debug |
|
} |
|
|
|
// get length of trace if it hit world |
|
|
|
// if hit solid and not sky (tr.DidHit() && !bskyhit) |
|
// get surface information |
|
|
|
if ( DAS_HitSolid( &tr) ) |
|
DAS_GetSurfaceData( proom, &tr, &surfdata ); |
|
|
|
// store surface data |
|
|
|
proom->dist[iwall] = surfdata.dist; |
|
proom->reflect[iwall] = clamp(surfdata.reflectivity, 0.0f, 1.0f); |
|
proom->skyhits[iwall] = bskyhit ? 0.1 : 0.0; |
|
proom->hit[iwall] = surfdata.hit; |
|
proom->norm[iwall] = surfdata.norm; |
|
|
|
// update wall counter |
|
|
|
proom->iwall++; |
|
|
|
if (proom->iwall == DAS_CWALLS) |
|
{ |
|
bool b_good_node_location; |
|
|
|
// calculate room mins, maxs, reflectivity etc |
|
|
|
b_good_node_location = DAS_CalcRoomProps( proom ); |
|
|
|
// reset wall counter |
|
|
|
proom->iwall = 0; |
|
proom->broomready = b_good_node_location; // room ready to check if good node location |
|
|
|
return b_good_node_location; |
|
} |
|
|
|
return false; // room not yet fully updated |
|
} |
|
|
|
// create entity enumerator for counting ents & summing volume of ents in room |
|
|
|
class CDasEntEnum : public IPartitionEnumerator |
|
{ |
|
public: |
|
int m_count; // # of ents in space |
|
float m_volume; // space occupied by ents |
|
|
|
public: |
|
|
|
void Reset() |
|
{ |
|
m_count = 0; |
|
m_volume = 0.0; |
|
} |
|
|
|
// called with each handle... |
|
|
|
IterationRetval_t EnumElement( IHandleEntity *pHandleEntity ) |
|
{ |
|
float vol; |
|
|
|
// get bounding box of entity |
|
// Generate a collideable |
|
|
|
ICollideable *pCollideable = g_pEngineTraceClient->GetCollideable( pHandleEntity ); |
|
|
|
if ( !pCollideable ) |
|
return ITERATION_CONTINUE; |
|
|
|
// Check for solid |
|
|
|
if ( !IsSolid( pCollideable->GetSolid(), pCollideable->GetSolidFlags() ) ) |
|
return ITERATION_CONTINUE; |
|
|
|
m_count++; |
|
|
|
// compute volume of space occupied by entity |
|
Vector mins = pCollideable->OBBMins(); |
|
Vector maxs = pCollideable->OBBMaxs(); |
|
|
|
vol = fabs((maxs.x - mins.x) * (maxs.y - mins.y) * (maxs.z - mins.z)); |
|
|
|
m_volume += vol; // add to total vol |
|
|
|
if (das_debug.GetInt() == 5) |
|
{ |
|
// draw box around all objects detected |
|
|
|
Vector orig = pCollideable->GetCollisionOrigin(); |
|
CDebugOverlay::AddBoxOverlay( orig, mins, maxs, pCollideable->GetCollisionAngles(), 255, 0, 255, 0, 60.0f ); |
|
} |
|
|
|
return ITERATION_CONTINUE; |
|
} |
|
}; |
|
|
|
// determine # of solid ents/props within detected room boundaries |
|
// and set diffusion based on count of ents and spatial volume of ents |
|
|
|
void DAS_SetDiffusion( das_room_t *proom ) |
|
{ |
|
// BRJ 7/12/05 |
|
// This was commented out because the y component of proom->room_mins, proom->room_maxs was never |
|
// being computed, causing a bogus box to be sent to the partition system. The results of |
|
// this computation (namely the diffusion + ent_count fields of das_room_t) were never being used. |
|
// Therefore, we'll avoid the enumeration altogether |
|
|
|
proom->diffusion = 0.0f; |
|
proom->ent_count = 0; |
|
|
|
/* |
|
CDasEntEnum enumerator; |
|
SpatialPartitionListMask_t mask = PARTITION_CLIENT_SOLID_EDICTS; // count only solid ents in room |
|
int count; |
|
float vol; |
|
float volroom; |
|
float dfn; |
|
|
|
enumerator.Reset(); |
|
|
|
SpatialPartition()->EnumerateElementsInBox(mask, proom->room_mins, proom->room_maxs, true, &enumerator ); |
|
|
|
count = enumerator.m_count; |
|
vol = enumerator.m_volume; |
|
|
|
// compute diffusion from volume |
|
|
|
// how much space around player is filled with props? |
|
|
|
volroom = (proom->room_maxs.x - proom->room_mins.x) * (proom->room_maxs.y - proom->room_mins.y) * (proom->room_maxs.z - proom->room_mins.z); |
|
volroom = fabs(volroom); |
|
|
|
if ( !(int)volroom ) |
|
volroom = 1.0; |
|
|
|
dfn = vol / volroom; // % of total volume occupied by props |
|
|
|
dfn = clamp (dfn, 0.0, 1.0); |
|
|
|
proom->diffusion = dfn; |
|
proom->ent_count = count; |
|
*/ |
|
} |
|
|
|
// debug routine to display current room params |
|
|
|
void DAS_DisplayRoomDEBUG( das_room_t *proom, bool fnew, float preset ) |
|
{ |
|
float dx,dy,dz; |
|
Vector ctr; |
|
float count; |
|
|
|
if (das_debug.GetInt() == 0) |
|
return; |
|
|
|
dx = proom->length_max / 12.0; |
|
dy = proom->width_max / 12.0; |
|
dz = proom->height_max / 12.0; |
|
|
|
float refl = proom->refl_avg; |
|
|
|
count = (float)(proom->ent_count); |
|
float fsky = (proom->bskyabove ? 1.0 : 0.0); |
|
|
|
if (fnew) |
|
DevMsg( "NEW DSP NODE: size:(%.0f,%.0f) height:(%.0f) dif %.4f : refl %.4f : cobj: %.0f : sky %.0f \n", dx, dy, dz, proom->diffusion, refl, count, fsky); |
|
|
|
if (!fnew && preset < 0.0) |
|
return; |
|
|
|
if (preset >= 0.0) |
|
{ |
|
if (proom == NULL) |
|
return; |
|
|
|
DevMsg( "DSP PRESET: %.0f size:(%.0f,%.0f) height:(%.0f) dif %.4f : refl %.4f : cobj: %.0f : sky %.0f \n", preset, dx, dy, dz, proom->diffusion, refl, count, fsky); |
|
return; |
|
} |
|
|
|
// draw box around new node location |
|
|
|
Vector mins; |
|
Vector maxs; |
|
mins.Init(-8,-8,-16); |
|
maxs.Init(8,8,0); |
|
|
|
CDebugOverlay::AddBoxOverlay( proom->vplayer, mins, maxs, vec3_angle, 0, 0, 255, 0, 1000.0f ); |
|
|
|
// draw red box around node origin |
|
|
|
mins.Init(-0.5,-0.5,-1.0); |
|
maxs.Init(0.5,0.5,0); |
|
|
|
CDebugOverlay::AddBoxOverlay( proom->vplayer, mins, maxs, vec3_angle, 255, 0, 0, 0, 1000.0f ); |
|
|
|
CDebugOverlay::AddTextOverlay( proom->vplayer, 0, 10, 1.0, "DSP NODE" ); |
|
} |
|
|
|
// check newly calculated room parameters against current stored params. |
|
// if different, return true. |
|
// NOTE: only call when all proom params have been calculated. |
|
// return false if this is not a good location for creating a new node |
|
|
|
bool DAS_CheckNewRoom( das_room_t *proom ) |
|
{ |
|
bool bnewroom; |
|
float dw,dw2,dr,ds,dh; |
|
int cchanged = 0; |
|
das_room_t *proom_prev = NULL; |
|
Vector2D v2d; |
|
Vector v3d; |
|
float dist; |
|
|
|
// player can't see previous node, determine if this is a good place to lay down |
|
// a new node. Get room at last seen node for comparison |
|
|
|
if (g_pdas_last_node) |
|
proom_prev = &(g_pdas_last_node->room); |
|
|
|
// no previous room node saw player, go create new room node |
|
|
|
if (!proom_prev) |
|
{ |
|
bnewroom = true; |
|
goto check_ret; |
|
} |
|
|
|
// if player not at least n feet from last node, return false |
|
|
|
v3d = proom->vplayer - proom_prev->vplayer; |
|
v2d.Init(v3d.x, v3d.y); |
|
|
|
dist = Vector2DLength(v2d); |
|
|
|
if (dist <= DAS_DIST_MIN) |
|
return false; |
|
|
|
// see if room size has changed significantly since last node |
|
|
|
bnewroom = true; |
|
|
|
dw = 0.0; |
|
dw2 = 0.0; |
|
dh = 0.0; |
|
dr = 0.0; |
|
|
|
if ( proom_prev->width_max != 0 ) |
|
dw = (float)proom->width_max / (float)proom_prev->width_max; // max width delta |
|
|
|
if ( proom_prev->length_max != 0 ) |
|
dw2 = (float)proom->length_max / (float)proom_prev->length_max; // max length delta |
|
|
|
if ( proom_prev->height_max != 0 ) |
|
dh = (float)proom->height_max / (float)proom_prev->height_max; // max height delta |
|
|
|
if ( proom_prev->refl_avg != 0.0 ) |
|
dr = proom->refl_avg / proom_prev->refl_avg; // reflectivity delta |
|
|
|
ds = fabs( proom->sky_pct - proom_prev->sky_pct); // sky hits delta |
|
|
|
if (dw > 1.0) dw = 1.0 / dw; |
|
if (dw2 > 1.0) dw = 1.0 / dw2; |
|
if (dh > 1.0) dh = 1.0 / dh; |
|
if (dr > 1.0) dr = 1.0 / dr; |
|
|
|
if ( (1.0 - dw) >= DAS_WIDTH_MIN ) |
|
cchanged++; |
|
|
|
if ( (1.0 - dw2) >= DAS_WIDTH_MIN ) |
|
cchanged++; |
|
|
|
// if ( (1.0 - dh) >= DAS_WIDTH_MIN ) // don't change room based on height change |
|
// cchanged++; |
|
|
|
// new room only if at least 1 changed |
|
|
|
if (cchanged >= 1) |
|
goto check_ret; |
|
|
|
// if ( (1.0 - dr) >= DAS_REFL_MIN ) // don't change room based on reflectivity change |
|
// goto check_ret; |
|
|
|
// if (ds >= DAS_SKYHIT_MIN ) |
|
// goto check_ret; |
|
|
|
// new room if sky above changes state |
|
|
|
if (proom->bskyabove != proom_prev->bskyabove) |
|
goto check_ret; |
|
|
|
// room didn't change significantly, return false |
|
|
|
bnewroom = false; |
|
|
|
check_ret: |
|
|
|
if ( bnewroom ) |
|
{ |
|
// if low ceiling detected < 112 units, and max height is > low ceiling height by 20%, discard - no change |
|
// this detects player in doorway, under pipe or narrow bridge |
|
|
|
if ( proom->lowceiling && (proom->lowceiling < proom->height_max)) |
|
{ |
|
float h = (float)(proom->lowceiling) / (float)proom->height_max; |
|
|
|
if (h < 0.8) |
|
return false; |
|
} |
|
|
|
DAS_SetDiffusion( proom ); |
|
} |
|
|
|
DAS_DisplayRoomDEBUG( proom, bnewroom, -1.0 ); |
|
|
|
return bnewroom; |
|
} |
|
|
|
|
|
extern int DSP_ConstructPreset( bool bskyabove, int width, int length, int height, float fdiffusion, float freflectivity, float *psurf_refl, int inode, int cnodes ); |
|
|
|
// select new dsp_room based on size, wall materials |
|
// (or modulate params for current dsp) |
|
// returns new preset # for dsp_automatic |
|
|
|
int DAS_GetRoomDSP( das_room_t *proom, int inode ) |
|
{ |
|
|
|
// preset constructor |
|
// call dsp module with params, get dsp preset back |
|
|
|
bool bskyabove = proom->bskyabove; |
|
int width = proom->width_max; |
|
int length = proom->length_max; |
|
int height = proom->height_max; |
|
float fdiffusion = proom->diffusion; |
|
float freflectivity = proom->refl_avg; |
|
float surf_refl[6]; |
|
|
|
// fill array of surface reflectivities - for left,right,front,back,ceiling,floor |
|
|
|
for (int i = 0; i < 6; i++) |
|
surf_refl[i] = proom->refl_walls[i]; |
|
|
|
return DSP_ConstructPreset( bskyabove, width, length, height, fdiffusion, freflectivity, surf_refl, inode, DAS_CNODES ); |
|
|
|
} |
|
|
|
|
|
// main entry point: call once per frame to update dsp_automatic |
|
// for automatic room detection. dsp_room must be set to DSP_AUTOMATIC to enable. |
|
// NOTE: this routine accumulates traceline information over several frames - it |
|
// never traces more than 3 times per call, and normally just once per call. |
|
|
|
void DAS_CheckNewRoomDSP( ) |
|
{ |
|
VPROF("DAS_CheckNewRoomDSP"); |
|
das_room_t *proom = &g_das_room; |
|
int dsp_preset; |
|
bool bRoom_ready = false; |
|
|
|
// if listener has not been updated, do nothing |
|
|
|
if ((listener_origin == vec3_origin) && |
|
(listener_forward == vec3_origin) && |
|
(listener_right == vec3_origin) && |
|
(listener_up == vec3_origin) ) |
|
return; |
|
|
|
if ( !SND_IsInGame() ) |
|
return; |
|
|
|
// make sure we init nodes & vectors first time this is called |
|
|
|
if ( !g_bdas_init_nodes ) |
|
{ |
|
g_bdas_init_nodes = 1; |
|
DAS_InitNodes(); |
|
} |
|
|
|
if ( !DSP_CheckDspAutoEnabled()) |
|
{ |
|
// make sure room params are reinitialized each time autoroom is selected |
|
|
|
g_bdas_room_init = 0; |
|
return; |
|
} |
|
|
|
if ( !g_bdas_room_init ) |
|
{ |
|
g_bdas_room_init = 1; |
|
|
|
DAS_InitAutoRoom( proom ); |
|
} |
|
|
|
// get time |
|
|
|
double dtime = g_pSoundServices->GetHostTime(); |
|
|
|
// compare to previous time - don't check for new room until timer expires |
|
// ie: wait at least DAS_AUTO_WAIT seconds between preset changes |
|
|
|
if ( fabs(dtime - proom->last_dsp_change) < DAS_AUTO_WAIT ) |
|
return; |
|
|
|
// first, update room size parameters, see if room is ready to check - if room is updated, return true right away |
|
|
|
// 3 traces per frame while accumulating room size info |
|
|
|
for (int i = 0 ; i < 3; i++) |
|
bRoom_ready = DAS_UpdateRoomSize( proom ); |
|
|
|
if (!bRoom_ready) |
|
return; |
|
|
|
|
|
if ( !g_bdas_create_new_node ) |
|
{ |
|
// next, check all nodes for line of sight to player - if all checked, return true right away |
|
|
|
if ( !DAS_CheckNextNode( proom ) ) |
|
{ |
|
// check all nodes first |
|
|
|
return; |
|
} |
|
|
|
// find out if any previously stored nodes can see player, |
|
// if so, get closest node's dsp preset |
|
|
|
dsp_preset = DAS_GetDspPreset( proom->bskyabove ); |
|
|
|
if (dsp_preset != -1) |
|
{ |
|
// an existing node can see player - just set preset and return |
|
|
|
if (dsp_preset != dsp_room_GetInt()) |
|
{ |
|
// changed preset, so update timestamp |
|
|
|
proom->last_dsp_change = g_pSoundServices->GetHostTime(); |
|
|
|
if (g_pdas_last_node) |
|
DAS_DisplayRoomDEBUG( &(g_pdas_last_node->room), false, (float)dsp_preset ); |
|
} |
|
|
|
DSP_SetDspAuto( dsp_preset ); |
|
|
|
goto check_new_room_exit; |
|
} |
|
} |
|
|
|
g_bdas_create_new_node = true; |
|
|
|
// no nodes can see player, need to try to create a new one |
|
|
|
// check for 'new' room around player |
|
|
|
if ( DAS_CheckNewRoom( proom ) ) |
|
{ |
|
// new room found - update dsp_automatic |
|
|
|
dsp_preset = DAS_GetRoomDSP( proom, DAS_GetNextNodeIndex() ); |
|
|
|
DSP_SetDspAuto( dsp_preset ); |
|
|
|
// changed preset, so update timestamp |
|
|
|
proom->last_dsp_change = g_pSoundServices->GetHostTime(); |
|
|
|
// save room as new node |
|
|
|
DAS_StoreNode( proom, dsp_preset ); |
|
|
|
goto check_new_room_exit; |
|
} |
|
|
|
check_new_room_exit: |
|
|
|
// reset new node creation flag - start checking for visible nodes again |
|
|
|
g_bdas_create_new_node = false; |
|
|
|
// reset room checking flag - start checking room around player again |
|
|
|
proom->broomready = false; |
|
|
|
// reset node checking flag - start checking nodes around player again |
|
|
|
DAS_ResetNodes(); |
|
|
|
return; |
|
} |
|
|
|
// remap contents of volumes[] arrary if sound originates from player, or is music, and is 100% 'mono' |
|
// ie: same volume in all channels |
|
|
|
void RemapPlayerOrMusicVols( channel_t *ch, int volumes[CCHANVOLUMES/2], bool fplayersound, bool fmusicsound, float mono ) |
|
{ |
|
VPROF_("RemapPlayerOrMusicVols", 2, VPROF_BUDGETGROUP_OTHER_SOUND, false, BUDGETFLAG_OTHER ); |
|
|
|
if ( !fplayersound && !fmusicsound ) |
|
return; // no remapping |
|
|
|
if ( ch->flags.bSpeaker ) |
|
return; // don't remap speaker sounds rebroadcast on player |
|
|
|
// get total volume |
|
|
|
float vol_total = 0.0; |
|
int k; |
|
|
|
for (k = 0; k < CCHANVOLUMES/2; k++) |
|
vol_total += (float)volumes[k]; |
|
|
|
if ( !g_AudioDevice->IsSurround() ) |
|
{ |
|
if (mono < 1.0) |
|
return; |
|
|
|
// remap 2 chan non-spatialized versions of player and music sounds |
|
// note: this is required to keep volumes same as 4 & 5 ch cases! |
|
|
|
float vol_dist_music[] = {1.0, 1.0}; // FL, FR music volumes |
|
float vol_dist_player[] = {1.0, 1.0}; // FL, FR player volumes |
|
float *pvol_dist; |
|
|
|
pvol_dist = (fplayersound ? vol_dist_player : vol_dist_music); |
|
|
|
for (k = 0; k < 2; k++) |
|
volumes[k] = clamp((int)(vol_total * pvol_dist[k]), 0, 255); |
|
|
|
return; |
|
} |
|
|
|
// surround sound configuration... |
|
|
|
if ( fplayersound ) // && (ch->bstereowav && ch->wavtype != CHAR_DIRECTIONAL && ch->wavtype != CHAR_DISTVARIANT) ) |
|
{ |
|
// NOTE: player sounds also get n% overall volume boost. |
|
|
|
//float vol_dist5[] = {0.29, 0.29, 0.09, 0.09, 0.63}; // FL, FR, RL, RR, FC - 5 channel (mono source) volume distribution |
|
//float vol_dist5st[] = {0.29, 0.29, 0.09, 0.09, 0.63}; // FL, FR, RL, RR, FC - 5 channel (stereo source) volume distribution |
|
|
|
float vol_dist5[] = {0.30, 0.30, 0.09, 0.09, 0.59}; // FL, FR, RL, RR, FC - 5 channel (mono source) volume distribution |
|
float vol_dist5st[] = {0.30, 0.30, 0.09, 0.09, 0.59}; // FL, FR, RL, RR, FC - 5 channel (stereo source) volume distribution |
|
|
|
float vol_dist4[] = {0.50, 0.50, 0.15, 0.15, 0.00}; // FL, FR, RL, RR, 0 - 4 channel (mono source) volume distribution |
|
float vol_dist4st[] = {0.50, 0.50, 0.15, 0.15, 0.00}; // FL, FR, RL, RR, 0 - 4 channel (stereo source)volume distribution |
|
|
|
float *pvol_dist; |
|
|
|
if ( ch->flags.bstereowav && (ch->wavtype == CHAR_OMNI || ch->wavtype == CHAR_SPATIALSTEREO || ch->wavtype == 0)) |
|
{ |
|
pvol_dist = (g_AudioDevice->IsSurroundCenter() ? vol_dist5st : vol_dist4st); |
|
} |
|
else |
|
{ |
|
pvol_dist = (g_AudioDevice->IsSurroundCenter() ? vol_dist5 : vol_dist4); |
|
} |
|
|
|
for (k = 0; k < 5; k++) |
|
volumes[k] = clamp((int)(vol_total * pvol_dist[k]), 0, 255); |
|
|
|
return; |
|
} |
|
|
|
// Special case for music in surround mode |
|
|
|
if ( fmusicsound ) |
|
{ |
|
float vol_dist5[] = {0.5, 0.5, 0.25, 0.25, 0.0}; // FL, FR, RL, RR, FC - 5 channel distribution |
|
float vol_dist4[] = {0.5, 0.5, 0.25, 0.25, 0.0}; // FL, FR, RL, RR, 0 - 4 channel distribution |
|
float *pvol_dist; |
|
|
|
pvol_dist = (g_AudioDevice->IsSurroundCenter() ? vol_dist5 : vol_dist4); |
|
|
|
for (k = 0; k < 5; k++) |
|
volumes[k] = clamp((int)(vol_total * pvol_dist[k]), 0, 255); |
|
|
|
return; |
|
} |
|
|
|
return; |
|
} |
|
|
|
static int s_nSoundGuid = 0; |
|
|
|
void SND_ActivateChannel( channel_t *pChannel ) |
|
{ |
|
Q_memset( pChannel, 0, sizeof(*pChannel) ); |
|
g_ActiveChannels.Add( pChannel ); |
|
pChannel->guid = ++s_nSoundGuid; |
|
} |
|
|
|
/* |
|
================= |
|
SND_Spatialize |
|
================= |
|
*/ |
|
void SND_Spatialize(channel_t *ch) |
|
{ |
|
VPROF("SND_Spatialize"); |
|
|
|
vec_t dist; |
|
Vector source_vec; |
|
Vector source_vec_DL; |
|
Vector source_vec_DR; |
|
Vector source_doppler_left; |
|
Vector source_doppler_right; |
|
|
|
bool fdopplerwav = false; |
|
bool fplaydopplerwav = false; |
|
bool fvalidentity; |
|
float gain; |
|
float scale = 1.0; |
|
bool fplayersound = false; |
|
bool fmusicsound = false; |
|
float mono = 0.0; |
|
bool bAttenuated = true; |
|
|
|
ch->dspface = 1.0; // default facing direction: always facing player |
|
ch->dspmix = 0; // default mix 0% dsp_room fx |
|
ch->distmix = 0; // default 100% left (near) wav |
|
|
|
#if !defined( _X360 ) |
|
if ( ch->sfx && |
|
ch->sfx->pSource && |
|
ch->sfx->pSource->GetType() == CAudioSource::AUDIO_SOURCE_VOICE ) |
|
{ |
|
Voice_Spatialize( ch ); |
|
} |
|
#endif |
|
|
|
if ( IsSoundSourceLocalPlayer( ch->soundsource ) && !toolframework->InToolMode() ) |
|
{ |
|
// sounds coming from listener actually come from a short distance directly in front of listener |
|
// in tool mode however, the view entity is meaningless, since we're viewing from arbitrary locations in space |
|
fplayersound = true; |
|
} |
|
|
|
// assume 'dry', playeverwhere sounds are 'music' or 'voiceover' |
|
|
|
if ( ch->flags.bdry && ch->dist_mult <= 0 ) |
|
{ |
|
fmusicsound = true; |
|
fplayersound = false; |
|
} |
|
|
|
// update channel's position in case ent that made the sound is moving. |
|
QAngle source_angles; |
|
source_angles.Init(0.0, 0.0, 0.0); |
|
Vector entOrigin = ch->origin; |
|
|
|
bool looping = false; |
|
|
|
CAudioSource *pSource = ch->sfx ? ch->sfx->pSource : NULL; |
|
if ( pSource ) |
|
{ |
|
looping = pSource->IsLooped(); |
|
} |
|
|
|
SpatializationInfo_t si; |
|
si.info.Set( |
|
ch->soundsource, |
|
ch->entchannel, |
|
ch->sfx ? ch->sfx->getname() : "", |
|
ch->origin, |
|
ch->direction, |
|
ch->master_vol, |
|
DIST_MULT_TO_SNDLVL( ch->dist_mult ), |
|
looping, |
|
ch->pitch, |
|
listener_origin, |
|
ch->speakerentity ); |
|
|
|
si.type = SpatializationInfo_t::SI_INSPATIALIZATION; |
|
si.pOrigin = &entOrigin; |
|
si.pAngles = &source_angles; |
|
si.pflRadius = NULL; |
|
if ( ch->soundsource != 0 && ch->radius == 0 ) |
|
{ |
|
si.pflRadius = &ch->radius; |
|
} |
|
|
|
{ |
|
VPROF_("SoundServices->GetSoundSpatializtion", 2, VPROF_BUDGETGROUP_OTHER_SOUND, false, BUDGETFLAG_OTHER ); |
|
fvalidentity = g_pSoundServices->GetSoundSpatialization( ch->soundsource, si ); |
|
} |
|
|
|
if ( ch->flags.bUpdatePositions ) |
|
{ |
|
AngleVectors( source_angles, &ch->direction ); |
|
ch->origin = entOrigin; |
|
} |
|
else |
|
{ |
|
VectorAngles( ch->direction, source_angles ); |
|
} |
|
|
|
if ( ch->userdata != 0 ) |
|
{ |
|
g_pSoundServices->GetToolSpatialization( ch->userdata, ch->guid, si ); |
|
if ( ch->flags.bUpdatePositions ) |
|
{ |
|
AngleVectors( source_angles, &ch->direction ); |
|
ch->origin = entOrigin; |
|
} |
|
} |
|
|
|
#if 0 |
|
// !!!UNDONE - above code assumes the ENT hasn't been removed or respawned as another ent! |
|
// !!!UNDONE - fix this by flagging some entities (ie: glass) as immobile. Don't spatialize them. |
|
if ( !fvalidendity) |
|
{ |
|
// Turn off the sound while the entity doesn't exist or is not in the PVS. |
|
goto ClearAllVolumes; |
|
} |
|
#endif // 0 |
|
|
|
|
|
fdopplerwav = ((ch->wavtype == CHAR_DOPPLER) && !fplayersound); |
|
if ( fdopplerwav ) |
|
{ |
|
VPROF_("SND_Spatialize doppler", 2, VPROF_BUDGETGROUP_OTHER_SOUND, false, BUDGETFLAG_OTHER ); |
|
Vector vnearpoint; // point of closest approach to listener, |
|
// along sound source forward direction (doppler wavs) |
|
|
|
vnearpoint = ch->origin; // default nearest sound approach point |
|
|
|
// calculate point of closest approach for CHAR_DOPPLER wavs, replace source_vec |
|
|
|
fplaydopplerwav = SND_GetClosestPoint( ch, source_angles, vnearpoint ); |
|
|
|
// if doppler sound was 'shot' away from listener, don't play it |
|
|
|
if ( !fplaydopplerwav ) |
|
goto ClearAllVolumes; |
|
|
|
// find location of doppler left & doppler right points |
|
|
|
SND_GetDopplerPoints( ch, source_angles, vnearpoint, source_doppler_left, source_doppler_right); |
|
|
|
// source_vec_DL is vector from listener to doppler left point |
|
// source_vec_DR is vector from listener to doppler right point |
|
|
|
VectorSubtract(source_doppler_left, listener_origin, source_vec_DL ); |
|
VectorSubtract(source_doppler_right, listener_origin, source_vec_DR ); |
|
|
|
// normalized vectors to left and right doppler locations |
|
|
|
dist = VectorNormalize( source_vec_DL ); |
|
VectorNormalize( source_vec_DR ); |
|
|
|
// don't play doppler if out of range |
|
// unless recording in the tool, since we may play back in range |
|
if ( dist > DOPPLER_RANGE_MAX && !toolframework->IsToolRecording() ) |
|
goto ClearAllVolumes; |
|
} |
|
else |
|
{ |
|
// source_vec is vector from listener to sound source |
|
|
|
if ( fplayersound ) |
|
{ |
|
// get 2d forward direction vector, ignoring pitch angle |
|
Vector listener_forward2d; |
|
|
|
ConvertListenerVectorTo2D( &listener_forward2d, &listener_right ); |
|
|
|
// player sounds originate from 1' in front of player, 2d |
|
|
|
VectorMultiply(listener_forward2d, 12.0, source_vec ); |
|
} |
|
else |
|
{ |
|
VectorSubtract(ch->origin, listener_origin, source_vec); |
|
} |
|
|
|
// normalize source_vec and get distance from listener to source |
|
|
|
dist = VectorNormalize( source_vec ); |
|
} |
|
|
|
// calculate dsp mix based on distance to listener & sound level (linear approximation) |
|
|
|
ch->dspmix = SND_GetDspMix( ch, dist ); |
|
|
|
// calculate sound source facing direction for CHAR_DIRECTIONAL wavs |
|
|
|
if ( !fplayersound ) |
|
{ |
|
ch->dspface = SND_GetFacingDirection( ch, source_angles ); |
|
|
|
// calculate mixing parameter for CHAR_DISTVAR wavs |
|
|
|
ch->distmix = SND_GetDistanceMix( ch, dist ); |
|
} |
|
|
|
// for sounds with a radius, spatialize left/right/front/rear evenly within the radius |
|
|
|
if ( ch->radius > 0 && dist < ch->radius && !fdopplerwav ) |
|
{ |
|
float interval = ch->radius * 0.5; |
|
mono = dist - interval; |
|
if ( mono < 0.0 ) |
|
mono = 0.0; |
|
mono /= interval; |
|
|
|
mono = 1.0 - mono; |
|
|
|
// mono is 0.0 -> 1.0 from radius 100% to radius 50% |
|
} |
|
|
|
// don't pan sounds with no attenuation |
|
if ( ch->dist_mult <= 0 && !fdopplerwav ) |
|
{ |
|
// sound is centered left/right/front/back |
|
|
|
mono = 1.0; |
|
bAttenuated = false; |
|
} |
|
|
|
if ( ch->wavtype == CHAR_OMNI ) |
|
{ |
|
// omni directional sound sources are mono mix, all speakers |
|
// ie: they only attenuate by distance, not by source direction. |
|
|
|
mono = 1.0; |
|
bAttenuated = false; |
|
} |
|
|
|
// calculate gain based on distance, atmospheric attenuation, interposed objects |
|
// perform compression as gain approaches 1.0 |
|
|
|
gain = SND_GetGain( ch, fplayersound, fmusicsound, looping, dist, bAttenuated ); |
|
|
|
// map gain through global mixer by soundtype |
|
|
|
// gain *= SND_GetVolFromSoundtype( ch->soundtype ); |
|
int last_mixgroupid; |
|
|
|
gain *= MXR_GetVolFromMixGroup( ch->mixgroups, &last_mixgroupid ); |
|
|
|
// if playing a word, get volume scale of word - scale gain |
|
|
|
scale = VOX_GetChanVol(ch); |
|
|
|
gain *= scale; |
|
|
|
// save spatialized volume and mixgroupid for display later |
|
|
|
ch->last_mixgroupid = last_mixgroupid; |
|
|
|
if ( fdopplerwav ) |
|
{ |
|
VPROF_("SND_Spatialize doppler", 2, VPROF_BUDGETGROUP_OTHER_SOUND, false, BUDGETFLAG_OTHER ); |
|
// fill out channel volumes for both doppler sound source locations |
|
int volumes[CCHANVOLUMES/2]; |
|
|
|
// left doppler location |
|
|
|
g_AudioDevice->SpatializeChannel( volumes, ch->master_vol, source_vec_DL, gain, mono ); |
|
|
|
// load volumes into channel as crossfade targets |
|
|
|
ChannelSetVolTargets( ch, volumes, IFRONT_LEFT, CCHANVOLUMES/2 ); |
|
|
|
// right doppler location |
|
|
|
g_AudioDevice->SpatializeChannel( volumes, ch->master_vol, source_vec_DR, gain, mono ); |
|
|
|
// load volumes into channel as crossfade targets |
|
|
|
ChannelSetVolTargets( ch, volumes, IFRONT_LEFTD, CCHANVOLUMES/2 ); |
|
} |
|
else |
|
{ |
|
// fill out channel volumes for single sound source location |
|
int volumes[CCHANVOLUMES/2]; |
|
|
|
g_AudioDevice->SpatializeChannel( volumes, ch->master_vol, source_vec, gain, mono ); |
|
|
|
// Special case for stereo sounds originating from player in surround mode |
|
// and special case for musci: remap volumes directly to channels. |
|
|
|
RemapPlayerOrMusicVols( ch, volumes, fplayersound, fmusicsound, mono ); |
|
|
|
// load volumes into channel as crossfade volume targets |
|
|
|
ChannelSetVolTargets( ch, volumes, IFRONT_LEFT, CCHANVOLUMES/2 ); |
|
} |
|
|
|
|
|
// prevent left/right/front/rear/center volumes from changing too quickly & producing pops |
|
|
|
ChannelUpdateVolXfade( ch ); |
|
|
|
// end of first time spatializing sound |
|
|
|
if ( SND_IsInGame() || toolframework->InToolMode() ) |
|
{ |
|
ch->flags.bfirstpass = false; |
|
} |
|
|
|
// calculate total volume for display later |
|
ch->last_vol = gain * (ch->master_vol/255.0); |
|
|
|
return; |
|
|
|
ClearAllVolumes: |
|
|
|
// Clear all volumes and return. |
|
// This shuts the sound off permanently. |
|
|
|
ChannelClearVolumes( ch ); |
|
|
|
// end of first time spatializing sound |
|
|
|
ch->flags.bfirstpass = false; |
|
} |
|
|
|
ConVar snd_defer_trace("snd_defer_trace","1"); |
|
void SND_SpatializeFirstFrameNoTrace( channel_t *pChannel) |
|
{ |
|
if ( snd_defer_trace.GetBool() ) |
|
{ |
|
// set up tracing state to be non-obstructed |
|
pChannel->flags.bfirstpass = false; |
|
pChannel->flags.bTraced = true; |
|
pChannel->ob_gain = 1.0; |
|
pChannel->ob_gain_inc = 1.0; |
|
pChannel->ob_gain_target = 1.0; |
|
// now spatialize without tracing |
|
SND_Spatialize(pChannel); |
|
// now reset tracing state to firstpass so the trace gets done on next spatialize |
|
pChannel->ob_gain = 0.0; |
|
pChannel->ob_gain_inc = 0.0; |
|
pChannel->ob_gain_target = 0.0; |
|
pChannel->flags.bfirstpass = true; |
|
pChannel->flags.bTraced = false; |
|
} |
|
else |
|
{ |
|
pChannel->ob_gain = 0.0; |
|
pChannel->ob_gain_inc = 0.0; |
|
pChannel->ob_gain_target = 0.0; |
|
pChannel->flags.bfirstpass = true; |
|
pChannel->flags.bTraced = false; |
|
SND_Spatialize(pChannel); |
|
} |
|
} |
|
|
|
|
|
// search through all channels for a channel that matches this |
|
// soundsource, entchannel and sfx, and perform alteration on channel |
|
// as indicated by 'flags' parameter. If shut down request and |
|
// sfx contains a sentence name, shut off the sentence. |
|
// returns TRUE if sound was altered, |
|
// returns FALSE if sound was not found (sound is not playing) |
|
|
|
int S_AlterChannel( int soundsource, int entchannel, CSfxTable *sfx, int vol, int pitch, int flags ) |
|
{ |
|
THREAD_LOCK_SOUND(); |
|
int ch_idx; |
|
|
|
const char *name = sfx->getname(); |
|
if ( name && TestSoundChar( name, CHAR_SENTENCE ) ) |
|
{ |
|
// This is a sentence name. |
|
// For sentences: assume that the entity is only playing one sentence |
|
// at a time, so we can just shut off |
|
// any channel that has ch->isentence >= 0 and matches the |
|
// soundsource. |
|
|
|
CChannelList list; |
|
g_ActiveChannels.GetActiveChannels( list ); |
|
for ( int i = 0; i < list.Count(); i++ ) |
|
{ |
|
ch_idx = list.GetChannelIndex(i); |
|
if (channels[ch_idx].soundsource == soundsource |
|
&& channels[ch_idx].entchannel == entchannel |
|
&& channels[ch_idx].sfx != NULL ) |
|
{ |
|
|
|
if (flags & SND_CHANGE_PITCH) |
|
channels[ch_idx].basePitch = pitch; |
|
|
|
if (flags & SND_CHANGE_VOL) |
|
channels[ch_idx].master_vol = vol; |
|
|
|
if (flags & SND_STOP) |
|
{ |
|
S_FreeChannel(&channels[ch_idx]); |
|
} |
|
|
|
return TRUE; |
|
} |
|
} |
|
// channel not found |
|
return FALSE; |
|
|
|
} |
|
|
|
// regular sound or streaming sound |
|
CChannelList list; |
|
g_ActiveChannels.GetActiveChannels( list ); |
|
|
|
bool bSuccess = false; |
|
|
|
for ( int i = 0; i < list.Count(); i++ ) |
|
{ |
|
ch_idx = list.GetChannelIndex(i); |
|
if ( channels[ch_idx].soundsource == soundsource && |
|
( ( flags & SND_IGNORE_NAME ) || |
|
( channels[ch_idx].entchannel == entchannel && channels[ch_idx].sfx == sfx ) ) ) |
|
{ |
|
if (flags & SND_CHANGE_PITCH) |
|
channels[ch_idx].basePitch = pitch; |
|
|
|
if (flags & SND_CHANGE_VOL) |
|
channels[ch_idx].master_vol = vol; |
|
|
|
if (flags & SND_STOP) |
|
{ |
|
S_FreeChannel(&channels[ch_idx]); |
|
} |
|
|
|
if ( ( flags & SND_IGNORE_NAME ) == 0 ) |
|
return TRUE; |
|
else |
|
bSuccess = true; |
|
} |
|
} |
|
|
|
return ( bSuccess ) ? ( TRUE ) : ( FALSE ); |
|
} |
|
|
|
// set channel flags during initialization based on |
|
// source name |
|
|
|
void S_SetChannelWavtype( channel_t *target_chan, CSfxTable *pSfx ) |
|
{ |
|
// if 1st or 2nd character of name is CHAR_DRYMIX, sound should be mixed dry with no dsp (ie: music) |
|
|
|
if ( TestSoundChar(pSfx->getname(), CHAR_DRYMIX) ) |
|
target_chan->flags.bdry = true; |
|
else |
|
target_chan->flags.bdry = false; |
|
|
|
if ( TestSoundChar(pSfx->getname(), CHAR_FAST_PITCH) ) |
|
target_chan->flags.bfast_pitch = true; |
|
else |
|
target_chan->flags.bfast_pitch = false; |
|
|
|
// get sound spatialization encoding |
|
|
|
target_chan->wavtype = 0; |
|
|
|
if ( TestSoundChar( pSfx->getname(), CHAR_DOPPLER )) |
|
target_chan->wavtype = CHAR_DOPPLER; |
|
|
|
if ( TestSoundChar( pSfx->getname(), CHAR_DIRECTIONAL )) |
|
target_chan->wavtype = CHAR_DIRECTIONAL; |
|
|
|
if ( TestSoundChar( pSfx->getname(), CHAR_DISTVARIANT )) |
|
target_chan->wavtype = CHAR_DISTVARIANT; |
|
|
|
if ( TestSoundChar( pSfx->getname(), CHAR_OMNI )) |
|
target_chan->wavtype = CHAR_OMNI; |
|
|
|
if ( TestSoundChar( pSfx->getname(), CHAR_SPATIALSTEREO )) |
|
target_chan->wavtype = CHAR_SPATIALSTEREO; |
|
} |
|
|
|
|
|
// Sets bstereowav flag in channel if source is true stere wav |
|
// sets default wavtype for stereo wavs to CHAR_DISTVARIANT - |
|
// ie: sound varies with distance (left is close, right is far) |
|
// Must be called after S_SetChannelWavtype |
|
|
|
void S_SetChannelStereo( channel_t *target_chan, CAudioSource *pSource ) |
|
{ |
|
if ( !pSource ) |
|
{ |
|
target_chan->flags.bstereowav = false; |
|
return; |
|
} |
|
|
|
// returns true only if source data is a stereo wav file. |
|
// ie: mp3, voice, sentence are all excluded. |
|
|
|
target_chan->flags.bstereowav = pSource->IsStereoWav(); |
|
|
|
// Default stereo wavtype: |
|
|
|
// just player standard stereo wavs on player entity - no override. |
|
|
|
if ( IsSoundSourceLocalPlayer( target_chan->soundsource ) ) |
|
return; |
|
|
|
// default wavtype for stereo wavs is OMNI - except for drymix or sounds with 0 attenuation |
|
|
|
if ( target_chan->flags.bstereowav && !target_chan->wavtype && !target_chan->flags.bdry && target_chan->dist_mult ) |
|
// target_chan->wavtype = CHAR_DISTVARIANT; |
|
target_chan->wavtype = CHAR_OMNI; |
|
} |
|
|
|
// ======================================================================= |
|
// Channel volume management routines: |
|
|
|
// channel volumes crossfade between values over time |
|
// to prevent pops due to rapid spatialization changes |
|
// ======================================================================= |
|
|
|
// return true if all volumes and target volumes for channel are less/equal to 'vol' |
|
|
|
bool BChannelLowVolume( channel_t *pch, int vol_min ) |
|
{ |
|
int max = -1; |
|
int max_target = -1; |
|
int vol; |
|
int vol_target; |
|
|
|
for (int i = 0; i < CCHANVOLUMES; i++) |
|
{ |
|
vol = (int)(pch->fvolume[i]); |
|
vol_target = (int)(pch->fvolume_target[i]); |
|
|
|
if (vol > max) |
|
max = vol; |
|
|
|
if (vol_target > max_target) |
|
max_target = vol_target; |
|
} |
|
|
|
return (max <= vol_min && max_target <= vol_min); |
|
} |
|
|
|
// Get the loudest actual volume for a channel (not counting targets). |
|
float ChannelLoudestCurVolume( const channel_t * RESTRICT pch ) |
|
{ |
|
float loudest = pch->fvolume[0]; |
|
for (int i = 1; i < CCHANVOLUMES; i++) |
|
{ |
|
loudest = fpmax(loudest, pch->fvolume[i]); |
|
} |
|
return loudest; |
|
} |
|
|
|
// clear all volumes, targets, crossfade increments |
|
|
|
void ChannelClearVolumes( channel_t *pch ) |
|
{ |
|
for (int i = 0; i < CCHANVOLUMES; i++) |
|
{ |
|
pch->fvolume[i] = 0.0; |
|
pch->fvolume_target[i] = 0.0; |
|
pch->fvolume_inc[i] = 0.0; |
|
} |
|
} |
|
|
|
// return current volume as integer |
|
|
|
int ChannelGetVol( channel_t *pch, int ivol ) |
|
{ |
|
Assert(ivol < CCHANVOLUMES); |
|
return (int)(pch->fvolume[ivol]); |
|
} |
|
|
|
// return maximum current output volume |
|
|
|
int ChannelGetMaxVol( channel_t *pch ) |
|
{ |
|
float max = 0.0; |
|
|
|
for (int i = 0; i < CCHANVOLUMES; i++) |
|
{ |
|
if (pch->fvolume[i] > max) |
|
max = pch->fvolume[i]; |
|
} |
|
|
|
return (int)max; |
|
} |
|
|
|
// set current volume (clears crossfading - instantaneous value change) |
|
|
|
void ChannelSetVol( channel_t *pch, int ivol, int vol ) |
|
{ |
|
Assert(ivol < CCHANVOLUMES); |
|
|
|
pch->fvolume[ivol] = (float)(clamp(vol, 0, 255)); |
|
|
|
pch->fvolume_target[ivol] = pch->fvolume[ivol]; |
|
pch->fvolume_inc[ivol] = 0.0; |
|
} |
|
|
|
// copy current channel volumes into target array, starting at ivol, copying cvol entries |
|
|
|
void ChannelCopyVolumes( channel_t *pch, int *pvolume_dest, int ivol_start, int cvol ) |
|
{ |
|
Assert (ivol_start < CCHANVOLUMES); |
|
Assert (ivol_start + cvol <= CCHANVOLUMES); |
|
|
|
for (int i = 0; i < cvol; i++) |
|
pvolume_dest[i] = (int)(pch->fvolume[i + ivol_start]); |
|
} |
|
|
|
// volume has hit target, shut off crossfading increment |
|
|
|
inline void ChannelStopVolXfade( channel_t *pch, int ivol ) |
|
{ |
|
pch->fvolume[ivol] = pch->fvolume_target[ivol]; |
|
pch->fvolume_inc[ivol] = 0.0; |
|
} |
|
|
|
#define VOL_XFADE_TIME 0.070 // channel volume crossfade time in seconds |
|
|
|
#define VOL_INCR_MAX 20.0 // never change volume by more than +/-N units per frame |
|
|
|
// set volume target and volume increment (for crossfade) for channel & speaker |
|
|
|
void ChannelSetVolTarget( channel_t *pch, int ivol, int volume_target ) |
|
{ |
|
float frametime = g_pSoundServices->GetHostFrametime(); |
|
float speed; |
|
float vol_target = (float)(clamp(volume_target, 0, 255)); |
|
float vol_current; |
|
|
|
Assert(ivol < CCHANVOLUMES); |
|
|
|
// set volume target |
|
|
|
pch->fvolume_target[ivol] = vol_target; |
|
|
|
// current volume |
|
|
|
vol_current = pch->fvolume[ivol]; |
|
|
|
// if first time spatializing, set target = volume with no crossfade |
|
// if current & target volumes are close - don't bother crossfading |
|
|
|
if ( pch->flags.bfirstpass || (fabs(vol_target - vol_current) < 5.0)) |
|
{ |
|
// set current volume = target, no increment |
|
|
|
ChannelStopVolXfade( pch, ivol); |
|
return; |
|
} |
|
|
|
// get crossfade increment 'speed' (volume change per frame) |
|
|
|
speed = ( frametime / VOL_XFADE_TIME ) * (vol_target - vol_current); |
|
|
|
// make sure we never increment by more than +/- VOL_INCR_MAX volume units per frame |
|
|
|
speed = clamp(speed, (float) -VOL_INCR_MAX, (float) VOL_INCR_MAX); |
|
|
|
pch->fvolume_inc[ivol] = speed; |
|
} |
|
|
|
// set volume targets, using array pvolume as source volumes. |
|
// set into channel volumes starting at ivol_offset index |
|
// set cvol volumes |
|
|
|
void ChannelSetVolTargets( channel_t *pch, int *pvolumes, int ivol_offset, int cvol ) |
|
{ |
|
int volume_target; |
|
|
|
Assert(ivol_offset + cvol <= CCHANVOLUMES); |
|
|
|
for (int i = 0; i < cvol; i++) |
|
{ |
|
volume_target = pvolumes[i]; |
|
|
|
ChannelSetVolTarget( pch, ivol_offset + i, volume_target ); |
|
} |
|
} |
|
|
|
|
|
// Call once per frame, per channel: |
|
// update all volume crossfades, from fvolume -> fvolume_target |
|
// if current volume reaches target, set increment to 0 |
|
|
|
void ChannelUpdateVolXfade( channel_t *pch ) |
|
{ |
|
float fincr; |
|
|
|
for (int i = 0; i < CCHANVOLUMES; i++) |
|
{ |
|
fincr = pch->fvolume_inc[i]; |
|
|
|
if (fincr != 0.0) |
|
{ |
|
pch->fvolume[i] += fincr; |
|
|
|
// test for hit target |
|
|
|
if (fincr > 0.0) |
|
{ |
|
if (pch->fvolume[i] >= pch->fvolume_target[i]) |
|
ChannelStopVolXfade( pch, i ); |
|
} |
|
else |
|
{ |
|
if (pch->fvolume[i] <= pch->fvolume_target[i]) |
|
ChannelStopVolXfade( pch, i ); |
|
} |
|
} |
|
} |
|
} |
|
|
|
// ======================================================================= |
|
// S_StartDynamicSound |
|
// ======================================================================= |
|
// Start a sound effect for the given entity on the given channel (ie; voice, weapon etc). |
|
// Try to grab a channel out of the 8 dynamic spots available. |
|
// Currently used for looping sounds, streaming sounds, sentences, and regular entity sounds. |
|
// NOTE: volume is 0.0 - 1.0 and attenuation is 0.0 - 1.0 when passed in. |
|
// Pitch changes playback pitch of wave by % above or below 100. Ignored if pitch == 100 |
|
|
|
// NOTE: it's not a good idea to play looping sounds through StartDynamicSound, because |
|
// if the looping sound starts out of range, or is bumped from the buffer by another sound |
|
// it will never be restarted. Use StartStaticSound (pass CHAN_STATIC to EMIT_SOUND or |
|
// SV_StartSound. |
|
|
|
int S_StartDynamicSound( StartSoundParams_t& params ) |
|
{ |
|
Assert( params.staticsound == false ); |
|
|
|
channel_t *target_chan; |
|
int vol; |
|
|
|
if ( !g_AudioDevice || !g_AudioDevice->IsActive()) |
|
return 0; |
|
|
|
if (!params.pSfx) |
|
return 0; |
|
|
|
// For debugging to see the actual name of the sound... |
|
char sndname[ MAX_OSPATH ]; |
|
Q_strncpy( sndname, params.pSfx->getname(), sizeof( sndname ) ); |
|
|
|
// Msg("Start sound %s\n", pSfx->getname() ); |
|
|
|
// override the entchannel to CHAN_STREAM if this is a |
|
// non-voice stream sound. |
|
if ( TestSoundChar(sndname, CHAR_STREAM ) && params.entchannel != CHAN_VOICE && params.entchannel != CHAN_VOICE2 ) |
|
params.entchannel = CHAN_STREAM; |
|
|
|
vol = params.fvol*255; |
|
|
|
if (vol > 255) |
|
{ |
|
DevMsg("S_StartDynamicSound: %s volume > 255", sndname ); |
|
vol = 255; |
|
} |
|
|
|
THREAD_LOCK_SOUND(); |
|
|
|
if ( params.flags & (SND_STOP|SND_CHANGE_VOL|SND_CHANGE_PITCH) ) |
|
{ |
|
if ( S_AlterChannel( params.soundsource, params.entchannel, params.pSfx, vol, params.pitch, params.flags) ) |
|
return 0; |
|
if ( params.flags & SND_STOP ) |
|
return 0; |
|
// fall through - if we're not trying to stop the sound, |
|
// and we didn't find it (it's not playing), go ahead and start it up |
|
} |
|
|
|
if (params.pitch == 0) |
|
{ |
|
DevMsg ("Warning: S_StartDynamicSound (%s) Ignored, called with pitch 0\n", sndname ); |
|
return 0; |
|
} |
|
|
|
// pick a channel to play on |
|
target_chan = SND_PickDynamicChannel(params.soundsource, params.entchannel, params.origin, params.pSfx, params.delay, (params.flags & SND_DO_NOT_OVERWRITE_EXISTING_ON_CHANNEL) != 0 ); |
|
if ( !target_chan ) |
|
return 0; |
|
|
|
int channelIndex = (int)( target_chan - channels ); |
|
g_AudioDevice->ChannelReset( params.soundsource, channelIndex, target_chan->dist_mult ); |
|
|
|
#ifdef DEBUG_CHANNELS |
|
{ |
|
char szTmp[128]; |
|
Q_snprintf(szTmp, sizeof( szTmp ), "Sound %s playing on Dynamic game channel %d\n", sndname, IWavstreamOfCh(target_chan)); |
|
Plat_DebugString(szTmp); |
|
} |
|
#endif |
|
|
|
bool bIsSentence = TestSoundChar( sndname, CHAR_SENTENCE ); |
|
|
|
SND_ActivateChannel( target_chan ); |
|
ChannelClearVolumes( target_chan ); |
|
|
|
target_chan->userdata = params.userdata; |
|
target_chan->initialStreamPosition = params.initialStreamPosition; |
|
|
|
VectorCopy(params.origin, target_chan->origin); |
|
VectorCopy(params.direction, target_chan->direction); |
|
|
|
// never update positions if source entity is 0 |
|
target_chan->flags.bUpdatePositions = params.bUpdatePositions && (params.soundsource == 0 ? 0 : 1); |
|
|
|
// reference_dist / (reference_power_level / actual_power_level) |
|
target_chan->flags.m_bCompatibilityAttenuation = SNDLEVEL_IS_COMPATIBILITY_MODE( params.soundlevel ); |
|
if ( target_chan->flags.m_bCompatibilityAttenuation ) |
|
{ |
|
// Translate soundlevel from its 'encoded' value to a real soundlevel that we can use in the sound system. |
|
params.soundlevel = SNDLEVEL_FROM_COMPATIBILITY_MODE( params.soundlevel ); |
|
} |
|
|
|
target_chan->dist_mult = SNDLVL_TO_DIST_MULT( params.soundlevel ); |
|
|
|
S_SetChannelWavtype( target_chan, params.pSfx ); |
|
|
|
target_chan->master_vol = vol; |
|
target_chan->soundsource = params.soundsource; |
|
target_chan->entchannel = params.entchannel; |
|
target_chan->basePitch = params.pitch; |
|
target_chan->flags.isSentence = false; |
|
target_chan->radius = 0; |
|
target_chan->sfx = params.pSfx; |
|
target_chan->special_dsp = params.specialdsp; |
|
target_chan->flags.fromserver = params.fromserver; |
|
target_chan->flags.bSpeaker = (params.flags & SND_SPEAKER) ? 1 : 0; |
|
target_chan->speakerentity = params.speakerentity; |
|
|
|
target_chan->flags.m_bShouldPause = (params.flags & SND_SHOULDPAUSE) ? 1 : 0; |
|
|
|
// initialize dsp room mixing params |
|
target_chan->dsp_mix_min = -1; |
|
target_chan->dsp_mix_max = -1; |
|
|
|
CAudioSource *pSource = NULL; |
|
|
|
if ( bIsSentence ) |
|
{ |
|
// this is a sentence |
|
// link all words and load the first word |
|
|
|
// NOTE: sentence names stored in the cache lookup are |
|
// prepended with a '!'. Sentence names stored in the |
|
// sentence file do not have a leading '!'. |
|
VOX_LoadSound( target_chan, PSkipSoundChars( sndname ) ); |
|
} |
|
else |
|
{ |
|
// regular or streamed sound fx |
|
pSource = S_LoadSound( params.pSfx, target_chan ); |
|
if ( pSource && !IsValidSampleRate( pSource->SampleRate() ) ) |
|
{ |
|
Warning( "*** Invalid sample rate (%d) for sound '%s'.\n", pSource->SampleRate(), sndname ); |
|
} |
|
|
|
if ( !pSource && !params.pSfx->m_bIsLateLoad ) |
|
{ |
|
Warning( "Failed to load sound \"%s\", file probably missing from disk/repository\n", sndname ); |
|
} |
|
|
|
} |
|
|
|
if (!target_chan->pMixer) |
|
{ |
|
// couldn't load the sound's data, or sentence has 0 words (this is not an error) |
|
S_FreeChannel( target_chan ); |
|
return 0; |
|
} |
|
|
|
int nSndShowStart = snd_showstart.GetInt(); |
|
|
|
// TODO: Support looping sounds through speakers. |
|
// If the sound is from a speaker, and it's looping, ignore it. |
|
if ( target_chan->flags.bSpeaker ) |
|
{ |
|
if ( params.pSfx->pSource && params.pSfx->pSource->IsLooped() ) |
|
{ |
|
if (nSndShowStart > 0 && nSndShowStart < 7 && nSndShowStart != 4) |
|
{ |
|
DevMsg("DynamicSound : Speaker ignored looping sound: %s\n", sndname ); |
|
} |
|
|
|
S_FreeChannel( target_chan ); |
|
return 0; |
|
} |
|
} |
|
|
|
S_SetChannelStereo( target_chan, pSource ); |
|
|
|
if (nSndShowStart == 5) |
|
{ |
|
snd_showstart.SetValue(6); // debug: show gain for next spatialize only |
|
nSndShowStart = 6; |
|
} |
|
|
|
// get sound type before we spatialize |
|
MXR_GetMixGroupFromSoundsource( target_chan, params.soundsource, params.soundlevel ); |
|
|
|
// skip the trace on the first spatialization. This channel may be stolen |
|
// by another sound played this frame. Defer the trace to the mix loop |
|
SND_SpatializeFirstFrameNoTrace(target_chan); |
|
|
|
if (nSndShowStart > 0 && nSndShowStart < 7 && nSndShowStart != 4) |
|
{ |
|
channel_t *pTargetChan = target_chan; |
|
|
|
DevMsg( "DynamicSound %s : src %d : channel %d : %d dB : vol %.2f : time %.3f\n", sndname, params.soundsource, params.entchannel, params.soundlevel, params.fvol, g_pSoundServices->GetHostTime() ); |
|
if (nSndShowStart == 2 || nSndShowStart == 5) |
|
DevMsg( "\t dspmix %1.2f : distmix %1.2f : dspface %1.2f : lvol %1.2f : cvol %1.2f : rvol %1.2f : rlvol %1.2f : rrvol %1.2f\n", |
|
pTargetChan->dspmix, pTargetChan->distmix, pTargetChan->dspface, |
|
pTargetChan->fvolume[IFRONT_LEFT], pTargetChan->fvolume[IFRONT_CENTER], pTargetChan->fvolume[IFRONT_RIGHT], pTargetChan->fvolume[IREAR_LEFT], pTargetChan->fvolume[IREAR_RIGHT] ); |
|
if (nSndShowStart == 3) |
|
DevMsg( "\t x: %4f y: %4f z: %4f\n", pTargetChan->origin.x, pTargetChan->origin.y, pTargetChan->origin.z ); |
|
|
|
if ( snd_visualize.GetInt() ) |
|
{ |
|
CDebugOverlay::AddTextOverlay( pTargetChan->origin, 2.0f, sndname ); |
|
} |
|
} |
|
|
|
// If a client can't hear a sound when they FIRST receive the StartSound message, |
|
// the client will never be able to hear that sound. This is so that out of |
|
// range sounds don't fill the playback buffer. For streaming sounds, we bypass this optimization. |
|
|
|
if ( BChannelLowVolume( target_chan, 0 ) && !toolframework->IsToolRecording() ) |
|
{ |
|
// Looping sounds don't use this optimization because they should stick around until they're killed. |
|
// Also bypass for speech (GetSentence) |
|
if ( !params.pSfx->pSource || (!params.pSfx->pSource->IsLooped() && !params.pSfx->pSource->GetSentence()) ) |
|
{ |
|
// if this is long sound, play the whole thing. |
|
if (!SND_IsLongWave( target_chan )) |
|
{ |
|
// DevMsg("S_StartDynamicSound: spatialized to 0 vol & ignored %s", sndname); |
|
S_FreeChannel( target_chan ); |
|
return 0; // not audible at all |
|
} |
|
} |
|
} |
|
|
|
// Init client entity mouth movement vars |
|
target_chan->flags.m_bIgnorePhonemes = ( params.flags & SND_IGNORE_PHONEMES ) != 0; |
|
SND_InitMouth(target_chan); |
|
|
|
if ( IsX360() && params.delay < 0 ) |
|
{ |
|
params.delay = 0; |
|
target_chan->flags.delayed_start = true; |
|
} |
|
|
|
// Pre-startup delay. Compute # of samples over which to mix in zeros from data source before |
|
// actually reading first set of samples |
|
if ( params.delay != 0.0f ) |
|
{ |
|
Assert( target_chan->sfx ); |
|
Assert( target_chan->sfx->pSource ); |
|
|
|
// delay count is computed at the sampling rate of the source because the output rate will |
|
// match the source rate when the sound is mixed |
|
float rate = target_chan->sfx->pSource->SampleRate(); |
|
int delaySamples = (int)( params.delay * rate ); |
|
|
|
if ( params.delay > 0 ) |
|
{ |
|
target_chan->pMixer->SetStartupDelaySamples( delaySamples ); |
|
target_chan->flags.delayed_start = true; |
|
} |
|
else |
|
{ |
|
int skipSamples = -delaySamples; |
|
int totalSamples = target_chan->sfx->pSource->SampleCount(); |
|
if ( target_chan->sfx->pSource->IsLooped() ) |
|
{ |
|
skipSamples = skipSamples % totalSamples; |
|
} |
|
if ( skipSamples >= totalSamples ) |
|
{ |
|
S_FreeChannel( target_chan ); |
|
return 0; |
|
} |
|
target_chan->pitch = target_chan->basePitch * 0.01f; |
|
target_chan->pMixer->SkipSamples( target_chan, skipSamples, rate, 0 ); |
|
target_chan->ob_gain_target = 1.0f; |
|
target_chan->ob_gain = 1.0f; |
|
target_chan->ob_gain_inc = 0.0; |
|
target_chan->flags.bfirstpass = false; |
|
target_chan->flags.delayed_start = true; |
|
} |
|
} |
|
|
|
g_pSoundServices->OnSoundStarted( target_chan->guid, params, sndname ); |
|
return target_chan->guid; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : *name - |
|
// Output : CSfxTable |
|
//----------------------------------------------------------------------------- |
|
CSfxTable *S_DummySfx( const char *name ) |
|
{ |
|
dummySfx.setname( name ); |
|
return &dummySfx; |
|
} |
|
|
|
/* |
|
================= |
|
S_StartStaticSound |
|
================= |
|
Start playback of a sound, loaded into the static portion of the channel array. |
|
Currently, this should be used for looping ambient sounds, looping sounds |
|
that should not be interrupted until complete, non-creature sentences, |
|
and one-shot ambient streaming sounds. Can also play 'regular' sounds one-shot, |
|
in case designers want to trigger regular game sounds. |
|
Pitch changes playback pitch of wave by % above or below 100. Ignored if pitch == 100 |
|
|
|
NOTE: volume is 0.0 - 1.0 and attenuation is 0.0 - 1.0 when passed in. |
|
*/ |
|
|
|
int S_StartStaticSound( StartSoundParams_t& params ) |
|
{ |
|
Assert( params.staticsound == true ); |
|
|
|
channel_t *ch; |
|
CAudioSource *pSource = NULL; |
|
|
|
if ( !g_AudioDevice->IsActive() ) |
|
return 0; |
|
|
|
if ( !params.pSfx ) |
|
return 0; |
|
|
|
// For debugging to see the actual name of the sound... |
|
char sndname[ MAX_OSPATH ]; |
|
Q_strncpy( sndname, params.pSfx->getname(), sizeof( sndname ) ); |
|
// Msg("Start static sound %s\n", pSfx->getname() ); |
|
|
|
int vol = params.fvol * 255; |
|
if ( vol > 255 ) |
|
{ |
|
DevMsg( "S_StartStaticSound: %s volume > 255", sndname ); |
|
vol = 255; |
|
} |
|
|
|
int nSndShowStart = snd_showstart.GetInt(); |
|
|
|
if ((params.flags & SND_STOP) && nSndShowStart > 0) |
|
DevMsg("S_StartStaticSound: %s Stopped.\n", sndname); |
|
|
|
if ((params.flags & SND_STOP) || (params.flags & SND_CHANGE_VOL) || (params.flags & SND_CHANGE_PITCH)) |
|
{ |
|
if (S_AlterChannel(params.soundsource, params.entchannel, params.pSfx, vol, params.pitch, params.flags) || (params.flags & SND_STOP)) |
|
return 0; |
|
} |
|
|
|
if ( params.pitch == 0 ) |
|
{ |
|
DevMsg( "Warning: S_StartStaticSound Ignored, called with pitch 0\n"); |
|
return 0; |
|
} |
|
|
|
// First, make sure the sound source entity is even in the PVS. |
|
float flSoundRadius = 0.0f; |
|
|
|
bool looping = false; |
|
|
|
/* |
|
CAudioSource *pSource = pSfx ? pSfx->pSource : NULL; |
|
if ( pSource ) |
|
{ |
|
looping = pSource->IsLooped(); |
|
} |
|
*/ |
|
|
|
SpatializationInfo_t si; |
|
si.info.Set( |
|
params.soundsource, |
|
params.entchannel, |
|
params.pSfx ? sndname : "", |
|
params.origin, |
|
params.direction, |
|
vol, |
|
params.soundlevel, |
|
looping, |
|
params.pitch, |
|
listener_origin, |
|
params.speakerentity ); |
|
|
|
si.type = SpatializationInfo_t::SI_INCREATION; |
|
|
|
si.pOrigin = NULL; |
|
si.pAngles = NULL; |
|
si.pflRadius = &flSoundRadius; |
|
|
|
g_pSoundServices->GetSoundSpatialization( params.soundsource, si ); |
|
|
|
// pick a channel to play on from the static area |
|
THREAD_LOCK_SOUND(); |
|
|
|
ch = SND_PickStaticChannel(params.soundsource, params.pSfx); // Autolooping sounds are always fixed origin(?) |
|
if ( !ch ) |
|
return 0; |
|
|
|
SND_ActivateChannel( ch ); |
|
ChannelClearVolumes( ch ); |
|
|
|
ch->userdata = params.userdata; |
|
ch->initialStreamPosition = params.initialStreamPosition; |
|
|
|
if ( ch->userdata != 0 ) |
|
{ |
|
g_pSoundServices->GetToolSpatialization( ch->userdata, ch->guid, si ); |
|
} |
|
|
|
int channelIndex = ch - channels; |
|
g_AudioDevice->ChannelReset( params.soundsource, channelIndex, ch->dist_mult ); |
|
|
|
#ifdef DEBUG_CHANNELS |
|
{ |
|
char szTmp[128]; |
|
Q_snprintf(szTmp, sizeof( szTmp ), "Sound %s playing on Static game channel %d\n", sfxin->name, IWavstreamOfCh(ch)); |
|
Plat_DebugString(szTmp); |
|
} |
|
#endif |
|
|
|
if ( TestSoundChar(sndname, CHAR_SENTENCE) ) |
|
{ |
|
// this is a sentence. link words to play in sequence. |
|
|
|
// NOTE: sentence names stored in the cache lookup are |
|
// prepended with a '!'. Sentence names stored in the |
|
// sentence file do not have a leading '!'. |
|
|
|
// link all words and load the first word |
|
VOX_LoadSound( ch, PSkipSoundChars(sndname) ); |
|
} |
|
else |
|
{ |
|
// load regular or stream sound |
|
pSource = S_LoadSound( params.pSfx, ch ); |
|
if ( pSource && !IsValidSampleRate( pSource->SampleRate() ) ) |
|
{ |
|
Warning( "*** Invalid sample rate (%d) for sound '%s'.\n", pSource->SampleRate(), sndname ); |
|
} |
|
|
|
if ( !pSource && !params.pSfx->m_bIsLateLoad ) |
|
{ |
|
Warning( "Failed to load sound \"%s\", file probably missing from disk/repository\n", sndname ); |
|
} |
|
|
|
ch->sfx = params.pSfx; |
|
ch->flags.isSentence = false; |
|
} |
|
|
|
if ( !ch->pMixer ) |
|
{ |
|
// couldn't load sounds' data, or sentence has 0 words (not an error) |
|
S_FreeChannel( ch ); |
|
return 0; |
|
} |
|
|
|
VectorCopy (params.origin, ch->origin); |
|
VectorCopy (params.direction, ch->direction); |
|
|
|
// never update positions if source entity is 0 |
|
ch->flags.bUpdatePositions = params.bUpdatePositions && (params.soundsource == 0 ? 0 : 1); |
|
|
|
ch->master_vol = vol; |
|
|
|
ch->flags.m_bCompatibilityAttenuation = SNDLEVEL_IS_COMPATIBILITY_MODE( params.soundlevel ); |
|
if ( ch->flags.m_bCompatibilityAttenuation ) |
|
{ |
|
// Translate soundlevel from its 'encoded' value to a real soundlevel that we can use in the sound system. |
|
params.soundlevel = SNDLEVEL_FROM_COMPATIBILITY_MODE( params.soundlevel ); |
|
} |
|
|
|
ch->dist_mult = SNDLVL_TO_DIST_MULT( params.soundlevel ); |
|
|
|
S_SetChannelWavtype( ch, params.pSfx ); |
|
|
|
ch->basePitch = params.pitch; |
|
ch->soundsource = params.soundsource; |
|
ch->entchannel = params.entchannel; |
|
ch->special_dsp = params.specialdsp; |
|
ch->flags.fromserver = params.fromserver; |
|
ch->flags.bSpeaker = (params.flags & SND_SPEAKER) ? 1 : 0; |
|
ch->speakerentity = params.speakerentity; |
|
|
|
ch->flags.m_bShouldPause = (params.flags & SND_SHOULDPAUSE) ? 1 : 0; |
|
|
|
// TODO: Support looping sounds through speakers. |
|
// If the sound is from a speaker, and it's looping, ignore it. |
|
if ( ch->flags.bSpeaker ) |
|
{ |
|
if ( params.pSfx->pSource && params.pSfx->pSource->IsLooped() ) |
|
{ |
|
if (nSndShowStart > 0 && nSndShowStart < 7 && nSndShowStart != 4) |
|
{ |
|
DevMsg("StaticSound : Speaker ignored looping sound: %s\n", sndname); |
|
} |
|
|
|
S_FreeChannel( ch ); |
|
return 0; |
|
} |
|
} |
|
|
|
// set the default radius |
|
ch->radius = flSoundRadius; |
|
|
|
S_SetChannelStereo( ch, pSource ); |
|
|
|
// initialize dsp room mixing params |
|
ch->dsp_mix_min = -1; |
|
ch->dsp_mix_max = -1; |
|
|
|
if (nSndShowStart == 5) |
|
{ |
|
snd_showstart.SetValue(6); // display gain once only |
|
nSndShowStart = 6; |
|
} |
|
|
|
// get sound type before we spatialize |
|
|
|
MXR_GetMixGroupFromSoundsource( ch, params.soundsource, params.soundlevel ); |
|
|
|
// skip the trace on the first spatialization. This channel may be stolen |
|
// by another sound played this frame. Defer the trace to the mix loop |
|
SND_SpatializeFirstFrameNoTrace(ch); |
|
|
|
// Init client entity mouth movement vars |
|
ch->flags.m_bIgnorePhonemes = ( params.flags & SND_IGNORE_PHONEMES ) != 0; |
|
SND_InitMouth( ch ); |
|
|
|
if ( IsX360() && params.delay < 0 ) |
|
{ |
|
// X360TEMP: Can't support yet, but going to. |
|
params.delay = 0; |
|
} |
|
|
|
// Pre-startup delay. Compute # of samples over which to mix in zeros from data source before |
|
// actually reading first set of samples |
|
if ( params.delay != 0.0f ) |
|
{ |
|
Assert( ch->sfx ); |
|
Assert( ch->sfx->pSource ); |
|
|
|
float rate = ch->sfx->pSource->SampleRate(); |
|
|
|
int delaySamples = (int)( params.delay * rate * params.pitch * 0.01f ); |
|
|
|
ch->pMixer->SetStartupDelaySamples( delaySamples ); |
|
|
|
if ( params.delay > 0 ) |
|
{ |
|
ch->pMixer->SetStartupDelaySamples( delaySamples ); |
|
ch->flags.delayed_start = true; |
|
} |
|
else |
|
{ |
|
int skipSamples = -delaySamples; |
|
int totalSamples = ch->sfx->pSource->SampleCount(); |
|
|
|
if ( ch->sfx->pSource->IsLooped() ) |
|
{ |
|
skipSamples = skipSamples % totalSamples; |
|
} |
|
|
|
if ( skipSamples >= totalSamples ) |
|
{ |
|
S_FreeChannel( ch ); |
|
return 0; |
|
} |
|
|
|
ch->pitch = ch->basePitch * 0.01f; |
|
ch->pMixer->SkipSamples( ch, skipSamples, rate, 0 ); |
|
ch->ob_gain_target = 1.0f; |
|
ch->ob_gain = 1.0f; |
|
ch->ob_gain_inc = 0.0f; |
|
ch->flags.bfirstpass = false; |
|
} |
|
} |
|
|
|
if ( S_IsMusic( ch ) ) |
|
{ |
|
// See if we have "music" of same name playing from "world" which means we save/restored this sound already. If so, |
|
// kill the new version and update the soundsource |
|
CChannelList list; |
|
g_ActiveChannels.GetActiveChannels( list ); |
|
for ( int i = 0; i < list.Count(); i++ ) |
|
{ |
|
channel_t *pChannel = list.GetChannel(i); |
|
// Don't mess with the channel we just created, of course |
|
if ( ch == pChannel ) |
|
continue; |
|
if ( ch->sfx != pChannel->sfx ) |
|
continue; |
|
if ( pChannel->soundsource != SOUND_FROM_WORLD ) |
|
continue; |
|
if ( !S_IsMusic( pChannel ) ) |
|
continue; |
|
|
|
DevMsg( 1, "Hooking duplicate restored song track %s\n", sndname ); |
|
|
|
// the new channel will have an updated soundsource and probably |
|
// has an updated pitch or volume since we are receiving this sound message |
|
// after the sound has started playing (usually a volume change) |
|
// copy that data out of the source |
|
pChannel->soundsource = ch->soundsource; |
|
pChannel->master_vol = ch->master_vol; |
|
pChannel->basePitch = ch->basePitch; |
|
pChannel->pitch = ch->pitch; |
|
S_FreeChannel( ch ); |
|
|
|
return 0; |
|
} |
|
} |
|
|
|
g_pSoundServices->OnSoundStarted( ch->guid, params, sndname ); |
|
|
|
if (nSndShowStart > 0 && nSndShowStart < 7 && nSndShowStart != 4) |
|
{ |
|
DevMsg( "StaticSound %s : src %d : channel %d : %d dB : vol %.2f : radius %.0f : time %.3f\n", sndname, params.soundsource, params.entchannel, params.soundlevel, params.fvol, flSoundRadius, g_pSoundServices->GetHostTime() ); |
|
if (nSndShowStart == 2 || nSndShowStart == 5) |
|
DevMsg( "\t dspmix %1.2f : distmix %1.2f : dspface %1.2f : lvol %1.2f : cvol %1.2f : rvol %1.2f : rlvol %1.2f : rrvol %1.2f\n", |
|
ch->dspmix, ch->distmix, ch->dspface, |
|
ch->fvolume[IFRONT_LEFT], ch->fvolume[IFRONT_CENTER], ch->fvolume[IFRONT_RIGHT], ch->fvolume[IREAR_LEFT], ch->fvolume[IREAR_RIGHT] ); |
|
if (nSndShowStart == 3) |
|
DevMsg( "\t x: %4f y: %4f z: %4f\n", ch->origin.x, ch->origin.y, ch->origin.z ); |
|
} |
|
|
|
return ch->guid; |
|
} |
|
|
|
#ifdef STAGING_ONLY |
|
static ConVar snd_filter( "snd_filter", "", FCVAR_CHEAT ); |
|
#endif // STAGING_ONLY |
|
|
|
int S_StartSound( StartSoundParams_t& params ) |
|
{ |
|
|
|
if( ! params.pSfx ) |
|
{ |
|
return 0; |
|
} |
|
|
|
#ifdef STAGING_ONLY |
|
if ( snd_filter.GetString()[ 0 ] && !Q_stristr( params.pSfx->getname(), snd_filter.GetString() ) ) |
|
{ |
|
return 0; |
|
} |
|
#endif // STAGING_ONLY |
|
|
|
if ( IsX360() && params.delay < 0 && !params.initialStreamPosition && params.pSfx ) |
|
{ |
|
// calculate an initial stream position from the expected sample position |
|
float rate = params.pSfx->pSource->SampleRate(); |
|
int samplePosition = (int)( -params.delay * rate * params.pitch * 0.01f ); |
|
params.initialStreamPosition = params.pSfx->pSource->SampleToStreamPosition( samplePosition ); |
|
} |
|
|
|
if ( params.staticsound ) |
|
{ |
|
VPROF_( "StartStaticSound", 0, VPROF_BUDGETGROUP_OTHER_SOUND, false, BUDGETFLAG_OTHER ); |
|
return S_StartStaticSound( params ); |
|
} |
|
else |
|
{ |
|
VPROF_( "StartDynamicSound", 0, VPROF_BUDGETGROUP_OTHER_SOUND, false, BUDGETFLAG_OTHER ); |
|
return S_StartDynamicSound( params ); |
|
} |
|
} |
|
|
|
// Restart all the sounds on the specified channel |
|
inline bool IsChannelLooped( int iChannel ) |
|
{ |
|
return (channels[iChannel].sfx && |
|
channels[iChannel].sfx->pSource && |
|
channels[iChannel].sfx->pSource->IsLooped() ); |
|
} |
|
|
|
int S_GetCurrentStaticSounds( SoundInfo_t *pResult, int nSizeResult, int entchannel ) |
|
{ |
|
int nSpaceRemaining = nSizeResult; |
|
for (int i = MAX_DYNAMIC_CHANNELS; i < total_channels && nSpaceRemaining; i++) |
|
{ |
|
if ( channels[i].entchannel == entchannel && channels[i].sfx ) |
|
{ |
|
pResult->Set( channels[i].soundsource, |
|
channels[i].entchannel, |
|
channels[i].sfx->getname(), |
|
channels[i].origin, |
|
channels[i].direction, |
|
( (float)channels[i].master_vol / 255.0 ), |
|
DIST_MULT_TO_SNDLVL( channels[i].dist_mult ), |
|
IsChannelLooped( i ), |
|
channels[i].basePitch, |
|
listener_origin, |
|
channels[i].speakerentity ); |
|
pResult++; |
|
nSpaceRemaining--; |
|
} |
|
} |
|
return (nSizeResult - nSpaceRemaining); |
|
} |
|
|
|
|
|
// Stop all sounds for entity on a channel. |
|
void S_StopSound(int soundsource, int entchannel) |
|
{ |
|
THREAD_LOCK_SOUND(); |
|
CChannelList list; |
|
g_ActiveChannels.GetActiveChannels( list ); |
|
for ( int i = 0; i < list.Count(); i++ ) |
|
{ |
|
channel_t *pChannel = list.GetChannel(i); |
|
if (pChannel->soundsource == soundsource |
|
&& pChannel->entchannel == entchannel) |
|
{ |
|
S_FreeChannel( pChannel ); |
|
} |
|
} |
|
} |
|
|
|
channel_t *S_FindChannelByGuid( int guid ) |
|
{ |
|
CChannelList list; |
|
g_ActiveChannels.GetActiveChannels( list ); |
|
for ( int i = 0; i < list.Count(); i++ ) |
|
{ |
|
channel_t *pChannel = list.GetChannel(i); |
|
if ( pChannel->guid == guid ) |
|
{ |
|
return pChannel; |
|
} |
|
} |
|
return NULL; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : guid - |
|
//----------------------------------------------------------------------------- |
|
void S_StopSoundByGuid( int guid ) |
|
{ |
|
THREAD_LOCK_SOUND(); |
|
channel_t *pChannel = S_FindChannelByGuid( guid ); |
|
if ( pChannel ) |
|
{ |
|
S_FreeChannel( pChannel ); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : guid - |
|
//----------------------------------------------------------------------------- |
|
float S_SoundDurationByGuid( int guid ) |
|
{ |
|
channel_t *pChannel = S_FindChannelByGuid( guid ); |
|
if ( !pChannel || !pChannel->sfx ) |
|
return 0.0f; |
|
|
|
// NOTE: Looping sounds will return the length of a single loop |
|
// Use S_IsLoopingSoundByGuid to see if they are looped |
|
float flRate = pChannel->sfx->pSource->SampleRate() * pChannel->basePitch * 0.01f; |
|
int nTotalSamples = pChannel->sfx->pSource->SampleCount(); |
|
return (flRate != 0.0f) ? nTotalSamples / flRate : 0.0f; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Is this sound a looping sound? |
|
//----------------------------------------------------------------------------- |
|
bool S_IsLoopingSoundByGuid( int guid ) |
|
{ |
|
channel_t *pChannel = S_FindChannelByGuid( guid ); |
|
if ( !pChannel || !pChannel->sfx ) |
|
return false; |
|
|
|
return( pChannel->sfx->pSource->IsLooped() ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Note that the guid is preincremented, so we can just return the current value as the "last sound" indicator |
|
// Input : - |
|
// Output : int |
|
//----------------------------------------------------------------------------- |
|
int S_GetGuidForLastSoundEmitted() |
|
{ |
|
return s_nSoundGuid; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : guid - |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool S_IsSoundStillPlaying( int guid ) |
|
{ |
|
channel_t *pChannel = S_FindChannelByGuid( guid ); |
|
return pChannel != NULL ? true : false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : guid - |
|
// fvol - |
|
//----------------------------------------------------------------------------- |
|
void S_SetVolumeByGuid( int guid, float fvol ) |
|
{ |
|
channel_t *pChannel = S_FindChannelByGuid( guid ); |
|
pChannel->master_vol = 255.0f * clamp( fvol, 0.0f, 1.0f ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : guid - |
|
// Output : float |
|
//----------------------------------------------------------------------------- |
|
float S_GetElapsedTimeByGuid( int guid ) |
|
{ |
|
channel_t *pChannel = S_FindChannelByGuid( guid ); |
|
if ( !pChannel ) |
|
return 0.0f; |
|
|
|
CAudioMixer *mixer = pChannel->pMixer; |
|
if ( !mixer ) |
|
return 0.0f; |
|
|
|
float elapsed = mixer->GetSamplePosition() / ( mixer->GetSource()->SampleRate() * pChannel->pitch * 0.01f ); |
|
return elapsed; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : sndlist - |
|
//----------------------------------------------------------------------------- |
|
void S_GetActiveSounds( CUtlVector< SndInfo_t >& sndlist ) |
|
{ |
|
CChannelList list; |
|
g_ActiveChannels.GetActiveChannels( list ); |
|
for ( int i = 0; i < list.Count(); i++ ) |
|
{ |
|
channel_t *ch = list.GetChannel(i); |
|
|
|
SndInfo_t info; |
|
|
|
info.m_nGuid = ch->guid; |
|
info.m_filenameHandle = ch->sfx ? ch->sfx->GetFileNameHandle() : NULL; |
|
info.m_nSoundSource = ch->soundsource; |
|
info.m_nChannel = ch->entchannel; |
|
// If a sound is being played through a speaker entity (e.g., on a monitor,), this is the |
|
// entity upon which to show the lips moving, if the sound has sentence data |
|
info.m_nSpeakerEntity = ch->speakerentity; |
|
info.m_flVolume = (float)ch->master_vol / 255.0f; |
|
info.m_flLastSpatializedVolume = ch->last_vol; |
|
// Radius of this sound effect (spatialization is different within the radius) |
|
info.m_flRadius = ch->radius; |
|
info.m_nPitch = ch->basePitch; |
|
info.m_pOrigin = &ch->origin; |
|
info.m_pDirection = &ch->direction; |
|
|
|
// if true, assume sound source can move and update according to entity |
|
info.m_bUpdatePositions = ch->flags.bUpdatePositions; |
|
// true if playing linked sentence |
|
info.m_bIsSentence = ch->flags.isSentence; |
|
// if true, bypass all dsp processing for this sound (ie: music) |
|
info.m_bDryMix = ch->flags.bdry; |
|
// true if sound is playing through in-game speaker entity. |
|
info.m_bSpeaker = ch->flags.bSpeaker; |
|
// true if sound is using special DSP effect |
|
info.m_bSpecialDSP = ( ch->special_dsp != 0 ); |
|
// for snd_show, networked sounds get colored differently than local sounds |
|
info.m_bFromServer = ch->flags.fromserver; |
|
|
|
sndlist.AddToTail( info ); |
|
} |
|
} |
|
|
|
void S_StopAllSounds( bool bClear ) |
|
{ |
|
THREAD_LOCK_SOUND(); |
|
int i; |
|
|
|
if ( !g_AudioDevice ) |
|
return; |
|
|
|
if ( !g_AudioDevice->IsActive() ) |
|
return; |
|
|
|
total_channels = MAX_DYNAMIC_CHANNELS; // no statics |
|
|
|
CChannelList list; |
|
g_ActiveChannels.GetActiveChannels( list ); |
|
for ( i = 0; i < list.Count(); i++ ) |
|
{ |
|
channel_t *pChannel = list.GetChannel(i); |
|
if ( channels[i].sfx ) |
|
{ |
|
DevMsg( 1, "%2d:Stopped sound %s\n", i, channels[i].sfx->getname() ); |
|
} |
|
S_FreeChannel( pChannel ); |
|
} |
|
|
|
Q_memset( channels, 0, MAX_CHANNELS * sizeof(channel_t) ); |
|
|
|
if ( bClear ) |
|
{ |
|
S_ClearBuffer(); |
|
} |
|
|
|
// Clear any remaining soundfade |
|
memset( &soundfade, 0, sizeof( soundfade ) ); |
|
|
|
g_AudioDevice->StopAllSounds(); |
|
Assert( g_ActiveChannels.GetActiveCount() == 0 ); |
|
} |
|
|
|
void S_StopAllSoundsC( void ) |
|
{ |
|
S_StopAllSounds( true ); |
|
} |
|
|
|
void S_OnLoadScreen( bool value ) |
|
{ |
|
s_bOnLoadScreen = value; |
|
} |
|
|
|
void S_ClearBuffer( void ) |
|
{ |
|
if ( !g_AudioDevice ) |
|
return; |
|
|
|
g_AudioDevice->ClearBuffer(); |
|
DSP_ClearState(); |
|
MIX_ClearAllPaintBuffers( PAINTBUFFER_SIZE, true ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : percent - |
|
// holdtime - |
|
// intime - |
|
// outtime - |
|
//----------------------------------------------------------------------------- |
|
void S_SoundFade( float percent, float holdtime, float intime, float outtime ) |
|
{ |
|
soundfade.starttime = g_pSoundServices->GetHostTime(); |
|
|
|
soundfade.initial_percent = percent; |
|
soundfade.fadeouttime = outtime; |
|
soundfade.holdtime = holdtime; |
|
soundfade.fadeintime = intime; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Modulates sound volume on the client. |
|
//----------------------------------------------------------------------------- |
|
void S_UpdateSoundFade(void) |
|
{ |
|
float totaltime; |
|
float f; |
|
// Determine current fade value. |
|
|
|
// Assume no fading remains |
|
soundfade.percent = 0; |
|
|
|
totaltime = soundfade.fadeouttime + soundfade.fadeintime + soundfade.holdtime; |
|
|
|
float elapsed = g_pSoundServices->GetHostTime() - soundfade.starttime; |
|
|
|
// Clock wrapped or reset (BUG) or we've gone far enough |
|
if ( elapsed < 0.0f || elapsed >= totaltime || totaltime <= 0.0f ) |
|
{ |
|
return; |
|
} |
|
|
|
// We are in the fade time, so determine amount of fade. |
|
if ( soundfade.fadeouttime > 0.0f && ( elapsed < soundfade.fadeouttime ) ) |
|
{ |
|
// Ramp up |
|
f = elapsed / soundfade.fadeouttime; |
|
} |
|
// Inside the hold time |
|
else if ( elapsed <= ( soundfade.fadeouttime + soundfade.holdtime ) ) |
|
{ |
|
// Stay |
|
f = 1.0f; |
|
} |
|
else |
|
{ |
|
// Ramp down |
|
f = ( elapsed - ( soundfade.fadeouttime + soundfade.holdtime ) ) / soundfade.fadeintime; |
|
// backward interpolated... |
|
f = 1.0f - f; |
|
} |
|
|
|
// Spline it. |
|
f = SimpleSpline( f ); |
|
f = clamp( f, 0.0f, 1.0f ); |
|
|
|
soundfade.percent = soundfade.initial_percent * f; |
|
} |
|
|
|
|
|
//============================================================================= |
|
|
|
// Global Voice Ducker - enabled in vcd scripts, when characters deliver important dialog. Overrides all |
|
// other mixer ducking, and ducks all other sounds except dialog. |
|
|
|
ConVar snd_ducktovolume( "snd_ducktovolume", "0.55", FCVAR_ARCHIVE ); |
|
ConVar snd_duckerattacktime( "snd_duckerattacktime", "0.5", FCVAR_ARCHIVE ); |
|
ConVar snd_duckerreleasetime( "snd_duckerreleasetime", "2.5", FCVAR_ARCHIVE ); |
|
ConVar snd_duckerthreshold("snd_duckerthreshold", "0.15", FCVAR_ARCHIVE ); |
|
|
|
static void S_UpdateVoiceDuck( int voiceChannelCount, int voiceChannelMaxVolume, float frametime ) |
|
{ |
|
float volume_when_ducked = snd_ducktovolume.GetFloat(); |
|
int volume_threshold = (int)(snd_duckerthreshold.GetFloat() * 255.0); |
|
|
|
float duckTarget = 1.0; |
|
if ( voiceChannelCount > 0 ) |
|
{ |
|
voiceChannelMaxVolume = clamp(voiceChannelMaxVolume, 0, 255); |
|
|
|
// duckTarget = RemapVal( voiceChannelMaxVolume, 0, 255, 1.0, volume_when_ducked ); |
|
|
|
// KB: Change: ducker now active if any character is speaking above threshold volume. |
|
// KB: Active ducker drops all volumes to volumes * snd_duckvolume |
|
|
|
if ( voiceChannelMaxVolume > volume_threshold ) |
|
duckTarget = volume_when_ducked; |
|
} |
|
float rate = ( duckTarget < g_DuckScale ) ? snd_duckerattacktime.GetFloat() : snd_duckerreleasetime.GetFloat(); |
|
g_DuckScale = Approach( duckTarget, g_DuckScale, frametime * ((1-volume_when_ducked) / rate) ); |
|
} |
|
|
|
// set 2d forward vector, given 3d right vector. |
|
// NOTE: this should only be used for a listener forward |
|
// vector from a listener right vector. It is not a general use routine. |
|
|
|
void ConvertListenerVectorTo2D( Vector *pvforward, Vector *pvright ) |
|
{ |
|
// get 2d forward direction vector, ignoring pitch angle |
|
QAngle angles2d; |
|
Vector source2d; |
|
Vector listener_forward2d; |
|
|
|
source2d = *pvright; |
|
source2d.z = 0.0; |
|
|
|
VectorNormalize(source2d); |
|
|
|
// convert right vector to euler angles (yaw & pitch) |
|
|
|
VectorAngles(source2d, angles2d); |
|
|
|
// get forward angle of listener |
|
|
|
angles2d[PITCH] = 0; |
|
angles2d[YAW] += 90; // rotate 90 ccw |
|
angles2d[ROLL] = 0; |
|
|
|
if (angles2d[YAW] >= 360) |
|
angles2d[YAW] -= 360; |
|
|
|
AngleVectors(angles2d, &listener_forward2d); |
|
|
|
VectorNormalize(listener_forward2d); |
|
|
|
*pvforward = listener_forward2d; |
|
} |
|
|
|
// If this is nonzero, we will only spatialize some of the static |
|
// channels each frame. The round robin will spatialize 1 / (2 ^ x) |
|
// of the spatial channels each frame. |
|
ConVar snd_spatialize_roundrobin( "snd_spatialize_roundrobin", "0", FCVAR_ALLOWED_IN_COMPETITIVE, "Lowend optimization: if nonzero, spatialize only a fraction of sound channels each frame. 1/2^x of channels will be spatialized per frame." ); |
|
/* |
|
============ |
|
S_Update |
|
|
|
Called once each time through the main loop |
|
============ |
|
*/ |
|
void S_Update( const AudioState_t *pAudioState ) |
|
{ |
|
VPROF("S_Update"); |
|
channel_t *ch; |
|
channel_t *combine; |
|
static unsigned int s_roundrobin = 0 ; ///< number of times this function is called. |
|
///< used instead of host_frame because that number |
|
///< isn't necessarily available here (sez Yahn). |
|
|
|
if ( !g_AudioDevice->IsActive() ) |
|
return; |
|
|
|
g_SndMutex.Lock(); |
|
|
|
// Update any client side sound fade |
|
S_UpdateSoundFade(); |
|
|
|
if ( pAudioState ) |
|
{ |
|
VectorCopy( pAudioState->m_Origin, listener_origin ); |
|
AngleVectors( pAudioState->m_Angles, &listener_forward, &listener_right, &listener_up ); |
|
s_bIsListenerUnderwater = pAudioState->m_bIsUnderwater; |
|
} |
|
else |
|
{ |
|
VectorCopy( vec3_origin, listener_origin ); |
|
VectorCopy( vec3_origin, listener_forward ); |
|
VectorCopy( vec3_origin, listener_right ); |
|
VectorCopy( vec3_origin, listener_up ); |
|
s_bIsListenerUnderwater = false; |
|
} |
|
|
|
g_AudioDevice->UpdateListener( listener_origin, listener_forward, listener_right, listener_up ); |
|
|
|
combine = NULL; |
|
|
|
int voiceChannelCount = 0; |
|
int voiceChannelMaxVolume = 0; |
|
|
|
// reset traceline counter for this frame |
|
g_snd_trace_count = 0; |
|
|
|
// calculate distance to nearest walls, update dsp_spatial |
|
// updates one wall only per frame (one trace per frame) |
|
SND_SetSpatialDelays(); |
|
|
|
// updates dsp_room if automatic room detection enabled |
|
DAS_CheckNewRoomDSP(); |
|
|
|
// update spatialization for static and dynamic sounds |
|
CChannelList list; |
|
g_ActiveChannels.GetActiveChannels( list ); |
|
|
|
if (snd_spatialize_roundrobin.GetInt() == 0) |
|
{ |
|
// spatialize each channel each time |
|
for ( int i = 0; i < list.Count(); i++ ) |
|
{ |
|
ch = list.GetChannel(i); |
|
Assert(ch->sfx); |
|
Assert(ch->activeIndex > 0); |
|
|
|
SND_Spatialize(ch); // respatialize channel |
|
|
|
if ( ch->sfx->pSource && ch->sfx->pSource->IsVoiceSource() ) |
|
{ |
|
voiceChannelCount++; |
|
voiceChannelMaxVolume = max(voiceChannelMaxVolume, ChannelGetMaxVol( ch) ); |
|
} |
|
} |
|
} |
|
else // lowend performance improvement: spatialize only some channels each frame. |
|
{ |
|
unsigned int robinmask = (1 << snd_spatialize_roundrobin.GetInt()) - 1; |
|
|
|
// now do static channels |
|
for ( int i = 0 ; i < list.Count() ; ++i ) |
|
{ |
|
ch = list.GetChannel(i); |
|
Assert(ch->sfx); |
|
Assert(ch->activeIndex > 0); |
|
|
|
// need to check bfirstpass because sound tracing may have been deferred |
|
if ( ch->flags.bfirstpass || (robinmask & s_roundrobin) == ( i & robinmask ) ) |
|
{ |
|
SND_Spatialize(ch); // respatialize channel |
|
} |
|
|
|
if ( ch->sfx->pSource && ch->sfx->pSource->IsVoiceSource() ) |
|
{ |
|
voiceChannelCount++; |
|
voiceChannelMaxVolume = max( voiceChannelMaxVolume, ChannelGetMaxVol( ch) ); |
|
} |
|
} |
|
|
|
++s_roundrobin; |
|
} |
|
|
|
|
|
|
|
SND_ChannelTraceReset(); |
|
|
|
// set new target for voice ducking |
|
float frametime = g_pSoundServices->GetHostFrametime(); |
|
S_UpdateVoiceDuck( voiceChannelCount, voiceChannelMaxVolume, frametime ); |
|
|
|
// update x360 music volume |
|
g_DashboardMusicMixValue = Approach( g_DashboardMusicMixTarget, g_DashboardMusicMixValue, g_DashboardMusicFadeRate * frametime ); |
|
|
|
// |
|
// debugging output |
|
// |
|
if (snd_show.GetInt()) |
|
{ |
|
con_nprint_t np; |
|
np.time_to_live = 2.0f; |
|
np.fixed_width_font = true; |
|
|
|
int total = 0; |
|
|
|
CChannelList activeChannels; |
|
g_ActiveChannels.GetActiveChannels( activeChannels ); |
|
for ( int i = 0; i < activeChannels.Count(); i++ ) |
|
{ |
|
channel_t *channel = activeChannels.GetChannel(i); |
|
if ( !channel->sfx ) |
|
continue; |
|
|
|
np.index = total + 2; |
|
if ( channel->flags.fromserver ) |
|
{ |
|
np.color[0] = 1.0; |
|
np.color[1] = 0.8; |
|
np.color[2] = 0.1; |
|
} |
|
else |
|
{ |
|
np.color[0] = 0.1; |
|
np.color[1] = 0.9; |
|
np.color[2] = 1.0; |
|
} |
|
|
|
unsigned int sampleCount = RemainingSamples( channel ); |
|
float timeleft = (float)sampleCount / (float)channel->sfx->pSource->SampleRate(); |
|
bool bLooping = channel->sfx->pSource->IsLooped(); |
|
|
|
if (snd_surround.GetInt() < 4) |
|
{ |
|
Con_NXPrintf ( &np, "%02i l(%03d) r(%03d) vol(%03d) ent(%03d) pos(%6d %6d %6d) timeleft(%f) looped(%d) %50s", |
|
total+ 1, |
|
(int)channel->fvolume[IFRONT_LEFT], |
|
(int)channel->fvolume[IFRONT_RIGHT], |
|
channel->master_vol, |
|
channel->soundsource, |
|
(int)channel->origin[0], |
|
(int)channel->origin[1], |
|
(int)channel->origin[2], |
|
timeleft, |
|
bLooping, |
|
channel->sfx->getname()); |
|
} |
|
else |
|
{ |
|
Con_NXPrintf ( &np, "%02i l(%03d) c(%03d) r(%03d) rl(%03d) rr(%03d) vol(%03d) ent(%03d) pos(%6d %6d %6d) timeleft(%f) looped(%d) %50s", |
|
total+ 1, |
|
(int)channel->fvolume[IFRONT_LEFT], |
|
(int)channel->fvolume[IFRONT_CENTER], |
|
(int)channel->fvolume[IFRONT_RIGHT], |
|
(int)channel->fvolume[IREAR_LEFT], |
|
(int)channel->fvolume[IREAR_RIGHT], |
|
channel->master_vol, |
|
channel->soundsource, |
|
(int)channel->origin[0], |
|
(int)channel->origin[1], |
|
(int)channel->origin[2], |
|
timeleft, |
|
bLooping, |
|
channel->sfx->getname()); |
|
} |
|
|
|
if ( snd_visualize.GetInt() ) |
|
{ |
|
CDebugOverlay::AddTextOverlay( channel->origin, 0.05f, channel->sfx->getname() ); |
|
} |
|
|
|
total++; |
|
} |
|
|
|
while ( total <= 128 ) |
|
{ |
|
Con_NPrintf( total + 2, "" ); |
|
total++; |
|
} |
|
} |
|
|
|
g_SndMutex.Unlock(); |
|
|
|
if ( s_bOnLoadScreen ) |
|
return; |
|
|
|
// not time to update yet? |
|
double tNow = Plat_FloatTime(); |
|
// this is the last time we ran a sound frame |
|
g_LastSoundFrame = tNow; |
|
// this is the last time we did mixing (extraupdate also advances this if it mixes) |
|
g_LastMixTime = tNow; |
|
// mix some sound |
|
// try to stay at least one frame + mixahead ahead in the mix. |
|
g_EstFrameTime = (g_EstFrameTime * 0.9f) + (g_pSoundServices->GetHostFrametime() * 0.1f); |
|
S_Update_( g_EstFrameTime + snd_mixahead.GetFloat() ); |
|
} |
|
|
|
CON_COMMAND( snd_dumpclientsounds, "Dump sounds to VXConsole" ) |
|
{ |
|
con_nprint_t np; |
|
np.time_to_live = 2.0f; |
|
np.fixed_width_font = true; |
|
|
|
int total = 0; |
|
|
|
CChannelList list; |
|
g_ActiveChannels.GetActiveChannels( list ); |
|
for ( int i = 0; i < list.Count(); i++ ) |
|
{ |
|
channel_t *ch = list.GetChannel(i); |
|
if ( !ch->sfx ) |
|
continue; |
|
|
|
unsigned int sampleCount = RemainingSamples( ch ); |
|
float timeleft = (float)sampleCount / (float)ch->sfx->pSource->SampleRate(); |
|
bool bLooping = ch->sfx->pSource->IsLooped(); |
|
const char *pszclassname = GetClientClassname(ch->soundsource); |
|
|
|
Msg( "%02i %s l(%03d) c(%03d) r(%03d) rl(%03d) rr(%03d) vol(%03d) pos(%6d %6d %6d) timeleft(%f) looped(%d) %50s chan:%d ent(%03d):%s\n", |
|
total+ 1, |
|
ch->flags.fromserver ? "SERVER" : "CLIENT", |
|
(int)ch->fvolume[IFRONT_LEFT], |
|
(int)ch->fvolume[IFRONT_CENTER], |
|
(int)ch->fvolume[IFRONT_RIGHT], |
|
(int)ch->fvolume[IREAR_LEFT], |
|
(int)ch->fvolume[IREAR_RIGHT], |
|
ch->master_vol, |
|
(int)ch->origin[0], |
|
(int)ch->origin[1], |
|
(int)ch->origin[2], |
|
timeleft, |
|
bLooping, |
|
ch->sfx->getname(), |
|
ch->entchannel, |
|
ch->soundsource, |
|
pszclassname ? pszclassname : "NULL" ); |
|
|
|
total++; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Set g_soundtime to number of full samples that have been transfered out to hardware |
|
// since start. |
|
//----------------------------------------------------------------------------- |
|
void GetSoundTime(void) |
|
{ |
|
int fullsamples; |
|
int sampleOutCount; |
|
|
|
// size of output buffer in *full* 16 bit samples |
|
// A 2 channel device has a *full* sample consisting of a 16 bit LR pair. |
|
// A 1 channel device has a *full* sample consiting of a 16 bit single sample. |
|
fullsamples = g_AudioDevice->DeviceSampleCount() / g_AudioDevice->DeviceChannels(); |
|
|
|
// NOTE: it is possible to miscount buffers if it has wrapped twice between |
|
// calls to S_Update. However, since the output buffer size is > 1 second of sound, |
|
// this should only occur for framerates lower than 1hz |
|
|
|
// sampleOutCount is counted in 16 bit *full* samples, of number of samples output to hardware |
|
// for current output buffer |
|
sampleOutCount = g_AudioDevice->GetOutputPosition(); |
|
if ( sampleOutCount < s_oldsampleOutCount ) |
|
{ |
|
// buffer wrapped |
|
s_buffers++; |
|
if ( g_paintedtime > 0x70000000 ) |
|
{ |
|
// time to chop things off to avoid 32 bit limits |
|
s_buffers = 0; |
|
g_paintedtime = fullsamples; |
|
S_StopAllSounds( true ); |
|
} |
|
} |
|
|
|
s_oldsampleOutCount = sampleOutCount; |
|
|
|
if ( cl_movieinfo.IsRecording() || IsReplayRendering() ) |
|
{ |
|
// when recording a replay, we look at the record frame rate, not the engine frame rate |
|
|
|
#if defined( REPLAY_ENABLED ) |
|
extern IClientReplayContext *g_pClientReplayContext; |
|
if ( IsReplayRendering() ) |
|
{ |
|
IReplayMovieRenderer *pMovieRenderer = (g_pClientReplayContext != NULL) ? g_pClientReplayContext->GetMovieRenderer() : NULL; |
|
|
|
if ( pMovieRenderer && pMovieRenderer->IsAudioSyncFrame() ) |
|
{ |
|
float t = g_pSoundServices->GetHostTime(); |
|
if ( s_lastsoundtime != t ) |
|
{ |
|
float frameTime = pMovieRenderer->GetRecordingFrameDuration(); |
|
float fSamples = frameTime * (float) g_AudioDevice->DeviceDmaSpeed() + g_ReplaySoundTimeFracAccumulator; |
|
|
|
float intPart = (float) floor( fSamples ); |
|
g_ReplaySoundTimeFracAccumulator = fSamples - intPart; |
|
|
|
g_soundtime += (int) intPart; |
|
s_lastsoundtime = t; |
|
} |
|
} |
|
|
|
} |
|
else // cl_movieinfo.IsRecording() |
|
// in movie, just mix one frame worth of sound |
|
#endif |
|
{ |
|
|
|
float t = g_pSoundServices->GetHostTime(); |
|
if ( s_lastsoundtime != t ) |
|
{ |
|
g_soundtime += g_pSoundServices->GetHostFrametime() * g_AudioDevice->DeviceDmaSpeed(); |
|
|
|
s_lastsoundtime = t; |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
// g_soundtime indicates how many *full* samples have actually been |
|
// played out to dma |
|
g_soundtime = s_buffers*fullsamples + sampleOutCount; |
|
} |
|
} |
|
|
|
void S_ExtraUpdate( void ) |
|
{ |
|
if ( !g_AudioDevice || !g_pSoundServices ) |
|
return; |
|
|
|
if ( !g_AudioDevice->IsActive() ) |
|
return; |
|
|
|
if ( s_bOnLoadScreen ) |
|
return; |
|
|
|
if ( snd_noextraupdate.GetInt() || cl_movieinfo.IsRecording() || IsReplayRendering() ) |
|
return; // don't pollute timings |
|
|
|
// If listener position and orientation has not yet been updated (ie: no call to S_Update since level load) |
|
// then don't mix. Important - mixing with listener at 'false' origin causes |
|
// some sounds to incorrectly spatialize to 0 volume, killing them before they can play. |
|
|
|
if ((listener_origin == vec3_origin) && |
|
(listener_forward == vec3_origin) && |
|
(listener_right == vec3_origin) && |
|
(listener_up == vec3_origin) ) |
|
return; |
|
|
|
// Only mix if you have used up 90% of the mixahead buffer |
|
double tNow = Plat_FloatTime(); |
|
float delta = (tNow - g_LastMixTime); |
|
// we know we were at least snd_mixahead seconds ahead of the output the last time we did mixing |
|
// if we're not close to running out just exit to avoid small mix batches |
|
if ( delta > 0 && delta < (snd_mixahead.GetFloat() * 0.9f) ) |
|
return; |
|
g_LastMixTime = tNow; |
|
|
|
g_pSoundServices->OnExtraUpdate(); |
|
// Shouldn't have to do any work here if your framerate hasn't dropped |
|
S_Update_( snd_mixahead.GetFloat() ); |
|
} |
|
|
|
extern void DEBUG_StartSoundMeasure(int type, int samplecount ); |
|
extern void DEBUG_StopSoundMeasure(int type, int samplecount ); |
|
|
|
void S_Update_Guts( float mixAheadTime ) |
|
{ |
|
VPROF( "S_Update_Guts" ); |
|
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ ); |
|
|
|
DEBUG_StartSoundMeasure(4, 0); |
|
|
|
// Update our perception of audio time. |
|
// 'g_soundtime' tells how many samples have |
|
// been played out of the dma buffer since sound system startup. |
|
// 'g_paintedtime' indicates how many samples we've actually mixed |
|
// and sent to the dma buffer since sound system startup. |
|
GetSoundTime(); |
|
|
|
// if ( g_soundtime > g_paintedtime ) |
|
// { |
|
// // if soundtime > paintedtime, then the dma buffer |
|
// // has played out more sound than we've actually |
|
// // mixed. We need to call S_Update_ more often. |
|
// |
|
// DevMsg ("S_Update_ : Underflow\n"); |
|
// paintedtime = g_soundtime; |
|
// } |
|
// (kdb) above code doesn't handle underflow correctly |
|
// should actually zero out the paintbuffer to advance to the new |
|
// time. |
|
|
|
// mix ahead of current position |
|
unsigned endtime = g_AudioDevice->PaintBegin( mixAheadTime, g_soundtime, g_paintedtime ); |
|
|
|
int samples = endtime - g_paintedtime; |
|
samples = samples < 0 ? 0 : samples; |
|
if ( samples ) |
|
{ |
|
THREAD_LOCK_SOUND(); |
|
|
|
DEBUG_StartSoundMeasure( 2, samples ); |
|
|
|
MIX_PaintChannels( endtime, s_bIsListenerUnderwater ); |
|
|
|
MXR_DebugShowMixVolumes(); |
|
|
|
MXR_UpdateAllDuckerVolumes(); |
|
|
|
DEBUG_StopSoundMeasure( 2, 0 ); |
|
} |
|
|
|
g_AudioDevice->PaintEnd(); |
|
DEBUG_StopSoundMeasure( 4, samples ); |
|
} |
|
|
|
#if !defined( _X360 ) |
|
#define THREADED_MIX_TIME 33 |
|
#else |
|
#define THREADED_MIX_TIME XMA_POLL_RATE |
|
#endif |
|
|
|
ConVar snd_ShowThreadFrameTime( "snd_ShowThreadFrameTime", "0" ); |
|
|
|
bool g_bMixThreadExit; |
|
ThreadHandle_t g_hMixThread; |
|
void S_Update_Thread() |
|
{ |
|
float frameTime = THREADED_MIX_TIME * 0.001f; |
|
double lastFrameTime = Plat_FloatTime(); |
|
|
|
while ( !g_bMixThreadExit ) |
|
{ |
|
// mixing (for 360) needs to be updated at a steady rate |
|
// large update times causes the mixer to demand more audio data |
|
// the 360 decoder has finite latency and cannot fulfill spike requests |
|
float t0 = Plat_FloatTime(); |
|
S_Update_Guts( frameTime + snd_mixahead.GetFloat() ); |
|
int updateTime = ( Plat_FloatTime() - t0 ) * 1000.0f; |
|
|
|
// try to maintain a steadier rate by compensating for fluctuating mix times |
|
int sleepTime = THREADED_MIX_TIME - updateTime; |
|
if ( sleepTime > 0 ) |
|
{ |
|
ThreadSleep( sleepTime ); |
|
} |
|
|
|
// mimic a frametime needed for sound update |
|
double t1 = Plat_FloatTime(); |
|
frameTime = t1 - lastFrameTime; |
|
lastFrameTime = t1; |
|
|
|
if ( snd_ShowThreadFrameTime.GetBool() ) |
|
{ |
|
Msg( "S_Update_Thread: frameTime: %d ms\n", (int)( frameTime * 1000.0f ) ); |
|
} |
|
} |
|
} |
|
|
|
void S_ShutdownMixThread() |
|
{ |
|
if ( g_hMixThread ) |
|
{ |
|
g_bMixThreadExit = true; |
|
ThreadJoin( g_hMixThread ); |
|
ReleaseThreadHandle( g_hMixThread ); |
|
g_hMixThread = NULL; |
|
} |
|
} |
|
|
|
void S_Update_( float mixAheadTime ) |
|
{ |
|
if ( !IsConsole() || !snd_mix_async.GetBool() ) |
|
{ |
|
S_ShutdownMixThread(); |
|
S_Update_Guts( mixAheadTime ); |
|
} |
|
else |
|
{ |
|
if ( !g_hMixThread ) |
|
{ |
|
g_bMixThreadExit = false; |
|
g_hMixThread = ThreadExecuteSolo( "SndMix", S_Update_Thread ); |
|
if ( IsX360() ) |
|
{ |
|
ThreadSetAffinity( g_hMixThread, XBOX_PROCESSOR_5 ); |
|
} |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Threaded mixing enable. Purposely hiding enable/disable details. |
|
//----------------------------------------------------------------------------- |
|
void S_EnableThreadedMixing( bool bEnable ) |
|
{ |
|
if ( snd_mix_async.GetBool() != bEnable ) |
|
{ |
|
snd_mix_async.SetValue( bEnable ); |
|
} |
|
} |
|
|
|
/* |
|
=============================================================================== |
|
|
|
console functions |
|
|
|
=============================================================================== |
|
*/ |
|
extern void DSP_DEBUGSetParams(int ipreset, int iproc, float *pvalues, int cparams); |
|
extern void DSP_DEBUGReloadPresetFile( void ); |
|
|
|
void S_DspParms( const CCommand &args ) |
|
{ |
|
if ( args.ArgC() == 1) |
|
{ |
|
// if dsp_parms with no arguments, reload entire preset file |
|
|
|
DSP_DEBUGReloadPresetFile(); |
|
|
|
return; |
|
} |
|
|
|
if ( args.ArgC() < 4 ) |
|
{ |
|
Msg( "Usage: dsp_parms PRESET# PROC# param0 param1 ...up to param15 \n" ); |
|
return; |
|
} |
|
|
|
int cparam = min( args.ArgC() - 4, 16); |
|
|
|
float params[16]; |
|
Q_memset( params, 0, sizeof(float) * 16 ); |
|
|
|
// get preset & proc |
|
int idsp, iproc; |
|
idsp = Q_atof( args[1] ); |
|
iproc = Q_atof( args[2] ); |
|
|
|
// get params |
|
for (int i = 0; i < cparam; i++) |
|
{ |
|
params[i] = Q_atof( args[i+4] ); |
|
} |
|
|
|
// set up params & switch preset |
|
DSP_DEBUGSetParams(idsp, iproc, params, cparam); |
|
} |
|
|
|
static ConCommand dsp_parm("dsp_reload", S_DspParms ); |
|
|
|
void S_Play( const char *pszName, bool flush = false ) |
|
{ |
|
int inCache; |
|
char szName[256]; |
|
CSfxTable *pSfx; |
|
|
|
Q_strncpy( szName, pszName, sizeof( szName ) ); |
|
if ( !Q_strrchr( pszName, '.' ) ) |
|
{ |
|
Q_strncat( szName, ".wav", sizeof( szName ), COPY_ALL_CHARACTERS ); |
|
} |
|
|
|
pSfx = S_FindName( szName, &inCache ); |
|
if ( inCache && flush ) |
|
{ |
|
pSfx->pSource->CacheUnload(); |
|
} |
|
|
|
StartSoundParams_t params; |
|
params.staticsound = false; |
|
params.soundsource = g_pSoundServices->GetViewEntity(); |
|
params.entchannel = CHAN_REPLACE; |
|
params.pSfx = pSfx; |
|
params.origin = listener_origin; |
|
params.fvol = 1.0f; |
|
params.soundlevel = SNDLVL_NONE; |
|
params.flags = 0; |
|
params.pitch = PITCH_NORM; |
|
|
|
S_StartSound( params ); |
|
} |
|
|
|
static void S_Play( const CCommand &args ) |
|
{ |
|
bool bFlush = !Q_stricmp( args[0], "playflush" ); |
|
for ( int i = 1; i < args.ArgC(); ++i ) |
|
{ |
|
S_Play( args[i], bFlush ); |
|
} |
|
} |
|
|
|
static void S_PlayVol( const CCommand &args ) |
|
{ |
|
static int hash=543; |
|
float vol; |
|
char name[256]; |
|
CSfxTable *pSfx; |
|
|
|
for ( int i = 1; i<args.ArgC(); i += 2 ) |
|
{ |
|
if ( !Q_strrchr( args[i], '.') ) |
|
{ |
|
Q_strncpy( name, args[i], sizeof( name ) ); |
|
Q_strncat( name, ".wav", sizeof( name ), COPY_ALL_CHARACTERS ); |
|
} |
|
else |
|
{ |
|
Q_strncpy( name, args[i], sizeof( name ) ); |
|
} |
|
|
|
pSfx = S_PrecacheSound( name ); |
|
vol = Q_atof( args[i+1] ); |
|
|
|
StartSoundParams_t params; |
|
params.staticsound = false; |
|
params.soundsource = hash++; |
|
params.entchannel = CHAN_AUTO; |
|
params.pSfx = pSfx; |
|
params.origin = listener_origin; |
|
params.fvol = vol; |
|
params.soundlevel = SNDLVL_NONE; |
|
params.flags = 0; |
|
params.pitch = PITCH_NORM; |
|
|
|
S_StartDynamicSound( params ); |
|
} |
|
} |
|
|
|
static void S_PlayDelay( const CCommand &args ) |
|
{ |
|
if ( args.ArgC() != 3 ) |
|
{ |
|
Msg( "Usage: sndplaydelay delay_in_sec (negative to skip ahead) soundname\n" ); |
|
return; |
|
} |
|
|
|
char szName[256]; |
|
CSfxTable *pSfx; |
|
|
|
float delay = Q_atof( args[ 1 ] ); |
|
|
|
Q_strncpy(szName, args[ 2 ], sizeof( szName ) ); |
|
if ( !Q_strrchr( args[ 2 ], '.' ) ) |
|
{ |
|
Q_strncat( szName, ".wav", sizeof( szName ), COPY_ALL_CHARACTERS ); |
|
} |
|
|
|
pSfx = S_FindName( szName, NULL ); |
|
|
|
StartSoundParams_t params; |
|
params.staticsound = false; |
|
params.soundsource = g_pSoundServices->GetViewEntity(); |
|
params.entchannel = CHAN_REPLACE; |
|
params.pSfx = pSfx; |
|
params.origin = listener_origin; |
|
params.fvol = 1.0f; |
|
params.soundlevel = SNDLVL_NONE; |
|
params.flags = 0; |
|
params.pitch = PITCH_NORM; |
|
params.delay = delay; |
|
|
|
S_StartSound( params ); |
|
|
|
} |
|
static ConCommand sndplaydelay( "sndplaydelay", S_PlayDelay, "Usage: sndplaydelay delay_in_sec (negative to skip ahead) soundname", FCVAR_SERVER_CAN_EXECUTE ); |
|
|
|
static bool SortByNameLessFunc( const int &lhs, const int &rhs ) |
|
{ |
|
CSfxTable *pSfx1 = s_Sounds[lhs].pSfx; |
|
CSfxTable *pSfx2 = s_Sounds[rhs].pSfx; |
|
|
|
return CaselessStringLessThan( pSfx1->getname(), pSfx2->getname() ); |
|
} |
|
|
|
void S_SoundList(void) |
|
{ |
|
CSfxTable *sfx; |
|
CAudioSource *pSource; |
|
int size, total; |
|
|
|
total = 0; |
|
for ( int i = s_Sounds.FirstInorder(); i != s_Sounds.InvalidIndex(); i = s_Sounds.NextInorder( i ) ) |
|
{ |
|
sfx = s_Sounds[i].pSfx; |
|
|
|
pSource = sfx->pSource; |
|
if ( !pSource || !pSource->IsCached() ) |
|
continue; |
|
|
|
size = pSource->SampleSize() * pSource->SampleCount(); |
|
total += size; |
|
|
|
if ( pSource->IsLooped() ) |
|
Msg ("L"); |
|
else |
|
Msg (" "); |
|
Msg("(%2db) %6i : %s\n", pSource->SampleSize(), size, sfx->getname()); |
|
} |
|
Msg( "Total resident: %i\n", total ); |
|
} |
|
|
|
#if defined( _X360 ) |
|
CON_COMMAND( vx_soundlist, "Dump sounds to VXConsole" ) |
|
{ |
|
CSfxTable *sfx; |
|
CAudioSource *pSource; |
|
int dataSize; |
|
char *pFormatStr; |
|
int sampleRate; |
|
int sampleBits; |
|
int streamed; |
|
int looped; |
|
int channels; |
|
int numSamples; |
|
|
|
int numSounds = s_Sounds.Count(); |
|
xSoundList_t* pSoundList = new xSoundList_t[numSounds]; |
|
|
|
int i = 0; |
|
for ( int iSrcSound=s_Sounds.FirstInorder(); iSrcSound != s_Sounds.InvalidIndex(); iSrcSound = s_Sounds.NextInorder( iSrcSound ) ) |
|
{ |
|
dataSize = -1; |
|
sampleRate = -1; |
|
sampleBits = -1; |
|
pFormatStr = "???"; |
|
streamed = -1; |
|
looped = -1; |
|
channels = -1; |
|
numSamples = -1; |
|
|
|
sfx = s_Sounds[iSrcSound].pSfx; |
|
pSource = sfx->pSource; |
|
if ( pSource && pSource->IsCached() ) |
|
{ |
|
numSamples = pSource->SampleCount(); |
|
dataSize = pSource->DataSize(); |
|
sampleRate = pSource->SampleRate(); |
|
streamed = pSource->IsStreaming(); |
|
looped = pSource->IsLooped(); |
|
channels = pSource->IsStereoWav() ? 2 : 1; |
|
|
|
if ( pSource->Format() == WAVE_FORMAT_ADPCM ) |
|
{ |
|
pFormatStr = "ADPCM"; |
|
sampleBits = 16; |
|
} |
|
else if ( pSource->Format() == WAVE_FORMAT_PCM ) |
|
{ |
|
pFormatStr = "PCM"; |
|
sampleBits = (pSource->SampleSize() * 8)/channels; |
|
} |
|
else if ( pSource->Format() == WAVE_FORMAT_XMA ) |
|
{ |
|
pFormatStr = "XMA"; |
|
sampleBits = 16; |
|
} |
|
} |
|
|
|
V_strncpy( pSoundList[i].name, sfx->getname(), sizeof( pSoundList[i].name ) ); |
|
V_strncpy( pSoundList[i].formatName, pFormatStr, sizeof( pSoundList[i].formatName ) ); |
|
pSoundList[i].rate = sampleRate; |
|
pSoundList[i].bits = sampleBits; |
|
pSoundList[i].channels = channels; |
|
pSoundList[i].looped = looped; |
|
pSoundList[i].dataSize = dataSize; |
|
pSoundList[i].numSamples = numSamples; |
|
pSoundList[i].streamed = streamed; |
|
++i; |
|
} |
|
|
|
XBX_rSoundList( numSounds, pSoundList ); |
|
delete [] pSoundList; |
|
} |
|
#endif |
|
|
|
extern unsigned g_snd_time_debug; |
|
extern unsigned g_snd_call_time_debug; |
|
extern unsigned g_snd_count_debug; |
|
extern unsigned g_snd_samplecount; |
|
extern unsigned g_snd_frametime; |
|
extern unsigned g_snd_frametime_total; |
|
extern int g_snd_profile_type; |
|
|
|
// start measuring sound perf, 100 reps |
|
// type 1 - dsp, 2 - mix, 3 - load sound, 4 - all sound |
|
// set type via ConVar snd_profile |
|
|
|
void DEBUG_StartSoundMeasure(int type, int samplecount ) |
|
{ |
|
if (type != g_snd_profile_type) |
|
return; |
|
|
|
if (samplecount) |
|
g_snd_samplecount += samplecount; |
|
|
|
g_snd_call_time_debug = Plat_MSTime(); |
|
} |
|
|
|
// show sound measurement after 25 reps - show as % of total frame |
|
// type 1 - dsp, 2 - mix, 3 - load sound, 4 - all sound |
|
|
|
// BUGBUG: snd_profile 4 reports a lower average because it's average cost |
|
// PER CALL and most calls (via SoundExtraUpdate()) don't do any work and |
|
// bring the average down. If you want an average PER FRAME instead, it's generally higher. |
|
void DEBUG_StopSoundMeasure(int type, int samplecount ) |
|
{ |
|
if (type != g_snd_profile_type) |
|
return; |
|
|
|
if (samplecount) |
|
g_snd_samplecount += samplecount; |
|
|
|
// add total time since last frame |
|
|
|
g_snd_frametime_total += Plat_MSTime() - g_snd_frametime; |
|
|
|
// performance timing |
|
|
|
g_snd_time_debug += Plat_MSTime() - g_snd_call_time_debug; |
|
|
|
if (++g_snd_count_debug >= 100) |
|
{ |
|
switch (g_snd_profile_type) |
|
{ |
|
case 1: |
|
Msg("dsp: (%2.2f) millisec ", ((float)g_snd_time_debug) / 100.0); |
|
Msg("(%2.2f) pct of frame \n", 100.0 * ((float)g_snd_time_debug) / ((float)g_snd_frametime_total)); |
|
break; |
|
case 2: |
|
Msg("mix+dsp:(%2.2f) millisec ", ((float)g_snd_time_debug) / 100.0); |
|
Msg("(%2.2f) pct of frame \n", 100.0 * ((float)g_snd_time_debug) / ((float)g_snd_frametime_total)); |
|
break; |
|
case 3: |
|
//if ( (((float)g_snd_time_debug) / 100.0) < 0.01 ) |
|
// break; |
|
Msg("snd load: (%2.2f) millisec ", ((float)g_snd_time_debug) / 100.0); |
|
Msg("(%2.2f) pct of frame \n", 100.0 * ((float)g_snd_time_debug) / ((float)g_snd_frametime_total)); |
|
break; |
|
case 4: |
|
Msg("sound: (%2.2f) millisec ", ((float)g_snd_time_debug) / 100.0); |
|
Msg("(%2.2f) pct of frame (%d samples) \n", 100.0 * ((float)g_snd_time_debug) / ((float)g_snd_frametime_total), g_snd_samplecount); |
|
break; |
|
} |
|
|
|
g_snd_count_debug = 0; |
|
g_snd_time_debug = 0; |
|
g_snd_samplecount = 0; |
|
g_snd_frametime_total = 0; |
|
} |
|
|
|
g_snd_frametime = Plat_MSTime(); |
|
} |
|
|
|
// speak a sentence from console; works by passing in "!sentencename" |
|
// or "sentence" |
|
|
|
extern ConVar dsp_room; |
|
|
|
static void S_Say( const CCommand &args ) |
|
{ |
|
CSfxTable *pSfx; |
|
|
|
if ( !g_AudioDevice->IsActive() ) |
|
return; |
|
|
|
char sound[256]; |
|
Q_strncpy( sound, args[1], sizeof( sound ) ); |
|
|
|
// DEBUG - test performance of dsp code |
|
if ( !Q_stricmp( sound, "dsp" ) ) |
|
{ |
|
unsigned time; |
|
int i; |
|
int count = 10000; |
|
int idsp; |
|
|
|
for (i = 0; i < PAINTBUFFER_SIZE; i++) |
|
{ |
|
g_paintbuffer[i].left = RandomInt(0,2999); |
|
g_paintbuffer[i].right = RandomInt(0,2999); |
|
} |
|
|
|
Msg ("Start profiling 10,000 calls to DSP\n"); |
|
|
|
idsp = dsp_room.GetInt(); |
|
|
|
// get system time |
|
|
|
time = Plat_MSTime(); |
|
|
|
for (i = 0; i < count; i++) |
|
{ |
|
// SX_RoomFX(PAINTBUFFER_SIZE, TRUE, TRUE); |
|
|
|
DSP_Process(idsp, g_paintbuffer, NULL, NULL, PAINTBUFFER_SIZE); |
|
|
|
} |
|
// display system time delta |
|
Msg("%d milliseconds \n", Plat_MSTime() - time); |
|
return; |
|
} |
|
|
|
if ( !Q_stricmp(sound, "paint") ) |
|
{ |
|
unsigned time; |
|
int count = 10000; |
|
static int hash=543; |
|
int psav = g_paintedtime; |
|
|
|
Msg ("Start profiling MIX_PaintChannels\n"); |
|
|
|
pSfx = S_PrecacheSound("ambience/labdrone1.wav"); |
|
|
|
StartSoundParams_t params; |
|
params.staticsound = false; |
|
params.soundsource = hash++; |
|
params.entchannel = CHAN_AUTO; |
|
params.pSfx = pSfx; |
|
params.origin = listener_origin; |
|
params.fvol = 1.0f; |
|
params.soundlevel = SNDLVL_NONE; |
|
params.flags = 0; |
|
params.pitch = PITCH_NORM; |
|
|
|
S_StartDynamicSound( params ); |
|
|
|
// get system time |
|
time = Plat_MSTime(); |
|
|
|
// paint a boatload of sound |
|
|
|
MIX_PaintChannels( g_paintedtime + 512*count, s_bIsListenerUnderwater ); |
|
|
|
// display system time delta |
|
Msg("%d milliseconds \n", Plat_MSTime() - time); |
|
g_paintedtime = psav; |
|
return; |
|
} |
|
|
|
// DEBUG |
|
if ( !TestSoundChar( sound, CHAR_SENTENCE ) ) |
|
{ |
|
// build a fake sentence name, then play the sentence text |
|
|
|
Q_strncpy(sound, "xxtestxx ", sizeof( sound ) ); |
|
Q_strncat(sound, args[1], sizeof( sound ), COPY_ALL_CHARACTERS ); |
|
|
|
int addIndex = g_Sentences.AddToTail(); |
|
sentence_t *pSentence = &g_Sentences[addIndex]; |
|
pSentence->pName = sound; |
|
pSentence->length = 0; |
|
|
|
// insert null terminator after sentence name |
|
sound[8] = 0; |
|
|
|
pSfx = S_PrecacheSound ("!xxtestxx"); |
|
if (!pSfx) |
|
{ |
|
Msg ("S_Say: can't cache %s\n", sound); |
|
return; |
|
} |
|
|
|
StartSoundParams_t params; |
|
params.staticsound = false; |
|
params.soundsource = g_pSoundServices->GetViewEntity(); |
|
params.entchannel = CHAN_REPLACE; |
|
params.pSfx = pSfx; |
|
params.origin = vec3_origin; |
|
params.fvol = 1.0f; |
|
params.soundlevel = SNDLVL_NONE; |
|
params.flags = 0; |
|
params.pitch = PITCH_NORM; |
|
|
|
S_StartDynamicSound ( params ); |
|
|
|
// remove last |
|
g_Sentences.Remove( g_Sentences.Size() - 1 ); |
|
} |
|
else |
|
{ |
|
pSfx = S_FindName(sound, NULL); |
|
if (!pSfx) |
|
{ |
|
Msg ("S_Say: can't find sentence name %s\n", sound); |
|
return; |
|
} |
|
|
|
StartSoundParams_t params; |
|
params.staticsound = false; |
|
params.soundsource = g_pSoundServices->GetViewEntity(); |
|
params.entchannel = CHAN_REPLACE; |
|
params.pSfx = pSfx; |
|
params.origin = vec3_origin; |
|
params.fvol = 1.0f; |
|
params.soundlevel = SNDLVL_NONE; |
|
params.flags = 0; |
|
params.pitch = PITCH_NORM; |
|
|
|
S_StartDynamicSound( params ); |
|
} |
|
} |
|
|
|
|
|
//------------------------------------------------------------------------------ |
|
// |
|
// Sound Mixers |
|
// |
|
// Sound mixers are referenced by name from Soundscapes, and are used to provide |
|
// custom volume control over various sound categories, called 'mix groups' |
|
// |
|
// see scripts/soundmixers.txt for data format |
|
//------------------------------------------------------------------------------ |
|
|
|
#define CMXRGROUPMAX 64 // up to n mixgroups |
|
#define CMXRGROUPRULESMAX (CMXRGROUPMAX + 16) // max number of group rules |
|
#define CMXRSOUNDMIXERSMAX 32 // up to n sound mixers per project |
|
|
|
// mix groups - these equivalent to submixes on an audio mixer |
|
|
|
// list of rules for determining sound membership in mix groups. |
|
// All conditions which are not null are ANDed together |
|
#define CMXRCLASSMAX 16 |
|
#define CMXRNAMEMAX 32 |
|
|
|
struct classlistelem_t |
|
{ |
|
char szclassname[CMXRNAMEMAX]; // name of entities' class, such as CAI_BaseNPC or CHL2_Player |
|
}; |
|
|
|
|
|
struct grouprule_t |
|
{ |
|
char szmixgroup[CMXRNAMEMAX]; // mix group name |
|
int mixgroupid; // mix group unique id |
|
char szdir[CMXRNAMEMAX]; // substring to search for in ch->sfx |
|
int classId; // index of classname |
|
int chantype; // channel type (CHAN_WEAPON, etc) |
|
int soundlevel_min; // min soundlevel |
|
int soundlevel_max; // max soundlevel |
|
|
|
int priority; // 0..100 higher priority sound groups duck all lower pri groups if enabled |
|
int is_ducked; // if 1, sound group is ducked by all higher priority 'causes_duck" sounds |
|
int causes_ducking; // if 1, sound group ducks other 'is_ducked' sounds of lower priority |
|
float duck_target_pct; // if sound group is ducked, target percent of original volume |
|
|
|
float total_vol; // total volume of all sounds in this group, if group can cause ducking |
|
float ducker_threshold; // ducking is caused by this group if total_vol > ducker_threshold |
|
// and causes_ducking is enabled. |
|
float duck_target_vol; // target volume while ducking |
|
float duck_ramp_val; // current value of ramp - moves towards duck_target_vol |
|
}; |
|
|
|
// sound mixer |
|
|
|
struct soundmixer_t |
|
{ |
|
char szsoundmixer[CMXRNAMEMAX]; // name of this soundmixer |
|
float mapMixgroupidToValue[CMXRGROUPMAX]; // sparse array of mix group values for this soundmixer |
|
}; |
|
|
|
int g_mapMixgroupidToGrouprulesid[CMXRGROUPMAX]; // map mixgroupid (one per unique group name) |
|
// back to 1st entry of this name in g_grouprules |
|
|
|
// sound mixer globals |
|
|
|
classlistelem_t g_groupclasslist[CMXRCLASSMAX]; |
|
soundmixer_t g_soundmixers[CMXRSOUNDMIXERSMAX]; // all sound mixers |
|
grouprule_t g_grouprules[CMXRGROUPRULESMAX]; // all rules for determining mix group membership |
|
|
|
|
|
// set current soundmixer index g_isoundmixer, search for match in soundmixers |
|
// Only change current soundmixer if new name is different from current name. |
|
|
|
int g_isoundmixer = -1; // index of current sound mixer |
|
char g_szsoundmixer_cur[64]; // current soundmixer name |
|
|
|
ConVar snd_soundmixer("snd_soundmixer", "Default_Mix"); // current soundmixer name |
|
|
|
|
|
void MXR_SetCurrentSoundMixer( const char *szsoundmixer ) |
|
{ |
|
// if soundmixer name is not different from current name, return |
|
|
|
if ( !Q_stricmp(szsoundmixer, g_szsoundmixer_cur) ) |
|
{ |
|
return; |
|
} |
|
|
|
for (int i = 0; i < g_csoundmixers; i++) |
|
{ |
|
if ( !Q_stricmp(g_soundmixers[i].szsoundmixer, szsoundmixer) ) |
|
{ |
|
g_isoundmixer = i; |
|
|
|
// save new current sound mixer name |
|
V_strcpy_safe(g_szsoundmixer_cur, szsoundmixer); |
|
|
|
return; |
|
} |
|
} |
|
} |
|
|
|
ConVar snd_showclassname("snd_showclassname", "0"); // if 1, show classname of ent making sound |
|
// if 2, show all mixgroup matches |
|
// if 3, show all mixgroup matches with current soundmixer for ent |
|
// get the client class name if an entity was specified |
|
const char *GetClientClassname( SoundSource soundsource ) |
|
{ |
|
IClientEntity *pClientEntity = NULL; |
|
if ( entitylist ) |
|
{ |
|
pClientEntity = entitylist->GetClientEntity( soundsource ); |
|
if ( pClientEntity ) |
|
{ |
|
ClientClass *pClientClass = pClientEntity->GetClientClass(); |
|
// check npc sounds |
|
if ( pClientClass ) |
|
{ |
|
return pClientClass->GetName(); |
|
} |
|
} |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
// builds a cached list of rules that match the directory name on the sound |
|
int MXR_GetMixGroupListFromDirName( const char *pDirname, byte *pList, int listMax ) |
|
{ |
|
// if we call this before the groups are parsed we'll get bad data |
|
Assert(g_cgrouprules>0); |
|
int count = 0; |
|
for ( int i = 0; i < listMax; i++ ) |
|
{ |
|
pList[i] = 255; |
|
} |
|
|
|
for ( int i = 0; i < g_cgrouprules; i++ ) |
|
{ |
|
grouprule_t *prule = &g_grouprules[i]; |
|
if ( prule->szdir[ 0 ] && Q_stristr( pDirname, prule->szdir ) ) |
|
{ |
|
pList[count] = i; |
|
count++; |
|
if ( count >= listMax ) |
|
return count; |
|
} |
|
} |
|
return count; |
|
} |
|
|
|
|
|
// determine which mixgroups sound is in, and save those mixgroupids in sound. |
|
// use current soundmixer indicated with g_isoundmixer, and contents of g_rgpgrouprules. |
|
// Algorithm: |
|
// 1. all conditions in a row are AND conditions, |
|
// 2. all rows sharing the same groupname are OR conditions. |
|
// so - if a sound matches all conditions of a row, it is given that row's mixgroup id |
|
// if a sound doesn't match all conditions of a row, the next row is checked. |
|
|
|
// returns 0, default mixgroup if no match |
|
void MXR_GetMixGroupFromSoundsource( channel_t *pchan, SoundSource soundsource, soundlevel_t soundlevel) |
|
{ |
|
int i; |
|
grouprule_t *prule; |
|
bool fmatch; |
|
bool classMatch[CMXRCLASSMAX]; |
|
|
|
// init all mixgroups for channel |
|
for ( i = 0; i < 8; i++ ) |
|
{ |
|
pchan->mixgroups[i] = -1; |
|
} |
|
|
|
char sndname[MAX_OSPATH]; |
|
Q_strncpy( sndname, pchan->sfx->getname(), sizeof( sndname ) ); |
|
// Use forward slashes here |
|
Q_FixSlashes( sndname, '/' ); |
|
const char *pszclassname = GetClientClassname(soundsource); |
|
|
|
for ( i = 0; i < g_cgroupclass; i++ ) |
|
{ |
|
classMatch[i] = false; |
|
if ( pszclassname && Q_stristr(pszclassname, g_groupclasslist[i].szclassname ) ) |
|
{ |
|
classMatch[i] = true; |
|
} |
|
} |
|
|
|
if ( snd_showclassname.GetInt() == 1) |
|
{ |
|
// utility: show classname of ent making sound |
|
|
|
if (pszclassname) |
|
{ |
|
DevMsg("(%s:%s) \n", pszclassname, sndname); |
|
} |
|
} |
|
|
|
// check all group rules for a match, save |
|
// up to 8 matches in channel mixgroup. |
|
|
|
int cmixgroups = 0; |
|
if (!pchan->sfx->m_bMixGroupsCached) |
|
{ |
|
pchan->sfx->OnNameChanged( pchan->sfx->getname() ); |
|
} |
|
|
|
// since this is a sorted list (in group rule order) we only need to test against the next matching rule |
|
// this avoids a search inside the loop |
|
int currentDirRuleIndex = 0; |
|
int currentDirRule = pchan->sfx->m_mixGroupList[0]; |
|
|
|
for (i = 0; i < g_cgrouprules; i++) |
|
{ |
|
prule = &g_grouprules[i]; |
|
fmatch = true; |
|
|
|
// check directory or name substring |
|
#if _DEBUG |
|
// check dir table is correct in CSfxTable cache |
|
if ( prule->szdir[ 0 ] && Q_stristr( sndname, prule->szdir ) ) |
|
{ |
|
Assert(currentDirRule == i); |
|
} |
|
else |
|
{ |
|
Assert(currentDirRule != i); |
|
} |
|
if ( prule->classId >= 0 ) |
|
{ |
|
// rule has a valid class id and table is correct |
|
Assert(prule->classId < g_cgroupclass); |
|
if ( pszclassname && Q_stristr(pszclassname, g_groupclasslist[prule->classId].szclassname) ) |
|
{ |
|
Assert(classMatch[prule->classId] == true); |
|
} |
|
else |
|
{ |
|
Assert(classMatch[prule->classId] == false); |
|
} |
|
} |
|
#endif |
|
// this is the next matching dir for this sound, no need to search |
|
// becuse the list is sorted and we visit all elements |
|
if ( currentDirRule == i ) |
|
{ |
|
Assert(prule->szdir[0]); |
|
currentDirRuleIndex++; |
|
currentDirRule = 255; |
|
if ( currentDirRuleIndex < pchan->sfx->m_mixGroupCount ) |
|
{ |
|
currentDirRule = pchan->sfx->m_mixGroupList[currentDirRuleIndex]; |
|
} |
|
} |
|
else if ( prule->szdir[ 0 ] ) |
|
{ |
|
fmatch = false; // substring doesn't match, keep looking |
|
} |
|
|
|
// check class name |
|
|
|
if ( fmatch && prule->classId >= 0 ) |
|
{ |
|
fmatch = classMatch[prule->classId]; |
|
} |
|
|
|
// check channel type |
|
|
|
if ( fmatch && prule->chantype >= 0) |
|
{ |
|
if ( pchan->entchannel != prule->chantype ) |
|
fmatch = false; // channel type doesn't match, keep looking |
|
} |
|
|
|
// check sndlvlmin/max |
|
|
|
if ( fmatch && prule->soundlevel_min >= 0) |
|
{ |
|
if ( soundlevel < prule->soundlevel_min ) |
|
fmatch = false; // soundlevel is less than min, keep looking |
|
} |
|
|
|
if ( fmatch && prule->soundlevel_max >= 0) |
|
{ |
|
if ( soundlevel > prule->soundlevel_max ) |
|
fmatch = false; // soundlevel is greater than max, keep looking |
|
} |
|
|
|
if ( fmatch ) |
|
{ |
|
pchan->mixgroups[cmixgroups] = prule->mixgroupid; |
|
cmixgroups++; |
|
if (cmixgroups >= 8) |
|
return; // too many matches, stop looking |
|
} |
|
|
|
if (fmatch && snd_showclassname.GetInt() >= 2) |
|
{ |
|
// show all mixgroups for this sound |
|
if (cmixgroups == 1) |
|
{ |
|
DevMsg("\n%s:%s: ", g_szsoundmixer_cur, sndname); |
|
} |
|
if (prule->szmixgroup[0]) |
|
{ |
|
// int rgmixgroupid[8]; |
|
// for (int i = 0; i < 8; i++) |
|
// rgmixgroupid[i] = -1; |
|
// rgmixgroupid[0] = prule->mixgroupid; |
|
// float vol = MXR_GetVolFromMixGroup( rgmixgroupid ); |
|
// DevMsg("%s(%1.2f) ", prule->szmixgroup, vol); |
|
DevMsg("%s ", prule->szmixgroup); |
|
} |
|
} |
|
} |
|
} |
|
|
|
struct debug_showvols_t |
|
{ |
|
char *psz; // group name |
|
int mixgroupid; // groupid |
|
float vol; // group volume |
|
float totalvol; // total volume of all sounds playing in this group |
|
}; |
|
|
|
|
|
// display routine for MXR_DebugShowMixVolumes |
|
|
|
#define MXR_DEBUG_INCY (1.0/40.0) // vertical text spacing |
|
#define MXR_DEBUG_GREENSTART 0.3 // start position on screen of bar |
|
|
|
#define MXR_DEBUG_MAXVOL 1.0 // max volume scale |
|
#define MXR_DEBUG_REDLIMIT 1.0 // volume limit into yellow |
|
#define MXR_DEBUG_YELLOWLIMIT 0.7 // volume limit into red |
|
|
|
#define MXR_DEBUG_VOLSCALE 48 // length of graph in characters |
|
#define MXR_DEBUG_CHAR '-' // bar character |
|
|
|
extern ConVar dsp_volume; |
|
int g_debug_mxr_displaycount = 0; |
|
|
|
void MXR_DebugGraphMixVolumes( debug_showvols_t *groupvols, int cgroups) |
|
{ |
|
float flXpos, flYpos, flXposBar, duration; |
|
int r,g,b,a; |
|
int rb, gb, bb, ab; |
|
flXpos = 0; |
|
flYpos = 0; |
|
char text[128]; |
|
char bartext[MXR_DEBUG_VOLSCALE*3]; |
|
|
|
duration = 0.01; |
|
|
|
g_debug_mxr_displaycount++; |
|
|
|
if (!(g_debug_mxr_displaycount % 10)) |
|
return; // only display every 10 frames |
|
|
|
|
|
r = 96; g = 86; b = 226; a = 255; ab = 255; |
|
|
|
// show volume, dsp_volume |
|
|
|
Q_snprintf( text, 128, "Game Volume: %1.2f", volume.GetFloat()); |
|
CDebugOverlay::AddScreenTextOverlay(flXpos, flYpos, duration, r, g, b,a, text); |
|
flYpos += MXR_DEBUG_INCY; |
|
|
|
Q_snprintf( text, 128, "DSP Volume: %1.2f", dsp_volume.GetFloat()); |
|
CDebugOverlay::AddScreenTextOverlay(flXpos, flYpos, duration, r, g, b,a, text); |
|
flYpos += MXR_DEBUG_INCY; |
|
|
|
for (int i = 0; i < cgroups; i++) |
|
{ |
|
// r += 64; g += 64; b += 16; |
|
|
|
r = r % 255; g = g % 255; b = b % 255; |
|
|
|
Q_snprintf( text, 128, "%s: %1.2f (%1.2f)", groupvols[i].psz, |
|
groupvols[i].vol * g_DuckScale, groupvols[i].totalvol * g_DuckScale); |
|
|
|
CDebugOverlay::AddScreenTextOverlay(flXpos, flYpos, duration, r, g, b,a, text); |
|
|
|
// draw volume bar graph |
|
|
|
float vol = (groupvols[i].totalvol * g_DuckScale) / MXR_DEBUG_MAXVOL; |
|
|
|
// draw first 70% green |
|
float vol1 = 0.0; |
|
float vol2 = 0.0; |
|
float vol3 = 0.0; |
|
int cbars; |
|
|
|
vol1 = clamp(vol, 0.0f, 0.7f); |
|
vol2 = clamp(vol, 0.0f, 0.95f); |
|
vol3 = vol; |
|
|
|
flXposBar = flXpos + MXR_DEBUG_GREENSTART; |
|
|
|
if (vol1 > 0.0) |
|
{ |
|
//flXposBar = flXpos + MXR_DEBUG_GREENSTART; |
|
|
|
rb = 0; gb= 255; bb = 0; // green bar |
|
Q_memset(bartext, 0, sizeof(bartext)); |
|
|
|
cbars = (int)((float)vol1 * (float)MXR_DEBUG_VOLSCALE); |
|
cbars = clamp(cbars, 0, MXR_DEBUG_VOLSCALE*3-1); |
|
Q_memset(bartext, MXR_DEBUG_CHAR, cbars); |
|
|
|
CDebugOverlay::AddScreenTextOverlay(flXposBar, flYpos, duration, rb, gb, bb,ab, bartext); |
|
} |
|
|
|
|
|
// yellow bar |
|
if (vol2 > MXR_DEBUG_YELLOWLIMIT) |
|
{ |
|
rb = 255; gb = 255; bb = 0; |
|
Q_memset(bartext, 0, sizeof(bartext)); |
|
|
|
cbars = (int)((float)vol2 * (float)MXR_DEBUG_VOLSCALE); |
|
cbars = clamp(cbars, 0, MXR_DEBUG_VOLSCALE*3-1); |
|
Q_memset(bartext, MXR_DEBUG_CHAR, cbars); |
|
|
|
CDebugOverlay::AddScreenTextOverlay(flXposBar, flYpos, duration, rb, gb, bb,ab, bartext); |
|
} |
|
|
|
// red bar |
|
if (vol3 > MXR_DEBUG_REDLIMIT) |
|
{ |
|
//flXposBar = flXpos + MXR_DEBUG_REDSTART; |
|
rb = 255; gb = 0; bb = 0; |
|
Q_memset(bartext, 0, sizeof(bartext)); |
|
|
|
cbars = (int)((float)vol3 * (float)MXR_DEBUG_VOLSCALE); |
|
cbars = clamp(cbars, 0, MXR_DEBUG_VOLSCALE*3-1); |
|
Q_memset(bartext, MXR_DEBUG_CHAR, cbars); |
|
|
|
CDebugOverlay::AddScreenTextOverlay(flXposBar, flYpos, duration, rb, gb, bb,ab, bartext); |
|
} |
|
|
|
flYpos += MXR_DEBUG_INCY; |
|
} |
|
} |
|
|
|
ConVar snd_disable_mixer_duck("snd_disable_mixer_duck", "0"); // if 1, soundmixer ducking is disabled |
|
|
|
// given mix group id, return current duck volume |
|
|
|
float MXR_GetDuckVolume( int mixgroupid ) |
|
{ |
|
|
|
if ( snd_disable_mixer_duck.GetInt() ) |
|
return 1.0; |
|
|
|
Assert ( mixgroupid < g_cgrouprules ); |
|
|
|
int grouprulesid = g_mapMixgroupidToGrouprulesid[mixgroupid]; |
|
|
|
// if this mixgroup is not ducked, return 1.0 |
|
|
|
if ( !g_grouprules[grouprulesid].is_ducked ) |
|
return 1.0; |
|
|
|
// return current duck value for this group, scaled by current fade in/out ramp |
|
|
|
return g_grouprules[grouprulesid].duck_ramp_val; |
|
|
|
} |
|
|
|
#define SND_DUCKER_UPDATETIME 0.1 // seconds to wait between ducker updates |
|
|
|
double g_mxr_ducktime = 0.0; // time of last update to ducker |
|
|
|
// Get total volume currently playing in all groups, |
|
// process duck volumes for all groups |
|
// Call once per frame - updates occur at 10hz |
|
|
|
void MXR_UpdateAllDuckerVolumes( void ) |
|
{ |
|
if ( snd_disable_mixer_duck.GetInt() ) |
|
return; |
|
|
|
// check timer since last update, only update at 10hz |
|
|
|
int i; |
|
double dtime = g_pSoundServices->GetHostTime(); |
|
|
|
// don't update until timer expires |
|
|
|
if (fabs(dtime - g_mxr_ducktime) < SND_DUCKER_UPDATETIME) |
|
return; |
|
|
|
g_mxr_ducktime = dtime; |
|
|
|
// clear out all total volume values for groups |
|
|
|
for ( i = 0; i < g_cgrouprules; i++) |
|
g_grouprules[i].total_vol = 0.0; |
|
|
|
// for every channel in a mix group which can cause ducking: |
|
// get total volume, store total in grouprule: |
|
|
|
CChannelList list; |
|
int ch_idx; |
|
|
|
channel_t *pchan; |
|
bool b_found_ducked_channel = false; |
|
|
|
g_ActiveChannels.GetActiveChannels( list ); |
|
|
|
for ( i = 0; i < list.Count(); i++ ) |
|
{ |
|
ch_idx = list.GetChannelIndex(i); |
|
pchan = &channels[ch_idx]; |
|
|
|
if (pchan->last_vol > 0.0) |
|
{ |
|
// account for all mix groups this channel belongs to... |
|
|
|
for (int j = 0; j < 8; j++) |
|
{ |
|
int imixgroup = pchan->mixgroups[j]; |
|
|
|
if (imixgroup < 0) |
|
continue; |
|
|
|
int grouprulesid = g_mapMixgroupidToGrouprulesid[imixgroup]; |
|
|
|
if (g_grouprules[grouprulesid].causes_ducking) |
|
g_grouprules[grouprulesid].total_vol += pchan->last_vol; |
|
|
|
if (g_grouprules[grouprulesid].is_ducked) |
|
b_found_ducked_channel = true; |
|
} |
|
} |
|
} |
|
|
|
// if no channels playing which may be ducked, do nothing |
|
|
|
if ( !b_found_ducked_channel ) |
|
return; |
|
|
|
// for all groups that can be ducked: |
|
// see if a higher priority sound group has a volume > threshold, |
|
// if so, then duck this group by setting duck_target_vol to duck_target_pct. |
|
// if no sound group is causing ducking in this group, reset duck_target_vol to 1.0 |
|
|
|
for (i = 0; i < g_cgrouprules; i++) |
|
{ |
|
if (g_grouprules[i].is_ducked) |
|
{ |
|
int priority = g_grouprules[i].priority; |
|
|
|
float duck_volume = 1.0; // clear to 1.0 if no channel causing ducking |
|
|
|
// make sure we interact appropriately with global voice ducking... |
|
// if global voice ducking is active, skip sound group ducking and just set duck_volume target to 1.0 |
|
|
|
if ( g_DuckScale >= 1.0 ) |
|
{ |
|
// check all sound groups for higher priority duck trigger |
|
|
|
for (int j = 0; j < g_cgrouprules; j++) |
|
{ |
|
if (g_grouprules[j].priority > priority && |
|
g_grouprules[j].causes_ducking && |
|
g_grouprules[j].total_vol > g_grouprules[j].ducker_threshold) |
|
{ |
|
// a higher priority group is causing this group to be ducked |
|
// set duck volume target to the ducked group's duck target percent |
|
// and break |
|
|
|
duck_volume = g_grouprules[i].duck_target_pct; |
|
|
|
// UNDONE: to prevent edge condition caused by crossing threshold, may need to have secondary |
|
// UNDONE: timer which allows ducking at 0.2 hz |
|
|
|
break; |
|
} |
|
} |
|
} |
|
|
|
g_grouprules[i].duck_target_vol = duck_volume; |
|
} |
|
} |
|
|
|
// update all ducker ramps if current duck value is not target |
|
// if ramp is greater than duck_volume, approach at 'attack rate' |
|
// if ramp is less than duck_volume, approach at 'decay rate' |
|
|
|
for (i = 0; i < g_cgrouprules; i++) |
|
{ |
|
float target = g_grouprules[i].duck_target_vol; |
|
float current = g_grouprules[i].duck_ramp_val; |
|
|
|
if (g_grouprules[i].is_ducked && (current != target)) |
|
{ |
|
|
|
float ramptime = target < current ? snd_duckerattacktime.GetFloat() : snd_duckerreleasetime.GetFloat(); |
|
|
|
// delta is volume change per update (we can do this |
|
// since we run at an approximate fixed update rate of 10hz) |
|
|
|
float delta = (1.0 - g_grouprules[i].duck_target_pct); |
|
|
|
delta *= ( SND_DUCKER_UPDATETIME / ramptime ); |
|
|
|
if (current > target) |
|
delta = -delta; |
|
|
|
// update ramps |
|
|
|
current += delta; |
|
|
|
if (current < target && delta < 0) |
|
current = target; |
|
if (current > target && delta > 0) |
|
current = target; |
|
|
|
g_grouprules[i].duck_ramp_val = current; |
|
} |
|
} |
|
|
|
} |
|
|
|
ConVar snd_showmixer("snd_showmixer", "0"); // set to 1 to show mixer every frame |
|
|
|
// show the current soundmixer output |
|
|
|
void MXR_DebugShowMixVolumes( void ) |
|
{ |
|
if (snd_showmixer.GetInt() == 0) |
|
return; |
|
|
|
// for the current soundmixer: |
|
// make a totalvolume bucket for each mixgroup type in the soundmixer. |
|
// for every active channel, add its spatialized volume to |
|
// totalvolume bucket for that channel's selected mixgroup |
|
|
|
// display all mixgroup/volume/totalvolume values as horizontal bars |
|
|
|
debug_showvols_t groupvols[CMXRGROUPMAX]; |
|
|
|
int i; |
|
int cgroups = 0; |
|
|
|
if (g_isoundmixer < 0) |
|
{ |
|
DevMsg("No sound mixer selected!"); |
|
return; |
|
} |
|
|
|
soundmixer_t *pmixer = &g_soundmixers[g_isoundmixer]; |
|
|
|
// for every entry in mapMixgroupidToValue which is not -1, |
|
// set up groupvols |
|
|
|
for (i = 0; i < CMXRGROUPMAX; i++) |
|
{ |
|
if (pmixer->mapMixgroupidToValue[i] >= 0) |
|
{ |
|
groupvols[cgroups].mixgroupid = i; |
|
groupvols[cgroups].psz = MXR_GetGroupnameFromId( i ); |
|
groupvols[cgroups].totalvol = 0.0; |
|
groupvols[cgroups].vol = pmixer->mapMixgroupidToValue[i]; |
|
cgroups++; |
|
} |
|
} |
|
|
|
// for every active channel, get its volume and |
|
// the selected mixgroupid, add to groupvols totalvol |
|
|
|
CChannelList list; |
|
int ch_idx; |
|
channel_t *pchan; |
|
|
|
g_ActiveChannels.GetActiveChannels( list ); |
|
|
|
for ( i = 0; i < list.Count(); i++ ) |
|
{ |
|
ch_idx = list.GetChannelIndex(i); |
|
pchan = &channels[ch_idx]; |
|
if (pchan->last_vol > 0.0) |
|
{ |
|
// find entry in groupvols |
|
for (int j = 0; j < CMXRGROUPMAX; j++) |
|
{ |
|
if (pchan->last_mixgroupid == groupvols[j].mixgroupid) |
|
{ |
|
groupvols[j].totalvol += pchan->last_vol; |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
// groupvols is now fully initialized - just display it |
|
|
|
MXR_DebugGraphMixVolumes( groupvols, cgroups); |
|
} |
|
|
|
#ifdef _DEBUG |
|
|
|
// set the named mixgroup volume to vol for the current soundmixer |
|
static void MXR_DebugSetMixGroupVolume( const CCommand &args ) |
|
{ |
|
if ( args.ArgC() != 3 ) |
|
{ |
|
DevMsg("Parameters: mix group name, volume"); |
|
return; |
|
} |
|
|
|
const char *szgroupname = args[1]; |
|
float vol = atof( args[2] ); |
|
|
|
int imixgroup = MXR_GetMixgroupFromName( szgroupname ); |
|
|
|
if ( g_isoundmixer < 0 ) |
|
return; |
|
|
|
soundmixer_t *pmixer = &g_soundmixers[g_isoundmixer]; |
|
|
|
pmixer->mapMixgroupidToValue[imixgroup] = vol; |
|
} |
|
|
|
#endif //_DEBUG |
|
|
|
// given array of groupids (ie: the sound is in these groups), |
|
// return a mix volume. |
|
|
|
// return first mixgroup id in the provided array |
|
// which maps to a non -1 volume value for this |
|
// sound mixer |
|
|
|
float MXR_GetVolFromMixGroup( int rgmixgroupid[8], int *plast_mixgroupid ) |
|
{ |
|
|
|
// if no soundmixer currently set, return 1.0 volume |
|
|
|
if (g_isoundmixer < 0) |
|
{ |
|
*plast_mixgroupid = 0; |
|
return 1.0; |
|
} |
|
|
|
float duckgain = 1.0; |
|
|
|
if (g_csoundmixers) |
|
{ |
|
soundmixer_t *pmixer = &g_soundmixers[g_isoundmixer]; |
|
|
|
if (pmixer) |
|
{ |
|
// search mixgroupid array, return first match (non -1) |
|
|
|
for (int i = 0; i < 8; i++) |
|
{ |
|
int imixgroup = rgmixgroupid[i]; |
|
|
|
if (imixgroup < 0) |
|
continue; |
|
|
|
// save lowest duck gain value for any of the mix groups this sound is in |
|
|
|
float duckgain_new = MXR_GetDuckVolume( imixgroup ); |
|
|
|
if ( duckgain_new < duckgain) |
|
duckgain = duckgain_new; |
|
|
|
Assert(imixgroup < CMXRGROUPMAX); |
|
|
|
// return first mixgroup id in the passed in array |
|
// that maps to a non -1 volume value for this |
|
// sound mixer |
|
|
|
if ( pmixer->mapMixgroupidToValue[imixgroup] >= 0) |
|
{ |
|
*plast_mixgroupid = imixgroup; |
|
|
|
// get gain due to mixer settings |
|
|
|
float gain = pmixer->mapMixgroupidToValue[imixgroup]; |
|
|
|
// modify gain with ducker settings for this group |
|
|
|
return gain * duckgain; |
|
} |
|
} |
|
} |
|
} |
|
|
|
*plast_mixgroupid = 0; |
|
return duckgain; |
|
} |
|
|
|
// get id of mixgroup name |
|
|
|
int MXR_GetMixgroupFromName( const char *pszgroupname ) |
|
{ |
|
// scan group rules for mapping from name to id |
|
if ( !pszgroupname ) |
|
return -1; |
|
|
|
if ( Q_strlen(pszgroupname) == 0 ) |
|
return -1; |
|
|
|
for (int i = 0; i < g_cgrouprules; i++) |
|
{ |
|
if ( !Q_stricmp(g_grouprules[i].szmixgroup, pszgroupname ) ) |
|
return g_grouprules[i].mixgroupid; |
|
} |
|
|
|
return -1; |
|
} |
|
|
|
// get mixgroup name from id |
|
char *MXR_GetGroupnameFromId( int mixgroupid) |
|
{ |
|
// scan group rules for mapping from name to id |
|
if (mixgroupid < 0) |
|
return NULL; |
|
|
|
for (int i = 0; i < g_cgrouprules; i++) |
|
{ |
|
if ( g_grouprules[i].mixgroupid == mixgroupid) |
|
return g_grouprules[i].szmixgroup; |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
// assign a unique mixgroup id to each unique named mix group |
|
// within grouprules. Note: all mixgroupids in grouprules must be -1 |
|
// when this routine starts. |
|
|
|
void MXR_AssignGroupIds( void ) |
|
{ |
|
int cmixgroupid = 0; |
|
|
|
for (int i = 0; i < g_cgrouprules; i++) |
|
{ |
|
int mixgroupid = MXR_GetMixgroupFromName( g_grouprules[i].szmixgroup ); |
|
|
|
if (mixgroupid == -1) |
|
{ |
|
// groupname is not yet assigned, provide a unique mixgroupid. |
|
|
|
g_grouprules[i].mixgroupid = cmixgroupid; |
|
|
|
// save reverse mapping, from mixgroupid to the first grouprules entry for this name |
|
|
|
g_mapMixgroupidToGrouprulesid[cmixgroupid] = i; |
|
|
|
cmixgroupid++; |
|
} |
|
} |
|
} |
|
|
|
int MXR_AddClassname( const char *pName ) |
|
{ |
|
char szclassname[CMXRNAMEMAX]; |
|
Q_strncpy( szclassname, pName, CMXRNAMEMAX ); |
|
for ( int i = 0; i < g_cgroupclass; i++ ) |
|
{ |
|
if ( !Q_stricmp( szclassname, g_groupclasslist[i].szclassname ) ) |
|
return i; |
|
} |
|
if ( g_cgroupclass >= CMXRCLASSMAX ) |
|
{ |
|
Assert(g_cgroupclass < CMXRCLASSMAX); |
|
return -1; |
|
} |
|
Q_memcpy(g_groupclasslist[g_cgroupclass].szclassname, pName, min((size_t)CMXRNAMEMAX-1, strlen(pName))); |
|
g_cgroupclass++; |
|
return g_cgroupclass-1; |
|
} |
|
|
|
#define CHAR_LEFT_PAREN '{' |
|
#define CHAR_RIGHT_PAREN '}' |
|
|
|
// load group rules and sound mixers from file |
|
|
|
bool MXR_LoadAllSoundMixers( void ) |
|
{ |
|
// init soundmixer globals |
|
|
|
g_isoundmixer = -1; |
|
g_szsoundmixer_cur[0] = 0; |
|
|
|
g_csoundmixers = 0; // total number of soundmixers found |
|
g_cgrouprules = 0; // total number of group rules found |
|
|
|
Q_memset(g_soundmixers, 0, sizeof(g_soundmixers)); |
|
Q_memset(g_grouprules, 0, sizeof(g_grouprules)); |
|
|
|
// load file |
|
|
|
// build rules |
|
|
|
// build array of sound mixers |
|
|
|
char szFile[MAX_OSPATH]; |
|
const char *pstart; |
|
bool bResult = false; |
|
char *pbuffer; |
|
|
|
Q_snprintf( szFile, sizeof( szFile ), "scripts/soundmixers.txt" ); |
|
|
|
pbuffer = (char *)COM_LoadFile( szFile, 5, NULL ); // Use malloc - free at end of this routine |
|
if ( !pbuffer ) |
|
{ |
|
Error( "MXR_LoadAllSoundMixers: unable to open '%s'\n", szFile ); |
|
return bResult; |
|
} |
|
|
|
pstart = pbuffer; |
|
|
|
// first pass: load g_grouprules[] |
|
|
|
// starting at top of file, |
|
// scan for first '{', skipping all comment lines |
|
// get strings for: groupname, directory, classname, chan, sndlvl_min, sndlvl_max |
|
// convert chan to CHAN_ lookup |
|
// convert sndlvl_min, sndl_max to ints |
|
// store all in g_grouprules, update g_cgrouprules; |
|
// get next line |
|
// when hit '}' we're done with grouprules |
|
|
|
// check for first CHAR_LEFT_PAREN |
|
|
|
while (1) |
|
{ |
|
pstart = COM_Parse( pstart ); |
|
|
|
if ( strlen(com_token) <= 0) |
|
break; // eof |
|
|
|
if ( com_token[0] != CHAR_LEFT_PAREN ) |
|
continue; |
|
|
|
break; |
|
} |
|
|
|
while (1) |
|
{ |
|
pstart = COM_Parse( pstart ); |
|
if (com_token[0] == CHAR_RIGHT_PAREN) |
|
break; |
|
|
|
grouprule_t *pgroup = &g_grouprules[g_cgrouprules]; |
|
|
|
// copy mixgroup name, directory, classname |
|
// if no value specified, set to 0 length string |
|
|
|
if (com_token[0]) |
|
Q_memcpy(pgroup->szmixgroup, com_token, min((size_t)CMXRNAMEMAX-1, strlen(com_token))); |
|
|
|
pstart = COM_Parse( pstart ); |
|
if (com_token[0]) |
|
Q_memcpy(pgroup->szdir, com_token, min((size_t)CMXRNAMEMAX-1, strlen(com_token))); |
|
|
|
pgroup->classId = -1; |
|
pstart = COM_Parse( pstart ); |
|
if (com_token[0]) |
|
{ |
|
pgroup->classId = MXR_AddClassname( com_token ); |
|
} |
|
|
|
// make sure all copied strings are null terminated |
|
pgroup->szmixgroup[CMXRNAMEMAX-1] = 0; |
|
pgroup->szdir[CMXRNAMEMAX-1] = 0; |
|
|
|
// lookup chan |
|
pstart = COM_Parse( pstart ); |
|
if (com_token[0]) |
|
{ |
|
if (!Q_stricmp(com_token, "CHAN_STATIC")) |
|
pgroup->chantype = CHAN_STATIC; |
|
else if (!Q_stricmp(com_token, "CHAN_WEAPON")) |
|
pgroup->chantype = CHAN_WEAPON; |
|
else if (!Q_stricmp(com_token, "CHAN_VOICE")) |
|
pgroup->chantype = CHAN_VOICE; |
|
else if (!Q_stricmp(com_token, "CHAN_VOICE2")) |
|
pgroup->chantype = CHAN_VOICE2; |
|
else if (!Q_stricmp(com_token, "CHAN_BODY")) |
|
pgroup->chantype = CHAN_BODY; |
|
else if (!Q_stricmp(com_token, "CHAN_ITEM")) |
|
pgroup->chantype = CHAN_ITEM; |
|
} |
|
else |
|
pgroup->chantype = -1; |
|
|
|
// get sndlvls |
|
|
|
pstart = COM_Parse( pstart ); |
|
if (com_token[0]) |
|
pgroup->soundlevel_min = atoi(com_token); |
|
else |
|
pgroup->soundlevel_min = -1; |
|
|
|
pstart = COM_Parse( pstart ); |
|
if (com_token[0]) |
|
pgroup->soundlevel_max = atoi(com_token); |
|
else |
|
pgroup->soundlevel_max = -1; |
|
|
|
// get duck priority, IsDucked, Causes_ducking, duck_target_pct |
|
|
|
pstart = COM_Parse( pstart ); |
|
if (com_token[0]) |
|
pgroup->priority = atoi(com_token); |
|
else |
|
pgroup->priority = 50; |
|
|
|
pstart = COM_Parse( pstart ); |
|
if (com_token[0]) |
|
pgroup->is_ducked = atoi(com_token); |
|
else |
|
pgroup->is_ducked = 0; |
|
|
|
pstart = COM_Parse( pstart ); |
|
if (com_token[0]) |
|
pgroup->causes_ducking = atoi(com_token); |
|
else |
|
pgroup->causes_ducking = 0; |
|
|
|
pstart = COM_Parse( pstart ); |
|
if (com_token[0]) |
|
pgroup->duck_target_pct = ((float)(atoi(com_token))) / 100.0f; |
|
else |
|
pgroup->duck_target_pct = 0.5f; |
|
|
|
pstart = COM_Parse( pstart ); |
|
if (com_token[0]) |
|
pgroup->ducker_threshold = ((float)(atoi(com_token))) / 100.0f; |
|
else |
|
pgroup->ducker_threshold = 0.5f; |
|
|
|
pgroup->duck_ramp_val = 1.0; |
|
pgroup->duck_target_vol = 1.0; |
|
pgroup->total_vol = 0.0; |
|
|
|
// set mixgroup id to -1 |
|
pgroup->mixgroupid = -1; |
|
|
|
// update rule count |
|
|
|
g_cgrouprules++; |
|
|
|
if (g_cgrouprules >= CMXRGROUPRULESMAX) |
|
{ |
|
// UNDONE: error! too many rules |
|
break; |
|
} |
|
} |
|
|
|
// now process all groupids in groups, such that |
|
// each mixgroup gets a unique id. |
|
|
|
MXR_AssignGroupIds(); |
|
|
|
// now load g_soundmixers |
|
|
|
// while not at end of file... |
|
// scan for "<name>", if found save as new soundmixer name |
|
// while not '}' |
|
// scan for "<name>", save as groupname |
|
// scan for "<num>", save as mix value |
|
|
|
while(1) |
|
{ |
|
pstart = COM_Parse( pstart ); |
|
|
|
if ( strlen(com_token) <= 0) |
|
break; // eof |
|
|
|
// save name in soundmixer |
|
|
|
soundmixer_t *pmixer = &g_soundmixers[g_csoundmixers]; |
|
|
|
Q_memcpy(pmixer->szsoundmixer, com_token, min((size_t)CMXRNAMEMAX-1, strlen(com_token))); |
|
|
|
// init all mixer values to -1. |
|
|
|
for (int j = 0; j < CMXRGROUPMAX; j++) |
|
{ |
|
pmixer->mapMixgroupidToValue[j] = -1.0; |
|
} |
|
|
|
// load all groupnames for this soundmixer |
|
|
|
while (1) |
|
{ |
|
pstart = COM_Parse( pstart ); |
|
|
|
if (com_token[0] == CHAR_LEFT_PAREN) |
|
continue; // skip { |
|
|
|
if (com_token[0] == CHAR_RIGHT_PAREN) |
|
break; // finished with this sounmixer |
|
|
|
// lookup mixgroupid for groupname |
|
int mixgroupid = MXR_GetMixgroupFromName( com_token ); |
|
float value; |
|
|
|
// get mix value |
|
pstart = COM_Parse( pstart ); |
|
value = atof( com_token ); |
|
|
|
// store value for mixgroupid |
|
Assert(mixgroupid <= CMXRGROUPMAX); |
|
|
|
pmixer->mapMixgroupidToValue[mixgroupid] = value; |
|
} |
|
|
|
g_csoundmixers++; |
|
if (g_csoundmixers >= CMXRSOUNDMIXERSMAX) |
|
{ |
|
// UNDONE: error! to many sound mixers |
|
break; |
|
} |
|
} |
|
|
|
bResult = true; |
|
|
|
// loadmxr_exit: |
|
free( pbuffer ); |
|
return bResult; |
|
} |
|
|
|
void MXR_ReleaseMemory( void ) |
|
{ |
|
// free all resources |
|
} |
|
|
|
float S_GetMono16Samples( const char *pszName, CUtlVector< short >& sampleList ) |
|
{ |
|
CSfxTable *pSfx = S_PrecacheSound( PSkipSoundChars( pszName ) ); |
|
if ( !pSfx ) |
|
return 0.0f; |
|
|
|
CAudioSource *pWave = pSfx->pSource; |
|
if ( !pWave ) |
|
return 0.0f; |
|
|
|
int nType = pWave->GetType(); |
|
if ( nType != CAudioSource::AUDIO_SOURCE_WAV ) |
|
return 0.0f; |
|
|
|
CAudioMixer *pMixer = pWave->CreateMixer(); |
|
if ( !pMixer ) |
|
return 0.0f; |
|
|
|
float duration = AudioSource_GetSoundDuration( pSfx ); |
|
|
|
// Determine start/stop positions |
|
int totalsamples = (int)( duration * pWave->SampleRate() ); |
|
if ( totalsamples <= 0 ) |
|
return 0; |
|
|
|
bool bStereo = pWave->IsStereoWav(); |
|
int mix_sample_size = pMixer->GetMixSampleSize(); |
|
int nNumChannels = bStereo ? 2 : 1; |
|
|
|
char *pData = NULL; |
|
|
|
int pos = 0; |
|
int remaining = totalsamples; |
|
while ( remaining > 0 ) |
|
{ |
|
int blockSize = min( remaining, 1000 ); |
|
|
|
char copyBuf[AUDIOSOURCE_COPYBUF_SIZE]; |
|
int copied = pWave->GetOutputData( (void **)&pData, pos, blockSize, copyBuf ); |
|
if ( !copied ) |
|
{ |
|
break; |
|
} |
|
|
|
remaining -= copied; |
|
pos += copied; |
|
|
|
// Now get samples out of output data |
|
switch ( nNumChannels ) |
|
{ |
|
default: |
|
case 1: |
|
{ |
|
for ( int i = 0; i < copied; ++i ) |
|
{ |
|
int offset = i * mix_sample_size; |
|
|
|
short sample = 0; |
|
if ( mix_sample_size == 1 ) |
|
{ |
|
char s = *( char * )( pData + offset ); |
|
// Upscale it to fit into a short |
|
sample = s << 8; |
|
} |
|
else if ( mix_sample_size == 2 ) |
|
{ |
|
sample = *( short * )( pData + offset ); |
|
} |
|
else if ( mix_sample_size == 4 ) |
|
{ |
|
// Not likely to have 4 bytes mono!!! |
|
Assert( 0 ); |
|
|
|
int s = *( int * )( pData + offset ); |
|
sample = s >> 16; |
|
} |
|
else |
|
{ |
|
Assert( 0 ); |
|
} |
|
|
|
sampleList.AddToTail( sample ); |
|
} |
|
} |
|
break; |
|
|
|
case 2: |
|
{ |
|
for ( int i = 0; i < copied; ++i ) |
|
{ |
|
int offset = i * mix_sample_size; |
|
|
|
short left = 0; |
|
short right = 0; |
|
|
|
if ( mix_sample_size == 1 ) |
|
{ |
|
// Not possible!!!, must be at least 2 bytes!!! |
|
Assert( 0 ); |
|
|
|
char v = *( char * )( pData + offset ); |
|
left = right = ( v << 8 ); |
|
} |
|
else if ( mix_sample_size == 2 ) |
|
{ |
|
// One byte per channel |
|
left = (short)( ( *(char *)( pData + offset ) ) << 8 ); |
|
right = (short)( ( *(char *)( pData + offset + 1 ) ) << 8 ); |
|
} |
|
else if ( mix_sample_size == 4 ) |
|
{ |
|
// 2 bytes per channel |
|
left = *( short * )( pData + offset ); |
|
right = *( short * )( pData + offset + 2 ); |
|
} |
|
else |
|
{ |
|
Assert( 0 ); |
|
} |
|
|
|
short sample = ( left + right ) >> 1; |
|
sampleList.AddToTail( sample ); |
|
} |
|
} |
|
break; |
|
} |
|
} |
|
|
|
delete pMixer; |
|
|
|
return duration; |
|
}
|
|
|