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) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
This commit is contained in:
2026-08-06 14:50:42 -05:00
co-authored by Claude Opus 5 GitHub Copilot
parent b3c5ecfe56
commit 020dc04612
+253
View File
@@ -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`).