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>
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 atMW4Application.cpp:1164. Not read fromoptions.ini, no command-line switch. -
Caps disagree:
MAX_SOUNDCHANNELS 32inSound Channel.hpp:20vs 64 inSound 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_idjumps 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.soundHiFiis hardcodedfalseatMW4Application.cpp:1162, so the 44.1 kHz branch is dead code. Never read from ini or command line.)- Applied with
wSetFormat(m_lpPrimaryBuffer, ...)atSound DS3DMixer.cpp:124(and again inReset()at line 580) underDSSCL_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.
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:
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
hardwaremixingcurrently defaults tofalsein our tree (the 2016 release shippedtrue). Read atMW4Application.cpp:364from[sound options]. Without it,if (!Environment.soundMixInHardware) dsbd.dwFlags |= DSBCAPS_LOCSOFTWARE;forces the software path even with DSOAL present.- The startup hardware-mixing self-test (
Sound Renderer.cpp:94-165) plays a silent buffer on all 32 voices and silently setssoundMixInHardware = 0if 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=trueinoptions.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:
-
GameOS.HPP:466-- addgosAudio_5Point1 = 512,gosAudio_7Point1 = 1024togosAudio_SpeakerConfig(bitfield, so 512/1024 are free). -
Sound DS3DMixer.hpp-- addDWORD m_dwSpeakerConfig; bool m_bMultiChannel;and aDetectSpeakerConfig()method. All members are alreadypublic. -
Sound DS3DMixer.cpp-- callDetectSpeakerConfig()afterGetCaps()in the ctor (~line 89) and inReset(). It callswGetSpeakerConfigonce and setsm_bMultiChannelfor QUAD / SURROUND / 5POINT1 / 7POINT1 / *_SURROUND. -
The branch, at both
Sound DS3DChannel.cpp:342andSound 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 -
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. -
GetSpeakerConfigfix -- compare withDSSPEAKER_CONFIG(dsflags)to mask off the geometry bits in the high word, and add the 5.1/7.1 cases. -
Missing SDK defines -- the bundled DX7
dsound.hstops atDSSPEAKER_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.txtnext to the exe, same open-append-close-per-line pattern asDispLog/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, finalhardwaremixingstate, voice count, and -- written at the end ofSoundRendererInstallafter 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/-tmonconvention; parse next to them atMW4Application.cpp:~1748, global defined inSound DS3DMixer.cpp,externdeclared atMW4Application.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
- Set Windows to Quadraphonic, run
MW4pro.exe, read the debugger sound page -- confirm Windows is actually reporting QUAD before touching anything. - Drop DSOAL + OpenAL Soft next to
MW4.exe, setalsoft.inito quad andhardwaremixing=trueinoptions.ini. Launch the stock exe. If the rear speakers come alive, done -- no code change needed. - If not, check whether the hardware-mixing self-test vetoed it (this is what
gos-audio.txtwould answer instantly; without it,MW4pro.exe's debugger page shows "Hardware Mixing Disabled"). - Only if DSOAL is rejected as a prerequisite, implement section 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_CTRLPANwith manualSetPan(Sound DS3DChannel.cpp:691), inherently front-stereo. Only the 21 general spatialized voices can use surrounds. - 22050 Hz mixer.
soundHiFiis hardcodedfalse. Worth flipping totrue(44100) atMW4Application.cpp:1162while 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).