Red Planet's original AWE32 soundbanks are back in the tree, and the game's sound effects are now generated from them instead of from an incomplete one-off extraction. AUDIO1.RES and AUDIO2.RES come from the 1996 release in the TeslaRel410 archive, hash-identical. AUDIO.INI has named them all along - they were simply never carried into the port. tools/rp_sf2extract.py reads them and regenerates both the WAV set and RP_L4/WTPresets.cpp, so the assets are reproducible from the banks rather than hand-maintained. Two things were wrong with the old set: Pitch. Every shipped WAV was flat 44100 Hz with the banks' tuning discarded, so 202 of the 219 zones played at the wrong speed - the worst by nine semitones. The EMU8000's per-zone root key and tuning are now baked into each file's declared sample rate, which is exact and needs no engine change. Layers that were meant to be deep now are: a collision sub-thud that lasted 18 milliseconds at the wrong rate is a 0.66 second one at 1228 Hz. Missing layers. 93 presets were short of zones and 176 were missing outright, 219 of 395. Nothing was lost recovering them - the 46 preset slots that disappeared were all empty placeholders. The old files were also over-read, running past the end of their sample into whatever PCM came next; WellheadDrill02a was six seconds where the bank says eight hundred milliseconds. Every one of the 395 files now matches its bank record exactly. Also baked in: per-zone layer attenuation, and the static resonant low-pass the EMU8000 applied in hardware. Measured while doing it, and worth knowing: RP's banks contain no key-splits at all - every multi-zone preset is a pure layer stack - and no preset has more than four zones, which is what the engine's own "AWE appears to only play 1st 4 voices" warning has been asserting since 1995. Still to do: loop regions and the release fades, which 349 zones ask for and which need new SAMPLEINFO fields. And voice demand per sound has gone from about one zone to about two and a half, so the per-event alGenSources and alDeleteSources churn roughly doubles - the BT tree measured pooling as the fix for that, and a CPU win besides. Builds clean. The extreme baked rates, 1228 Hz up to 88200, were checked through the real path - libsndfile, alBufferData, alSourcePlay - and all load. Not yet listened to on the pod. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
26 KiB
Red Planet — the sound system, from two AWE32s to OpenAL
How a 1996 arcade pod produced true quadraphonic positional audio out of two consumer sound cards, what the modern port kept, what it silently dropped, and exactly where the original assets are.
Sources. The surviving engine in MUNGA_L4/ (the L4AUD* family) and the
preserved hardware layer in MUNGA_L4/sos/; the complete original RP 4.10 C++
source and shipping assets in ../TeslaRel410/; and the BattleTech sibling
tree ../BT411/, which shares this engine verbatim and has already fixed most
of what's described here.
Companion docs: docs/audionotes.rtf (Stephen Baynham, 2007) covers the
renderer's control flow — sources, sockets, the mix/running/dormant plugs.
../BT411/docs/AUDIO_FIDELITY.md is the 685-line fidelity audit this document
maps onto RP. This doc covers the hardware model underneath both.
Two headlines:
- The quadraphonic engine is still in the tree, still runs every frame, and its output is discarded. Nothing was deleted in the port. Four channel gains and four time-delay offsets are computed for every sound in the world, then dropped, because the OpenAL back-end that replaced the sound cards never reads them.
- Red Planet's original soundbanks, authored sequences, source code and
hardware configuration all survive in
../TeslaRel410/. Nothing about the original audio is lost. RP412 simply ships without them.
source azimuth
│
▼
CalculateSpatialization() ← quadrant pan + ITD, L4AUDIO.cpp:60
│
├──► frontLeftScale / frontRightScale ─┐
├──► rearLeftScale / rearRightScale │ 1996: CC7 volume to
├──► 4 × ITD delay targets ├── 4 MIDI channels across
└──► 4 × ITD pitch offsets (cents) │ 2 AWE32 cards
│
└── today: /* ... */ dead code,
OpenAL pans from AL_POSITION
1. Why two cards
AudioHardware held exactly two, named for what they drove
(MUNGA_L4/L4AUDHDW.h:345-346):
AudioCard frontCard;
AudioCard rearCard;
The second card was not for extra voices. Each AWE32 gives you one stereo
pair, and a four-corner speaker layout needs two. The allocator makes this
explicit (MUNGA_L4/L4AUDRND.cpp:1309-1352): front-left and front-right
channels are requested from front_card, rear-left and rear-right from
rear_card. Either card refusing kills the whole allocation and the sound
doesn't play.
Per card the engine assumed a stock EMU8000 (MUNGA_L4/L4AUDHDW.h:80-82):
| Constant | Value |
|---|---|
AWE_VOICE_COUNT |
32 |
AWE_CHANNEL_COUNT |
16 |
AWE_PERCUSSIVE_CHANNEL |
9 |
64 hardware voices total, 32 MIDI channels, two independent stereo outputs.
2. The hardware layer: HMI SOS
Everything went through Human Machine Interfaces' Sound Operating System,
selected at compile time (MUNGA_L4/L4AUDHDW.h:87):
#define _MIDI_DRIVER_TYPE _MIDI_AWE32
MUNGA_L4/sos/ still carries the complete driver headers in both flavours the
build needed — bc4/ for Borland C++ 4 and wc/ for Watcom — alongside
SOSMAWE.C, whose header comment reads "Module to handle AWE32 .SBK file
uploads." That file is the bank loader: sosMIDIAWE32SetSBKFile,
sosMIDIAWE32ReleaseSBKFiles, sosMIDIAWE32NoteOn/NoteOff.
AudioCard also poked the hardware directly for MPU-401 UART setup — the
_inp/_outp port macros and MPU_RESET_CMD/MPU_ENTER_UART at
MUNGA_L4/L4AUDHDW.cpp:14-25 are still there.
2.1 The actual pod hardware configuration
Card addresses were parsed by GetEnvironmentSettings
(MUNGA_L4/L4AUDHDW.cpp:269-350) out of a BLASTER-format string. The standard
single BLASTER= variable can only describe one card, so each got its own
(MUNGA_L4/L4AUDHDW.cpp:935-936):
frontCard.GetEnvironmentSettings(FRONT_CARD_ENV_VAR);
rearCard.GetEnvironmentSettings(REAR_CARD_ENV_VAR);
Those two macros are referenced in four places and defined nowhere in this tree.
The values survive in the shipping release —
../TeslaRel410/ALPHA_1/REL410/RP/SETENV.BAT:
set AWE_FRONT=A220 I5 D1 H5 P330 T6
set AWE_REAR=A240 I7 D3 H6 P300 T6
| Front card | Rear card | |
|---|---|---|
| Base I/O | 0x220 | 0x240 |
| IRQ | 5 | 7 |
| DMA (8-bit) | 1 | 3 |
| DMA (16-bit) | 5 | 6 |
| MPU-401 | 0x330 | 0x300 |
| Type | 6 | 6 |
Two fully independent SB16/AWE32s, non-conflicting across every resource — a
genuinely awkward ISA configuration to get stable, which is presumably why
SETENV.BAT hardcodes it rather than probing.
Two cards were mandatory, not optional. L4Application::MakeAudioRenderer
returned NULL — no audio renderer at all — unless both variables were present
(MUNGA_L4/L4APP.cpp:505-514, now commented out). There was no one-card or
stereo fallback in the shipping build.
SETENV.BAT also drove the SB16 mixer per card via sb16set, and carries three
details worth recording:
- Master volume defaults to
AWE_MASTER_VOLUME=200, overridable by an operator filec:\setvol.bat— the per-cabinet volume trim. - Intercom mode (
L4INTERCOM=ON) swapsaudio\ctmix.cfgforaudio\icom.cfgand addssb16set /li:220;0on the front card only. Diffing the two configs (ALPHA_1/REL410/RP/AUDIO/), the only change is line-in routing:LIL+/LIR+into the input and output paths. The intercom fed the front card's line input. - There are two
:SOUNDCOMMONlabels. DOS batch jumps to the first, so the second block — which trims the cards differently from each other (bass 245 vs 240, treble 110 vs 135) — is unreachable dead code. Someone tuned front and rear separately and it never shipped.
3. Nothing streamed — the game was a MIDI sequencer
There is no mixer and no audio thread in the original design. Sound effects were SoundFont samples resident in each card's onboard sample RAM, and playing a sound meant allocating a MIDI channel and sending note-on plus CC7 volume. The game drove two samplers in real time.
That is why both cards were loaded with identical banks (dist/AUDIO/AUDIO.INI):
[AudioResources]
front_audio_resource=audio\audio1.res
front_audio_resource=audio\audio2.res
rear_audio_resource=audio\audio1.res
rear_audio_resource=audio\audio2.res
Same content in both cards' RAM, so any sound could be placed anywhere in the ring without a reload. The cost is that the entire sound set had to fit twice over in AWE32 sample memory.
It also explains the shape of the whole audio API. AudioChannel exposes
SendNoteOn, SendProgramChange, SendPitchBend, SendNRPN, SelectBank —
a MIDI abstraction, not a sample-playback abstraction. Every positional and DSP
decision the engine makes has to be expressed as a MIDI controller value.
4. The quad panner
L4AudioSpatialization::CalculateSpatialization(azimuth)
(MUNGA_L4/L4AUDIO.cpp:60-290) is the whole positional model. It is
character-for-character identical to the 1995 original at
../TeslaRel410/CODE/RP/MUNGA_L4/L4AUDIO.CPP:150-290, down to the // HACK
comments.
Azimuth is rewrapped so 0° is dead ahead and the range is ±180°, then split into
four 90° quadrants around azimuth_max = 45°:
front
FL ─────┬───── FR
│ Q1 │
│ │
Q2 │ ▲ │ Q4
(left) │ │ │ (right)
│ +az │
RL ─────┴───── RR
Q3
rear
+azimuth → left −azimuth → right
Within a quadrant it constant-power pans between the two bracketing speakers only — a source never feeds more than two of the four, which is correct for a four-corner layout:
tangent_ratio = (tan(azimuthOfSource) / tan(azimuth_max)) * 0.5f;
frontLeftScale = Sqrt(0.5f + tangent_ratio);
frontRightScale = Sqrt(0.5f - tangent_ratio);
tangent_ratio runs ±0.5, so the gains trace sqrt(0.5±t) — sum of squares
constant at 1.0, i.e. constant acoustic power across the sweep, no hole in the
middle.
| Quadrant | Arc | Active pair |
|---|---|---|
| Q1 | −45° … +45° | front-left / front-right |
| Q2 | +45° … +135° | rear-left / front-left |
| Q3 | ±135° … 180° | rear-right / rear-left |
| Q4 | −135° … −45° | front-right / rear-right |
Q1/Q3 use tan(azimuth_max) as the half-width while Q2/Q4 use
tan(DEG_90 - azimuth_max). With azimuth_max = 45° these are equal and all
four arcs are 90°, but the code is written so front/rear arcs could be widened
against the side arcs independently. azimuth_max is hardcoded with a
// HACK - should come from audio.ini comment at L4AUDIO.cpp:103.
Q3 relies on tan having period 180° to handle the wrap at ±180° — for
az < −135 the expression azimuthOfSource - DEG_180 goes below −315°, and the
result is only correct because tangent is periodic. It works; it is not obvious.
5. The ITD trick
Amplitude panning alone gives direction but not much externalization. The engine also modelled interaural time difference — the sub-millisecond arrival-time gap between your ears that the brain actually uses to localize. AUDIO.INI:
distance_between_ears=12.0
itd_difference=0.0015
The problem: an EMU8000 has no delay line. You cannot ask an AWE32 to play a voice 1.5 ms late. There is no such MIDI message and no such hardware path.
The solution: don't delay the voice — detune it. To make a voice arrive progressively earlier or later, momentarily shift its pitch, which shifts its playback rate, which slides it through time. Return the pitch to normal and the voice stays there, phase-shifted. Doppler used as a phase-steering primitive.
CalculateSpatialization sets a delay target per channel; the caller converts
the rate of change of that target into a cents offset
(MUNGA_L4/L4AUDIO.cpp:414-438):
const Scalar itd_pitch_offset_constant =
0.003831f / 0.000002f; // period / delay
frontLeftITDPitchOffset =
itd_pitch_offset_constant *
(spatialization.frontLeftDelay - currentFrontLeftDelay) /
(Scalar)itd_delta_time;
currentFrontLeftDelay = spatialization.frontLeftDelay;
Only one of the two active channels gets a nonzero delay target, scaled by the
same tangent_ratio as the gain, so maximum offset at full pan is exactly
itd_difference — 1.5 ms. The rear quadrant negates the sign
(rearLeftDelay = -(itd_delay * tangent_ratio * 2.0f), L4AUDIO.cpp:214),
flipping the lead/lag relationship behind the listener.
On the magic constant — a derivation, not something the source states.
0.003831 / 0.000002 = 1915.5 cents per unit of delay slew. The exact
small-signal value for a Doppler-style rate-to-pitch conversion is 1200 / ln 2
≈ 1731 cents. They agree within about 10%, which confirms the mechanism: a
hand-tuned first-order approximation, presumably trimmed by ear on the pod.
Three further details:
- Computed against real elapsed frame time (
Now() - lastITDFrameTime), so the slew is framerate-independent. - Applied only while the target is moving. A stationary source contributes zero pitch offset and sits at whatever phase it reached.
distance_between_ears=12.0is commented "-> average size of cockpit". BT uses2.0. This is the clearest surviving fingerprint of the pod build: the head model was scaled to the physical cabinet, because the speakers really were in the corners around the player. Confirmed authentic — RP412'sAUDIO.INIis byte-identical to the shipping 4.10 file dated 31 August 1995 (../TeslaRel410/ALPHA_1/REL410/RP/AUDIO/AUDIO.INI). Every tuning constant in this repo is the original; there has been zero config drift in thirty years.
6. The rest of the per-frame model
All of it expressed as MIDI, all driven from AUDIO.INI:
| Effect | Mechanism | INI keys |
|---|---|---|
| Distance attenuation | CC7 volume, knee + rolloff curve | amplitude_rolloff, _knee, _distance_scale |
| Distance muffling | AWE initial-filter-cutoff NRPN 21 (100–8000 Hz) | high_frequency_rolloff, _knee, _distance_scale |
| Doppler | pitch bend in cents | doppler_range, speed_of_sound |
| Reverb | CC91 send, wet exterior / dry cockpit | global_reverb_scale |
| Source compression | gain curve on the summed mix | compression_scale, compression_exponent |
| Clipping | hard cull sphere | clipping_radius |
NRPN constants are still declared at MUNGA_L4/L4AUDHDW.h:63-68
(AWE_FILTER_CUTOFF_NRPN 21, AWE_VOL_ATTACK_TIME_NRPN 11, AWE_PITCH_NRPN 16).
AUDIOMR.INI is a shipped variant differing from AUDIO.INI in exactly one
respect — compression is far more aggressive (compression_scale=0.1,
compression_exponent=9.0 vs 0.92/8.5). Everything else is identical.
7. What the port did
Both trees replaced the AWE32/SOS back-end with OpenAL Soft, by commenting
out rather than deleting. MUNGA_L4/L4AUDHDW.h is 530 lines of which the great
majority is preserved-in-amber AWE code: AudioChannel, AudioCard and
AudioHardware are entirely inside /* */. The quad CC7 volume switch survives
the same way from MUNGA_L4/L4AUDIO.cpp:1964.
The replacement is SourceSet (MUNGA_L4/L4AUDHDW.h:9-13):
struct SourceSet
{
int count;
ALuint sources[5];
};
Four MIDI channels-per-sound became up to five OpenAL sources — one per sample
zone in the preset, not one per speaker. Placement is handed to OpenAL via
alSource3f(..., AL_POSITION, ...) (MUNGA_L4/L4AUDIO.cpp:1413).
The consequence is the first headline. CalculateSpatialization is still
called every frame from UpdateSpatialModelImplementation, still computes four
gains and four ITD pitch offsets. Every consumer of those values is commented out.
8. Where the original assets actually are
Update (2026-08-05): both banks now live in
assets/RP411/AUDIO/, and the WAV set and preset table are generated from them bytools/rp_sf2extract.py. The history below is kept because it is what made that possible, and because the.SCPsources are still only in TeslaRel410.
RP412 used to ship 223 loose .wav files loaded through libsndfile, a
hand-maintained preset table in RP_L4/WTPresets.cpp, and a 1-byte stub
AUDIO.RES. The front_audio_resource/rear_audio_resource lines in AUDIO.INI
were never stale leftovers — they are the original authored configuration, and
the banks they name existed all along, simply never carried into the port.
Everything below is verified present in ../TeslaRel410/:
| Asset | Location | Detail |
|---|---|---|
| RP soundbanks | ALPHA_1/REL410/RP/AUDIO/AUDIO1.RES, AUDIO2.RES |
Genuine SoundFonts (RIFF…sfbk), 3,781,754 B (Oct 1996) and 3,708,348 B (May 1996) |
| Earlier bank revision | sda4/RPLIVE/AUDIO/ |
Nov 1995 / Oct 1995; AUDIO1 differs by 4 bytes |
| Authored sequences | sda4/RPLIVE/AUDIO/*.SCP |
70 files including STATIC.SCP |
| Sequences (partial) | CONTENT/RP/AUDIO/*.SCP |
62 files, no STATIC.SCP |
| Original C++ source | CODE/RP/MUNGA_L4/L4AUD*.CPP |
Complete pre-port DOS source |
| Hardware config | ALPHA_1/REL410/RP/SETENV.BAT |
The AWE_FRONT/AWE_REAR strings in §2.1 |
| Mixer configs | ALPHA_1/REL410/RP/AUDIO/CTMIX.CFG, ICOM.CFG |
Normal and intercom routing |
Three things this settles:
- RP's banks are its own. MD5s differ from BT's, which are byte-identical
between
TeslaRel410/ALPHA_1/REL410/BT/AUDIO/andBT411/content/AUDIO/— so the provenance chain is proven on the BT side, and RP's distinct content is sitting unused. - The
.SCPfiles are build-time sources, not runtime assets.CreateStaticAudioStreamResource(MUNGA_L4/L4AUDRES.cpp:769) is called only from the asset tool (MUNGA/TOOL.cpp:100), which compiles them intoRPL4.RES. RP412 ships a workingRPL4.RES, so the authored audio objects are present — what's missing is the editable source form, now recovered. - RP has a reference BT lacks. BT411's audit had to Ghidra-decompile
BTL4OPT.EXEto confirm F4, F9, F10, F11 and F12. For Red Planet the actual C++ source exists, so every one of those can be verified directly rather than inferred.
9. Fidelity gaps — the BT411 audit mapped onto RP412
BT411's audit graded its OpenAL port across 23 findings and has since fixed most
of them. Its sections C and D (dead attribute bindings, ReportLeak, torso-twist
servos) are BattleTech-entity-specific and do not transfer. Its synthesis and
spatial findings do.
Every gap below was re-verified against RP412's own code, not assumed. The comment-block state of each cited line was checked programmatically.
Engine-side — asset-independent
Status: all fixed (2026-08-05). Line references are to the pre-fix tree.
| # | Gap | Evidence found in RP412 | What landed |
|---|---|---|---|
| F3 | Authored distance curve computed then discarded; AL_LINEAR_DISTANCE used instead |
volume_scale *= GetDistanceVolumeScale() commented at L4AUDIO.cpp:1449; alDistanceModel(AL_LINEAR_DISTANCE) live at MUNGA/AUDIO.cpp:97; AL_MAX_DISTANCE written at :1081,1416,1941 |
alDistanceModel(AL_NONE); multiply restored on Dynamic3D; new Static3DPatchSource::CalculateSourceVolumeScale override; the three AL_MAX_DISTANCE writes dropped |
| F4 | Volume written linearly where the original used the CC7 squared law | three live alSourcef(..., AL_GAIN, volume_scale) at L4AUDIO.cpp:1082,1414,1939 |
AL_GAIN, volume_scale * volume_scale at all three |
| F9 | Brightness / HF-rolloff chain dead | GetHighFreqCutoffScale() had zero callers |
new L4AUDEFX lowpass: Dynamic3D takes HF-rolloff × brightness, Static3D and Direct take brightness alone |
| F10 | Doppler wrong constants and wrong sign | alDopplerFactor(0.3f); GetDopplerCents() zero callers |
alDopplerFactor(0.0f) + pitch_offset += GetDopplerCents() on the dynamic path only |
| F11 | Reverb wet-exterior / dry-cockpit split dead | CC91 sends commented at L4AUDIO.cpp:1227,1717 |
EFX EAXReverb aux slot at global_reverb_scale; sends attached on Dynamic3D/Static3D, Direct left dry |
| F12 | Direct placement dead — everything dead-centre | all three switch (audioPosition) blocks commented |
AL_POSITION written per the authored enum after SetupPatch |
| P1 | AL_PITCH never called anywhere in the tree |
relativePitch computed at :1034,1408,1922 and discarded at all three |
pitch applied at all three sites |
| F22 | Quad + ITD model dead | §4–5 above | still open — needs multichannel output (§10 step 4) |
P1 is an RP-specific find with no BT counterpart, and it is larger than F10
alone. RP412 had no AL_PITCH call at all, so the entire pitch chain was
inert — not just doppler but pitch_mix_offset / PitchAudioControlID, which
RP's own sequences author 97 times. Fixing F10 without this would have changed
nothing audible.
A note on note-pitch: BT411 applies 2^((note-60)/12), because its SF2-derived
presets carry authored key-splits. RP is different — SAMPLEINFO has no root-key
field, and RP's authored content predates NoteAudioControlID entirely (its
AudioControlID enum stops at AttackTimeAudioControlID), so every source runs
at DEFAULT_NOTE=60 and the factor is identically 1.0. It is applied anyway for
engine parity, clearly marked as inert for current content.
F3 was the highest-leverage single change: restoring the authored curve also repairs the distance-blind transient cull, the voice-steal weighting, and the mix-ducking chain, all of which were treating far sources as full-presence.
F22 is the one where RP is the lead repo rather than the follower. BT411 classes it low-priority because it "matters mostly for pod-hardware target" — which is precisely what this project is.
Verified on this machine: ALC_EXT_EFX is present and all nine EFX entry
points resolve, so F9/F11 are live rather than silently inert. The driver grants
256 mono sources — OpenAL Soft's default budget, which BT411 raised
explicitly via context attributes. RP412 still accepts the default; worth
revisiting if voice starvation shows up in a busy match.
Asset-side — measured against RP's own banks
RP's banks turned out to differ from BT's in ways that matter, so BT's
prevalences do not transfer. Measured directly (tools/rp_sf2extract.py --stats):
| RP total | |
|---|---|
| Presets | 154 (67 in bank 1, 87 in bank 2) |
| Instrument zones | 395 |
| Multi-zone presets | 130 / 154 (84%) |
| Key-splits | zero |
| Max zones in any preset | 4 |
| Looping zones | 129 |
| Zones with an authored low-pass | 154 |
| Zones with layer attenuation | 97 |
Zones with releaseVolEnv |
349 |
Two RP-specific findings that change the work:
- RP's banks contain no key-splits at all. Every multi-zone preset is a pure layer stack whose zones share one key range, all covering note 60. Combined with RP's content never authoring a note (§9 above), key ranges are unreachable here — so zone selection is a non-issue and every zone is simply a simultaneous voice. BT's F1, which is largely about key-splits, mostly does not apply; what applies is the plain zone count.
- Max 4 zones per preset, which fits
PRESETINFO.samples[5]as it stands. No structural change was needed — and the engine's long-standingWarn(GetVoiceCount() > 4)("AWE appears to only play 1st 4 voices",L4AUDLVL.cpp:29) matches the bank data exactly.
| # | Gap | RP status |
|---|---|---|
| F1 | Zones dropped | fixed — 93 presets were short; 176 zones recovered, 219 → 395 |
| F2 | Root key / tuning dropped | fixed — every shipped WAV was flat 44100 Hz; 202 of 219 checked zones were off, worst ~9 semitones. Tuning is now baked into each WAV's declared rate |
| F14 | Per-zone generators | partly fixed — initialAttenuation (SBK inverted scale) and the authored initialFilterFc/Q resonant low-pass are baked into the PCM. Volume envelopes still dropped |
| F13 | Loop regions and release envelopes | open — needs SAMPLEINFO fields plus engine work (see below) |
F13 remains the outstanding asset-side item. It needs loopStart/loopEnd
and releaseSec on SAMPLEINFO, AL_SOFT_loop_points at buffer setup, and a
gain ramp on the stop path. 349 of 395 zones carry an authored release envelope
that is currently an instant cut.
The ordering hazard BT flagged — loop regions must land with or before multi-zone extraction, or whole-buffer looping over a short loop region replays an entire sample every cycle — is worth re-checking for RP now that zone counts have gone up. RP's 129 looping zones should be measured for loop-region coverage before F13 lands.
A churn risk this work introduces. Voice demand per sound has gone from ~1.1
to ~2.6 zones. RequestAudioChannels calls alGenSources per sound event and
ReleaseSourceSet calls alDeleteSources on release, so the allocation churn
roughly doubles. BT411 hit exactly this and its measured conclusion was that
raising the source budget was not the fix — pooling the sources was, and it
was a net CPU win (8.67 ms → 7.79 ms per frame). RP412 has the same churn
pattern and no pooling. Worth doing before any complaint arrives, not after.
10. A recovery path, in order
Engine-side fidelity first.Done (2026-08-05). F3, F4, F9, F10, F11, F12 and P1 all landed;L4AUDEFX.cpp/.hported and added toMunga_L4.vcxproj. Builds clean on VS2022Release|Win32; smoke-tested against vRIO on COM1 withRP412STEAM=0— reaches gameplay and holds a steady frame loop. Not yet listened to on the pod, which is the real acceptance test: F4 in particular changes the level of everything.Wire RP's banks in.Done (2026-08-05). Both banks are now inassets/RP411/AUDIO/, hash-identical to the 1996 originals.tools/rp_sf2extract.pyextracts all 395 zones with tuning, layer attenuation and the authored low-pass baked in, and regeneratesRP_L4/WTPresets.cpp. Verified: 176 zones recovered with none lost (the 46 preset slots that disappeared were all empty placeholders); every extreme baked rate (1228 Hz – 88200 Hz) accepted by libsndfile →alBufferData→alSourcePlayon the real runtime path. Still open here: F13.- Recover the
.SCPsources fromsda4/RPLIVE/AUDIO/into the asset pipeline, so authored audio becomes editable again rather than frozen inRPL4.RES. - Then quad. With the above in place:
- Ask ALC for a multichannel format instead of accepting the stereo default
(
MUNGA_L4/L4AUDRND.cpp:380). - Place four
AL_SOURCE_RELATIVEsources at fixed corner positions and drive theirAL_GAINfrom the existingGetFrontLeftScale()family, bypassing OpenAL's panner. - Feed the ITD offsets to
AL_PITCH— or implement a real fractional delay, which a software mixer can do and the EMU8000 could not. The detune path is already written and is the authentic behaviour. - Re-derive
azimuth_maxfrom the actual cabinet speaker angles instead of the hardcoded 45°.
- Ask ALC for a multichannel format instead of accepting the stereo default
(
Steps 1 and 2 are where nearly all the audible improvement is. Step 4 is what made the pod feel like the sound was in the room with you.
11. Verifying any of this
| File | What's in it |
|---|---|
MUNGA_L4/L4AUDHDW.h |
AWE/MIDI constants, AudioCard/AudioHardware (commented), SourceSet |
MUNGA_L4/L4AUDHDW.cpp |
MPU-401 port I/O, BLASTER parsing (:269), card init (:935) |
MUNGA_L4/L4AUDIO.cpp |
CalculateSpatialization (:60), ITD pitch (:414), dead quad CC7 path (:1964) |
MUNGA_L4/L4AUDRND.cpp |
renderer, OpenAL init (:380), dead quad channel allocator (:1309) |
MUNGA_L4/L4AUDRES.cpp |
resource manager, WAV → AL buffers, SCP compile path (:769) |
MUNGA_L4/L4APP.cpp:505 |
the dead two-card gate on renderer creation |
MUNGA_L4/sos/ |
HMI SOS driver headers (bc4 + wc), SOSMAWE.C bank uploader |
dist/AUDIO/AUDIO.INI |
every tuning constant — byte-identical to the 1995 original |
../TeslaRel410/CODE/RP/MUNGA_L4/ |
the original DOS source, for anything the comments don't answer |
../BT411/docs/AUDIO_FIDELITY.md |
the full 23-finding audit this section maps from |
The commented-out regions are a faithful copy of the original — verified against
TeslaRel410/CODE/RP/MUNGA_L4/L4AUDIO.CPP, which matches character-for-character
in the spatialization and ITD paths. They were preserved deliberately and they
describe exactly how the pod's audio hardware was driven.