Files
RP412/MUNGA_L4/L4AUDEFX.h
T
CydandClaude Opus 5 523f713a30 Volume and bass knobs, for players without an amplifier
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>
2026-08-05 23:45:29 -05:00

74 lines
3.1 KiB
C

#pragma once
//###########################################################################
//
// L4AUDEFX.h -- OpenAL EFX bridge for the authored filter/reverb chains
// (docs/SOUND.md, findings F9 and F11).
//
// The original drove the AWE32's initial-filter-cutoff NRPN (21) every frame
// -- brightness x the distance high-frequency rolloff -- and sent CC91 reverb
// on the 3D channels (global_reverb_scale=0.35 in RP's AUDIO.INI) while
// keeping the cockpit DirectPatch channels dry. The OpenAL port computed
// both and applied neither: GetHighFreqCutoffScale() had no callers at all
// and every CC91 send site sat inside a comment block, so RP played
// spectrally full-bright at every distance and bone-dry everywhere.
//
// This bridge reproduces both through OpenAL Soft's EFX extension: one
// EAXReverb auxiliary slot plus a scratch AL_FILTER_LOWPASS whose parameters
// are copied at attach time. Without ALC_EXT_EFX it stays inert and every
// entry point below is a no-op, so the game still runs on a bare OpenAL.
//
//###########################################################################
#include "openal/al.h"
//
// Load the EFX entry points, create the reverb slot (gain = the authored
// global_reverb_scale) and the scratch lowpass. Call once, with the AL
// context current. Returns false (and stays inert) without ALC_EXT_EFX.
//
bool EFX_Initialize(float global_reverb_scale);
bool EFX_Available();
//
// Per-frame direct-path filter: gainhf is the linear high-frequency gain at the
// EFX 5 kHz reference, carrying the authored brightness x distance model.
// Callers map the AWE cutoff through EFX_CutoffScaleToGainHF below.
//
// At unity the filter is detached rather than attached at no-op settings.
//
// NOTE: this is a LOWPASS and can only ever be one. The OpenAL this game ships
// (Creative's) implements no other filter type -- see L4AUDEFX.cpp -- so the
// bass trim could not ride here as a bandpass GAINLF and lives in the resource
// loader instead (RPApplyBassTrim, L4AUDRES.cpp).
//
void EFX_SetSourceLowpassGainHF(ALuint source, float gainhf);
//
// AWE NRPN 21 curve -> EFX gainhf. cutoff_scale is [0,1] of the 100-8000 Hz
// span; approximated as the attenuation of a 2-pole lowpass at the 5 kHz
// reference. Curve shape is approximate, endpoints exact.
//
inline float EFX_CutoffScaleToGainHF(float cutoff_scale)
{
if (cutoff_scale < 0.0f) cutoff_scale = 0.0f;
if (cutoff_scale > 1.0f) cutoff_scale = 1.0f;
float cutoff_hz = 100.0f + cutoff_scale * 7900.0f;
float g = (cutoff_hz / 5000.0f) * (cutoff_hz / 5000.0f);
return (g > 1.0f) ? 1.0f : ((g < 0.001f) ? 0.001f : g);
}
//
// Wet-exterior routing: attach the source's auxiliary send to the reverb slot
// (Dynamic3D / Static3D). Direct cockpit sources stay dry.
//
void EFX_AttachReverbSend(ALuint source);
//
// Drop both the direct-path filter and the reverb send. Required when a source
// is recycled through the pool: without it a dry cockpit sound can inherit the
// wet send of the 3D source that used the name before it, and a full-bright
// source can inherit a distant source's lowpass.
//
void EFX_ClearSourceEffects(ALuint source);