# 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`).