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>
This commit is contained in:
Cyd
2026-08-05 23:45:29 -05:00
co-authored by Claude Opus 5
parent e133d4c993
commit 523f713a30
8 changed files with 256 additions and 15 deletions
+52
View File
@@ -508,6 +508,58 @@ inherit the wet send of the 3D source that held the name before it. Verified: a
source deliberately dirtied then released comes back with looping=0, gain=1.0,
pitch=1.0, relative=0.
## 9a. Tuning knobs — standing in for hardware the pod had
The cabinets ran the game at **unity gain** and did all their volume and tone
shaping 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 why AUDIO.INI has
no level control: the operator turned a knob on an amp.
A desktop player has neither, so the port has to provide them. Two env vars,
both documented in `environ.ini`, both defaulting to leaving the mix exactly as
the pod played it:
| Knob | Stands in for | Range | Default |
|---|---|---|---|
| `RP412AUDIOVOLUME` | the amplifier's volume | 0.0 4.0 | 1.0 (unity, as the pod ran) |
| `RP412AUDIOBASS` | the crossover's low band | 0.0 1.0 | 1.0 (as authored) |
`RP412AUDIOVOLUME` is a straight `alListenerf(AL_GAIN, …)` at renderer init.
There was no listener gain call at all before, so the default is a genuine
no-op.
`RP412AUDIOBASS` is **not** an EFX filter, and the reason is worth recording:
**the OpenAL this game ships implements only `AL_FILTER_LOWPASS`.** It is
Creative's (installed by `oalinst.exe`; the renderer reports "Generic Software"),
not OpenAL Soft, and it rejects both `AL_FILTER_HIGHPASS` and
`AL_FILTER_BANDPASS` — verified on the build machine. A bandpass would have been
the neat answer, carrying the authored brightness model on `GAINHF` and the trim
on `GAINLF` across the one direct filter a source gets. It is not available.
So the trim scales sample data as buffers load. That works because of *how* RP's
low end is built: the weight sits in discrete deep layer zones whose per-zone
tuning bakes out to a very low playback rate — 13 zones below 8 kHz, 3.4 to 5.2
octaves below their recorded pitch, against 81% of the set at 22 kHz and above.
A zone's baked rate is a reliable proxy for which band it occupies, so
attenuating the low-rate zones is a real low-band trim rather than a blunt
overall cut. The ramp is untouched at/above 22050 Hz, full trim at/below
5512 Hz, log-interpolated between. At `0.7` that is 3.1 dB on the deepest
layers, 1.4 dB at 11 kHz, nothing from 22 kHz up.
**A caution for anyone extending the EFX work:** `EFX_Initialize` reads
`alGetError()` after configuring the scratch filter, so asking for a filter type
this driver does not support leaves an error pending and takes the *whole*
bridge down with it — reverb included. That is not hypothetical; it is exactly
what the bandpass attempt did before the filter-type probe caught it.
**AUDIO.INI is also a live mixing desk now**, for the first time since 1995 —
those constants used to be computed and discarded. `global_reverb_scale` is the
wet amount, `amplitude_rolloff`/`_knee`/`_distance_scale` set how loud distant
things are, `high_frequency_rolloff*` how dull, `compression_*` the ducking,
`clipping_radius` the cull. Read once at init. Editing it diverges from the
authored 1995 values, which is a real cost — it is byte-identical to the
shipping original today.
## 10. A recovery path, in order
1. ~~**Engine-side fidelity first.**~~ **Done (2026-08-05).** F3, F4, F9, F10,