A game's sound effects play at volume 0 under Wine, because it reads its volume from the mixer
Symptoms
The game opens its wave outputs and writes sound to them (WINEDEBUG=+winmm shows waveOutWrite), but nothing is heard, or its volume setting reads 0 and cannot be raised. The trace shows the game asking the mixer for a source line by component type (mixerGetLineInfoA with flags 0x80000003, dwComponentType: 0x1008 for WAVEOUT or 0x1005 for COMPACTDISC).
Cause
Some games of 1997 keep their effect and music volumes in the WinMM mixer's source lines, as the sound card's driver offered them. Wine's mixer has a destination line for the speakers and answers the WAVEOUT query with it, and that line's volume control reads 0. It has no COMPACTDISC line (measured, Wine 11.0 on macOS: the game's first read of the wave line's volume gives 0, before it sets anything). POD's Maxi Sound 64 builds take that 0 as the effect volume, write it back, and give their wave streams volume 0 (observed, PODMAX64.EXE FUN_004b9710, FUN_004b9ad4, FUN_004bbe0c).
Evidence
Logging the game's mixer calls from a stand-in DLL that hooks them in the import table: mixerGetLineControlsA finds Wine's volume control (ID 2) on the line, and mixerGetControlDetailsA reads its value as 0. WINEDEBUG=+winmm shows the queries but not the values.
What to do
Answer the game's two lines from a DLL the game loads, as the card's driver did. Hook mixerGetLineInfoA, mixerGetLineControlsA, mixerGetControlDetailsA and mixerSetControlDetails in the game's import table (shim::iat). For a WAVEOUT or COMPACTDISC query, give a source line with one volume control (0 to 0xffff, starting at full), keep the value the game sets and read it back. Pass every other query to Wine. wrappers/maxi94 does this for POD (maxi94/src/mixer.rs, mixer_dll.rs). A game without a card library of its own needs the hooks in another stand-in it loads, such as ddraw.dll.
Tried
- Answering only when Wine's call fails: Wine answers the WAVEOUT query, so the game kept Wine's line and its volume of 0.
- Reading the component type after Wine's failed
mixerGetLineInfoA: Wine had written its own fields back into the structure, so every query looked like the first. Read the query before calling Wine.