Audio: silent GetBuffer, 7.1.4 fold-down, ring-fill pacing - #180
Merged
Merged
Conversation
Ori and the Will of the Wisps played nothing but crackle. Its Wwise renders through Wine's ISpatialAudioObjectRenderStream: spatialaudio.c takes the render buffer with IAudioRenderClient_GetBuffer and mixes each static object into it with `*out += *in`, but never clears it. WASAPI leaves GetBuffer contents undefined, and ios_get_render_buffer handed back the same scratch area every period, so every period was added onto the previous one. The [ios_audio] ml1065 source level of the 12-channel stream climbed from mean|x| 0.01 to 5 with peaks of 36. (winecoreaudio and winepulse do not clear the buffer either; upstream Wine has the same latent bug.) - ios_get_render_buffer zeroes the frames it hands out. - The stereo fold-down honours a channel mask up to 12 channels (mix_gain[12][2], mixer, attach log and source statistics loop to 12): the top front/back left/right speakers go to their own side at -3 dB, the top centres to both sides at -3 dB. Before, a stream with more than 8 channels got the default 7.1 layout and channels 9-12 were dropped. - The leftover-channel comment no longer claims the top speakers are not placed. Tagged ml1227. Verified on the device (Ori and the Will of the Wisps, 2026-10-02): the ml1065 level stayed bounded (mean|x| 0.0005-0.01, peak <= 0.63, no clipping) and the sound became recognisable; the crackle that remained was gaps, fixed separately by the event pacing change. tests/host/check-audio-downmix.py gains a 7.1.4 case (fails before this change, passes after). Rebuild app/Madeira/libntdll_unix.a with build/ntdll-unix/build.sh (the archive is not committed). Signed-off-by: spitefulowl <spitefulowll@gmail.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With the GetBuffer fix Ori and the Will of the Wisps' sound was recognisable but still crackled: the game delivered 212 s of audio in ~230 s of gameplay, a 10 ms hole every ~120 ms, ~89 GetBuffer calls a second instead of 100. Wine's ISpatialAudioObjectRenderStream (which Wwise renders through) asks for exactly one period per event, and ios_timer_loop signalled the event after every usleep(10000). Every late wake-up of that thread was 10 ms of audio never written, so the ring ran dry and the device played a gap. The same holds for any client that writes a fixed period per event. ios_timer_loop now paces by ring fill for live streams (null mode keeps the plain 10 ms beat): - It keeps a lead of MADEIRA_AUDIO_LEAD_MS (default 60 ms, 10..500, at most the ring minus one period) queued. Below the lead it signals again as soon as the client has written since the last signal, or after 10 ms if it has not, polling every 1 ms; at or above the lead it sleeps until the lead would be used up (1-10 ms), on mach_wait_until deadlines. - The timer thread runs under THREAD_TIME_CONSTRAINT_POLICY (period 10 ms, computation 0.5 ms, constraint 5 ms), as Core Audio's I/O thread does. - The RT callback counts per stream the frames it asked for and did not get, short passes, passes and the largest pass (relaxed atomics), and device-wide the samples clamped to +-1. Every ~10 s per live stream: `[ios_audio] ml1230 pacing stream=... client wrote X s of audio in Y s`. Fixed relative to the device build: the loop loaded write_pos before play_pos, so a render pass between the two loads made wr - play wrap to ~2^64; the loop then slept 10 ms exactly when the ring was low. It now loads play_pos first and clamps the padding at 0 (a reset stores the two positions one after the other). Tagged ml1230. docs/madeira.cfg.example documents the lead; the config catalog lists env.MADEIRA_AUDIO_LEAD_MS. Verified on the device (Ori and the Will of the Wisps, 2026-10-02, the build without the load-order fix): clean sound by ear; 38 of 39 ten-second windows wrote 99.8-100.4% with 0 short passes, ring 1856..3328 frames, worst late wake <= 0.1 ms. The one bad window was a 37 s load at 4-7 FPS in which the game's audio thread itself did not run. Latency grows to ~60 ms in the ring plus the RemoteIO buffer. The load-order fix is built only (syntax-checked with the build/ntdll-unix/build.sh flags). Rebuild app/Madeira/libntdll_unix.a with build/ntdll-unix/build.sh (the archive is not committed). Signed-off-by: spitefulowl <spitefulowll@gmail.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
willfaust
added a commit
that referenced
this pull request
Oct 4, 2026
- metal-validation (#182) is read through madeira_cfg.h, so the settings catalog lists it. - A game's fastsync semaphore choice (#181) is exported only when the game made one, so madeira.cfg's env.MADEIRA_FASTSYNC_SEM still applies; MADEIRA_CPU_COUNT and DXMT_D9_ANISO_LIMIT are unset when a game does not choose them, so a previous game's value never beats madeira.cfg. - The JIT-pool dump (#179) stays on by default, sparse; MADEIRA_JIT_DUMP=0 turns it off. The 256 MB pool choice says large games can run short. - MADEIRA_AUDIO_LEAD_MS=0 (#180) restores the plain 10 ms beat without the time-constraint policy; a stream that leaves two wake-ups unanswered is polled on the 10 ms beat instead of every 1 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Ori and the Will of the Wisps played nothing but crackle. Its Wwise renders through Wine's
ISpatialAudioObjectRenderStream, and two problems added up.Commits
spatialaudio.cmixes each static object into the render buffer with*out += *inbut never clears it. WASAPI leaves GetBuffer contents undefined, andios_get_render_bufferhanded back the same scratch area every period, so every period was added onto the previous one. The source level of the 12-channel stream climbed from mean |x| 0.01 to 5. winecoreaudio and winepulse do not clear the buffer either, so upstream Wine has the same latent bug.ios_get_render_buffernow zeroes the frames it hands out.usleep(10000), so every late wake-up was 10 ms of audio never written.MADEIRA_AUDIO_LEAD_MSqueued (default 60 ms, 10-500). Below the lead it signals again as soon as the client has written; at or above the lead it sleeps until the lead would be used up.THREAD_TIME_CONSTRAINT_POLICY, as Core Audio's I/O thread does.[ios_audio] ml1230 pacingline is printed every ~10 s per live stream.Testing
check-audio-downmix.pygains a 7.1.4 case, which fails before this change and passes after.play_posbeforewrite_posand clamps the padding at 0.Notes
No binary is included.
libntdll_unix.ais built bybuild/ntdll-unix/build.sh.🤖 Generated with Claude Code