Repository navigation
No Sound fix - #69
Merged
Merged
No Sound fix#69
Conversation
added 2 commits
October 21, 2026 23:46
Two silent-system reports (murkantor#19 Batocera, murkantor#56 Windows 11) share a cause that looks nothing like "no sound": the AL synthesizer was never executed. portAudioPump is the only caller of alAudioFrame, and alAudioFrame is the only caller of every AL client handler (synthesizer.c:189), so the SFX event queue, the sound-state list and the three music sequencers advance only while the pump renders. The pump had two device-gated exits - a permanent s_AudioDead flag set by any SDL_OpenAudioDevice failure, and a per-pump break while the playback queue sat above PORT_AUDIO_QUEUE_LOW - so a device that could not be opened, or that opened and then stopped consuming, froze game audio state outright. The Linux report shows the result exactly: the SFX queue pinned at its capacity of 256 (0x40 x GE_SNDSCALE 2 x MUSIC_SFX_EVENT_HEADROOM 2), 3328 events dropped, no voice ever allocated, and the API heartbeat still alive. No system has 22050 Hz s16 stereo natively; it works only because every healthy backend converts behind SDL. Where a 32-bit client cannot reach that layer, the open failed and the game went mute with no message - this was the only SDL subsystem in the port failing without logging, and the Windows build opens no console for anyone to notice. Changes, all in port/src/audio_pc.c: - portAudioInit returns success/failure and logs the driver and the obtained format, or the failing operation with SDL_GetError(). A failed open is retried every 2 s instead of being permanent, so a sink that appears or moves after boot (headset plugged in, PipeWire restart, late 32-bit client stack) is picked up. Failures are rate-limited so a permanently mute machine cannot bury the rest of the log. - The render pace is wall time, with the playback queue as a catch-up trigger rather than a gate: a frame renders when either the audio clock owes one or the queue is below PORT_AUDIO_QUEUE_LOW, so no device state can stall the synthesizer. The clock is kept in samples x1000 so a 147 Hz pump's fractional share of a frame cannot round to zero, and catch-up is clamped to 100 ms per pump so a debugger pause or a long load cannot replay minutes of audio back to back. An over-cap queue now discards that frame, not the render. - An unmoving full queue for ~1 s is treated as a dead sink: close it and let the retry path reopen whatever is there now. This is the one heuristic piece and the part most worth watching in field reports. - portAudioPumpForce renders exactly one frame, as its comment always claimed. It previously bypassed the queue gate per iteration, so a forced call could render eight, and music.c's stop-wait spins it up to 512 times. The healthy path is unchanged: queue-driven catch-up still runs and the wall clock only adds a floor, so systems where audio already worked render at the same pace as before. printf is declared locally rather than pulling <stdio.h>: PR/os.h declares sprintf itself, which collides with the hosted header's fortified macros on macOS, and this TU includes ultra64.h before any libc header. Same workaround as src/snd.c and port/src/printf_port.c. Verified: compiles clean with the macOS build's exact flags (-fsyntax-only, -std=gnu11, -arch arm64, -DGE_NATIVE_64=1 -DGE_VIDEO=1 ...) and adds no -Wall -Wextra diagnostics in the changed region. Not verified: no full build and no runtime test here - upstream is 32-bit and this machine is arm64 macOS - so frame cadence at 60/120/147 Hz pumps and the stall detector still want a real play session. Follow-ups, deliberately not in this commit: - An SDL_AudioStream fallback so a device that refuses 22050 Hz is accepted at its own rate (the synth rate is baked into g_FrameSize, audi.c:344, so the request cannot simply be relaxed). - portAcmdValidate's whole-frame bail-out discards every frame of a run while reporting only through GE_DUMP_BOND. That is invisible in normal logs and is the leading suspect for the Windows murkantor#56 report, which shows no event-queue starvation at all. - Trim the queue back to target instead of pinning it at the cap, so a long session cannot accumulate fixed audio latency.
Author
|
Hey all, throwing this up as a draft because I know this touches the core audio pacing. I've verified the syntax compiles, but as noted, I couldn't reproduce the original mute bug on my Mac, so the stall detector and wall-clock logic haven't been stress-tested on a real Windows/Linux machine. I'm hoping the logging improvements alone are valuable enough to start a discussion on how to properly handle the audio device failures. Happy to iterate on the pacing logic if anyone has thoughts. Edit: Oh god, I did it again, forgot to change branches...lemme try and clean up this mess... |
11 of 13 tasks
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.
Fixes the "completely silent on some systems" reports — #19 (Batocera /
32-bit PipeWire), #67 (Linux: No Audio on Arch with kernel "7.2.11-zen1-1-zen" using PipeWire audio) and (might not) #56 (Windows 11, all-zero
sdlaudio.raw).Symptom
The game runs, renders, responds to input, and produces no sound at all.
Music and SFX are both at 100%. The log looks healthy, because the only audio
line it prints is:
On Linux the failure also leaves a trail, because an SFX event queue that is
never drained eventually reports itself:
That line is the whole diagnosis, and the numbers in it are exact:
256 queuedis this build's queue capacity, not a round number:MUSIC_SFX_SEQ_CONFIG_MAX_EVENTS (0x40)×MUSIC_POOL_SCALE (2)×MUSIC_SFX_EVENT_HEADROOM (2)= 256. The census only prints on a drop,so 256 means 100% full.
254 due now— the events are ready to run and nothing is running them.0 voiced,0 of 16 voices— no synth voice was ever allocated.heartbeat alive— the game thread is still posting happily.Sound is not quiet. The synthesizer was never executed.
Root cause
portAudioPump()(port/src/audio_pc.c) is the only thing that runs the ALsynthesizer:
alAudioFrame()is the sole caller of every AL client handler(
src/libultra/audio/synthesizer.c:189), so the SFX player's event queue, thesound-state list and all three music sequencers advance only when the pump
renders. The synthesizer's clock is the game's audio clock.
The pump had three one-way exits, and two of them let the audio device decide
whether that clock runs at all:
So a device that cannot be opened — or that opens and then stops consuming —
freezes the entire audio subsystem, and the event queue saturates exactly as
logged above.
s_AudioDeadwas set on anySDL_OpenAudioDevicefailure andnever cleared, and the failure path printed nothing: no
SDL_GetError(), nodriver name, no obtained format. This was the only SDL subsystem in the port
that failed without logging (
input_pc.c:2119,video.c:5156,raphnet.c:289all log).That is also why the reports are machine-specific while the code is not. The
port asks for an exact 22050 Hz / s16 / stereo device with
allowed_changes = 0. Almost no system has that natively; it works onlybecause the backend converts behind SDL's back — WASAPI
AUTOCONVERTPCM,CoreAudio, ALSA
plug, PulseAudio/PipeWire resampling. Where that layer is notreachable from a 32-bit client (the Batocera report), or the endpoint refuses
the spec, the open fails and the game goes permanently and silently mute.
Machines where audio works never touch either exit, which is why this survived
so much testing.
Changes
Both are in
port/src/audio_pc.c.1. Device setup is logged and retried, not fatal
portAudioInit()now returns success/failure and reports why on every exitthat leaves the game mute — the operation,
SDL_GetError(), the current audiodriver, the attempt number, and the retry cadence. A successful open logs the
driver and the obtained spec:
s_AudioDeadis gone. The pump retries everyPORT_AUDIO_RETRY_MS(2 s), so asink that appears or changes after boot is picked up: a USB headset plugged in,
a PipeWire restart, a 32-bit client stack that was not up yet. Failures are
rate-limited (first, then every 30th) so a permanently mute machine cannot bury
the rest of the log.
2. The render pace is wall time, and the device can no longer stall it
The pump now keeps an audio clock in samples owed (stored ×1000 to avoid
rounding a 147 Hz pump's fractional share of a 736-sample frame down to zero),
advanced by real elapsed wall time, clamped to 100 ms per pump so a debugger
pause or a long load cannot replay minutes of audio at once.
A frame is rendered when either:
no device attached, and
PORT_AUDIO_QUEUE_LOW— the existing catch-upbehaviour, unchanged, so a live DAC still sets the fine pace.
Neither branch can be blocked by the device.
SDL_QueueAudiois stillskipped when the queue is over
PORT_AUDIO_QUEUE_MAX; the frame is discarded,not the render: the clock has already advanced, so dropping it costs sound, not
sequencer time.
Also fixed while here:
portAudioPumpForce()now renders exactly one frame, asits comment always claimed. The old code bypassed the queue gate per iteration,
so a forced call could render up to 8 frames, and
music.c's stop-wait spins itup to 512 times.
3. A sink that opens and never consumes is detected
The second mute shape: the device opens, the queue never drains, the top-up
stops, and nothing plays again. An unmoving full backlog for
PORT_AUDIO_STALL_PUMPS(~1 s) now closes the device and lets the retry pathreopen it, logged and rate-limited. This is the one heuristic piece here — it
is deliberately conservative (queue at the cap and byte-identical across
pumps), but it is the part most worth watching in field reports.
What this does not change
adds a floor, so a machine where audio already works renders at the same
rate as before.
allowed_changes = 0. Acceptinga device rate would need an
SDL_AudioStreamin front of the synth, becauseaudi.c:344bakes the rate intog_FrameSize. That is the natural follow-up(see below), not part of this patch.
portAcmdRun's whole-frame validation bail-out (port/src/audio_pc.c,portAcmdValidate), which can silently discard every frame of a run. It onlyreports through
GE_DUMP_BOND, so it stays invisible in normal logs. Flaggedfor a separate change.
Verification
What was checked:
(
-fsyntax-only,-std=gnu11 -arch arm64 -DGE_NATIVE_64=1 -DGE_VIDEO=1 ...,taken from that build's
build.ninja). The unmodified file compiles clean inthe same harness, so the comparison is meaningful.
-Wall -Wextraproduces no new diagnostics in the changed region; the onlywarnings are the pre-existing
u32→pointer casts in the aspMain interpreter.What was not checked, and should be before merge:
the available machine is arm64 macOS, so the game was not run with this
patch. Timing behaviour in particular (frame cadence at 60/120/147 Hz pumps)
wants a real play session.
What to look for in a run log after this lands:
ge: audio: device open - driver ...— confirms the sink and its real rate.ge: audio: ... failed ... retrying— the mute report now names its cause.ge: snd: evtq FULLon a machine with no audio — the game'saudio clock should stay healthy even when nothing is plugged in.
Follow-ups
SDL_AudioStreamfallback so a device that refuses 22050 Hz is accepted atits own rate instead of failing (open with
SDL_AUDIO_ALLOW_FREQUENCY_CHANGE, stream 22050 → device rate).portAcmdValidate's bail-out zero the frame and log unconditionally,so a garbage Acmd can never silently produce an all-zero run.
session cannot accumulate fixed audio latency.
For reporters, after the build lands
Windows:
SDL_AUDIODRIVER=disk ProjectMidas.exe, then checksdlaudio.rawsize and look for
ge: audio:lines inData/Logs/ge_direct.log. Before thispatch a mute system produced no
ge: audio:line beyond the volume print; fromnow on it produces a reason.
Type of Change
Please delete options that are not relevant.
How Has This Been Tested?
Checklist