/* =========================================================================== IceTech GPL Source Code Copyright (C) 2026 Justin Marshall This file is part of the IceTech GPL Source Code (?IceTech Source Code?). IceTech Source Code is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. IceTech Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with IceTech Source Code. If not, see . In addition, the IceTech Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the IceTech Source Code. If not, please request a copy in writing from id Software at the address below. If you have questions concerning this license or the applicable additional terms, you may contact in writing Justin Marshall, justinmarshall20@gmail.com =========================================================================== */ #include "precompiled.h" #pragma hdrstop #include "snd_local.h" /* =============== idSoundShader::Init =============== */ void idSoundShader::Init( void ) { desc = ""; errorDuringParse = false; onDemand = false; numEntries = 0; numLeadins = 0; leadinVolume = 0; altSound = NULL; } /* =============== idSoundShader::idSoundShader =============== */ idSoundShader::idSoundShader( void ) { Init(); } /* =============== idSoundShader::~idSoundShader =============== */ idSoundShader::~idSoundShader( void ) { } /* ================= idSoundShader::Size ================= */ size_t idSoundShader::Size( void ) const { return sizeof( idSoundShader ); } /* =============== idSoundShader::idSoundShader::FreeData =============== */ void idSoundShader::FreeData() { numEntries = 0; numLeadins = 0; } /* =================== idSoundShader::SetDefaultText =================== */ bool idSoundShader::SetDefaultText( void ) { idStr wavname; wavname = GetName(); wavname.DefaultFileExtension( ".wav" ); // if the name has .ogg in it, that will stay // if there exists a wav file with the same name if ( 1 ) { //fileSystem->ReadFile( wavname, NULL ) != -1 ) { char generated[2048]; idStr::snPrintf( generated, sizeof( generated ), "sound %s // IMPLICITLY GENERATED\n" "{\n" "%s\n" "}\n", GetName(), wavname.c_str() ); SetText( generated ); return true; } else { return false; } } /* =================== DefaultDefinition =================== */ const char *idSoundShader::DefaultDefinition() const { return "{\n" "\t" "_default.wav\n" "}"; } /* =============== idSoundShader::Parse this is called by the declManager =============== */ bool idSoundShader::Parse( const char *text, const int textLength ) { idLexer src; src.LoadMemory( text, textLength, GetFileName(), GetLineNum() ); src.SetFlags( DECL_LEXER_FLAGS ); src.SkipUntilString( "{" ); // deeper functions can set this, which will cause MakeDefault() to be called at the end errorDuringParse = false; if ( !ParseShader( src ) || errorDuringParse ) { MakeDefault(); return false; } return true; } /* =============== idSoundShader::ParseShader =============== */ /* =============== idSoundShader::ParseShader =============== */ bool idSoundShader::ParseShader(idLexer& src) { int i; idToken token; #ifdef QUAKE4 parms.minDistance = 40.0f; parms.maxDistance = 400.0f; parms.volume = 1.0f; parms.attenuatedVolume = 0.0f; parms.shakes = 0.0f; parms.soundShaderFlags = 0; parms.soundClass = 0; parms.frequencyShift = 1.0f; parms.wetLevel = 0.0f; parms.dryLevel = 0.0f; minFrequencyShift = 1.0f; maxFrequencyShift = 1.0f; noShakes = false; frequentlyUsed = false; #else parms.minDistance = 1; parms.maxDistance = 10; parms.volume = 1; parms.shakes = 0; parms.soundShaderFlags = 0; parms.soundClass = 0; #endif #ifdef PREY parms.subIndex = 0; parms.profanityIndex = -1; parms.profanityDelay = 0.0f; parms.profanityDuration = 0.0f; #endif speakerMask = 0; altSound = NULL; for (i = 0; i < SOUND_MAX_LIST_WAVS; i++) { leadins[i] = NULL; entries[i] = NULL; } numEntries = 0; numLeadins = 0; int maxSamples = idSoundSystemLocal::s_maxSoundsPerShader.GetInteger(); if (com_makingBuild.GetBool() || maxSamples <= 0 || maxSamples > SOUND_MAX_LIST_WAVS) { maxSamples = SOUND_MAX_LIST_WAVS; } while (1) { if (!src.ExpectAnyToken(&token)) { return false; } // end of definition if (token == "}") { break; } // minimum number of sounds else if (!token.Icmp("minSamples")) { #ifdef QUAKE4 const int requestedSamples = src.ParseInt(); if (maxSamples < requestedSamples) { maxSamples = requestedSamples; } if (maxSamples > SOUND_MAX_LIST_WAVS) { maxSamples = SOUND_MAX_LIST_WAVS; } #else maxSamples = idMath::ClampInt(src.ParseInt(), SOUND_MAX_LIST_WAVS, maxSamples); #endif } #ifdef QUAKE4 // Raven: frequency shift range else if (!token.Icmp("frequencyshift")) { minFrequencyShift = src.ParseFloat(); if (!src.ExpectTokenString(",")) { src.FreeSource(); return false; } maxFrequencyShift = src.ParseFloat(); parms.frequencyShift = minFrequencyShift; } #endif // description else if (!token.Icmp("description")) { src.ReadTokenOnLine(&token); desc = token.c_str(); } // mindistance else if (!token.Icmp("mindistance")) { parms.minDistance = src.ParseFloat(); } // maxdistance else if (!token.Icmp("maxdistance")) { parms.maxDistance = src.ParseFloat(); } // shakes screen else if (!token.Icmp("shakes")) { src.ExpectAnyToken(&token); if (token.type == TT_NUMBER) { parms.shakes = token.GetFloatValue(); } else { src.UnreadToken(&token); parms.shakes = 1.0f; } } #ifdef QUAKE4 // Raven: indexed shake amplitude data else if (!token.Icmp("shakeData")) { if (!src.ExpectAnyToken(&token)) { return false; } const int shakeIndex = atoi(token.c_str()); if (!src.ExpectAnyToken(&token)) { return false; } //SetShakeData(shakeIndex, token.c_str()); } #endif // reverb -- parsed but unsupported else if (!token.Icmp("reverb")) { src.ParseFloat(); if (!src.ExpectTokenString(",")) { src.FreeSource(); return false; } src.ParseFloat(); } // volume else if (!token.Icmp("volume")) { parms.volume = src.ParseFloat(); } #ifdef QUAKE4 // Raven: volume specified in decibels, clamped to +10dB else if (!token.Icmp("volumeDb")) { float volumeDb = src.ParseFloat(); if (volumeDb > 10.0f) { common->Warning("Clamping volume of '%s' to +10dB (3 times louder)", GetName()); volumeDb = 10.0f; } parms.volume = idMath::dBToScale(volumeDb); } #endif // leadinVolume is used to allow light breaking leadin sounds to be much louder than the broken loop else if (!token.Icmp("leadinVolume")) { leadinVolume = src.ParseFloat(); } // speaker masks else if (!token.Icmp("mask_center")) { speakerMask |= 1 << SPEAKER_CENTER; } else if (!token.Icmp("mask_left")) { speakerMask |= 1 << SPEAKER_LEFT; } else if (!token.Icmp("mask_right")) { speakerMask |= 1 << SPEAKER_RIGHT; } else if (!token.Icmp("mask_backright")) { speakerMask |= 1 << SPEAKER_BACKRIGHT; } else if (!token.Icmp("mask_backleft")) { speakerMask |= 1 << SPEAKER_BACKLEFT; } else if (!token.Icmp("mask_lfe")) { speakerMask |= 1 << SPEAKER_LFE; } // soundClass else if (!token.Icmp("soundClass")) { #ifdef PREY if (!src.ReadToken(&token)) { src.Warning("Expected classname after soundClass"); return false; } if (!token.Icmp("SC_NORMAL")) { parms.soundClass = SOUNDCLASS_NORMAL; } else if (!token.Icmp("SC_VOICEDUCKER")) { parms.soundClass = SOUNDCLASS_VOICEDUCKER; } else if (!token.Icmp("SC_SPIRIT")) { parms.soundClass = SOUNDCLASS_SPIRITWALK; } else if (!token.Icmp("SC_VOICE")) { parms.soundClass = SOUNDCLASS_VOICE; } else if (!token.Icmp("SC_MUSIC")) { parms.soundClass = SOUNDCLASS_MUSIC; } else { src.Warning("SoundClass takes args of SC_NORMAL, SC_SPIRIT, SC_VOICE, SC_VOICEDUCKER"); return false; } #else parms.soundClass = src.ParseInt(); if (parms.soundClass < 0 || parms.soundClass >= SOUND_MAX_CLASSES) { src.Warning("SoundClass out of range"); return false; } #endif } // altSound else if (!token.Icmp("altSound")) { if (!src.ExpectAnyToken(&token)) { return false; } altSound = declManager->FindSound(token.c_str()); } // ordered -- unsupported else if (!token.Icmp("ordered")) { } // no_dups else if (!token.Icmp("no_dups")) { parms.soundShaderFlags |= SSF_NO_DUPS; } // no_flicker else if (!token.Icmp("no_flicker")) { parms.soundShaderFlags |= SSF_NO_FLICKER; } // plain -- unsupported else if (!token.Icmp("plain")) { } // looping else if (!token.Icmp("looping")) { parms.soundShaderFlags |= SSF_LOOPING; } // no occlusion else if (!token.Icmp("no_occlusion")) { parms.soundShaderFlags |= SSF_NO_OCCLUSION; } #ifdef PREY // HumanHead/Prey: no game portal sound flow else if (!token.Icmp("noportalflow")) { #if GAMEPORTAL_SOUND parms.soundShaderFlags |= SSF_NOPORTALFLOW; #endif } // HumanHead/Prey: no reverb else if (!token.Icmp("noreverb")) { parms.soundShaderFlags |= SSF_NOREVERB; } // HumanHead/Prey: include this shader in voice amplitude queries else if (!token.Icmp("jawflap")) { parms.soundShaderFlags |= SSF_VOICEAMPLITUDE; } // HumanHead/Prey: become omni when inside minDistance else if (!token.Icmp("omniwhenclose")) { parms.soundShaderFlags |= SSF_OMNI_WHEN_CLOSE; } #endif // private else if (!token.Icmp("private")) { parms.soundShaderFlags |= SSF_PRIVATE_SOUND; } // antiPrivate else if (!token.Icmp("antiPrivate")) { parms.soundShaderFlags |= SSF_ANTI_PRIVATE_SOUND; } // once else if (!token.Icmp("playonce")) { parms.soundShaderFlags |= SSF_PLAY_ONCE; } // global else if (!token.Icmp("global")) { parms.soundShaderFlags |= SSF_GLOBAL; } // unclamped else if (!token.Icmp("unclamped")) { parms.soundShaderFlags |= SSF_UNCLAMPED; } // omnidirectional else if (!token.Icmp("omnidirectional")) { parms.soundShaderFlags |= SSF_OMNIDIRECTIONAL; } #ifdef QUAKE4 // Raven: allow doppler pitch shifting else if (!token.Icmp("useDoppler")) { parms.soundShaderFlags |= SSF_USEDOPPLER; } // Raven: don't randomly seek into looping sounds else if (!token.Icmp("noRandomStart")) { parms.soundShaderFlags |= SSF_NO_RANDOMSTART; } // Raven: marks VO directed at player/radio chatter else if (!token.Icmp("voForPlayer")) { parms.soundShaderFlags |= SSF_VO_FOR_PLAYER; } // Raven: disable generated shake data else if (!token.Icmp("no_shakes")) { noShakes = true; } // Raven: force expansion / keep hot else if (!token.Icmp("frequentlyUsed")) { frequentlyUsed = true; } #endif // onDemand can't be a parms, because we must track all references and overrides would confuse it else if (!token.Icmp("onDemand")) { // no longer loading sounds on demand // onDemand = true; } #ifdef PREY // HumanHead/Prey: profanity bleep data else if (!token.Icmp("bleep")) { parms.profanityIndex = src.ParseInt(); parms.profanityDelay = src.ParseFloat(); parms.profanityDuration = src.ParseFloat(); } // HumanHead/Prey: subtitle / subtitlecombat parser else if (!token.Icmpn("subtitle", 8)) { const bool combatSubtitle = (token.Icmpn("subtitlecombat", 14) == 0); const char* numText; if (combatSubtitle) { numText = token.c_str() + 14; } else { numText = token.c_str() + 8; } const int subNum = (numText[0] != '\0') ? atoi(numText) : 1; if (!src.ReadToken(&token)) { src.Warning("Expected time after subtext"); return false; } int subChannel = 0; if (token.c_str()[0] == 'c' || token.c_str()[0] == 'C') { const int channelNumber = atoi(token.c_str() + 1); subChannel = channelNumber - 1; if (subChannel < 0 || subChannel >= MAX_SUBTITLE_CHANNELS) { src.Warning("Value '%i' is out of range for MAX_SUBTITLE_CHANNELS.", channelNumber); return false; } if (!src.ReadToken(&token)) { src.Warning("Expected time after subtitle channel"); return false; } } const float subTime = atof(token.c_str()); if (!src.ReadToken(&token)) { src.Warning("Expected text after subtext and time"); return false; } if (combatSubtitle) { subChannel += 3; } if (parms.subIndex == 0) { parms.subIndex = soundSystemLocal.GetSubtitleIndex(GetName()); } soundSystemLocal.SetSubtitleData(parms.subIndex, subNum, token.c_str(), subTime, subChannel); } #endif // the leadin wave files else if (!token.Icmp("leadin")) { if (!src.ReadToken(&token)) { src.Warning("Expected sound after leadin"); return false; } #ifdef QUAKE4 if (soundSystemLocal.soundCache && numLeadins < maxSamples) { leadins[numLeadins] = soundSystem->FindSample(token.c_str()); numLeadins++; } #else if (soundSystemLocal.soundCache && numLeadins < maxSamples) { #ifdef PREY leadins[numLeadins] = soundSystemLocal.soundCache->FindSound(token.c_str(), onDemand); #else leadins[numLeadins] = soundSystemLocal.soundCache->FindSound(token.c_str(), onDemand); #endif numLeadins++; } #endif } // the regular wave files else if (token.Find(".wav", false) != -1 || token.Find(".ogg", false) != -1) { #ifdef QUAKE4 if (soundSystemLocal.soundCache && numEntries < maxSamples) { token.BackSlashesToSlashes(); entries[numEntries] = soundSystem->FindSample(token.c_str()); if (entries[numEntries]) { numEntries++; } } #else if (soundSystemLocal.soundCache && numEntries < maxSamples) { token.BackSlashesToSlashes(); #ifdef PREY // Prey behavior grabs sys_lang but does not do Doom 3's // sound/vo// replacement lookup before FindSound. idStr lang = cvarSystem->GetCVarString("sys_lang"); #else idStr lang = cvarSystem->GetCVarString("sys_lang"); if (lang.Icmp("english") != 0 && token.Find("sound/vo/", false) >= 0) { idStr work = token; work.ToLower(); work.StripLeading("sound/vo/"); work = va("sound/vo/%s/%s", lang.c_str(), work.c_str()); if (fileSystem->ReadFile(work, NULL, NULL) > 0) { token = work; } else { // also try to find it with the .ogg extension work.SetFileExtension(".ogg"); if (fileSystem->ReadFile(work, NULL, NULL) > 0) { token = work; } } } #endif entries[numEntries] = soundSystemLocal.soundCache->FindSound(token.c_str(), onDemand); numEntries++; } #endif } else { src.Warning("unknown token '%s'", token.c_str()); return false; } } #ifdef QUAKE4 // if (!soundSystem->GetInsideLevelLoad()) { // soundSystem->ValidateSoundShader(this); // } #else if (parms.shakes > 0.0f) { CheckShakesAndOgg(); } #endif return true; } /* =============== idSoundShader::CheckShakesAndOgg =============== */ bool idSoundShader::CheckShakesAndOgg( void ) const { int i; bool ret = false; for ( i = 0; i < numLeadins; i++ ) { if ( leadins[ i ]->objectInfo.wFormatTag == WAVE_FORMAT_TAG_OGG ) { common->Warning( "sound shader '%s' has shakes and uses OGG file '%s'", GetName(), leadins[ i ]->name.c_str() ); ret = true; } } for ( i = 0; i < numEntries; i++ ) { if ( entries[ i ]->objectInfo.wFormatTag == WAVE_FORMAT_TAG_OGG ) { common->Warning( "sound shader '%s' has shakes and uses OGG file '%s'", GetName(), entries[ i ]->name.c_str() ); ret = true; } } return ret; } /* =============== idSoundShader::List =============== */ void idSoundShader::List() const { idStrList shaders; common->Printf( "%4i: %s\n", Index(), GetName() ); if ( idStr::Icmp( GetDescription(), "" ) != 0 ) { common->Printf( " description: %s\n", GetDescription() ); } for( int k = 0; k < numLeadins ; k++ ) { const idSoundSample *objectp = leadins[k]; if ( objectp ) { common->Printf( " %5dms %4dKb %s (LEADIN)\n", soundSystemLocal.SamplesToMilliseconds(objectp->LengthIn44kHzSamples()), (objectp->objectMemSize/1024) ,objectp->name.c_str() ); } } for( int k = 0; k < numEntries; k++ ) { const idSoundSample *objectp = entries[k]; if ( objectp ) { common->Printf( " %5dms %4dKb %s\n", soundSystemLocal.SamplesToMilliseconds(objectp->LengthIn44kHzSamples()), (objectp->objectMemSize/1024) ,objectp->name.c_str() ); } } } /* =============== idSoundShader::GetAltSound =============== */ const idSoundShader *idSoundShader::GetAltSound( void ) const { return altSound; } /* =============== idSoundShader::GetMinDistance =============== */ float idSoundShader::GetMinDistance() const { return parms.minDistance; } /* =============== idSoundShader::GetMaxDistance =============== */ float idSoundShader::GetMaxDistance() const { return parms.maxDistance; } /* =============== idSoundShader::GetDescription =============== */ const char *idSoundShader::GetDescription() const { return desc; } /* =============== idSoundShader::HasDefaultSound =============== */ bool idSoundShader::HasDefaultSound() const { for ( int i = 0; i < numEntries; i++ ) { if ( entries[i] && entries[i]->defaultSound ) { return true; } } return false; } /* =============== idSoundShader::GetParms =============== */ const soundShaderParms_t *idSoundShader::GetParms() const { return &parms; } /* =============== idSoundShader::GetNumSounds =============== */ int idSoundShader::GetNumSounds() const { return numLeadins + numEntries; } /* =============== idSoundShader::GetSound =============== */ const char *idSoundShader::GetSound( int index ) const { if ( index >= 0 ) { if ( index < numLeadins ) { return leadins[index]->name.c_str(); } index -= numLeadins; if ( index < numEntries ) { return entries[index]->name.c_str(); } } return ""; } #ifdef QUAKE4 /* =============== idSoundShader::IsVO_ForPlayer =============== */ bool idSoundShader::IsVO_ForPlayer() const { return (parms.soundShaderFlags & SSF_VO_FOR_PLAYER) != 0; } #endif