Files
BT411/engine/MUNGA_L4/L4AUDRND.cpp
T
Joe DiPrimaandClaude Opus 5 3354db4bd1 #32: RETRACT the retention diagnosis; count what matters (steals + true drops); name the saturating class
The reopen said "the pool fills and never returns a source".  The 30s census in
the SAME field logs disproves it: free returns to ~227-230 between bursts and
reuses climbs ~20/s all session.  free=0 on the failure line is true by
DEFINITION at the instant of a failed acquire -- the third instance of the
counter-sampling trap (live=256-vs-6, then hsparm greps, now this), read off the
alarm line instead of the trend line.

WHAT THE LOGS ACTUALLY SHOW
  * The pool cycles; release-on-stop exists and works (the engine steal loop).
  * During firefights CONCURRENT demand exceeds 240 and the priority steal loop
    services each new sound by killing an old one -- continuously through
    combat (failures spread across every decile of every combat session).
  * Idle standing demand is ~13 sources.  My "each component holds its SourceSet
    to entity teardown, ~20-25 per mech" narrative was wrong.
  * The raw ACQUIRE FAILED line count (6.8k-19k per log) is NOISE: the steal
    loop retries after every failed attempt, so lines accumulate per EVENT and
    most events still play via a steal.  True drops were never counted.

CHANGES
  1. Census now carries steals= and drops= (drops = the steal loop ran dry and
     the sound NEVER played) plus a per-class drop histogram
     ("[audio] dropped by class: {class 1005 x4v: N} ...") -- all ungated, so
     the next field logs are decisive instead of suggestive.
  2. The ACQUIRE FAILED print is rate-limited to 1/30s and now names the
     requesting class + voice count.  19k-line log spam distorted this triage.
  3. BT_AUDIO_SOURCES=<n> now raises the POOL cap too (it previously raised the
     AL context budget while the pool stayed at 240, making the field
     experiment impossible to run).

MEASURED (9 mechs, missile autofire, 150s)
  * cap 240: peak 130 sources, 0 fails -- demand tracks SHOOTER count, not mech
    count; one shooter cannot saturate.  A 6-shooter lobby pins 240.
  * cap 48 (BT_AUDIO_SOURCES=64): saturation reproduced -- census
    steals=223 drops=455, histogram names the classes.
  * Dominant field requester (requested=4) = class 1005 Static3DPatchSource:
    world-placed effect sounds, i.e. EXPLOSIONS.  1001 DirectPatchSource x1v
    dominates drops at low cap; 1002 Dynamic3DPatchSource x3v present.

CONSEQUENCE FOR THE FIX ORDER: #84's stale-aim double detonation duplicates
exactly the saturating class on observer nodes.  Fix #84 FIRST, then re-read the
field census; only if it still saturates does the budget experiment
(BT_AUDIO_SOURCES with frame time measured) become the play.

KB: the wrong night-9 entry in open-questions.md replaced with the corrected
diagnosis; gotcha candidate noted -- an alarm-line counter is not a trend.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 01:42:10 -05:00

1888 lines
56 KiB
C++

#include <cstdlib>
#include "mungal4.h"
#pragma hdrstop
#include "l4audrnd.h"
#include "..\munga\notation.h"
#include "openal/alc.h"
//
//#############################################################################
// L4AudioRenderer
//#############################################################################
//
L4AudioRenderer::L4AudioRenderer(
RendererRate render_rate,
Logical mission_review_mode
):
AudioRenderer(render_rate),
runningAudioSourceSocket(NULL, False),
dormantAudioSourceSocket(NULL),
mixingAudioSourceSocket(NULL, False)
{
missionReviewMode = mission_review_mode;
nextCalculateMixFrame = NullAudioFrameCount;
}
//
//#############################################################################
// ~L4AudioRenderer
//#############################################################################
//
L4AudioRenderer::~L4AudioRenderer()
{
//Close OpenAL
ALCcontext *context = alcGetCurrentContext();
ALCdevice *device = alcGetContextsDevice(context);
alcMakeContextCurrent(NULL);
alcDestroyContext(context);
alcCloseDevice(device);
//
//----------------------------------------------------------------------
// Verify that all of entities have become uninteresting
//----------------------------------------------------------------------
//
#if DEBUG_LEVEL>0
{
RunningSourceIterator iterator(&runningAudioSourceSocket);
Verify(iterator.GetSize() == 0);
}
{
DormantSourceIterator iterator(&dormantAudioSourceSocket);
Verify(iterator.GetSize() == 0);
}
#endif
//
//----------------------------------------------------------------------
// Unload resources
//----------------------------------------------------------------------
//
Check(&audioResourceManager);
// audioResourceManager.UnloadResources(&audioHardware);
//
//----------------------------------------------------------------------
// Close audio hardware
//----------------------------------------------------------------------
//
// Check(&audioHardware);
// audioHardware.Close();
}
//
//#############################################################################
// TestInstance
//#############################################################################
//
Logical
L4AudioRenderer::TestInstance() const
{
AudioRenderer::TestInstance();
Check(&audioHardware);
Check(&runningAudioSourceSocket);
Check(&dormantAudioSourceSocket);
Check(&mixingAudioSourceSocket);
return True;
}
//
//#############################################################################
// Initialize
//#############################################################################
//
void
L4AudioRenderer::Initialize()
{
Check(this);
if (getenv("BT_AUDIO_LOG")) DEBUG_STREAM << "[audio] L4AudioRenderer::Initialize ENTERED\n" << std::flush;
//
//----------------------------------------------------------------------
// Call inherited method
//----------------------------------------------------------------------
//
AudioRenderer::Initialize();
//
//----------------------------------------------------------------------
// Get the audio head
//----------------------------------------------------------------------
//
AudioHead *audio_head = GetAudioHead();
Check(audio_head);
//
//----------------------------------------------------------------------
// Open the audio renderer notation file
//----------------------------------------------------------------------
//
int ret;
CString audio_file_name;
if (missionReviewMode)
{
audio_file_name = "audio\\audiomr.ini";
}
else
{
audio_file_name = "audio\\audio.ini";
}
NotationFile notation_file(audio_file_name);
Check(&notation_file);
if (getenv("BT_AUDIO_LOG")) DEBUG_STREAM << "[audio] notation file opened; about to read scalars\n" << std::flush;
//
//----------------------------------------------------------------------
// Define clipping sphere (m)
//----------------------------------------------------------------------
//
Scalar clipping_radius;
ret = notation_file.GetEntry(
"AudioRenderer",
"clipping_radius",
&clipping_radius
);
if (!ret)
{
Fail("L4AudioRenderer::Initialize - clipping_radius not defined");
}
Check(audio_head);
audio_head->DefineClippingSphere(clipping_radius);
//
//----------------------------------------------------------------------
// Distance to speakers or width of head entity (m)
//----------------------------------------------------------------------
//
Scalar distance_between_ears;
ret = notation_file.GetEntry(
"AudioRenderer",
"distance_between_ears",
&distance_between_ears
);
if (!ret)
{
Fail("L4AudioRenderer::Initialize - distance_between_ears not defined");
}
Check(audio_head);
audio_head->SetDistanceBetweenEars(distance_between_ears);
//
//----------------------------------------------------------------------
// Define amplitude rolloff characteristics
//----------------------------------------------------------------------
//
Scalar amplitude_rolloff;
Scalar amplitude_rolloff_knee;
Scalar amplitude_rolloff_distance_scale;
ret = notation_file.GetEntry(
"AudioRenderer",
"amplitude_rolloff",
&amplitude_rolloff
);
if (!ret)
{
Fail("L4AudioRenderer::Initialize - amplitude_rolloff not defined");
}
ret = notation_file.GetEntry(
"AudioRenderer",
"amplitude_rolloff_distance_scale",
&amplitude_rolloff_distance_scale
);
if (!ret)
{
Fail("L4AudioRenderer::Initialize - amplitude_rolloff_distance_scale not defined");
}
ret = notation_file.GetEntry(
"AudioRenderer",
"amplitude_rolloff_knee",
&amplitude_rolloff_knee
);
if (!ret)
{
Fail("L4AudioRenderer::Initialize - amplitude_rolloff_knee not defined");
}
Check(audio_head);
audio_head->ControlAmplitudeRollOff(
amplitude_rolloff,
amplitude_rolloff_knee,
amplitude_rolloff_distance_scale
);
//
//----------------------------------------------------------------------
// Define high frequency rolloff characteristics
//----------------------------------------------------------------------
//
Scalar high_frequency_rolloff;
Scalar high_frequency_rolloff_knee;
Scalar high_frequency_rolloff_distance_scale;
ret = notation_file.GetEntry(
"AudioRenderer",
"high_frequency_rolloff",
&high_frequency_rolloff
);
if (!ret)
{
Fail("L4AudioRenderer::Initialize - high_frequency_rolloff not defined");
}
ret = notation_file.GetEntry(
"AudioRenderer",
"high_frequency_rolloff_knee",
&high_frequency_rolloff_knee
);
if (!ret)
{
Fail("L4AudioRenderer::Initialize - high_frequency_rolloff_knee not defined");
}
ret = notation_file.GetEntry(
"AudioRenderer",
"high_frequency_rolloff_distance_scale",
&high_frequency_rolloff_distance_scale
);
if (!ret)
{
Fail("L4AudioRenderer::Initialize - high_frequency_rolloff_distance_scale not defined");
}
Check(audio_head);
audio_head->ControlHighFrequencyRollOff(
high_frequency_rolloff,
high_frequency_rolloff_knee,
high_frequency_rolloff_distance_scale
);
//
//----------------------------------------------------------------------
// Define Doppler characterisitics
//----------------------------------------------------------------------
//
Scalar doppler_range;
Scalar speed_of_sound;
ret = notation_file.GetEntry(
"AudioRenderer",
"doppler_range",
&doppler_range
);
if (!ret)
{
Fail("L4AudioRenderer::Initialize - doppler_range not defined");
}
ret = notation_file.GetEntry(
"AudioRenderer",
"speed_of_sound",
&speed_of_sound
);
if (!ret)
{
Fail("L4AudioRenderer::Initialize - speed_of_sound not defined");
}
Check(audio_head);
audio_head->ControlDopplerEffect(
doppler_range,
speed_of_sound
);
//
//----------------------------------------------------------------------
// Define source compression characterisitics
//----------------------------------------------------------------------
//
Scalar compression_exponent;
Scalar compression_scale;
ret = notation_file.GetEntry(
"AudioRenderer",
"compression_exponent",
&compression_exponent
);
if (!ret)
{
Fail("L4AudioRenderer::Initialize - compression_exponent not defined");
}
ret = notation_file.GetEntry(
"AudioRenderer",
"compression_scale",
&compression_scale
);
if (!ret)
{
Fail("L4AudioRenderer::Initialize - compression_scale not defined");
}
Check(audio_head);
audio_head->ControlSourceCompression(
compression_exponent,
compression_scale
);
//
//----------------------------------------------------------------------
// Set ITD difference
//----------------------------------------------------------------------
//
Scalar itd_difference;
ret = notation_file.GetEntry(
"AudioRenderer",
"itd_difference",
&itd_difference
);
if (!ret)
{
Fail("L4AudioRenderer::Initialize - itd_difference not defined");
}
Check(audio_head);
audio_head->SetITDDifference(itd_difference);
//
//----------------------------------------------------------------------
// Set global reverb scale
//----------------------------------------------------------------------
//
Scalar global_reverb_scale;
ret = notation_file.GetEntry(
"AudioRenderer",
"global_reverb_scale",
&global_reverb_scale
);
if (!ret)
{
Fail("L4AudioRenderer::Initialize - global_reverb_scale not defined");
}
Check(audio_head);
audio_head->SetGlobalReverbScale(global_reverb_scale);
//
//----------------------------------------------------------------------
// Initialize the audio hardware
//----------------------------------------------------------------------
//
// Check(&audioHardware);
// audioHardware.Initialize();
//Start OpenAL
ALCdevice *device = alcOpenDevice(NULL);
if (device)
{
// AUDIO DROPOUTS IN HEAVY COMBAT (field: "audio cutting in and out toward
// the end of the match"; night-5 logs show 2017 ACQUIRE FAILED lines in a
// single match, every one of them at live=256).
//
// We were passing NULL for the attribute list, so OpenAL Soft applied its
// DEFAULT budget of 256 mono sources -- an OpenAL default, nothing to do
// with the 1995 audio hardware. The pool is not leaking: the night-5
// census sits at a healthy 45-60 live, spikes to the 256 ceiling during
// firefights, and drains straight back afterwards, so the engine's
// priority steal loop (AudioSourceStop/SuspendMaintenance -> ReleaseChannels
// -> ReleaseSourceSet) is doing its job. It simply cannot keep up with the
// burst: a busy match logs ~7200 explosions, each spawning three
// DPLIndependantEffect voices, and while the pool is pinned every new
// acquire fails outright and that sound is silently dropped.
//
// ⚠ OFF BY DEFAULT, DELIBERATELY. Raising the cap is NOT a free win.
//
// ⚠⚠ UPDATE 2026-08-01 (#32): the "no field complaints since" claim below
// did NOT hold. Every 4.11.674 player log is saturated -- 3031/4657/5245/
// 6275/6571 failures across five machines, first failure ~10% into a match
// and still failing at 97%. The root cause was NOT the source budget at
// all: RequestAudioChannels was alGenSources'ing per sound event and
// ReleaseSourceSet alDeleteSources'ing on release, so combat CHURNED
// through the ceiling. Sources are now POOLED and recycled (see
// BTAudioPoolAcquire below), which fixes it WITHOUT touching this budget.
// Measured on the same bench: frame time went 8.67ms -> 7.79ms over ~10k
// frames, i.e. removing the churn is a net CPU WIN, so the governor
// argument below does not apply to pooling. The reasoning about raising
// the CAP (more voices mixing = more CPU) still stands on its own, which
// is why BT_AUDIO_SOURCES remains opt-in and unset by default.
//
// (Historical:) the 2026-07-23 fix a999e5c addressed the atomic-delete
// leak. What remained in the night-5 logs is transient exhaustion at the
// peak of a firefight, where a dropped voice competes with ~256 already
// sounding, so it is very likely sub-perceptual. Against that: the 256
// ceiling also acts as a
// GOVERNOR. The steal loop only steals when the incoming source outranks
// a running one, so a higher cap means many more voices mixing at once --
// with EFX reverb + the lowpass chains live, that is real CPU, spent
// exactly during heavy combat when the frame budget is tightest.
//
// So this stays opt-in: set BT_AUDIO_SOURCES=<n> to raise it (no rebuild
// needed) if dropouts are ever reported again, and measure frame time
// while you do. Unset = the shipped OpenAL Soft default, unchanged.
ALCint monoWanted = 0;
if (const char *sv = getenv("BT_AUDIO_SOURCES"))
{
int n = atoi(sv);
if (n >= 64 && n <= 4096)
monoWanted = n;
}
ALCcontext *context;
if (monoWanted > 0)
{
ALCint attrs[5];
attrs[0] = ALC_MONO_SOURCES; attrs[1] = monoWanted;
attrs[2] = ALC_STEREO_SOURCES; attrs[3] = 16;
attrs[4] = 0;
context = alcCreateContext(device, attrs);
}
else
{
context = alcCreateContext(device, NULL); // default budget (256)
}
alcMakeContextCurrent(context);
// Report what the driver actually GRANTED -- a request is not a promise.
// (Asking for 64 grants 240: OpenAL Soft has a floor of its own, which is
// also why the NULL default lands on 256.)
if (getenv("BT_AUDIO_LOG") || monoWanted > 0)
{
ALCint monoGot = 0, stereoGot = 0;
alcGetIntegerv(device, ALC_MONO_SOURCES, 1, &monoGot);
alcGetIntegerv(device, ALC_STEREO_SOURCES, 1, &stereoGot);
DEBUG_STREAM << "[audio] source budget: requested mono="
<< (monoWanted > 0 ? monoWanted : 0) << " (0 = driver default)"
<< " granted mono=" << monoGot << " stereo=" << stereoGot
<< std::endl << std::flush;
}
// Master volume: AL_GAIN on the listener scales EVERY source. Default 0.6
// (the raw samples are hot). Priority: BT_AUDIO_VOLUME env >
// content\volume.cfg (written by the runtime -/+ keys, issue #26) > 0.6.
float masterVol = 0.6f;
{
FILE *cfg = fopen("volume.cfg", "rt");
if (cfg != NULL)
{
float f = -1.0f;
if (fscanf(cfg, "%f", &f) == 1 && f >= 0.0f && f <= 1.5f)
masterVol = f;
fclose(cfg);
}
}
if (const char *v = getenv("BT_AUDIO_VOLUME")) { float f = (float)atof(v); if (f >= 0.0f) masterVol = f; }
extern float gBTMasterVolume;
gBTMasterVolume = masterVol;
alListenerf(AL_GAIN, masterVol);
if (getenv("BT_AUDIO_LOG")) DEBUG_STREAM << "[audio] OpenAL device OPENED + context current; master gain=" << masterVol << "\n" << std::flush;
// (task #50, AUDIO_FIDELITY F9/F11) EFX bridge: the authored lowpass
// chains + the wet-exterior/dry-cockpit reverb split, at the authentic
// AUDIO.INI global_reverb_scale read above.
{
extern bool EFX_Initialize(float global_reverb_scale);
EFX_Initialize(global_reverb_scale);
}
}
else
{
if (getenv("BT_AUDIO_LOG")) DEBUG_STREAM << "[audio] OpenAL device FAILED to open (alcOpenDevice NULL)\n" << std::flush;
}
//
//----------------------------------------------------------------------
// Preload resources
//----------------------------------------------------------------------
//
Check(&audioResourceManager);
audioResourceManager.PreloadResources(&notation_file);
}
//
//#############################################################################
// NotifyOfNewInterestingEntity
//#############################################################################
//
void
L4AudioRenderer::NotifyOfNewInterestingEntity(Entity *interesting_entity)
{
SET_AUDIO_RENDERER();
SET_AUDIO_RENDERER_CREATE_OBJECTS();
Check(this);
Check(interesting_entity);
Entity *linked_entity = GetLinkedEntity();
Check(linked_entity);
AudioRepresentation audio_representation =
(AudioRepresentation)interesting_entity->GetAudioRepresentation(
linked_entity
);
Check(&audioResourceManager);
audioResourceManager.CreateEntityAudioObjects(
interesting_entity,
audio_representation
);
CLEAR_AUDIO_RENDERER_CREATE_OBJECTS();
CLEAR_AUDIO_RENDERER();
}
//
//#############################################################################
// NotifyOfBecomingUninterestingEntity
//#############################################################################
//
void
L4AudioRenderer::NotifyOfBecomingUninterestingEntity(
Entity *uninteresting_entity
)
{
SET_AUDIO_RENDERER();
SET_AUDIO_RENDERER_DESTROY_OBJECTS();
Check(this);
Check(uninteresting_entity);
Check(&audioResourceManager);
audioResourceManager.DestroyEntityAudioObjects(uninteresting_entity);
CLEAR_AUDIO_RENDERER_DESTROY_OBJECTS();
CLEAR_AUDIO_RENDERER();
}
//
//#############################################################################
// LoadMissionImplementation
//#############################################################################
//
void
L4AudioRenderer::LoadMissionImplementation(Mission*)
{
Check(this);
}
//
//#############################################################################
// ShutdownImplementation
//#############################################################################
//
void
L4AudioRenderer::ShutdownImplementation()
{
Check(this);
}
//
//#############################################################################
// SuspendImplementation
//#############################################################################
//
void
L4AudioRenderer::SuspendImplementation()
{
Check(this);
}
//
//#############################################################################
// ResumeImplementation
//#############################################################################
//
void
L4AudioRenderer::ResumeImplementation()
{
Check(this);
}
//
//#############################################################################
// StartRequest
//#############################################################################
//
void
L4AudioRenderer::StartRequest(L4AudioSource *audio_source)
{
Check(this);
Check(audio_source);
if (getenv("BT_AUDIO_SPATIAL")) { static int s_sr=0; if (s_sr++<5000)
DEBUG_STREAM << "[startreq] src=" << (void*)audio_source
<< " state=" << (int)audio_source->GetAudioSourceState()
<< " class=" << (int)audio_source->GetClassID()
<< "\n" << std::flush; }
//
//--------------------------------------------------------------------------
// Verify that the source is stopped
//--------------------------------------------------------------------------
//
Verify(audio_source->GetAudioSourceState() == StoppedAudioSourceState);
//
//--------------------------------------------------------------------------
// Request audio resources
//--------------------------------------------------------------------------
//
Logical
resources_available;
/* AudioChannelSet
channel_set_result;*/
SourceSet *source_result = audio_source->GetAudioChannelSet();
resources_available =
RequestAudioResources(
audio_source,
source_result
);
if (!resources_available)
{
if (getenv("BT_AUDIO_SPATIAL")) { static int s_rf=0; if (s_rf++<40)
DEBUG_STREAM << "[startreq] RESOURCE FAIL src=" << (void*)audio_source
<< "\n" << std::flush; }
// Audio-dropout fix: a failed acquisition can leave a PARTIAL set
// (alGenSources succeeded for some slots before the pool ran dry).
// A dropped transient never plays and nothing else ever released it,
// so the partial sources dangled forever -- once the pool exhausted
// ONCE, every drop leaked a little more and it never recovered.
audio_source->ReleaseChannels();
if (audio_source->GetAudioRenderType() == SustainedAudioRenderType)
{
//
// Set state to dormant and add the source to the dormant list
//
audio_source->AssignAudioSourceState(DormantAudioSourceState);
dormantAudioSourceSocket.Add(audio_source);
}
return;
}
//
//--------------------------------------------------------------------------
// Start the audio source
//--------------------------------------------------------------------------
//
//
// Update source spatial model
// Set channel set of source
// Source start implementation
// Set state of source to running
//
audio_source->UpdateSpatialModel(GetAudioHead());
audio_source->SetAudioChannelSet(*source_result);
audio_source->StartImplementation();
audio_source->AssignAudioSourceState(RunningAudioSourceState);
//
// Add source to the running list and the mixing list
//
runningAudioSourceSocket.AddValue(
audio_source,
audio_source->CalculateAudioWeighting()
);
if (
audio_source->GetAudioSourceMixPresence() !=
ManualAudioSourceMixPresence
)
{
mixingAudioSourceSocket.AddValue(
audio_source,
audio_source->GetAudioSourceMixPresence()
);
}
#ifdef LAB_ONLY
sourceStartCount++;
#endif
}
//
//#############################################################################
// StopRequest
//#############################################################################
//
void
L4AudioRenderer::StopRequest(L4AudioSource *audio_source)
{
Check(this);
Check(audio_source);
//
//--------------------------------------------------------------------------
// Verify that the source is not stopped
//--------------------------------------------------------------------------
//
Verify(audio_source->GetAudioSourceState() != StoppedAudioSourceState);
//
//--------------------------------------------------------------------------
// If state of the source is dormant or suspended
// Then set source to stop state and remove from dormant list
//--------------------------------------------------------------------------
//
if (
audio_source->GetAudioSourceState() == DormantAudioSourceState ||
audio_source->GetAudioSourceState() == SuspendedAudioSourceState
)
{
//
// Set state of source to stopped
// Verify that source is in dormant list
// Remove source from dormant list
// Verify that source is not in dormant list
//
audio_source->AssignAudioSourceState(StoppedAudioSourceState);
#if DEBUG_LEVEL>0
{
DormantSourceIterator iterator(&dormantAudioSourceSocket);
Verify(iterator.IsPlugMember(audio_source) == True);
}
#endif
PlugIterator remover(audio_source);
remover.RemoveSocket(&dormantAudioSourceSocket);
#if DEBUG_LEVEL>0
{
DormantSourceIterator iterator(&dormantAudioSourceSocket);
Verify(iterator.IsPlugMember(audio_source) == False);
}
#endif
return;
}
//
//--------------------------------------------------------------------------
// Stop the source
//--------------------------------------------------------------------------
//
AudioSourceStopMaintenance(audio_source);
}
//
//#############################################################################
// ExecuteImplementation
//#############################################################################
//
void
L4AudioRenderer::ExecuteImplementation(
RendererComplexity complexity_update,
RendererOrigin::InterestingEntityIterator *iterator
)
{
SET_AUDIO_RENDERER();
SET_AUDIO_RENDERER_EXECUTE();
Check(this);
AudioRenderer::ExecuteImplementation(complexity_update, iterator);
CalculateMix();
RunningSourceCheckup();
// RunningAudioSourceSort();
DormantSourceCheckup();
CLEAR_AUDIO_RENDERER_EXECUTE();
CLEAR_AUDIO_RENDERER();
}
//
//#############################################################################
// CalculateMix
//#############################################################################
//
void
L4AudioRenderer::CalculateMix()
{
SET_AUDIO_CALCULATE_MIX();
//
//--------------------------------------------------------------------------
// Is this the next calculate mix frame
//--------------------------------------------------------------------------
//
#if 0
if (nextCalculateMixFrame > GetAudioFrameCount())
{
CLEAR_AUDIO_CALCULATE_MIX();
return;
}
nextCalculateMixFrame = GetAudioFrameCount() + DefaultAudioFrameDelay;
#endif
//
//--------------------------------------------------------------------------
// Calculate the compression volume scaling
//
// For each source, calculate the amount to scale its volume by
// according to those sources which have a higher mix presence
//--------------------------------------------------------------------------
//
{
AudioControlValue
compression_scale = 1.0f;
AudioControlValue
compression_effect = 0.0f;
AudioSourceMixPresence
current_mix_presence = MedAudioSourceMixPresence;
MixingSourceIterator
iterator(&mixingAudioSourceSocket);
L4AudioSource
*audio_source;
Check(GetAudioHead());
Scalar comp_scale = GetAudioHead()->GetSourceCompressionScale();
Scalar comp_exp = GetAudioHead()->GetSourceCompressionExponent();
//
// Get mix presence of the first source
//
if ((audio_source = iterator.GetCurrent()) != NULL)
{
Check(audio_source);
current_mix_presence = audio_source->GetAudioSourceMixPresence();
}
//
//-----------------------------------------------------------------------
// For each audio source, calculate the compression scale
//-----------------------------------------------------------------------
//
while ((audio_source = iterator.ReadAndNext()) != NULL)
{
Check(audio_source);
//
//--------------------------------------------------------------------
// If the mix presence of this source is greater then the last
// source then we have stepped to a lower mix presence.
//--------------------------------------------------------------------
//
if (
current_mix_presence !=
audio_source->GetAudioSourceMixPresence()
)
{
Verify(
current_mix_presence <
audio_source->GetAudioSourceMixPresence()
);
current_mix_presence = audio_source->GetAudioSourceMixPresence();
//
// Calculate the compression scale for this mix presence
//
// scale = 1 - ((comp_scale*x)^comp_exp)
//
compression_scale =
1.0f - pow(comp_scale * compression_effect, comp_exp);
Verify(compression_scale >= 0.0f && compression_scale <= 1.0f);
}
//
//--------------------------------------------------------------------
// Set the compression scale for this source
//--------------------------------------------------------------------
//
audio_source->SetVolumeCompressionScale(compression_scale);
//
//--------------------------------------------------------------------
// Calculate the compression effect for this source
//--------------------------------------------------------------------
//
AudioControlValue source_compression_effect;
source_compression_effect =
audio_source->CalculateSourceCompressionEffect();
Verify(
source_compression_effect >= 0.0f &&
source_compression_effect <= 1.0f
);
//
//--------------------------------------------------------------------
// Update the compression effect for this mix presence
//--------------------------------------------------------------------
//
compression_effect =
Max(compression_effect, source_compression_effect);
Verify(compression_effect >= 0.0f && compression_effect <= 1.0f);
#if 0
Tell(
"compression_effect " << compression_effect <<
" : compression_scale " << compression_scale << "\n"
);
#endif
}
}
CLEAR_AUDIO_CALCULATE_MIX();
}
//
//#############################################################################
// RunningSourceCheckup
//#############################################################################
//
void
L4AudioRenderer::RunningSourceCheckup()
{
SET_AUDIO_RUNNING_SOURCES();
//
//--------------------------------------------------------------------------
// Iterate through all running sources
//--------------------------------------------------------------------------
//
ChainOf<L4AudioSource*>
resort_socket(NULL);
RunningSourceIterator
running_iterator(&runningAudioSourceSocket);
L4AudioSource
*audio_source;
while ((audio_source = running_iterator.GetCurrent()) != NULL)
{
Check(audio_source);
//
//-----------------------------------------------------------------------
// If the source does not require maintenance then skip it
//-----------------------------------------------------------------------
//
if (!audio_source->RequiresMaintenance(GetAudioHead()))
{
running_iterator.Next();
continue;
}
//Update object position
//
//-----------------------------------------------------------------------
// If the source is clipped then either stop or suspend
//-----------------------------------------------------------------------
//
if (audio_source->IsAudioSourceClipped(GetAudioHead()))
{
if (audio_source->GetAudioRenderType() == TransientAudioRenderType)
{
AudioSourceStopMaintenance(audio_source);
}
else
{
Verify(
audio_source->GetAudioRenderType() ==
SustainedAudioRenderType
);
AudioSourceSuspendMaintenance(audio_source);
}
continue;
}
//
//-----------------------------------------------------------------------
// If the source is finished playing then stop it
//-----------------------------------------------------------------------
//
if (audio_source->IsFinishedPlaying())
{
AudioSourceStopMaintenance(audio_source);
continue;
}
//
//-----------------------------------------------------------------------
// Execute
//-----------------------------------------------------------------------
//
SET_AUDIO_EXECUTE_SOURCES();
audio_source->Execute();
CLEAR_AUDIO_EXECUTE_SOURCES();
//
//-----------------------------------------------------------------------
// Add to resort list if weight has changed
//-----------------------------------------------------------------------
//
if (
audio_source->CalculateAudioWeighting() !=
running_iterator.GetValue()
)
{
running_iterator.Remove();
resort_socket.Add(audio_source);
}
else
{
running_iterator.Next();
}
}
//
//--------------------------------------------------------------------------
// Sort resort socket back into running socket
//--------------------------------------------------------------------------
//
ChainIteratorOf<L4AudioSource*>
resort_iterator(&resort_socket);
while ((audio_source = resort_iterator.ReadAndNext()) != NULL)
{
Check(audio_source);
Verify(running_iterator.IsPlugMember(audio_source) == False);
runningAudioSourceSocket.AddValue(
audio_source,
audio_source->CalculateAudioWeighting()
);
}
CLEAR_AUDIO_RUNNING_SOURCES();
}
//
//#############################################################################
// DormantSourceCheckup
//#############################################################################
//
void
L4AudioRenderer::DormantSourceCheckup()
{
SET_AUDIO_DORMANT_SOURCES();
//
//--------------------------------------------------------------------------
// Assemble the request socket
//--------------------------------------------------------------------------
//
VChainOf<L4AudioSource*, AudioWeighting>
resume_request_socket(NULL, False);
DormantSourceIterator
dormant_iterator(&dormantAudioSourceSocket);
L4AudioSource
*audio_source;
while ((audio_source = dormant_iterator.ReadAndNext()) != NULL)
{
Check(audio_source);
//
// If the suspend gate is not up then it can not resume
//
if (!audio_source->CanResume(GetAudioHead()))
continue;
//
// If the audio source is clipped then it can not resume
//
if (audio_source->IsAudioSourceClipped(GetAudioHead()))
continue;
//
// Add to request socket
//
resume_request_socket.AddValue(
audio_source,
audio_source->CalculateAudioWeighting()
);
}
//
//--------------------------------------------------------------------------
// Iterate through request socket
//--------------------------------------------------------------------------
//
VChainIteratorOf<L4AudioSource*, AudioWeighting>
resume_request_iterator(&resume_request_socket);
while ((audio_source = resume_request_iterator.GetCurrent()) != NULL)
{
Check(audio_source);
//
// Request audio resources
//
/* AudioChannelSet
channel_set_result;*/
SourceSet *source_result = audio_source->GetAudioChannelSet();
if (!RequestAudioResources(audio_source,
source_result)
)
{
// Audio-dropout fix: hand back any partial acquisition (see
// StartRequest) -- the source stays dormant and retries later.
audio_source->ReleaseChannels();
break;
}
//
// Remove from dormant list and request list
//
PlugIterator remover(audio_source);
remover.RemoveSocket(&dormantAudioSourceSocket);
AudioWeighting audio_weighting = resume_request_iterator.GetValue();
resume_request_iterator.Remove();
//
// Update Spatial model
//
audio_source->UpdateSpatialModel(GetAudioHead());
//
// Set channel set of source
// Source resume implementation
// Set state of source to running
//
audio_source->SetAudioChannelSet(*source_result);
audio_source->ResumeImplementation();
audio_source->AssignAudioSourceState(RunningAudioSourceState);
//
// Put on running socket and mixing socket
//
runningAudioSourceSocket.AddValue(audio_source, audio_weighting);
if (
audio_source->GetAudioSourceMixPresence() !=
ManualAudioSourceMixPresence
)
{
mixingAudioSourceSocket.AddValue(
audio_source,
audio_source->GetAudioSourceMixPresence()
);
}
}
CLEAR_AUDIO_DORMANT_SOURCES();
}
//
//#############################################################################
// AudioSourceStopMaintenance
//#############################################################################
//
void
L4AudioRenderer::AudioSourceStopMaintenance(L4AudioSource *source)
{
Check(source);
Verify(source->GetAudioSourceState() == RunningAudioSourceState);
#if DEBUG_LEVEL>0
{
RunningSourceIterator iterator(&runningAudioSourceSocket);
Verify(iterator.IsPlugMember(source) == True);
}
#endif
source->StopImplementation();
source->AssignAudioSourceState(StoppedAudioSourceState);
source->ReleaseChannels();
PlugIterator remover(source);
remover.RemoveSocket(&runningAudioSourceSocket);
remover.RemoveSocket(&mixingAudioSourceSocket);
#if DEBUG_LEVEL>0
{
RunningSourceIterator iterator(&runningAudioSourceSocket);
Verify(iterator.IsPlugMember(source) == False);
}
#endif
}
//
//#############################################################################
// AudioSourceSuspendMaintenance
//#############################################################################
//
void
L4AudioRenderer::AudioSourceSuspendMaintenance(L4AudioSource *source)
{
Check(source);
Verify(source->GetAudioRenderType() == SustainedAudioRenderType);
Verify(source->GetAudioSourceState() == RunningAudioSourceState);
#if DEBUG_LEVEL>0
{
RunningSourceIterator iterator(&runningAudioSourceSocket);
Verify(iterator.IsPlugMember(source) == True);
}
#endif
source->SuspendImplementation();
source->AssignAudioSourceState(SuspendedAudioSourceState);
source->ReleaseChannels();
PlugIterator remover(source);
remover.RemoveSocket(&runningAudioSourceSocket);
remover.RemoveSocket(&mixingAudioSourceSocket);
#if DEBUG_LEVEL>0
{
RunningSourceIterator iterator(&runningAudioSourceSocket);
Verify(iterator.IsPlugMember(source) == False);
}
#endif
dormantAudioSourceSocket.Add(source);
}
//
//#############################################################################
// RequestAudioResources
//#############################################################################
//
Logical
L4AudioRenderer::RequestAudioResources(
L4AudioSource *audio_source,
SourceSet *source_result
)
{
Check(this);
Check(audio_source);
Check(source_result);
//
//--------------------------------------------------------------------------
// This source must be within the culling volume
//--------------------------------------------------------------------------
//
if (audio_source->IsAudioSourceClipped(GetAudioHead()))
return False;
//
//--------------------------------------------------------------------------
// Request audio channels for this source
//--------------------------------------------------------------------------
//
Logical
resources_available;
source_result = audio_source->GetAudioChannelSet();
Check(source_result);
// #32 census: identify the requester while RequestAudioChannels runs, so a
// failure can be attributed to a component CLASS without threading an
// argument through the virtual (audio is main-thread only).
extern int gBTAudioReqClass, gBTAudioReqVoices;
gBTAudioReqClass = (int)audio_source->GetClassID();
gBTAudioReqVoices = audio_source->GetAudioVoiceCount();
resources_available =
RequestAudioChannels(
audio_source->GetAudioVoiceCount(),
source_result
);
//
//--------------------------------------------------------------------------
// If audio channels are not available then check running sources for
// lower weight sources
//--------------------------------------------------------------------------
//
if (!resources_available)
{
RunningSourceIterator
iterator(&runningAudioSourceSocket);
L4AudioSource
*running_audio_source;
AudioWeighting
audio_source_weighting;
iterator.Last();
running_audio_source = iterator.GetCurrent();
audio_source_weighting = audio_source->CalculateAudioWeighting();
//
//-----------------------------------------------------------------------
// while
// request for channels is not satisfied and
// there exist running sources that have lower weighting
//-----------------------------------------------------------------------
//
while (
!resources_available &&
running_audio_source != NULL &&
audio_source_weighting < iterator.GetValue() // Actual value is inverted
)
{
Check(running_audio_source);
Verify(
running_audio_source->GetAudioSourcePriority() <=
audio_source->GetAudioSourcePriority()
);
//
//--------------------------------------------------------------------
// Collect Statistics
//--------------------------------------------------------------------
//
#ifdef LAB_ONLY
if (
running_audio_source->GetAudioSourcePriority() <
audio_source->GetAudioSourcePriority()
)
{
resourceStealPriorityCount++;
}
else
{
Verify(
running_audio_source->GetAudioSourcePriority() ==
audio_source->GetAudioSourcePriority()
);
Warn(
running_audio_source->CalculateSourceVolumeScale() >=
audio_source->CalculateSourceVolumeScale()
);
resourceStealVolumeCount++;
}
AudioTime duration_cutoff(0.1f);
if (
running_audio_source->GetCurrentRunningTime() <=
duration_cutoff
)
{
stealShortDurationCount++;
}
resourceStealCount++;
#endif
//
//--------------------------------------------------------------------
// If running source is transient
// Then stop the source
// Else suspend the source
//--------------------------------------------------------------------
//
{ // #32 census: a steal ends a RUNNING sound early -- count it
// ungated so field logs show how hard the mixer is fighting.
extern long gBTAudioSteals;
++gBTAudioSteals;
}
if (
running_audio_source->GetAudioRenderType() ==
TransientAudioRenderType
)
{
AudioSourceStopMaintenance(running_audio_source);
}
else
{
Verify(
running_audio_source->GetAudioRenderType() ==
SustainedAudioRenderType
);
AudioSourceSuspendMaintenance(running_audio_source);
}
//
//--------------------------------------------------------------------
// Request resources again
//--------------------------------------------------------------------
//
resources_available =
RequestAudioChannels(
audio_source->GetAudioVoiceCount(),
source_result
);
iterator.Last();
running_audio_source = iterator.GetCurrent();
}
// #32 census: the steal loop is done and the request is STILL
// unsatisfied -- this sound is genuinely dropped. (The raw
// "ACQUIRE FAILED" print fires once per attempt INSIDE the steal loop,
// so its count wildly overstates real drops; this one does not.)
if (!resources_available)
{
extern long gBTAudioTrueDrops;
extern void BTAudioDropTally(int class_ID, int voices);
++gBTAudioTrueDrops;
BTAudioDropTally((int)audio_source->GetClassID(),
audio_source->GetAudioVoiceCount());
}
}
return resources_available;
}
//
// OpenAL source pool (gitea #32) -- defined below, used here.
//
extern Logical BTAudioPoolAcquire(ALuint *out);
extern void BTAudioPoolRelease(ALuint src);
extern int BTAudioPoolSize();
extern int BTAudioPoolFree();
extern long BTAudioPoolReuses();
//
//#############################################################################
// RequestAudioChannels
//#############################################################################
//
Logical
L4AudioRenderer::RequestAudioChannels(
int voices_requested,
SourceSet *source_request
)
{
Check(this);
Check(source_request);
//Do we have enough?
int requested = source_request->count;
// SOURCE-POOL CENSUS (field 2026-07-23: "audio cutting in and out toward
// the end of the match"). OpenAL's mixing capacity is finite; sources
// release only at entity teardown, so long matches accumulate looping
// occupants (wreck burn/smoke) until transients lose the voice fight.
// Track generated/deleted/failed and print a 30s health line + EVERY
// acquisition failure (rare + the smoking gun).
extern long gBTAudioSourcesLive, gBTAudioAcquireFails;
{
static unsigned long s_censusAt = 0;
unsigned long now_ms = GetTickCount();
if (now_ms - s_censusAt > 30000)
{
s_censusAt = now_ms;
extern long gBTAudioSteals, gBTAudioTrueDrops;
DEBUG_STREAM << "[audio] source census: live=" << gBTAudioSourcesLive
<< " pooled=" << BTAudioPoolSize()
<< " free=" << BTAudioPoolFree()
<< " reuses=" << BTAudioPoolReuses()
<< " acquireFails=" << gBTAudioAcquireFails
<< " steals=" << gBTAudioSteals
<< " drops=" << gBTAudioTrueDrops
<< std::endl << std::flush;
extern void BTAudioDropCensus();
BTAudioDropCensus();
}
}
// Gitea #12: hard cap at the SourceSet capacity so alGenSources can never
// write past sources[] (the death-crash heap overflow). count is set from
// the streamed voiceCount and is normally already clamped in the
// L4AudioSource ctor; this guards the acquisition site itself.
if (requested > AUDIO_SOURCESET_CAPACITY)
requested = AUDIO_SOURCESET_CAPACITY;
alGetError();
//
// SOURCE POOLING (gitea #32). This used to alGenSources() here and
// alDeleteSources() in ReleaseSourceSet -- i.e. CREATE and DESTROY OpenAL
// sources per sound event. Every 4.11.674 player log shows the result:
// thousands of acquisition failures each (3031-6571 across five machines),
// beginning ~10% into a match and never recovering, because a combat burst
// churns straight through OpenAL's hard per-context source limit (256).
// The 30s census reading `live=6` while the failure line read `live=256`
// was the tell: the same counter, sampled between bursts -- churn, not a
// steady leak.
//
// Sources are now generated ONCE and recycled through a free list, so
// steady-state demand costs no allocation at all and the driver cap is
// never approached.
//
for (int i = 0; i < requested; i++)
{
if (alIsSource(source_request->sources[i]))
continue; // slot already holds one
ALuint src = 0;
if (!BTAudioPoolAcquire(&src))
{
++gBTAudioAcquireFails;
// Rate-limited: the night-9 field logs carried 6.8k-19k of these per
// player, and the count is NOISE -- the steal loop retries after every
// failed attempt, so lines pile up per EVENT, and most events still
// end in a successful steal. One line per 30s band keeps the signal
// (the census carries the real counters: steals + true drops).
extern int gBTAudioReqClass, gBTAudioReqVoices;
static unsigned long s_failNoteAt = 0;
unsigned long fail_now = GetTickCount();
if (fail_now - s_failNoteAt > 30000)
{
s_failNoteAt = fail_now;
DEBUG_STREAM << "[audio] ACQUIRE FAILED (requested=" << requested
<< " reqClass=" << gBTAudioReqClass
<< " reqVoices=" << gBTAudioReqVoices
<< " live=" << gBTAudioSourcesLive
<< " pooled=" << BTAudioPoolSize()
<< " free=" << BTAudioPoolFree()
<< " fails=" << gBTAudioAcquireFails
<< ") -- saturated this instant; the steal loop decides what plays"
<< std::endl << std::flush;
}
return False;
}
source_request->sources[i] = src;
}
return True;
//
//----------------------------------------------------------------------
// Attempt to allocate channels from audio hardware
//----------------------------------------------------------------------
//
/* AudioChannel *channel;
Logical success = False;
AudioCard *front_card = GetFrontCard();
AudioCard *rear_card = GetRearCard();
Check(front_card);
Check(rear_card);
while (True)
{
if (channel_set_request->IsFrontLeftEnabled())
{
if ((channel =
front_card->RequestAudioChannel(voices_requested)) == NULL)
break;
Check(channel);
channel_set_request->SetFrontLeft(channel);
}
if (channel_set_request->IsFrontRightEnabled())
{
if ((channel =
front_card->RequestAudioChannel(voices_requested)) == NULL)
break;
Check(channel);
channel_set_request->SetFrontRight(channel);
}
if (channel_set_request->IsRearLeftEnabled())
{
if ((channel =
rear_card->RequestAudioChannel(voices_requested)) == NULL)
break;
Check(channel);
channel_set_request->SetRearLeft(channel);
}
if (channel_set_request->IsRearRightEnabled())
{
if ((channel =
rear_card->RequestAudioChannel(voices_requested)) == NULL)
break;
Check(channel);
channel_set_request->SetRearRight(channel);
}
success = True;
break;
}
//
//----------------------------------------------------------------------
// If did not succeed then release allocated channels
//----------------------------------------------------------------------
//
if (!success)
{
channel_set_request->ReleaseAll();
return False;
}
//
//----------------------------------------------------------------------
// else, return allocated channels
//----------------------------------------------------------------------
//
return True;*/
}
long gBTAudioSourcesLive = 0;
long gBTAudioAcquireFails = 0;
//
// #32 census v2. The night-9 field logs proved the FIRST generation of these
// counters mislead under pressure: "ACQUIRE FAILED ... free=0" reads as
// permanent retention, but free is 0 at the instant of any failed acquire BY
// DEFINITION -- the 30s census showed free back at ~230 between bursts, i.e.
// the pool cycles and the mixer is simply saturated DURING combat. (Same
// counter-sampling trap as the original live=256-vs-live=6, second offence.)
// These count what actually matters:
// gBTAudioSteals -- a running sound was ended early to service a new one
// gBTAudioTrueDrops -- the steal loop ran dry and the sound NEVER PLAYED
// plus a per-component-class tally of the dropped, so the next field log names
// the class that saturates the mixer instead of leaving it to inference.
//
int gBTAudioReqClass = -1;
int gBTAudioReqVoices = 0;
long gBTAudioSteals = 0;
long gBTAudioTrueDrops = 0;
struct BTAudioDropBin { int classID; int voices; long count; };
static BTAudioDropBin gBTAudioDropBins[12];
static int gBTAudioDropBinCount = 0;
void BTAudioDropTally(int class_ID, int voices)
{
for (int i = 0; i < gBTAudioDropBinCount; ++i)
{
if (gBTAudioDropBins[i].classID == class_ID
&& gBTAudioDropBins[i].voices == voices)
{
++gBTAudioDropBins[i].count;
return;
}
}
if (gBTAudioDropBinCount < 12)
{
gBTAudioDropBins[gBTAudioDropBinCount].classID = class_ID;
gBTAudioDropBins[gBTAudioDropBinCount].voices = voices;
gBTAudioDropBins[gBTAudioDropBinCount].count = 1;
++gBTAudioDropBinCount;
}
}
// One line under the 30s census, only when something was dropped since boot:
// which component classes lost sounds, and how many.
void BTAudioDropCensus()
{
if (gBTAudioDropBinCount == 0)
return;
DEBUG_STREAM << "[audio] dropped by class:";
for (int i = 0; i < gBTAudioDropBinCount; ++i)
DEBUG_STREAM << " {class " << gBTAudioDropBins[i].classID
<< " x" << gBTAudioDropBins[i].voices
<< "v: " << gBTAudioDropBins[i].count << "}";
DEBUG_STREAM << std::endl << std::flush;
}
//#############################################################################
// OpenAL SOURCE POOL (gitea #32)
//#############################################################################
//
// The renderer used to create an AL source per sound event and destroy it on
// release. OpenAL sources are a scarce driver resource -- OpenAL Soft caps a
// context at 256 -- and creating them is not cheap, so combat bursts ran the
// context dry. Field evidence (4.11.674, five machines): 3031-6571 acquisition
// failures per player, first failure ~10% into a match, still failing at 97%.
//
// Sources are now generated once, on demand, up to kAudioPoolCap and then
// RECYCLED: release scrubs the source and returns it to the free list instead
// of deleting it. Steady-state play performs no AL allocation at all.
//
// THE SCRUB IS LOAD-BEARING. A recycled source carries whatever the previous
// owner set on it, and the engine sets AL_LOOPING per sound (L4AUDLVL.cpp:327).
// Hand a looping source to a one-shot and it plays forever -- exactly the
// "sound stuck looping" family (#51, #5). Every reusable property is reset
// here, at the single point where sources change owner.
//
// Not locked: the audio renderer runs on the main thread (only the network RX
// socket has its own). If that ever changes, this needs a mutex.
//
// The pool's ceiling. DEFAULT: just under OpenAL Soft's 256-source context
// default. BT_AUDIO_SOURCES=<n> raises the AL context budget at Initialize
// (above); the pool cap now FOLLOWS it -- before this, the env raised the
// context and the pool still stopped at 240, so the experiment was impossible
// to run in the field. kAudioPoolMax bounds the static free-list array.
static const int kAudioPoolMax = 1024;
static int kAudioPoolCap = 240;
static int BTAudioPoolCapResolve()
{
static int s_done = 0;
if (!s_done)
{
s_done = 1;
const char *sv = getenv("BT_AUDIO_SOURCES");
if (sv != 0)
{
int n = atoi(sv);
if (n >= 64 && n <= 4096)
{
// stay under the context grant with a small reserve
kAudioPoolCap = (n - 16 < kAudioPoolMax) ? (n - 16) : kAudioPoolMax;
DEBUG_STREAM << "[audio] source pool cap follows BT_AUDIO_SOURCES: "
<< kAudioPoolCap << std::endl << std::flush;
}
}
}
return kAudioPoolCap;
}
//
// PEAK DEMAND is set by how many audio COMPONENTS are alive, not by how many
// sounds are audible: each component reserves a SourceSet of up to
// AUDIO_SOURCESET_CAPACITY (25) voices and holds them until it is released.
// Measured high-water: 138 solo vs one enemy, 149 across two nodes. That grows
// with player count, so the cap is deliberately near the driver ceiling and the
// pool reports its high-water mark once, to size this from FIELD logs rather
// than from a guess. Growth also stops on its own if a driver offers fewer
// sources than the cap -- alGenSources failing simply ends growth and the pool
// recycles what it has.
static ALuint gAudioPoolFree[kAudioPoolMax];
static int gAudioPoolFreeCount = 0; // entries in gAudioPoolFree
static int gAudioPoolTotal = 0; // sources ever generated (<= cap)
static long gAudioPoolReuses = 0; // diagnostics
int BTAudioPoolSize() { return gAudioPoolTotal; }
int BTAudioPoolFree() { return gAudioPoolFreeCount; }
long BTAudioPoolReuses() { return gAudioPoolReuses; }
//
// Reset a source to a known-neutral state so nothing carries across owners.
//
static void
BTAudioScrubSource(ALuint src)
{
ALint state = AL_STOPPED;
alGetSourcei(src, AL_SOURCE_STATE, &state);
if (state == AL_PLAYING || state == AL_PAUSED)
alSourceStop(src);
alSourcei(src, AL_BUFFER, 0); // detach (no queued buffers here)
alSourcei(src, AL_LOOPING, AL_FALSE); // <-- the stuck-loop guard
alSourcef(src, AL_GAIN, 1.0f);
alSourcef(src, AL_PITCH, 1.0f);
alSourcei(src, AL_SOURCE_RELATIVE, AL_FALSE);
alSource3f(src, AL_POSITION, 0.0f, 0.0f, 0.0f);
alSource3f(src, AL_VELOCITY, 0.0f, 0.0f, 0.0f);
alSourcef(src, AL_MIN_GAIN, 0.0f);
alSourcef(src, AL_MAX_GAIN, 1.0f);
alGetError(); // swallow any property complaint
}
//
// Hand out a source: recycle first, generate only when the pool has never been
// that large. False = genuinely out (peak concurrent demand exceeded the cap).
//
Logical
BTAudioPoolAcquire(ALuint *out)
{
Check_Pointer(out);
while (gAudioPoolFreeCount > 0)
{
ALuint src = gAudioPoolFree[--gAudioPoolFreeCount];
if (alIsSource(src)) // paranoia: a context reset invalidates names
{
++gAudioPoolReuses;
*out = src;
return True;
}
--gAudioPoolTotal; // stale name: forget it
}
if (gAudioPoolTotal >= BTAudioPoolCapResolve())
return False;
ALuint src = 0;
alGetError();
alGenSources(1, &src);
if (alGetError() != AL_NO_ERROR || !alIsSource(src))
return False; // driver said no before our cap
++gAudioPoolTotal;
++gBTAudioSourcesLive;
{
// One line per new high-water band, so a field log shows how close a
// real match gets to the ceiling without spamming.
static int s_notified = 0;
if (gAudioPoolTotal >= s_notified + 25)
{
s_notified = gAudioPoolTotal;
DEBUG_STREAM << "[audio] source pool high-water: " << gAudioPoolTotal
<< " of " << kAudioPoolCap << std::endl << std::flush;
}
}
*out = src;
return True;
}
//
// Take a source back. Scrubbed and parked, NOT deleted.
//
void
BTAudioPoolRelease(ALuint src)
{
if (!alIsSource(src))
return;
BTAudioScrubSource(src);
if (gAudioPoolFreeCount < kAudioPoolMax)
{
gAudioPoolFree[gAudioPoolFreeCount++] = src;
return;
}
alDeleteSources(1, &src); // cannot happen (max == array size)
--gAudioPoolTotal;
--gBTAudioSourcesLive;
}
void L4AudioRenderer::ReleaseSourceSet(SourceSet &sourceSet)
{
// Audio-dropout fix: the old bulk alDeleteSources(count, sources) is
// ATOMIC per the AL spec -- ONE invalid name in the array (an empty slot
// of a partial set, or the old -1 sentinel on a double release) and
// NOTHING is deleted. After the first pool-exhaustion event every
// partial release leaked its real sources and the pool never recovered
// ("audio cutting in and out toward the end of the match"). Delete each
// valid source individually and park the slot at 0 (never a valid name).
// SOURCE POOLING (#32): hand each source back to the free list instead of
// destroying it. BTAudioPoolRelease stops it, detaches its buffer and
// clears AL_LOOPING before parking it, so the next owner starts clean.
for (int i = 0; i < sourceSet.count; i++)
{
if (sourceSet.sources[i] != 0)
BTAudioPoolRelease(sourceSet.sources[i]);
sourceSet.sources[i] = 0; // 0 is never a valid AL name
}
}
//~~~~~~~~~~~~~~~~~~~~~~ L4AudioRenderer profile bits ~~~~~~~~~~~~~~~~~~~~~~~~~
#if defined(TRACE_AUDIO_RENDERER_RUNNING_SOURCES)
BitTrace Audio_Renderer_Running_Sources("Audio Renderer Running Sources");
#endif
#if defined(TRACE_AUDIO_RENDERER_RUNNING_SORT)
BitTrace Audio_Renderer_Running_Sort("Audio Renderer Running Sort");
#endif
#if defined(TRACE_AUDIO_RENDERER_CALCULATE_MIX)
BitTrace Audio_Renderer_Calculate_Mix("Audio Renderer Calculate Mix");
#endif
#if defined(TRACE_AUDIO_RENDERER_EXECUTE_SOURCES)
BitTrace Audio_Renderer_Execute_Sources("Audio Renderer Execute Sources");
#endif
#if defined(TRACE_AUDIO_RENDERER_DORMANT_SOURCES)
BitTrace Audio_Renderer_Dormant_Sources("Audio Renderer Dormant Sources");
#endif
//
//#############################################################################
// Master-volume runtime control (issue #26, 2026-07-23). The pod had a
// physical volume knob on the operator side; desktop players had NO way to
// duck the (hot) game audio under voice chat short of the Windows mixer.
// The -/= keys step the OpenAL listener gain (the master scale for every
// source); the value persists in content\volume.cfg (CWD) and reloads at
// the next boot (BT_AUDIO_VOLUME env still wins when set).
//#############################################################################
//
float gBTMasterVolume = 0.6f;
void BTAudioMasterVolumeStep(int direction)
{
gBTMasterVolume += (direction > 0) ? 0.05f : -0.05f;
if (gBTMasterVolume < 0.0f) gBTMasterVolume = 0.0f;
if (gBTMasterVolume > 1.5f) gBTMasterVolume = 1.5f;
alListenerf(AL_GAIN, gBTMasterVolume);
FILE *cfg = fopen("volume.cfg", "wt");
if (cfg != NULL)
{
fprintf(cfg, "%.2f\n", gBTMasterVolume);
fclose(cfg);
}
DEBUG_STREAM << "[audio] master volume "
<< (int)(gBTMasterVolume * 100.0f + 0.5f) << "%" << std::endl << std::flush;
}