Releases: The412Banner/directaudio
Release list
DirectAudio v1.3.1 — live in-game config
Driver packages — complete, both ABIs
Packaging refreshed: this release now attaches complete 3-file driver sets for both Wine majors (the earlier zips were Wine-11-only and missing the 32-bit PE). The driver code is unchanged v1.3.1.
| download | Wine | for these Proton layers |
|---|---|---|
directaudio-wine11-arm64ec-sdk28.zip / -sdk35.zip |
11 | Proton 11.0-1 / 11.0-3 / 11.0-5 |
directaudio-wine10-arm64ec-sdk28.zip / -sdk35.zip |
10 | Proton 10.0-4 |
sdk28 = 4 KB-page devices, sdk35 = 16 KB-page. Each zip is the full driver — three files dropped onto lib/wine/:
aarch64-windows/winedirectaudio.drv— loaded by 64-bit guest gamesi386-windows/winedirectaudio.drv— loaded by 32-bit (wow64) guest gamesaarch64-unix/winedirectaudio.so— the unixlib (shared by both; all AAudio logic lives here)
Which PE loads follows the guest game's bitness, not the Proton build — ship all three or 32-bit titles silently keep whatever driver the layer already had. One Wine-11 build serves all three 11.0-x point releases (they're ABI-compatible); Wine 10 needs the separate wine10 build.
Proton layers with DirectAudio built in: 11.0-1 · 11.0-3 · 11.0-5 · 10.0-4.
(*-diagnostics-*.zip = the same driver with per-callback logging, for field debugging.)
DirectAudio v1.3.1 — live in-game config
DirectAudio settings can now change while a game is running, no relaunch.
The driver normally reads its config from the environment at stream open, so a host UI change needed a relaunch. BANNER_AUDIO_DIRECT_RUNTIME=<file> opts into a "mailbox": a flat KEY=VALUE file the host rewrites in-game (keys MS / MAXMS in milliseconds, PERF as 0/1/2). A lazy 1-second watcher thread (off the real-time audio path) stats it and, on change, re-reads and rebuilds the stream via the existing reopen worker — a ~77 ms swap the guest's ring buffer covers, so it's inaudible. Values ≤ 0 or an absent key revert to the launch config. Unset = feature off, byte-identical to v1.3.0.
In Bannerlator this powers the in-game audio cog: change the DirectAudio latency or preset and Save, and the running driver reopens live.
Also fixed a mailbox PERF bug caught on device: the override set the raw 0/1/2 instead of the AAudio enum (NONE=10/POWER_SAVING=11/LOW_LATENCY=12), so a PERF=0 write opened an invalid mode and stalled. Now mapped correctly, and stress-proven (PERF toggles + background/foreground cycles, zero stalls).
Carries forward everything from v1.3.0 — soft-knee limiter, honest exclusive-mode reporting, daprobe, the 12 ms / 33 ms default, adaptive decay.
Assets — complete driver sets, both Wine ABIs
These assets are now built for both Wine ABIs (a driver is ABI-matched to the mmdevapi unixlib vtable of the Wine version it loads into), and each is a complete 3-file set:
| file | what | loaded for |
|---|---|---|
aarch64-windows/winedirectaudio.drv |
arm64ec PE | 64-bit guest games |
i386-windows/winedirectaudio.drv |
32-bit PE | 32-bit guest games |
aarch64-unix/winedirectaudio.so |
bionic unixlib (the AAudio backend) | always |
Which PE loads is decided by the guest game's bitness, not the device — earlier assets shipped only the arm64ec PE, so 32-bit guests had no driver. A complete set carries both.
Wine 11.0 (ge-proton11-5-bionic-staging) — overlay onto Wine-11 (Proton 11.0-x) layers:
directaudio-wine11-arm64ec-sdk28.zip— 4 KB-page devicesdirectaudio-wine11-arm64ec-sdk35.zip— 16 KB-page devices
Wine 10.0 (proton_10.0, the tree Proton 10.0-4 shipped from) — overlay onto Wine-10.x layers:
directaudio-wine10-arm64ec-sdk28.zip— 4 KB-page devicesdirectaudio-wine10-arm64ec-sdk35.zip— 16 KB-page devices
Diagnostics (23 logcat probes, Wine 11.0):
directaudio-v1.3.1-diagnostics-arm64ec-sdk28.zip/-sdk35.zip
Match the ABI to your layer's Wine version and the page size (sdk28 = 4 KB, sdk35 = 16 KB) to your device, then drop the whole set onto the layer's lib/wine/ (it mirrors that layout: aarch64-windows/, i386-windows/, aarch64-unix/).
DirectAudio v1.3.0 — soft-knee limiter, honest exclusive mode, daprobe
Native Wine → Android AAudio audio driver. A Wine mmdevapi backend that carries guest WASAPI audio straight to Android AAudio — no PulseAudio daemon and no ALSA server in the path. All per-stream mixing happens in-process, inside the driver.
v1.3.0 is a quality-and-honesty release: it fixes the one thing on the downmix path you could actually hear, stops the driver from lying about a capability it never had, and ships a tool to see what the audio stack is really doing.
What's new
🎚️ Soft-knee limiter on the surround downmix.
The 5.1 → stereo fold is a correct ITU-style mix (centre and surrounds at −3 dB), but it had no headroom — loud surround content could peak at ~2.41× full scale and hard-clip. It now passes through a soft-knee limiter, so surround downmix no longer clips, at no latency cost. This was the highest-value item on the roadmap ("downmix headroom") and it's now done.
🔎 Honest exclusive-mode reporting.
create_stream used to register every stream as an ordinary shared mixer voice while still advertising EXCLUSIVE support it didn't provide. That false claim is gone. The open log now prints the granted sharing mode alongside the requested one, so BANNER_AUDIO_DIRECT_EXCLUSIVE=1 is truthful about what the device actually gave back:
DirectAudio: open: buffer 576 frames (12 ms) burst 192 cap 4800 perf 12 sharing req=0 got=0 period 10 ms
sharing got=1 means a genuine exclusive stream was granted — the one path that bypasses AudioFlinger's 21 ms mixer and can take total latency below 25 ms on hardware that allows it. AAudio silently falls back to shared where it can't.
🧪 daprobe — a WASAPI capability probe.
A small probe .exe (built on its own workflow) that reports the three answers a game can get and confuse: what's accepted in shared mode (mmdevapi converts, so nearly everything), what's natively openable in exclusive mode (the honest answer), and what's actually rendered (GetMixFormat — 48 kHz float stereo). It's the measuring instrument for the surround work still ahead.
Carried forward from v1.2.2
The 12 ms / 33 ms shipped default (was 62.5 ms / 83 ms), adaptive decay (a grown buffer comes back down when calm), and the full millisecond latency knobs (_MS / _MAXMS / _PERIOD_MS, etc.). See the README for every environment variable.
Compatibility
ABI-matched to Wine 11.0 (ge-proton11-5-bionic-staging). The mmdevapi unixlib vtable is index-based; a single build cannot span Proton 10 and 11 — pin per Wine major version. MIDI is delegated to winealsa.drv. Verified on Adreno 750 / Android 14 across D3D9/11/12+VKD3D and WASAPI/FAudio/XAudio2/DirectSound. Mali GPUs untested. No microphone capture or true multichannel output yet.
Assets
Each zip is a flat winedirectaudio.so (unixlib) + winedirectaudio.drv (arm64ec PE) pair.
| asset | build | device |
|---|---|---|
directaudio-arm64ec-sdk28.zip |
release | 4 KB-page (most devices) |
directaudio-arm64ec-sdk35.zip |
release | 16 KB-page |
directaudio-v1.3.0-diagnostics-arm64ec-sdk28.zip |
diagnostics (23 logcat probes) | 4 KB-page |
directaudio-v1.3.0-diagnostics-arm64ec-sdk35.zip |
diagnostics | 16 KB-page |
Install (hot-swap into the device Proton layer):
winedirectaudio.so → lib/wine/aarch64-unix/
winedirectaudio.drv → lib/wine/aarch64-windows/
The diagnostics builds are byte-for-byte the release logic plus per-callback logcat tracing (tag DirectAudio); use them to trace on-device, ship the release ones.
Roadmap after this
Route-change format handling (re-derive the resampler ratio on a new route) → real surround (Android Spatializer 5.1) → microphone capture. Plus Mali verification and a lower host-app preset rung.
DirectAudio by The412Banner · LGPL-2.1-or-later
directaudio-v1.2.2
DirectAudio v1.2.2 — native Wine → Android AAudio (winedirectaudio.drv). Wine 11 only.
⚠️ The default changed
| v1.2.1 | v1.2.2 | |
|---|---|---|
| buffer | 62.5 ms | 12 ms |
| total latency | 83 ms | 33 ms |
| growth ceiling | 250 ms | 100 ms |
Latency figures are totals — the driver's buffer plus Android's fixed ~21 ms mixer/HAL cost,
which no application on the platform can go under. A "12 ms buffer" is 33 ms to the ear.
This affects hosts that bundle the driver and set no environment — they get the new value
directly. A host that writes its own buffer (Bannerlator's audio presets do) is unaffected.
The buffer can come back down
Adaptive growth used to be one-way: a single loading screen taxed latency for the rest of the
session. After 10 s with no underrun the engine now hands a burst back. Two guards stop the
shrink → underrun → grow oscillation that makes Oboe's LatencyTuner refuse to shrink at all:
- the floor is the size the stream opened at — decay only ever undoes growth, never undercuts
what was asked for; - an underrun within 5 s of a step down marks that level unsustainable for the title, pins it,
and doubles the patience (capped ~5 min). A game that needs headroom settles after a probe or two.
BANNER_AUDIO_DIRECT_DECAY=0 restores grow-only.
Latency in milliseconds
BANNER_AUDIO_DIRECT_MS / _MAXMS replace counting frames, and win over _BF/_MBF — a host
writes those from its own preset every launch, so a hand-typed frame count could never stick.
The value is rounded up to a burst multiple after the stream opens, since only then is the
device's burst known.
Nine values that were compiled in are now tunable
_PERIOD_MS / _MINPERIOD_MS (the device period reported to the guest — the guest half of
latency, previously a fixed 10 ms), _EXCLUSIVE, _WATCHDOG / _STALL_MS, _DECAY_QUIET_MS /
_DECAY_PUNISH_MS / _DECAY_MAXBACKOFF, and _LOG.
_LOG=1 gives heartbeats plus growth and decay steps on a release build (logcat -s DirectAudio) — a field report no longer needs a separate diagnostics binary. Every stream open
logs the buffer, burst, capacity, and the performance and sharing modes actually granted.
Two titles that ALSA can't run
FNAF Security Breach and DRAGON QUEST VII Reimagined are reported to crash or fail to boot
on ALSA. Both run on DirectAudio — at 25 ms and 29 ms respectively.
Fixes
- startup underruns were blamed on a decay step that had never happened, pinning the floor within
3 s of launch and silently disabling decay for the session - a stream no longer current still did the mixer's bookkeeping, resizing itself while the log
reported the live stream's size
Downloads
directaudio-arm64ec-sdk28.zip— 4KB-page devicesdirectaudio-arm64ec-sdk35.zip— 16KB-page devices
Each contains winedirectaudio.drv for lib/wine/aarch64-windows/ and winedirectaudio.so for
lib/wine/aarch64-unix/ of a Proton 11.0 layer.
Device-proven on Adreno 750 / Android 14 / Proton 11.0-5-arm64ec-5, across DiRT 3, FNAF Security
Breach and DRAGON QUEST VII Reimagined. Tested on one device; Mali untested. Wine 11 only —
the mmdevapi unixlib vtable differs on Wine 10, so one build cannot span Proton 10 and 11.
DirectAudio v1.2.1 - recover from a dead AAudio data callback
Fixes permanent audio loss — the game keeps running, but sound never returns until you relaunch it.
Reported by JT against GameNative, reproduced on Bannerlator with v1.2, so it is driver-side, not app-specific.
The trigger is rapid background/foreground of the app — not pause/resume. The in-drawer Pause/Resume does not reproduce it.
What was happening
Backgrounding freezes the guest, so the in-process AAudio stream starves. AudioTrack disables itself after the underruns and auto-restarts, but the data callback never resumes — and this raises no error, so the driver's only stream-rebuild path never ran. It kept reporting playing=1 while nothing reached the speaker.
Fixed
- Stall watchdog — if the AAudio callback goes silent for 1 s with audio playing, the stream is rebuilt. A disabled AAudio stream can't be restarted, so recreate is the only recovery.
- Liveness counter —
cb_countwas only incremented inside aTRACE_ON()test, so with tracing off a dead callback looked identical to a live one. - Adaptive buffer — no longer treats a frozen guest as timing pressure. It was inflating the buffer permanently (192 → 5568 frames, 4 ms → 116 ms).
- Rebuilds are now logged in the release build —
DirectAudio: reopen: <reason>. A few lines per session, no diagnostics build needed.
Verified on device (DiRT 3)
| before | after | |
|---|---|---|
| audio after bg/fg abuse | lost permanently | no dropout at all |
| buffer | 192 → 5568 fr, never shrinks | back to 192 fr each rebuild |
| recovery | relaunch required | 5 rebuilds, none audible |
| 5 min normal play | — | 0 false rebuilds, buffer flat at 8 ms |
Recovery is inaudible because the new stream is started before the old one is destroyed — a ~77 ms swap the guest's ring buffer covers.
Downloads
directaudio-v1.2.1-sdk28/35.zip— release build. Use this one.directaudio-v1.2.1-diagnostics-sdk28/35.zip— same fix plus 23 logcat probes, for debugging a specific title.
Each contains winedirectaudio.drv and winedirectaudio.so for lib/wine/aarch64-windows/ and lib/wine/aarch64-unix/ of a Proton 11.0 layer.
Includes everything from v1.2 (the mmdevapi MIDI thread that spun a CPU core all session). Wine 11 only — the vtable differs on Wine 10, so one build cannot span Proton 10 and 11.
Tested on one device (Adreno 750, Android 14). Mali untested.
Defaults in this release
With no host configuration, v1.2.1 runs:
| v1.2.1 default | total latency | |
|---|---|---|
| buffer | 62.5 ms (3000 frames) | 83 ms |
| growth ceiling | 250 ms (device capacity) | |
| performance mode | LOW_LATENCY |
|
| adaptive growth | on | |
| device period | 10 ms (min 5 ms) | |
| sharing mode | SHARED |
|
| callback watchdog | on, 1 s |
Latency figures are totals — the driver's buffer plus Android's fixed 21 ms
mixer/HAL cost. A 4 ms buffer is 25 ms total, and that 25 ms is the floor no
application on Android can go below. Hosts that select a preset (Bannerlator
does) override the buffer default; hosts that bundle the driver and set nothing
get the values above.
Environment variables recognised by this release:
| variable | meaning |
|---|---|
BANNER_AUDIO_DIRECT_PERF |
0 NONE · 1 LOW_LATENCY (default) · 2 POWER_SAVING |
BANNER_AUDIO_DIRECT_ADAPTIVE |
1 grow on underrun (default) · 0 fixed buffer |
BANNER_AUDIO_DIRECT_BF |
initial buffer in frames (0 = default) |
BANNER_AUDIO_DIRECT_MBF |
cap for adaptive growth (0 = device capacity) |
Buffer values here are in frames: 48 frames = 1 ms. Millisecond knobs
(_MS / _MAXMS), buffer decay, the tuning and logging knobs, and a lower
12 ms / 33 ms default are not in this release — they are in development.
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.