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>
557 lines
16 KiB
C++
557 lines
16 KiB
C++
#include "munga.h"
|
|
#pragma hdrstop
|
|
|
|
#include "audrend.h"
|
|
#include "audent.h"
|
|
#include "jmover.h"
|
|
#include "app.h"
|
|
|
|
//#############################################################################
|
|
//########################### AudioRenderer #############################
|
|
//#############################################################################
|
|
|
|
//
|
|
//#############################################################################
|
|
// AudioRenderer
|
|
//#############################################################################
|
|
//
|
|
AudioRenderer::AudioRenderer(RendererRate render_rate):
|
|
Renderer(
|
|
render_rate,
|
|
MaxRendererComplexity,
|
|
DefaultRendererPriority,
|
|
AudioInterestType,
|
|
DefaultInterestDepth,
|
|
AudioRendererClassID
|
|
),
|
|
audioEventSocket(NULL, False)
|
|
{
|
|
audioHead = NULL;
|
|
|
|
#ifdef LAB_ONLY
|
|
sourceClippedCount = 0;
|
|
sourceStartCount = 0;
|
|
sourceSuspendCount = 0;
|
|
sourceResumeCount = 0;
|
|
doubleTriggerCount = 0;
|
|
resourceStealCount = 0;
|
|
resourceStealPriorityCount = 0;
|
|
resourceStealVolumeCount = 0;
|
|
stealShortDurationCount = 0;
|
|
#endif
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// ~AudioRenderer
|
|
//#############################################################################
|
|
//
|
|
AudioRenderer::~AudioRenderer()
|
|
{
|
|
Unregister_Object(audioHead);
|
|
delete audioHead;
|
|
|
|
#ifdef LAB_ONLY
|
|
cout << "AudioRenderer::~AudioRenderer - sourceClippedCount="
|
|
<< sourceClippedCount << "\n";
|
|
cout << "AudioRenderer::~AudioRenderer - sourceStartCount="
|
|
<< sourceStartCount << "\n";
|
|
cout << "AudioRenderer::~AudioRenderer - sourceSuspendCount="
|
|
<< sourceSuspendCount << "\n";
|
|
cout << "AudioRenderer::~AudioRenderer - sourceResumeCount="
|
|
<< sourceResumeCount << "\n";
|
|
cout << "AudioRenderer::~AudioRenderer - doubleTriggerCount="
|
|
<< doubleTriggerCount << "\n";
|
|
cout << "AudioRenderer::~AudioRenderer - resourceStealCount="
|
|
<< resourceStealCount << "\n";
|
|
cout << "AudioRenderer::~AudioRenderer - resourceStealPriorityCount="
|
|
<< resourceStealPriorityCount << "\n";
|
|
cout << "AudioRenderer::~AudioRenderer - resourceStealVolumeCount="
|
|
<< resourceStealVolumeCount << "\n";
|
|
cout << "AudioRenderer::~AudioRenderer - stealShortDurationCount="
|
|
<< stealShortDurationCount << "\n";
|
|
#endif
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// Initialize
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioRenderer::Initialize()
|
|
{
|
|
audioHead = MakeAudioHead();
|
|
Register_Object(audioHead);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// TestInstance
|
|
//#############################################################################
|
|
//
|
|
Logical
|
|
AudioRenderer::TestInstance() const
|
|
{
|
|
Renderer::TestInstance();
|
|
if (audioHead != NULL)
|
|
{
|
|
Check(audioHead);
|
|
}
|
|
Check(&audioEventSocket);
|
|
return True;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// LinkToEntity
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioRenderer::LinkToEntity(Entity *entity)
|
|
{
|
|
Check(this);
|
|
Check(entity);
|
|
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// Link the head to the entity
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
Check(audioHead);
|
|
audioHead->LinkToEntity(entity);
|
|
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// Call inherited method
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
Renderer::LinkToEntity(entity);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// ExecuteImplementation
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioRenderer::ExecuteImplementation(
|
|
RendererComplexity,
|
|
RendererOrigin::InterestingEntityIterator*
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(audioHead);
|
|
audioHead->Execute();
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// PostAudioRequestMessage
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioRenderer::PostAudioRequestMessage(
|
|
AudioSource *audio_source,
|
|
AudioSource::RequestMessage *message
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(audio_source);
|
|
Check(message);
|
|
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// If audio source is clipped and the source is transient
|
|
// Then ignore
|
|
// Else set priority and volume
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
AudioSourcePriority
|
|
audio_source_priority;
|
|
AudioControlValue
|
|
audio_source_volume_scale;
|
|
|
|
if (audio_source->IsAudioSourceClipped(GetAudioHead()))
|
|
{
|
|
if (getenv("BT_AUDIO_SPATIAL")) { static int s_cl=0; if (s_cl++<40)
|
|
DEBUG_STREAM << "[spatial] CLIPPED src=" << (void*)audio_source
|
|
<< " headPos=(" << GetAudioHead()->GetHeadEntity()->localOrigin.linearPosition.x
|
|
<< "," << GetAudioHead()->GetHeadEntity()->localOrigin.linearPosition.z << ")\n" << std::flush; }
|
|
//
|
|
// If it is a transient source then ignore request
|
|
//
|
|
if (audio_source->GetAudioRenderType() == TransientAudioRenderType)
|
|
{
|
|
#ifdef LAB_ONLY
|
|
sourceClippedCount++;
|
|
#endif
|
|
return;
|
|
}
|
|
audio_source_priority = audio_source->GetAudioSourcePriority();
|
|
audio_source_volume_scale = 0.0f;
|
|
}
|
|
else
|
|
{
|
|
audio_source_priority = audio_source->GetAudioSourcePriority();
|
|
// (task #50, AUDIO_FIDELITY F19) COLD-START PRIME: an idle source's own
|
|
// watcher socket only executes at Start attempts, so its authored
|
|
// AudioControlSmoothers (footstep volume: N=30/15, fill 0) warmed ONE
|
|
// sample per attempt -- and the transient drop gate below (vol < 0.3)
|
|
// rejected the first ~25 footfalls (~10-20 s of silent steps) before
|
|
// the average could cross the gate. On a Start request, pump the
|
|
// source's watchers a full smoother window so the volume chain is
|
|
// evaluated at its true steady state; the smoother keeps its authored
|
|
// smoothing role for live variation once the source is playing.
|
|
if (message->controlID == StartAudioControlID)
|
|
{
|
|
audio_source->PrimeWatchers(30);
|
|
}
|
|
audio_source_volume_scale = audio_source->CalculateSourceVolumeScale();
|
|
if (getenv("BT_AUDIO_SPATIAL")) { static int s_vs=0; if ((s_vs++ % 120)==0)
|
|
DEBUG_STREAM << "[spatial] request src=" << (void*)audio_source
|
|
<< " vol=" << audio_source_volume_scale
|
|
<< " headPos=(" << GetAudioHead()->GetHeadEntity()->localOrigin.linearPosition.x
|
|
<< "," << GetAudioHead()->GetHeadEntity()->localOrigin.linearPosition.z << ")\n" << std::flush; }
|
|
}
|
|
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// If this a transient source and volume is lower than threshold
|
|
// Then return
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
// DIAG (BT_AUDIO_NODROP): deliver low-volume transient starts anyway (at a
|
|
// floor volume) to isolate volume-feed problems from the rest of the chain.
|
|
if (getenv("BT_AUDIO_NODROP") &&
|
|
message->controlID == StartAudioControlID &&
|
|
audio_source->GetAudioRenderType() == TransientAudioRenderType &&
|
|
audio_source_volume_scale < LowAudioVolumeThreshold)
|
|
{
|
|
DEBUG_STREAM << "[spatial] NODROP forcing start src=" << (void*)audio_source
|
|
<< " vol=" << audio_source_volume_scale << "\n" << std::flush;
|
|
audio_source->ReceiveControl(VolumeAudioControlID, 0.7f);
|
|
audio_source_volume_scale = 0.7f;
|
|
}
|
|
if (
|
|
message->controlID == StartAudioControlID &&
|
|
audio_source->GetAudioRenderType() == TransientAudioRenderType &&
|
|
audio_source_volume_scale < LowAudioVolumeThreshold
|
|
)
|
|
{
|
|
if (getenv("BT_AUDIO_SPATIAL")) { static int s_dr=0; if (s_dr++<40)
|
|
DEBUG_STREAM << "[spatial] DROP transient start t=" << (GetTickCount() % 1000000)
|
|
<< " src=" << (void*)audio_source
|
|
<< " vol=" << audio_source_volume_scale
|
|
<< " (below threshold " << LowAudioVolumeThreshold << ")\n" << std::flush; }
|
|
#ifdef LAB_ONLY
|
|
sourceClippedCount++;
|
|
#endif
|
|
return;
|
|
}
|
|
if (getenv("BT_AUDIO_SPATIAL") && message->controlID == StartAudioControlID) {
|
|
static int s_st=0; if (s_st++<40)
|
|
DEBUG_STREAM << "[spatial] START request src=" << (void*)audio_source
|
|
<< " vol=" << audio_source_volume_scale << "\n" << std::flush; }
|
|
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// Create audio weight based on priority and volume
|
|
// Add audio event
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
AudioWeighting
|
|
audio_weight(audio_source_priority, audio_source_volume_scale);
|
|
AudioEvent
|
|
*audio_event;
|
|
|
|
audio_event = new AudioEvent(audio_source, message);
|
|
Register_Object(audio_event);
|
|
audioEventSocket.AddValue(audio_event, audio_weight);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// ProcessAudioRequestMessage
|
|
//#############################################################################
|
|
//
|
|
Logical
|
|
AudioRenderer::ProcessAudioRequestMessage()
|
|
{
|
|
//
|
|
// Process high priority, high volume event
|
|
//
|
|
AudioEventIterator
|
|
iterator(&audioEventSocket);
|
|
AudioEvent
|
|
*audio_event;
|
|
|
|
Logical stuff = False;
|
|
while ((audio_event = iterator.GetCurrent()) != NULL)
|
|
{
|
|
stuff = True;
|
|
Check(audio_event);
|
|
audio_event->Process();
|
|
//return True;
|
|
}
|
|
return stuff;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// FlushAudioMessages
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioRenderer::FlushAudioMessages(AudioSource *audio_source)
|
|
{
|
|
Check(this);
|
|
Check(audio_source);
|
|
|
|
//
|
|
// Process matching events
|
|
//
|
|
AudioEventIterator
|
|
iterator(&audioEventSocket);
|
|
AudioEvent
|
|
*audio_event;
|
|
|
|
while ((audio_event = iterator.ReadAndNext()) != NULL)
|
|
{
|
|
Check(audio_event);
|
|
if (audio_event->targetReceiver.GetCurrent() == audio_source)
|
|
{
|
|
if (audio_event->messageToSend->controlID == StopAudioControlID)
|
|
{
|
|
audio_event->Process();
|
|
}
|
|
else
|
|
{
|
|
Unregister_Object(audio_event);
|
|
delete audio_event;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// MakeAudioHead
|
|
//#############################################################################
|
|
//
|
|
AudioHead*
|
|
AudioRenderer::MakeAudioHead()
|
|
{
|
|
return new AudioHead;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
// StartEntityEffectImplementation
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioRenderer::StartEntityEffectImplementation(
|
|
Entity *parent_entity,
|
|
DamageZone *damage_zone,
|
|
ResourceDescription::ResourceID resource_ID
|
|
)
|
|
{
|
|
SET_AUDIO_RENDERER();
|
|
|
|
Check(this);
|
|
Check(parent_entity);
|
|
Check(damage_zone);
|
|
Verify(resource_ID != ResourceDescription::NullResourceID);
|
|
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// Get the audio resource for this effect
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
ResourceDescription *audio_resource_description;
|
|
|
|
Check(application);
|
|
Check(application->GetResourceFile());
|
|
audio_resource_description =
|
|
application->GetResourceFile()->SearchList(
|
|
resource_ID,
|
|
ResourceDescription::AudioStreamListResourceType
|
|
);
|
|
if (audio_resource_description == NULL)
|
|
{
|
|
CLEAR_AUDIO_RENDERER();
|
|
return;
|
|
}
|
|
Check(audio_resource_description);
|
|
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// Get the resource ID for the model resource for the audio effect entity
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
ResourceDescription *model_resource_description;
|
|
|
|
Check(application);
|
|
Check(application->GetResourceFile());
|
|
Check(audio_resource_description);
|
|
model_resource_description =
|
|
application->GetResourceFile()->SearchList(
|
|
audio_resource_description->resourceID,
|
|
ResourceDescription::ModelListResourceType
|
|
);
|
|
if (model_resource_description == NULL)
|
|
{
|
|
CLEAR_AUDIO_RENDERER();
|
|
return;
|
|
}
|
|
Check(model_resource_description);
|
|
model_resource_description->Lock();
|
|
audio_resource_description->Lock();
|
|
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// Get the entity segment
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
Entity *linked_entity;
|
|
AudioRepresentation audio_representation;
|
|
EntitySegment *parent_entity_segment;
|
|
|
|
linked_entity = GetLinkedEntity();
|
|
Check(linked_entity);
|
|
audio_representation =
|
|
(AudioRepresentation)parent_entity->GetAudioRepresentation(
|
|
linked_entity
|
|
);
|
|
|
|
parent_entity_segment = damage_zone->GetCurrentEffectSite(
|
|
(audio_representation == InternalAudioRepresentation) ?
|
|
DamageZone::InternalAudioEffectSite :
|
|
DamageZone::ExternalAudioEffectSite
|
|
);
|
|
Check(parent_entity_segment);
|
|
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// Create the audio effect entity with this resource
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
JointedMover *jointed_mover;
|
|
|
|
Verify(parent_entity->IsDerivedFrom(JointedMover::GetClassDerivations()));
|
|
jointed_mover = Cast_Object(JointedMover*, parent_entity);
|
|
Check(jointed_mover);
|
|
|
|
AudioEntity::MakeMessage
|
|
make_message(
|
|
model_resource_description->resourceID,
|
|
jointed_mover,
|
|
parent_entity_segment
|
|
);
|
|
#if DEBUG_LEVEL>0
|
|
AudioEntity *audio_entity =
|
|
#endif
|
|
AudioEntity::Make(&make_message);
|
|
Register_Object(audio_entity);
|
|
|
|
model_resource_description->Unlock();
|
|
audio_resource_description->Unlock();
|
|
|
|
CLEAR_AUDIO_RENDERER();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~ AudioRenderer profile bits ~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
#if defined(TRACE_AUDIO_RENDERER)
|
|
BitTrace Audio_Renderer("Audio Renderer");
|
|
#endif
|
|
|
|
#if defined(TRACE_AUDIO_RENDERER_CREATE_OBJECTS)
|
|
BitTrace Audio_Renderer_Create_Objects("Audio Renderer Create Objects");
|
|
#endif
|
|
|
|
#if defined(TRACE_AUDIO_RENDERER_DESTROY_OBJECTS)
|
|
BitTrace Audio_Renderer_Destroy_Objects("Audio Renderer Destroy Objects");
|
|
#endif
|
|
|
|
#if defined(TRACE_AUDIO_RENDERER_START_HANDLER)
|
|
BitTrace Audio_Renderer_Start_Handler("Audio Renderer Start Handler");
|
|
#endif
|
|
|
|
#if defined(TRACE_AUDIO_RENDERER_STOP_HANDLER)
|
|
BitTrace Audio_Renderer_Stop_Handler("Audio Renderer Stop Handler");
|
|
#endif
|
|
|
|
#if defined(TRACE_AUDIO_RENDERER_EXECUTE)
|
|
BitTrace Audio_Renderer_Execute("Audio Renderer Execute");
|
|
#endif
|
|
|
|
//#############################################################################
|
|
//############################## AudioEvent #############################
|
|
//#############################################################################
|
|
|
|
MemoryBlock *AudioEvent::GetAllocatedMemory()
|
|
{
|
|
static MemoryBlock allocatedMemory(sizeof(Event), AUDIOEVENT_MEMORYBLOCK_ALLOCATION, AUDIOEVENT_MEMORYBLOCK_ALLOCATION, "AudioEvents");
|
|
return &allocatedMemory;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
AudioEvent::AudioEvent(
|
|
AudioSource *target,
|
|
AudioSource::RequestMessage *message
|
|
):
|
|
targetReceiver(this)
|
|
{
|
|
Check(target);
|
|
Check(message);
|
|
|
|
//
|
|
// Store the message
|
|
//
|
|
size_t long_size = (message->messageLength+3)>>2;
|
|
messageToSend = (AudioSource::RequestMessage*)new long[long_size];
|
|
Check_Pointer(messageToSend);
|
|
Register_Pointer(messageToSend);
|
|
|
|
Mem_Copy(
|
|
messageToSend,
|
|
message,
|
|
message->messageLength,
|
|
long_size*sizeof(long)
|
|
);
|
|
|
|
//
|
|
// Store the target receiver
|
|
//
|
|
targetReceiver.Add(target);
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
AudioEvent::~AudioEvent()
|
|
{
|
|
Unregister_Pointer(messageToSend);
|
|
delete messageToSend;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioEvent::ReleaseLinkHandler(
|
|
Socket*,
|
|
Plug*
|
|
)
|
|
{
|
|
Unregister_Object(this);
|
|
delete this;
|
|
}
|