Files
BT411/engine/MUNGA/AUDIO.cpp
T
arcattackandClaude Opus 4.8 a11a697824 Audio: footsteps arrive on the FIRST stride -- the 10-20 s warm-up bug
User report: footfalls silent for the first 10-20 s of every mission (all
mechs), then solid.  Root cause was three interlocking layers, each measured
with timestamped traces:

1. The authored footstep volume chain (LocalAcceleration [0,10]->ctl100 +
   LocalVelocity [0,0.6]->ctl101 through authored N=30/N=15
   AudioControlSmoothers, fill 0) hangs off the SOURCE's watcher chain
   (scale watches smoother watches mixer watches source), and an idle
   source's chain executes only at Start attempts -- one smoother sample
   per stride.
2. Each hop is frame-gated (AudioComponent::ExecuteWatchers,
   DefaultAudioFrameDelay), so any burst collapses to one execution.
3. The transient drop gate (vol < 0.3, AUDREND) rejected every Start while
   the smoother average crawled up 1/30th per attempt -> ~25 dropped strides
   before the first audible step, then per-frame execution while playing
   kept it warm forever ("solid after that").

Fixes (engine-level, each documented in place):
- AudioScaleOf<T>::Execute now sends EVERY poll (scales are continuous
  value-feeders; the base bitwise change-gate -- Motion::operator== is
  memcmp -- froze on our deterministic gait math, where the original's
  noisy physics floats never bit-repeated.  Triggers/matchers keep the
  change gate: their semantics are edge-based).
- Component/AudioComponent::PrimeWatchers(passes): recursive, GATE-FREE
  watcher pump; AUDREND runs 30 passes on every transient Start request so
  the authored smoothers evaluate at their true steady state before the
  drop gate reads the volume.
- localAcceleration derives via the binary's exact structure: 15-sample
  ring buffers of the raw position derivative + dt (ctor part_012.c:9836,
  derive :15169-15195), in the PerformAndWatch tail so it runs every frame.
- AttributeWatcherOf::GrabCurrentValue private -> protected (the scale
  override calls it).

Verified (30 s walk from cold start): drops 25 -> 3 (the survivors are
authentic quiet-stride gating: first gentle strides at vol ~0.28 vs the 0.3
gate), footfalls deliver from the first stride, 43 delivered with live
per-stride gain variation.  Diag traces added: [accwatch]/[fsscale]/
[smooth]/[smoothcfg]/[motionscalecfg] + timestamps on DROP/volset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 15:11:17 -05:00

563 lines
15 KiB
C++

#include <cstdlib>
#include "munga.h"
#pragma hdrstop
#include "audio.h"
#include "audrend.h"
#include "app.h"
#include "..\munga_l4\openal\al.h"
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Audio Constants ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const Scalar AudioDoubleTriggerCutoff = 0.2f;
const AudioControlValue MaxAudioVolume = 1.0f;
const AudioControlValue MinAudioVolume = 0.0f;
const AudioControlValue LowAudioVolumeThreshold = 0.3f;
const AudioControlValue MaxAudioBrightness = 1.0f;
const AudioControlValue MinAudioBrightness = 0.0f;
const AudioControlValue MaxAudioAttack = 1.0f;
const AudioControlValue MinAudioAttack = 0.0f;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ AudioHead ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//#############################################################################
//#############################################################################
//
AudioHead::AudioHead():
headEntitySocket(NULL)
{
//
// Start frame counter at 0, other counts should be at
// NullAudioFrameCount
//
audioFrameCount = 0;
//
// Other audio parameters
//
clippingRadius = 100.0f;
distanceBetweenEars = 1.0f;
amplitudeRollOffExponent = 1.0f;
amplitudeRollOffKnee = 10.0f,
amplitudeRollOffDistanceScale = 0.05f;
highFrequencyRollOffExponent = 0.5f;
highFrequencyRollOffKnee = 10.0f;
highFrequencyRollOffDistanceScale = 0.05f;
reverbToDryRatio = 0.1f;
audioSoundSpeed = 345.0f;
audioDopplerConstant = 20.0f;
sourceCompressionExponent = 0.5f;
sourceCompressionScale = 0.5f;
Check_Fpu();
}
//
//#############################################################################
//#############################################################################
//
AudioHead::~AudioHead()
{
Check(this);
Check_Fpu();
}
//
//#############################################################################
//#############################################################################
//
Logical
AudioHead::TestInstance() const
{
Check(&headEntitySocket);
return True;
}
//
//#############################################################################
//#############################################################################
//
void
AudioHead::LinkToEntity(Entity *entity)
{
Check(this);
Check(entity);
//
// Remove existing entity, add new one
//
if (headEntitySocket.GetCurrent() != NULL)
{
headEntitySocket.Remove();
}
headEntitySocket.Add(entity);
// FIDELITY (AUDIO_FIDELITY.md F3/F10): the engine computes the AUTHORED
// distance-attenuation curve (AUDIO.INI amplitude_rolloff knee/exponent ->
// AudioLocation::distanceVolumeScale) and the authored doppler-cents model.
// Disable OpenAL's own models so they can't double-apply / fight them:
// AL_LINEAR_DISTANCE made far battle audio fade to zero on a straight line
// (-6 dB vs authored at 300u), and AL doppler ran with the wrong constants
// AND a sign-inverted velocity feed (approaching sources pitched DOWN).
alDistanceModel(AL_NONE);
alDopplerFactor(0.0f);
#if 0
//
// Make the new clipping sphere
//
clippingSphere.SetCurrentOrigin(entity->localOrigin.linearPosition);
#endif
}
//
//#############################################################################
//#############################################################################
//
LinearMatrix
AudioHead::GetEarToWorld()
{
Check(this);
Entity *entity = headEntitySocket.GetCurrent();
Check(entity);
return entity->localToWorld;
}
//
//#############################################################################
//#############################################################################
//
void
AudioHead::Execute()
{
Check(this);
//
// Increment frame counter
//
// AUDIO CLOCK CALIBRATION FIX: AudioTime consumers (sequence event timing,
// compression curves) assume audioFrameCount advances at the renderer's
// calibrationRate (DefaultRendererRate = 30 frames/sec:
// Seconds_To_Frames = s * rate). The WinTesla port set raw Now().ticks here
// (~hundreds/sec), running sequences ~18x off the authored timing -- the
// AudioControlSequence events carrying the real footstep volumes never
// landed where authored. Convert ticks -> calibrated frames properly.
{
double tps = (double)SystemClock::GetTicksPerSecond();
if (tps <= 0.0) tps = 1000.0; // GetTickCount ms fallback (static not yet measured)
audioFrameCount = (AudioFrameCount)(
(double)Now().ticks
* (double)application->GetAudioRenderer()->GetCalibrationRate()
/ tps);
}
Verify(audioFrameCount < LONG_MAX);
if (getenv("BT_AUDIO_SPATIAL")) { static long s_hx=0; if ((++s_hx % 300)==0)
DEBUG_STREAM << "[audioclock] frame=" << audioFrameCount << " execs=" << s_hx << "\n" << std::flush; }
// (task #50, AUDIO_FIDELITY F13) service the authored release fades
// registered by PatchLevelOfDetail::StopNote (dB-linear note-off ramps).
{
extern void PRESET_serviceReleaseFades(float elapsed_seconds);
static long s_lastFadeTicks = 0;
long now_ticks = Now().ticks;
if (s_lastFadeTicks != 0 && now_ticks > s_lastFadeTicks)
{
double tps = (double)SystemClock::GetTicksPerSecond();
if (tps <= 0.0) tps = 1000.0;
PRESET_serviceReleaseFades((float)((now_ticks - s_lastFadeTicks) / tps));
}
s_lastFadeTicks = now_ticks;
}
//set current listener orientation
Vector3D headVelocity;
headVelocity.MultiplyByInverse(this->GetHeadEntity()->GetWorldLinearVelocity(), this->GetHeadEntity()->localToWorld);
alListener3f(AL_VELOCITY, -headVelocity.x, -headVelocity.y, -headVelocity.z);
// LIVE-PLAYING DUMP (BT_AUDIO_DUMP): once a second list every playing AL
// source with its sample name / gain / pitch / loop -- catches "mystery
// sounds" (wrong sample, wrong pitch, chopped) red-handed.
if (getenv("BT_AUDIO_DUMP")) {
static long s_dumpTick = 0;
if ((++s_dumpTick % 45) == 0) {
extern ALuint *g_buffers; extern int g_numBuffers; extern const char *g_bufferNames[512];
for (ALuint sid = 1; sid <= 40; ++sid) {
if (!alIsSource(sid)) continue;
ALint st = 0; alGetSourcei(sid, AL_SOURCE_STATE, &st);
if (st != AL_PLAYING) continue;
ALint buf = 0, looping = 0; ALfloat gain = 0, pitch = 0;
alGetSourcei(sid, AL_BUFFER, &buf);
alGetSourcei(sid, AL_LOOPING, &looping);
alGetSourcef(sid, AL_GAIN, &gain);
alGetSourcef(sid, AL_PITCH, &pitch);
const char *nm = "?";
for (int b = 0; b < g_numBuffers && b < 512; ++b)
if ((ALint)g_buffers[b] == buf) { nm = g_bufferNames[b] ? g_bufferNames[b] : "?"; break; }
DEBUG_STREAM << "[playing] src=" << sid << " " << nm
<< " gain=" << gain << " pitch=" << pitch
<< " loop=" << looping << "\n" << std::flush;
}
}
}
#if 0
//
// Get the entity
//
Entity *entity = headEntitySocket.GetCurrent();
Check(entity);
//
// Make the new clipping sphere
//
clippingSphere.SetCurrentOrigin(entity->localOrigin.linearPosition);
#endif
}
//
//#############################################################################
//#############################################################################
//
void
AudioHead::DefineClippingSphere(Scalar radius)
{
Check(this);
clippingRadius = radius;
#if 0
//
// Get entity position
//
Entity *entity;
Point3D position(0.0f, 0.0f, 0.0f);
if ((entity = headEntitySocket.GetCurrent()) != NULL)
{
Check(entity);
position = entity->localOrigin.linearPosition;
}
//
// Make the new clipping sphere
//
clippingSphere.SetCurrentOrigin(position);
clippingSphere.SetReferenceRadius(radius);
#endif
}
//
//#############################################################################
//#############################################################################
//
Logical
AudioHead::IsPointClipped(const Point3D &point)
{
Check(this);
Entity
*entity;
Vector3D
difference;
entity = headEntitySocket.GetCurrent();
Check(entity);
difference.Subtract(entity->localOrigin.linearPosition, point);
return difference.LengthSquared() > clippingRadius*clippingRadius;
}
//
//#############################################################################
//#############################################################################
//
void
AudioHead::ControlDopplerEffect(
Scalar doppler_constant,
Scalar sound_speed
)
{
Check(this);
audioDopplerConstant = doppler_constant;
audioSoundSpeed = sound_speed;
Check_Fpu();
}
//
//#############################################################################
//#############################################################################
//
void
AudioHead::ControlAmplitudeRollOff(
Scalar exponent,
Scalar amplitude_rolloff_knee,
Scalar distance_scale
)
{
Check(this);
amplitudeRollOffExponent = exponent;
amplitudeRollOffKnee = amplitude_rolloff_knee;
amplitudeRollOffDistanceScale = distance_scale;
Check_Fpu();
}
//
//#############################################################################
//#############################################################################
//
void
AudioHead::ControlHighFrequencyRollOff(
Scalar exponent,
Scalar high_frequency_rolloff_knee,
Scalar distance_scale
)
{
Check(this);
highFrequencyRollOffExponent = exponent;
highFrequencyRollOffKnee = high_frequency_rolloff_knee;
highFrequencyRollOffDistanceScale = distance_scale;
Check_Fpu();
}
//
//#############################################################################
//#############################################################################
//
void
AudioHead::ControlSourceCompression(
Scalar exponent,
Scalar scale
)
{
Check(this);
sourceCompressionExponent = exponent;
sourceCompressionScale = scale;
Check_Fpu();
}
//
//#############################################################################
//#############################################################################
//
void
AudioHead::SetPositionalCulling(Logical)
{
Check(this);
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ AudioComponent ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//#############################################################################
//#############################################################################
//
const Receiver::HandlerEntry
AudioComponent::MessageHandlerEntries[]=
{
{
AudioComponent::ReceiveControlMessageID,
"ReceiveControl",
(AudioComponent::Handler)&AudioComponent::ReceiveControlMessageHandler
}
};
AudioComponent::MessageHandlerSet& AudioComponent::GetMessageHandlers()
{
static AudioComponent::MessageHandlerSet messageHandlers(ELEMENTS(AudioComponent::MessageHandlerEntries), AudioComponent::MessageHandlerEntries, Component::GetMessageHandlers());
return messageHandlers;
}
//
//#############################################################################
//#############################################################################
//
Derivation* AudioComponent::GetClassDerivations()
{
static Derivation classDerivations(Component::GetClassDerivations(), "AudioComponent");
return &classDerivations;
}
AudioComponent::SharedData
AudioComponent::DefaultData(
AudioComponent::GetClassDerivations(),
AudioComponent::GetMessageHandlers()
);
//
//#############################################################################
//#############################################################################
//
AudioComponent::AudioComponent(
PlugStream *stream,
SharedData &shared_data
):
Component(stream, shared_data),
audioWatcherSocket(NULL)
{
nextExecuteWatcherFrame = NullAudioFrameCount;
}
//
//#############################################################################
//#############################################################################
//
AudioComponent::~AudioComponent()
{
}
//
//#############################################################################
//#############################################################################
//
Logical
AudioComponent::TestInstance() const
{
if (!IsDerivedFrom(*GetClassDerivations()))
{
return False;
}
Check(&audioWatcherSocket);
return True;
}
//
//#############################################################################
//#############################################################################
//
void
AudioComponent::ExecuteWatchers()
{
Check(this);
//
// Execute watchers if this is the next watcher frame
//
AudioFrameCount
audio_frame_count;
Check(application);
Check(application->GetAudioRenderer());
audio_frame_count = application->GetAudioRenderer()->GetAudioFrameCount();
if (nextExecuteWatcherFrame <= audio_frame_count)
{
nextExecuteWatcherFrame = audio_frame_count + DefaultAudioFrameDelay;
//
// Execute watchers
//
ChainIteratorOf<Component*>
iterator(&audioWatcherSocket);
Component
*component;
while ((component = iterator.ReadAndNext()) != NULL)
{
Check(component);
component->Execute();
}
}
}
//
//#############################################################################
//#############################################################################
//
void
AudioComponent::PrimeWatchers(int passes)
{
Check(this);
for (int pass = 0; pass < passes; ++pass)
{
ChainIteratorOf<Component*> iterator(&audioWatcherSocket);
Component *component;
while ((component = iterator.ReadAndNext()) != NULL)
{
Check(component);
// recurse gate-free: an AudioComponent child (mixer/smoother/
// splitter) pumps ITS chain; an attribute watcher re-reads +
// re-sends. One pass per level keeps total work = passes x chain.
component->PrimeWatchers(1);
}
}
}
//
//#############################################################################
//#############################################################################
//
void
AudioComponent::Execute()
{
Check(this);
ExecuteWatchers();
Check_Fpu();
}
//
//#############################################################################
//#############################################################################
//
void
AudioComponent::ReceiveControl(
AudioControlID,
AudioControlValue
)
{
Fail("AudioComponent::ReceiveControl - Should never reach here");
}
//
//#############################################################################
//#############################################################################
//
void
AudioComponent::PostReceiveControl(
AudioControlID control_ID,
AudioControlValue control_value
)
{
Check(this);
ReceiveControlMessage
message(control_ID, control_value);
Check(application);
// ECH 1/26/96 - application->Post(HighEventPriority, this, &message);
application->Post(DefaultEventPriority, this, &message);
Check_Fpu();
}
//
//#############################################################################
//#############################################################################
//
void
AudioComponent::ReceiveControlMessageHandler(ReceiveControlMessage *message)
{
Check(this);
Check(message);
ReceiveControl(message->controlID, message->controlValue);
Check_Fpu();
}
//~~~~~~~~~~~~~~~~~~ AudioComponent__ReceiveControlMessage ~~~~~~~~~~~~~~~~~~~~
AudioComponent__ReceiveControlMessage::AudioComponent__ReceiveControlMessage(
AudioControlID control_ID,
AudioControlValue control_value
):
Receiver::Message(
AudioComponent::ReceiveControlMessageID,
sizeof(AudioComponent__ReceiveControlMessage)
)
{
controlID = control_ID;
controlValue = control_value;
}