From c27af4800bfb59e6cb8d54d1fb021eadf8dcf001 Mon Sep 17 00:00:00 2001 From: arcattack Date: Wed, 15 Jul 2026 22:11:49 -0500 Subject: [PATCH] Audio: fix stuck weapon charge/sustain/loading loops -- maintain GaugeAlarm54 oldState (task #50) WeaponState -> weaponAlarm (mechweap.cpp:149), and the weapon's looping audio (LaserACharge/LaserASustain/MissileLoading, all SF2 sampleModes=1) is STARTED by an AudioStateWatcher on the firing/charging state and STOPPED by an inverse AudioStateTrigger that keys on old_state (== the state being LEFT). The subsystem-state commit fired GaugeAlarm54 watchers on SetLevel but deliberately left levelB (the oldState slot @0x10) untouched to avoid disturbing the weapon's LevelCountB read. Consequence: the audio watcher's StateChanged(oldState,newState) saw a stale oldState, so the inverse/STOP triggers never matched -> the charge/ sustain/loading loops played forever. Fix: SetLevel now sets levelB := level (oldState := currentState) before the change, exactly like StateIndicator::SetState. The stop triggers now match on leaving the state and StopNote the loop. This is also authentic: in the binary the 0x54 alarm +0x10 IS oldState, and the weapon's LevelCountB()/weaponIdle read of +0x10 is that same oldState (the old static-count reading was the reconstruction's accident). weaponIdle is only consumed in the MP replicant record-apply path, so single-player firing is unaffected; the value is now the authentic oldState-based one. Subsystem state audio unaffected (0 skips). Diagnostics: BT_AUDIO_LOG now traces SetupPatch src/file/loop + StopNote. Co-Authored-By: Claude Opus 4.8 (1M context) --- engine/MUNGA_L4/L4AUDLVL.cpp | 5 +++-- game/reconstructed/heat.hpp | 8 +++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/engine/MUNGA_L4/L4AUDLVL.cpp b/engine/MUNGA_L4/L4AUDLVL.cpp index 2c3aaee..53c5c3d 100644 --- a/engine/MUNGA_L4/L4AUDLVL.cpp +++ b/engine/MUNGA_L4/L4AUDLVL.cpp @@ -141,7 +141,7 @@ void if (sourceState == AL_INITIAL) { alSourcei(sourceSet.sources[i],AL_BUFFER,AL_getBuffer(info.bufferIndex)); - { static int s_a=0; if (getenv("BT_AUDIO_LOG") && s_a++<8) DEBUG_STREAM << "[audio] SetupPatch attach bank=" << (int)bankID << " patch=" << (int)patchID << " buf=" << info.bufferIndex << "\n" << std::flush; } + { static int s_a=0; if (getenv("BT_AUDIO_LOG") && s_a++<24) DEBUG_STREAM << "[audio] SetupPatch src=" << sourceSet.sources[i] << " file=" << (info.file?info.file:"?") << " loop=" << (info.loop==ForceStatic?0:1) << "\n" << std::flush; } alSourcei(sourceSet.sources[i],AL_SOURCE_RELATIVE,AL_TRUE); AudioRenderer *render = application->GetAudioRenderer(); @@ -190,7 +190,7 @@ void PatchLevelOfDetail::PlayNote(SourceSet sourceSet) if (state != AL_PLAYING) { alSourcePlay(sourceSet.sources[i]); - { static int s_p=0; if (getenv("BT_AUDIO_LOG") && s_p++<8) DEBUG_STREAM << "[audio] PlayNote alSourcePlay src=" << sourceSet.sources[i] << "\n" << std::flush; } + { static int s_p=0; if (getenv("BT_AUDIO_LOG") && s_p++<30) DEBUG_STREAM << "[audio] PlayNote alSourcePlay src=" << sourceSet.sources[i] << "\n" << std::flush; } } ALenum error = alGetError(); @@ -205,6 +205,7 @@ void PatchLevelOfDetail::PlayNote(SourceSet sourceSet) void PatchLevelOfDetail::StopNote(SourceSet sourceSet) { + { static int s_s=0; if (getenv("BT_AUDIO_LOG") && s_s++<30) DEBUG_STREAM << "[audio] StopNote count=" << sourceSet.count << " src0=" << (sourceSet.count>0?sourceSet.sources[0]:0) << "\n" << std::flush; } if (sourceSet.count >0 && sourceSet.sources != NULL) { alSourceStopv(sourceSet.count, sourceSet.sources); diff --git a/game/reconstructed/heat.hpp b/game/reconstructed/heat.hpp index b1959b0..9228691 100644 --- a/game/reconstructed/heat.hpp +++ b/game/reconstructed/heat.hpp @@ -97,7 +97,13 @@ public: : audioWatcherSocket(0), videoWatcherSocket(0), gaugeWatcherSocket(0) { levelA = levelB = levels; level = 0; } void Initialize(int levels) { levelA = levelB = levels; level = 0; } - void SetLevel(int n) { if (n != level) { level = n; NotifyWatchers(); } } + // StateIndicator::SetState semantics: oldState (levelB@0x10) := currentState + // BEFORE the change, so the audio watcher's StateChanged(oldState,newState) is + // correct -- this is what the inverse/STOP AudioStateTriggers key on (old_state == + // firing/charging state) to StopNote a looping charge/sustain/loading sample when + // the weapon leaves that state. (In the binary levelB@0x10 IS oldState; the + // weapon's LevelCountB() reading @0x10 is that same authentic oldState.) + void SetLevel(int n) { levelB = level; if (n != level) { level = n; NotifyWatchers(); } } int GetLevel() const { return level; } int Level() const { return level; } int LevelCountB() const { return levelB; } // +0x10 (the weapon update-record "reset" source)