The cabinets ran the game at unity and shaped volume and tone outside it, in an external amplifier and a 3-way crossover. That is why there is no master volume anywhere in the original code and none in AUDIO.INI - an operator turned a knob on an amp. A desktop player has no amp and no crossover, and the recovered soundbanks are a good deal livelier than what 4.12 shipped with, so the game has to offer the two controls the pod got from hardware. RP412AUDIOVOLUME, 0.0 to 4.0, is the amplifier: a listener gain, which the port had never set at all. RP412AUDIOBASS, 0.0 to 1.0, is the crossover's low band. Both default to leaving the mix exactly as the pod played it, so neither changes anything for anyone who does not go looking. The bass trim is not a filter, and the reason is worth writing down: the OpenAL we ship is Creative's, not OpenAL Soft, and it implements only AL_FILTER_LOWPASS. It rejects highpass and bandpass outright. A bandpass would have been the tidy answer, carrying the authored brightness model on GAINHF and the trim on GAINLF across the single direct filter a source gets. It is not on offer. So the trim scales sample data as it loads, which suits how this low end is actually built: the weight lives in discrete deep layer zones whose per-zone tuning bakes out to a very low playback rate - thirteen zones below 8kHz, three to five octaves under their recorded pitch, against four fifths of the set at 22kHz and up. Baked rate is a dependable proxy for band, so pulling down the low-rate zones is a real low-band trim and not a blunt cut. It eases in below 22kHz and reaches full depth at 5.5kHz. Caught while building this, and the reason for the probe: EFX_Initialize checks alGetError after configuring the scratch filter, so asking for a filter type the driver refuses leaves an error pending and takes the entire bridge down - reverb included. The bandpass attempt did precisely that and would have silently killed the reverb and brightness work. Initialize now survives losing the filter and says so. Builds clean, runs with both knobs set and with neither. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
178 lines
5.7 KiB
C++
178 lines
5.7 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);
|
|
|
|
//
|
|
// LOWPASS only, deliberately. A bandpass would have been convenient -- one
|
|
// direct filter carrying both the authored brightness model and a bass trim
|
|
// -- but the OpenAL this game ships (Creative's, via oalinst.exe; renderer
|
|
// reports "Generic Software") implements ONLY AL_FILTER_LOWPASS. It rejects
|
|
// both HIGHPASS and BANDPASS, verified on the build machine. Asking for one
|
|
// leaves an error pending, which the check below would read as total EFX
|
|
// failure and silently take the reverb down with it.
|
|
//
|
|
p_alGenFilters(1, &s_scratchFilter);
|
|
p_alFilteri(s_scratchFilter, AL_FILTER_TYPE, AL_FILTER_LOWPASS);
|
|
|
|
if (alGetError() != AL_NO_ERROR)
|
|
{
|
|
//
|
|
// No usable direct filter. The reverb slot above is independent of it,
|
|
// so keep the bridge alive and just make the filter path a no-op rather
|
|
// than losing F11 as well.
|
|
//
|
|
s_scratchFilter = 0;
|
|
Tell("L4AUDEFX: no lowpass filter available - brightness path inert\n");
|
|
}
|
|
|
|
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 || s_scratchFilter == 0)
|
|
{
|
|
return;
|
|
}
|
|
if (gainhf < 0.001f) gainhf = 0.001f;
|
|
if (gainhf > 1.0f) gainhf = 1.0f;
|
|
|
|
//
|
|
// Nothing to do at unity -- detach rather than attach a filter that would
|
|
// only cost mixing work to achieve nothing.
|
|
//
|
|
if (gainhf >= 0.999f)
|
|
{
|
|
alSourcei(source, AL_DIRECT_FILTER, AL_FILTER_NULL);
|
|
alGetError();
|
|
return;
|
|
}
|
|
|
|
//
|
|
// 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);
|
|
alGetError();
|
|
}
|
|
|
|
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
|
|
}
|