Files
BT411/engine/MUNGA_L4/L4AUDRND.cpp
T
Joe DiPrimaandClaude Fable 5 7e57816d39 audio cut out in firefights because nobody ever asked OpenAL for more than 256 voices
Field report from 2026-07-23 was "audio cutting in and out toward the end of the
match". The night-5 logs say it precisely: 2017 ACQUIRE FAILED lines in a single
match, and every last one of them at live=256.

It is not a leak, which is where I started and where the code comments still
pointed. The source census sits at a healthy 45-60 live, spikes during
firefights, and drains right back down afterwards -- 226 back to 42 in one
window -- so the engine's priority steal loop is doing exactly what it should:
AudioSourceStop/SuspendMaintenance -> ReleaseChannels -> ReleaseSourceSet ->
alDeleteSources, with the live counter following it down. The 2026-07-23 fix
(a999e5c, deleting sources one at a time instead of the spec-atomic bulk call)
was real and is what makes that drain work. It just was not the ceiling.

The ceiling was ours. MakeAudioRenderer called alcCreateContext(device, NULL) --
no attribute list at all -- so OpenAL Soft applied its default budget of 256
mono sources. That number has nothing to do with the 1995 audio hardware; it is
just what you get for not asking. Meanwhile a busy match logs about 7200
explosions, each spawning three DPLIndependantEffect voices, so the pool pins at
the cap and every acquire during that window fails outright and drops its sound.
The peak we actually observed was 226 against a 256 cap, i.e. the bursts were
already scraping the ceiling.

So ask. ALC_MONO_SOURCES at 1024 by default, BT_AUDIO_SOURCES to override for
A/B testing, and read back what the driver GRANTED rather than assuming the
request was honoured. That last part earns its keep: asking for 64 grants 240,
because OpenAL Soft has a floor of its own -- which is also the reason the NULL
default landed on 256 in the first place.

  requested 1024 -> granted 1024
  requested 2048 -> granted 2048
  requested   64 -> granted  240   (driver floor)

Costs mixing headroom, not hardware voices -- OpenAL Soft mixes in software and
only touches voices that are actually sounding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 16:24:50 -05:00

1571 lines
43 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.
//
// Ask for a budget that fits the content. OpenAL Soft mixes in software,
// so this costs mixing headroom, not hardware voices, and it clamps to what
// it can honour -- hence the read-back below rather than an assumption.
// BT_AUDIO_SOURCES overrides for A/B testing.
ALCint monoWanted = 1024;
if (const char *sv = getenv("BT_AUDIO_SOURCES"))
{
int n = atoi(sv);
if (n >= 64 && n <= 4096)
monoWanted = n;
}
ALCint attrs[5];
attrs[0] = ALC_MONO_SOURCES; attrs[1] = monoWanted;
attrs[2] = ALC_STEREO_SOURCES; attrs[3] = 16;
attrs[4] = 0;
ALCcontext *context = alcCreateContext(device, attrs);
alcMakeContextCurrent(context);
// Report what the driver actually GRANTED -- a request is not a promise.
{
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
<< " 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);
//
//--------------------------------------------------------------------------
// 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)
{
// 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);
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
//--------------------------------------------------------------------
//
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();
}
}
return resources_available;
}
//
//#############################################################################
// 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;
DEBUG_STREAM << "[audio] source census: live=" << gBTAudioSourcesLive
<< " acquireFails=" << gBTAudioAcquireFails
<< std::endl << std::flush;
}
}
// 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;
bool failed = true;
alGetError();
for (int i = 0; i < requested; i++)
{
if (!alIsSource(source_request->sources[i]))
{
alGenSources(1, source_request->sources + i);
if (alIsSource(source_request->sources[i]))
++gBTAudioSourcesLive;
}
}
ALenum error = alGetError();
if (error == AL_NO_ERROR)
{
failed = false;
}
if (failed)
{
++gBTAudioAcquireFails;
DEBUG_STREAM << "[audio] ACQUIRE FAILED (requested=" << requested
<< " live=" << gBTAudioSourcesLive
<< " fails=" << gBTAudioAcquireFails
<< ") -- the pool is exhausted; expect dropouts"
<< std::endl << std::flush;
return False;
}
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;
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).
extern long gBTAudioSourcesLive;
for (int i = 0; i < sourceSet.count; i++)
{
if (alIsSource(sourceSet.sources[i]))
{
ALint state = AL_STOPPED;
alGetSourcei(sourceSet.sources[i], AL_SOURCE_STATE, &state);
if (state == AL_PLAYING)
alSourceStop(sourceSet.sources[i]);
alDeleteSources(1, sourceSet.sources + i);
--gBTAudioSourcesLive;
}
sourceSet.sources[i] = 0;
}
}
//~~~~~~~~~~~~~~~~~~~~~~ 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;
}