DirectAudio v1.2 — MIDI spin fix (returns a CPU core to every game)
DirectAudio v1.2 fixes a bug that has been present since v1: a Wine MIDI notify thread spinning at 100% of one CPU core for the entire lifetime of every DirectAudio session.
The visible symptom was that heavy titles — God of War in particular — never got past a black screen while audio played perfectly. The invisible symptom was that every other game silently lost a core.
The bug
mmdevapi picks a MIDI driver at startup (dlls/mmdevapi/main.c):
midi_drvname[0] = 0;
wine_unix_call( midi_get_driver, midi_drvname );
if (midi_drvname[0]) load_driver( midi_drvname, &midi_driver );
else midi_driver = drvs; // <-- DirectAudio, by defaultDirectAudio left midi_get_driver unimplemented, so it became its own MIDI driver. DriverProc(DRV_LOAD) then spawned notify_thread():
while (1) {
MIDI_CALL( midi_notify_wait, ¶ms );
if (quit) break; // <-- `quit` is UNINITIALISED
if (notify.send_notify) notify_client(¬ify);
}midi_notify_wait is contractually blocking — winealsa's implementation waits on a real event and sets *quit on shutdown. DirectAudio's stub returned STATUS_SUCCESS immediately and never wrote *quit, turning the loop into a tight infinite spin that also hammered the PE→Unix call boundary.
On device the thread appears as mmdevapi_midi_n holding 34079 of the process's 34314 utime jiffies.
This is why the "0 fps / GPU 0% / CPU 17%" signature was so misleading: 17% is one pegged core out of eight, which reads as an idle, deadlocked process rather than a spinning one.
The fix
Mirror what winepulse.drv has always done — implement midi_get_driver to return L"alsa", so mmdevapi loads winealsa.drv for MIDI. midi_notify_wait is also implemented defensively (sets *quit on the first call) so the loop can never spin even if DirectAudio somehow becomes the MIDI driver. Both the native and wow64 vtables are updated.
The audio path is untouched. No change to stream creation, buffer sizing, mixing, resampling or the AAudio path.
Impact
- Unblocks heavy DX12/VKD3D titles that previously black-screened.
- Returns a full CPU core to every DirectAudio game. Titles that already "worked" were paying this tax invisibly.
- Makes low latency reachable in practice — with the core back, small buffers are sustainable.
Test results
Seven games, all on one container (Proton 11.0-5-arm64ec-5), all at a 4 ms requested buffer, all measured live during actual gameplay — not menus.
The fix itself
| Game | Process | Graphics | Audio API | mmdevapi_midi_n |
Threads | CPU /30 s |
|---|---|---|---|---|---|---|
| Insane 2 | i2.exe |
D3D9 | FAudio | none | 37 | 2 182 |
| GTA V Enhanced | PlayGTAV.exe |
D3D12 / VKD3D | WASAPI | none | 121 | 5 788 |
| DiRT Showdown | showdown.exe |
D3D11 | WASAPI | none | 92 | 5 928 |
| Hades | Hades.exe |
D3D11 | FAudio | none | 51 | 2 408 |
| GTA IV | GTAIV.exe |
D3D9 | DirectSound | none | 48 | 7 632 |
| God of War | GoW.exe |
D3D11 | FAudio / XAudio2 | none | 69 | 14 250 |
| DiRT 3 | dirt3_game.exe |
D3D11 | WASAPI | none | 103 | 5 557 |
The spinning thread is gone in every case. winedirectaudio.so confirmed loaded with in-process AAudio in all seven, so every result is genuinely this driver and not a fallback.
Latency and stability
| Game | Buffer | Track latency | Was | Underruns (30 s, foreground) |
|---|---|---|---|---|
| Insane 2 | 192 fr · 4 ms | 25.00 ms | — | 0 |
| GTA V Enhanced | 192 fr · 4 ms | 25.00 ms | — | 0 |
| DiRT Showdown | 192 fr · 4 ms | 25.00 ms | 83.50 ms | 0 |
| Hades | 192 fr · 4 ms | 25.00 ms | 81.00 ms | 0 |
| GTA IV | 192 fr · 4 ms | 25.00 ms | 81.00 ms | 0 |
| God of War | 384 fr · 8 ms | 29.00 ms | 45.00 ms | 0 |
| DiRT 3 | 576 fr · 12 ms | 33.00 ms | 87.50 ms | 0 |
Five of seven sit on the hardware floor. God of War and DiRT 3 could not sustain 4 ms, so the adaptive engine grew their buffers a burst at a time until they stopped underrunning — the safety net working, not a failure. God of War is the heaviest load in the set (14 250 CPU jiffies/30 s, roughly triple the others), which is exactly why it needed the headroom.
Typical improvement versus the previous default: 81–87 ms → 25 ms.
God of War, before and after
The title that exposed the bug:
| before | after | |
|---|---|---|
| screen | black | renders |
| fps | 0.0 | 26.9 |
| GPU | 0% | 70% |
| CPU | 17% (one pegged core of eight) | 97% (real work) |
| threads | 63, all futex_wait + 1 spinning |
75, 9 running incl. dxvk-cs, DxRenderThread |
mmdevapi_midi_n |
R, 34079 / 34314 utime |
gone |
Gameplay confirmed, not just process liveness
Screenshots taken at the end of each run: GTA V mid-heist at 66.4 fps, GTA IV mid-mission at 106.1 fps, DiRT 3 mid-race at 65.2 fps, Insane 2 mid-race at 60.0 fps, God of War at the main menu at 26.9 fps.
Listening test
A human played each title through menus and gameplay while the runs were in progress and reported no crackling on any of them. This is the one check no instrumentation performs, and it is the check a starved audio thread would fail first.
Method
Latency and underruns come from dumpsys media.audio_flinger's track row for the game's own PID. Underruns are reported as a delta sampled over 30 s with the game in the foreground — cumulative totals are unusable, because backgrounding the app freezes the guest process and manufactures underruns that never happened during play. The MIDI verdict is a scan of /proc/<pid>/task/*/comm for a *midi* thread.
Device: AYANEO Pocket FIT — Adreno 750, Android 14, 192-frame AudioFlinger burst, 21.00 ms output latency.
Not tested: Ninja Gaiden Σ.
About the latency numbers
Quote 25 ms, not 4 ms. The 4 ms figure is the buffer size — one AudioFlinger burst. Underneath it sits 21 ms of Android mixer and HAL that no application can remove; native Android games pay it too.
4 ms DirectAudio buffer (tunable)
+ 21 ms Android mixer + HAL (fixed, unavoidable)
= 25 ms track latency (what the player hears)
25 ms is the hardware floor on this device. For reference, PulseAudio's default PULSE_LATENCY_MSEC=100 works out to roughly 121 ms.
Behaviour change worth knowing
winealsa.drv is now loaded into the process as the MIDI driver. It is a dormant library in the game's own process — no daemon, no extra process, and no audio passes through it. On a device with no MIDI hardware it never even starts a thread. If winealsa is unavailable, mmdevapi skips MIDI entirely and DirectAudio is unaffected.
DirectAudio remains daemon-free: game → winedirectaudio.drv → in-process mixer → AAudio.
Downloads
Two builds, both arm64ec, for SDK 28 and SDK 35:
directaudio-v1.2— the release build. Use this one.directaudio-v1.2-diagnostics— identical fix plus 23 logcat probes under theDirectAudiotag (stream lifecycle, buffer/burst/xrun heartbeats, per-call tracing). For debugging a specific title; slightly more overhead.
Each archive contains winedirectaudio.drv (PE) and winedirectaudio.so (unixlib). Drop them into lib/wine/aarch64-windows/ and lib/wine/aarch64-unix/ of a Proton 11.0 layer.
Getting the low-latency configuration
The default preset is conservative (~62.5 ms buffer). The 25 ms results above require the custom preset — auto and low hardcode their buffer and ignore buffer_frames:
BANNER_AUDIO_DIRECT_PRESET=custom
BANNER_AUDIO_DIRECT_PERF=1
BANNER_AUDIO_DIRECT_ADAPTIVE=1
BANNER_AUDIO_DIRECT_BF=192
Keep ADAPTIVE=1. Every 25 ms result above depended on it — two of the seven games needed to grow their buffer, and without the safety net they would crackle permanently instead.
Known limits
- Tested on a single device (Adreno 750, Android 14). Mali GPUs untested.
- Microphone capture, true surround, and mature output-route handling are still gaps versus PulseAudio; see the roadmap.
- ABI-pinned to Wine 11.0 — the
mmdevapiunixlib vtable must match themmdevapi.dllit ships beside.
Credits
Root cause found by comparing DirectAudio's driver vtable against winealsa.drv and winepulse.drv in the Wine tree; the fix follows winepulse's existing pattern.