Files
RP412/MUNGA_L4/L4AUDEFX.cpp
T
CydandClaude Opus 5 e133d4c993 Sounds recycle their voices instead of churning through them
Recovering the soundbanks took voice demand per sound from about one zone to
about two and a half, and the audio path allocated an OpenAL source for every
sound event and destroyed it again on release. Sources are a hard
per-context resource - this driver grants 256 - so that churn doubled at
exactly the moment it got more expensive. Sources are now generated once and
recycled through a free list: measured, three sources generated across
twelve thousand acquisitions.

The BT tree reached the same conclusion the expensive way, from field logs
full of failed acquisitions: raising the source budget is not the fix,
because the ceiling also acts as a governor and more voices mixing is real
CPU during exactly the busiest moments. Recycling is the fix, and it costs
nothing.

Two older bugs were sitting underneath, both reproduced against the driver
rather than assumed:

Releasing a set leaked it. alDeleteSources is atomic - one bad name in the
array and nothing at all is deleted. ReleaseSourceSet handed it the whole
fixed-size array and then parked the slots at -1, so any partial set, and
any double release, leaked every source it held. Sources are now handed back
one at a time and slots park at 0, which is never a valid name.

A source set began life uninitialised. The constructor set only the count,
and the acquire path decided whether a slot was already filled by asking
OpenAL about uninitialised stack garbage. Garbage that happened to match a
live name meant two sounds silently sharing one source. Pooling would have
made that more likely, not less, since it keeps small names in circulation.

Recycled sources are scrubbed before parking - stopped, buffer detached,
looping, gain, pitch, relative flag, position and velocity reset, and the
EFX filter and reverb send dropped. Without that last part a dry cockpit
sound inherits the wet send of whatever 3D source held the name before it.
Verified: a deliberately dirtied source comes back clean.

Builds clean. Runs with memory and handle count flat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 23:33:45 -05:00

146 lines
4.6 KiB
C++

//###########################################################################
//
// L4AUDEFX.cpp -- OpenAL EFX bridge (docs/SOUND.md, findings F9 and F11).
// See L4AUDEFX.h for the fidelity rationale.
//
//###########################################################################
#include "mungal4.h"
#pragma hdrstop
#include "l4audefx.h"
#include "openal/alc.h"
#include "openal/efx.h"
#ifndef AL_EFFECT_EAXREVERB
#define AL_EFFECT_EAXREVERB 0x8000 // newer efx.h constant; OpenAL Soft supports it
#endif
namespace
{
bool s_available = false;
ALuint s_reverbSlot = 0;
ALuint s_reverbEffect = 0;
ALuint s_scratchFilter = 0;
LPALGENEFFECTS p_alGenEffects = 0;
LPALEFFECTI p_alEffecti = 0;
LPALEFFECTF p_alEffectf = 0;
LPALGENAUXILIARYEFFECTSLOTS p_alGenAuxiliaryEffectSlots = 0;
LPALAUXILIARYEFFECTSLOTI p_alAuxiliaryEffectSloti = 0;
LPALAUXILIARYEFFECTSLOTF p_alAuxiliaryEffectSlotf = 0;
LPALGENFILTERS p_alGenFilters = 0;
LPALFILTERI p_alFilteri = 0;
LPALFILTERF p_alFilterf = 0;
}
bool EFX_Available()
{
return s_available;
}
bool EFX_Initialize(float global_reverb_scale)
{
ALCcontext *context = alcGetCurrentContext();
if (context == 0)
{
return false;
}
ALCdevice *device = alcGetContextsDevice(context);
if (device == 0 || !alcIsExtensionPresent(device, "ALC_EXT_EFX"))
{
Tell("L4AUDEFX: ALC_EXT_EFX not present - filters and reverb inert\n");
return false;
}
p_alGenEffects = (LPALGENEFFECTS)alGetProcAddress("alGenEffects");
p_alEffecti = (LPALEFFECTI)alGetProcAddress("alEffecti");
p_alEffectf = (LPALEFFECTF)alGetProcAddress("alEffectf");
p_alGenAuxiliaryEffectSlots = (LPALGENAUXILIARYEFFECTSLOTS)alGetProcAddress("alGenAuxiliaryEffectSlots");
p_alAuxiliaryEffectSloti = (LPALAUXILIARYEFFECTSLOTI)alGetProcAddress("alAuxiliaryEffectSloti");
p_alAuxiliaryEffectSlotf = (LPALAUXILIARYEFFECTSLOTF)alGetProcAddress("alAuxiliaryEffectSlotf");
p_alGenFilters = (LPALGENFILTERS)alGetProcAddress("alGenFilters");
p_alFilteri = (LPALFILTERI)alGetProcAddress("alFilteri");
p_alFilterf = (LPALFILTERF)alGetProcAddress("alFilterf");
if (!p_alGenEffects || !p_alEffecti || !p_alEffectf
|| !p_alGenAuxiliaryEffectSlots || !p_alAuxiliaryEffectSloti || !p_alAuxiliaryEffectSlotf
|| !p_alGenFilters || !p_alFilteri || !p_alFilterf)
{
Tell("L4AUDEFX: EFX entry points missing - filters and reverb inert\n");
return false;
}
alGetError();
p_alGenAuxiliaryEffectSlots(1, &s_reverbSlot);
p_alGenEffects(1, &s_reverbEffect);
if (alGetError() != AL_NO_ERROR)
{
return false;
}
//
// EAXReverb where available (OpenAL Soft: yes), plain reverb otherwise.
//
p_alEffecti(s_reverbEffect, AL_EFFECT_TYPE, AL_EFFECT_EAXREVERB);
if (alGetError() != AL_NO_ERROR)
{
p_alEffecti(s_reverbEffect, AL_EFFECT_TYPE, AL_EFFECT_REVERB);
}
p_alAuxiliaryEffectSloti(s_reverbSlot, AL_EFFECTSLOT_EFFECT, (ALint)s_reverbEffect);
//
// The authentic wet level: the original sent CC91 = global_reverb_scale on
// every 3D channel, so one global slot gain reproduces the same uniform
// send. RP authors 0.35 (AUDIO.INI); BT used 0.3.
//
p_alAuxiliaryEffectSlotf(s_reverbSlot, AL_EFFECTSLOT_GAIN,
(global_reverb_scale < 0.0f) ? 0.0f :
(global_reverb_scale > 1.0f) ? 1.0f : global_reverb_scale);
p_alGenFilters(1, &s_scratchFilter);
p_alFilteri(s_scratchFilter, AL_FILTER_TYPE, AL_FILTER_LOWPASS);
s_available = (alGetError() == AL_NO_ERROR);
Tell("L4AUDEFX: " << (s_available ? "ready" : "failed")
<< " (reverb slot gain " << global_reverb_scale << ")\n");
return s_available;
}
void EFX_SetSourceLowpassGainHF(ALuint source, float gainhf)
{
if (!s_available)
{
return;
}
if (gainhf < 0.001f) gainhf = 0.001f;
if (gainhf > 1.0f) gainhf = 1.0f;
//
// Filter parameters are COPIED at attach time, so one scratch filter object
// serves every source -- no per-source filter allocation is needed.
//
p_alFilterf(s_scratchFilter, AL_LOWPASS_GAIN, 1.0f);
p_alFilterf(s_scratchFilter, AL_LOWPASS_GAINHF, gainhf);
alSourcei(source, AL_DIRECT_FILTER, (ALint)s_scratchFilter);
}
void EFX_AttachReverbSend(ALuint source)
{
if (!s_available)
{
return;
}
alSource3i(source, AL_AUXILIARY_SEND_FILTER, (ALint)s_reverbSlot, 0, AL_FILTER_NULL);
}
void EFX_ClearSourceEffects(ALuint source)
{
if (!s_available)
{
return;
}
alSourcei(source, AL_DIRECT_FILTER, AL_FILTER_NULL);
alSource3i(source, AL_AUXILIARY_SEND_FILTER, AL_EFFECTSLOT_NULL, 0, AL_FILTER_NULL);
alGetError(); // swallow any property complaint
}