Files
firestorm/HD-AUDIO.md
T
020dc04612 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>
2026-08-06 14:50:42 -05:00

13 KiB

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:

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.


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:

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:

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