From 020dc046121878afdf101add943c851497d2a7b0 Mon Sep 17 00:00:00 2001 From: RT Date: Thu, 6 Aug 2026 14:50:42 -0500 Subject: [PATCH] Add HD-AUDIO.md: multi-channel audio investigation notes Research only, no code changes. Captures why the pods play in stereo and what it would take to drive a 4-speaker (quad) rig on Win10/11. Key findings: - "Channels" is ambiguous in this codebase: 32 mixer voices (Adept splits them 1 VO / 2 music / 1 Betty / 6 mechanical / 21 spatialized SFX) versus the speaker output, which is hardcoded stereo at 22050 Hz. - The blocker is DS3DALG_HRTF_LIGHT, a two-speaker head model, requested unconditionally because dwFreeHw3DAllBuffers is always 0 on Vista+ where DirectSound hardware acceleration no longer exists. hardwaremixing=true is therefore inert, and the EAX reverb path has been dead since XP. - GameOS has a full gosAudio_SetSpeakerConfig API that nothing ever calls. GetSpeakerConfig also has no 5.1 case, so it returns 0 on such systems. - Cheapest route is DSOAL (drop-in dsound.dll over OpenAL Soft) plus hardwaremixing=true, which re-enables the existing hardware-3D branch with no code change. Verified in source that Libraries.cpp:376 calls LoadLibrary("dsound.dll") unqualified, and dsound.dll is not a KnownDLL, so a local copy next to MW4.exe wins over System32. - Same deployment rule as dgVoodoo2: per-machine Win10/11 prerequisite, must NOT be committed, XP pods need the native dsound.dll. Also documents the reverted fallback design (a third NO_VIRTUALIZATION branch, a -tspk override and a gos-audio.txt trace) as a re-implementation recipe, plus a verification plan and caveats. Co-authored-by: Claude Opus 5 (Anthropic) Co-authored-by: GitHub Copilot --- HD-AUDIO.md | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 HD-AUDIO.md diff --git a/HD-AUDIO.md b/HD-AUDIO.md new file mode 100644 index 00000000..414a0516 --- /dev/null +++ b/HD-AUDIO.md @@ -0,0 +1,253 @@ +# HD / Multi-Channel Audio -- Investigation Notes + +**Status: RESEARCHED, NOT IMPLEMENTED.** Investigated 2026-08-05/06. A prototype code change +was written and **reverted** -- nothing in this document is in the tree. Captured so the work +does not have to be redone. + +**Goal that prompted this:** get the "hardware 3D mixing" effect out of a **4-speaker +(quad: front L/R + rear L/R)** rig on Windows 10/11 pods. The original arcade hardware +supported multi-channel audio; the modern pods play in stereo. + +--- + +## 1. Where the audio code lives + +| Component | File | +|---|---| +| DirectSound mixer / listener | `Gameleap\code\CoreTech\Libraries\GameOS\Sound DS3DMixer.cpp` / `.hpp` | +| Per-voice channel | `Gameleap\code\CoreTech\Libraries\GameOS\Sound DS3DChannel.cpp`, `Sound Channel.hpp` | +| Voice pool / startup | `Gameleap\code\CoreTech\Libraries\GameOS\Sound Renderer.cpp` / `.hpp` | +| Public API | `Gameleap\code\CoreTech\Libraries\GameOS\Sound API.cpp`, enums in `GameOS.HPP` | +| Buffer creation from assets | `Gameleap\code\CoreTech\Libraries\GameOS\Sound Resource.cpp` | +| DirectSound call wrappers | `Gameleap\code\CoreTech\Libraries\GameOS\DirectSound.cpp`, `DirectX.hpp` | +| `dsound.dll` acquisition | `Gameleap\code\CoreTech\Libraries\GameOS\Libraries.cpp` | +| EAX header (bundled) | `Gameleap\code\CoreTech\Libraries\GameOS\Eax.h` | +| Game-side voice manager | `Gameleap\code\mw4\Libraries\Adept\AudioRenderer.cpp`, `AudioChannel.cpp` | +| Debug readout | `Gameleap\code\CoreTech\Libraries\GameOS\DebugGUI.cpp` (`DbgS_Sound`) | + +--- + +## 2. "Channels" means two different things -- keep them apart + +### (a) Mixer voices -- 32 + +- `Environment.soundChannels = 32`, hardcoded at `MW4Application.cpp:1164`. Not read from + `options.ini`, no command-line switch. +- Caps disagree: `MAX_SOUNDCHANNELS 32` in `Sound Channel.hpp:20` vs **64** in + `Sound Renderer.hpp:44` (the array size). 32 is what gets asserted against. +- Adept fixes the split at startup (`AudioRenderer.cpp:241-325`): + + | Voice ids | Purpose | + |---|---| + | 0 | Voiceover | + | 1 | *skipped/unused* (`channel_id` jumps 0 to 2) | + | 2-3 | Music | + | 4 | Betty | + | 5-10 | Mechanical (6) | + | 11-31 | General spatialized SFX (21) | + + `Verify(m_channels.GetLength() >= 32)` -- raising the count is possible, lowering it is not. + +### (b) Speaker output channels -- hardcoded stereo + +- `m_waveFormatEx.nChannels = 2`, 16-bit, **22050 Hz** -- `Sound DS3DMixer.cpp:50`. + (`Environment.soundHiFi` is hardcoded `false` at `MW4Application.cpp:1162`, so the 44.1 kHz + branch is dead code. Never read from ini or command line.) +- Applied with `wSetFormat(m_lpPrimaryBuffer, ...)` at `Sound DS3DMixer.cpp:124` + (and again in `Reset()` at line 580) under `DSSCL_PRIORITY` (line 86). + +--- + +## 3. The actual blocker + +`CreateStreamBuffer` (`Sound DS3DChannel.cpp:342-353`) and `CreateMasterBuffer` +(`Sound Resource.cpp:1345-1356`) both branch two ways: + +```cpp +if ( m_dscaps.dwFreeHw3DAllBuffers && Environment.soundMixInHardware ) +{ + dsbd.dwFlags |= DSBCAPS_CTRL3D; // true positional +} +else +{ + dsbd.dwFlags |= DSBCAPS_CTRL3D | DSBCAPS_LOCSOFTWARE; + dsbd.guid3DAlgorithm = DS3DALG_HRTF_LIGHT; // <-- the problem +} +``` + +**`DS3DALG_HRTF_LIGHT` is a two-speaker / headphone head-model.** Asking for it on a quad rig +folds the image into the front pair. `DS3DALG_NO_VIRTUALIZATION` is the DirectSound value that +means *"use the speaker configuration from Control Panel"*, which is what a 4-speaker setup +needs. The engine already uses `NO_VIRTUALIZATION` for its EAX effects buffer +(`Sound DS3DMixer.cpp:165`), so the GUID is present and linked. + +**On Vista+ `dwFreeHw3DAllBuffers` is always 0** -- Microsoft removed DirectSound hardware +acceleration. So every modern pod unconditionally takes the HRTF branch, and +`hardwaremixing=true` in `options.ini` is inert. Same reason EAX reverb is dead: the +`IKsPropertySet` query at `Sound DS3DMixer.cpp:177-205` fails and the effects buffer is +released (gracefully -- no error). + +### Speaker config is never set or queried + +GameOS has a complete API. `gosAudio_SetSpeakerConfig` -> `DS3DSoundMixer::SetSpeakerConfig` +(`Sound DS3DMixer.cpp:431-466`) maps onto `DSSPEAKER_MONO/STEREO/QUAD/SURROUND/HEADPHONE` +plus geometry arcs. **Nothing in MW4 ever calls it.** The only occurrence in the whole tree is +a commented-out line in `CoreTech\Code\Test Sound\Test Sound.cpp:46`. The game inherits +whatever Windows has and never overrides it. + +Related latent bug: `GetSpeakerConfig` (`Sound DS3DMixer.cpp:469`) has no case for +`DSSPEAKER_5POINT1`, so a 5.1 system falls through every branch and returns `0`. The +`gosAudio_SpeakerConfig` enum in `GameOS.HPP:466` stops at `gosAudio_Surround = 16` and has no +5.1/7.1 values. The `DebugGUI.cpp:2436` readout inherits this and also re-prints its label on +the fall-through. + +--- + +## 4. RECOMMENDED ROUTE: DSOAL (do this first) + +**DSOAL** is a drop-in `dsound.dll` that reimplements DirectSound -- including DirectSound3D +and EAX 1-4 -- on top of **OpenAL Soft**. It is the audio analogue of DDrawCompat (editor +viewport) and dgVoodoo2 (MFD panels), both already used by this project. + +### Why it works here, verified in source + +`Libraries.cpp:376` does: + +```cpp +LibDsound = LoadLibrary( "dsound.dll" ); // unqualified name +_DirectSoundCreate = GetProcAddress( LibDsound, "DirectSoundCreate" ); // :385 +_DirectSoundEnumerate = GetProcAddress( LibDsound, "DirectSoundEnumerateA" ); // :386 +``` + +The name is **unqualified** and `dsound.dll` is **not a KnownDLL**, so the executable's +directory is searched before `System32`. A DSOAL `dsound.dll` next to `MW4.exe` will be +loaded. Both required exports are ones DSOAL provides. + +### The payoff: no code change needed + +DSOAL **reports hardware 3D buffers**, so `dwFreeHw3DAllBuffers != 0`. With +`hardwaremixing=true` the *existing untouched first branch* fires with plain `DSBCAPS_CTRL3D` +and no `LOCSOFTWARE` -- literally the hardware-3D-mixing path. OpenAL Soft handles quad output +natively. Bonus: the EAX reverb block would light up for the first time since XP. + +### Two gates to watch + +1. **`hardwaremixing` currently defaults to `false`** in our tree (the 2016 release shipped + `true`). Read at `MW4Application.cpp:364` from `[sound options]`. Without it, + `if (!Environment.soundMixInHardware) dsbd.dwFlags |= DSBCAPS_LOCSOFTWARE;` forces the + software path even with DSOAL present. +2. **The startup hardware-mixing self-test** (`Sound Renderer.cpp:94-165`) plays a silent + buffer on all 32 voices and silently sets `soundMixInHardware = 0` if the timing looks + wrong. A real veto that would look like "DSOAL did nothing". + +### Setup notes (verify against the current DSOAL release) + +- Needs OpenAL Soft alongside, as `dsoal-aldrv.dll`. +- Output mode configured in `alsoft.ini` -- **pin it to quad**, do not leave on auto. +- Set `hardwaremixing=true` in `options.ini` `[sound options]`. + +### WARNING -- deployment constraint, same rule as dgVoodoo2 + +**DSOAL must NOT go in the repo.** The pod fleet is mixed: XP pods have genuine DirectSound +hardware acceleration and need the native `dsound.dll`. DSOAL is a **per-machine, Win10/11-only +prerequisite** installed by the pod owner and documented in the release notes. Add `dsound.dll` +to the `deploy-mw4.ps1` skip list defensively, the way `ddraw.dll` already is +(see CLAUDE.md STEP 9 / commit `0ceba9c7` for why this rule exists). + +--- + +## 5. Fallback route: a third code branch (prototyped, reverted) + +For pods where DSOAL is not installed. Cheap and self-contained. What was built and backed out: + +1. **`GameOS.HPP:466`** -- add `gosAudio_5Point1 = 512`, `gosAudio_7Point1 = 1024` to + `gosAudio_SpeakerConfig` (bitfield, so 512/1024 are free). +2. **`Sound DS3DMixer.hpp`** -- add `DWORD m_dwSpeakerConfig; bool m_bMultiChannel;` and a + `DetectSpeakerConfig()` method. All members are already `public`. +3. **`Sound DS3DMixer.cpp`** -- call `DetectSpeakerConfig()` after `GetCaps()` in the ctor + (~line 89) and in `Reset()`. It calls `wGetSpeakerConfig` once and sets `m_bMultiChannel` + for QUAD / SURROUND / 5POINT1 / 7POINT1 / *_SURROUND. +4. **The branch**, at both `Sound DS3DChannel.cpp:342` and `Sound Resource.cpp:1345`: + + ```cpp + if ( dwFreeHw3DAllBuffers && Environment.soundMixInHardware ) + ... DSBCAPS_CTRL3D; // 1. hardware 3D + else if ( SoundRenderer.m_Mixer->m_bMultiChannel ) + ... DSBCAPS_CTRL3D | DSBCAPS_LOCSOFTWARE, + guid3DAlgorithm = DS3DALG_NO_VIRTUALIZATION; // 2. NEW: real speakers + else + ... DSBCAPS_CTRL3D | DSBCAPS_LOCSOFTWARE, + guid3DAlgorithm = DS3DALG_HRTF_LIGHT; // 3. two-speaker virtualization + ``` + +5. **Skip the stereo clamp** when multi-channel: guard `wSetFormat(m_lpPrimaryBuffer, ...)` + at lines 124 and 580. Only bites pre-Vista (where the primary buffer format is real), but + correct either way. +6. **`GetSpeakerConfig` fix** -- compare with `DSSPEAKER_CONFIG(dsflags)` to mask off the + geometry bits in the high word, and add the 5.1/7.1 cases. +7. **Missing SDK defines** -- the bundled DX7 `dsound.h` stops at `DSSPEAKER_5POINT1` (6). + Add locally under `#ifndef`: + `DSSPEAKER_DIRECTOUT 0`, `DSSPEAKER_7POINT1 7`, `DSSPEAKER_7POINT1_SURROUND 8`, + `DSSPEAKER_5POINT1_SURROUND 9`. + +### Instrumentation that went with it (worth rebuilding regardless) + +Per CLAUDE.md STEPs 10-12 -- *instrument, don't infer*. `SPEW` is compiled out of shipping +builds, so a Release pod has **no audio trace at all** today. + +- **`gos-audio.txt`** next to the exe, same open-append-close-per-line pattern as + `DispLog`/`gos-displays.txt` (`VideoCard.cpp:350-399`), so a crash cannot lose the tail. + Logs: device name, requested mixer format, Windows speaker config (decoded by name plus raw + hex), free/max hardware 3D buffers, final `hardwaremixing` state, voice count, and -- written + at the end of `SoundRendererInstall` *after* the self-test has had its chance to veto -- the + **final 3D path actually chosen** (`hardware 3D` / `NO_VIRTUALIZATION` / `HRTF_LIGHT`). + This one line tells you at a glance whether DSOAL engaged. +- **`-tspk <0-2>`** -- 0 auto, 1 force HRTF, 2 force multi-speaker. Follows the existing + `-tcoop` / `-tmr` / `-tmon` convention; parse next to them at `MW4Application.cpp:~1748`, + global defined in `Sound DS3DMixer.cpp`, `extern` declared at `MW4Application.cpp:~106`. + Lets you A/B the paths on a pod without a rebuild. + +Requires rebuilding **`MW4Application - Win32 Release`** and **Profile**. +Untested/uncompiled -- the likeliest compile error is `va_list`/`vsprintf` in +`Sound DS3DMixer.cpp`, though `VideoCard.cpp` uses the identical pattern via `pch.hpp`. + +--- + +## 6. Existing diagnostics you already have + +`MW4pro.exe` (LAB_ONLY) has a GameOS debugger sound page (`DbgS_Sound`, +`DebugGUI.cpp:2390-2470`) that already prints **Speaker Configuration** read live from +`GetSpeakerConfig`, plus device name, certification, and 3D hardware buffer counts. Note the +5.1 fall-through bug above. Crash dumps also carry `FreeHw3DAllBuffers` (`ErrorHandler.cpp:659`). + +--- + +## 7. Verification plan + +1. Set Windows to **Quadraphonic**, run `MW4pro.exe`, read the debugger sound page -- confirm + Windows is actually reporting QUAD before touching anything. +2. Drop **DSOAL + OpenAL Soft** next to `MW4.exe`, set `alsoft.ini` to quad and + `hardwaremixing=true` in `options.ini`. Launch the **stock** exe. If the rear speakers come + alive, done -- no code change needed. +3. If not, check whether the hardware-mixing self-test vetoed it (this is what `gos-audio.txt` + would answer instantly; without it, `MW4pro.exe`'s debugger page shows + "Hardware Mixing Disabled"). +4. Only if DSOAL is rejected as a prerequisite, implement section 5. +5. Confirm rear-speaker placement audibly with a mission that has strong positional cues + (something passing behind the player). + +--- + +## 8. Caveats + +- **Non-3D sounds will never reach the rear speakers.** Music, Betty, and UI use + `DSBCAPS_CTRLPAN` with manual `SetPan` (`Sound DS3DChannel.cpp:691`), inherently front-stereo. + Only the 21 general spatialized voices can use surrounds. +- **22050 Hz mixer.** `soundHiFi` is hardcoded `false`. Worth flipping to `true` (44100) at + `MW4Application.cpp:1162` while doing audio work -- trivial change, and moot under DSOAL + since the primary-buffer format is ignored on Vista+. +- **EAX returning changes the sound character.** DSOAL enabling the long-dead reverb path is a + real audible difference, not just a fidelity win. A/B it before shipping to pods. +- **Engine is stereo-era throughout.** No surround-aware mixing decisions exist anywhere; + everything above is about letting DirectSound/OpenAL spatialize what the engine already emits. +- **No audio command-line switches exist today** except `-nosound` (`MW4Application.cpp:1369`).