From d9757e5f8eba8020d92809bcea4bee697f4812af Mon Sep 17 00:00:00 2001 From: Dalexanco Date: Sun, 16 Aug 2026 23:46:57 +0200 Subject: [PATCH] fix(audio): unbreak bluetooth audio - no sound, and freeze on disconnect Two bugs, one cause. Over a bluetooth headset games had no sound at all, and disconnecting the headset mid-game froze the emulator for good. alsa-lib guards every PCM with a recursive lock, and on the bluealsa sink that lock is never released once the PCM is open. SDL's audio thread blocks inside libasound and never runs our callback - that is the silence. The freeze follows from it: on disconnect minarch resets the audio device from the game loop, and SDL_CloseAudioDevice() waits forever for that stuck thread to exit. LIBASOUND_THREAD_SAFE=0 fixes both. SDL already serialises opening and closing a device against its own audio thread, so alsa's internal locking buys us nothing. The same lock is why SND_quit() was commented out, with a note blaming SDL stream reconnection: closing the device on exit hit the same deadlock. It works now, so the call comes back at the end of main(). It stays out of Core_unload(), which runs while the core is still loaded and can still emit audio into the buffer SND_quit() frees. Tested on a Brick (tg5040): sound in the headset, no freeze on disconnect, clean exit back to the launcher, and audio still fine on the next launch. --- skeleton/SYSTEM/tg5040/paks/MinUI.pak/launch.sh | 9 +++++++++ skeleton/SYSTEM/tg5050/paks/MinUI.pak/launch.sh | 9 +++++++++ workspace/all/minarch/ma_core.c | 7 +++---- workspace/all/minarch/minarch.c | 5 +---- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/skeleton/SYSTEM/tg5040/paks/MinUI.pak/launch.sh b/skeleton/SYSTEM/tg5040/paks/MinUI.pak/launch.sh index e4a585713..7fcf85dfa 100755 --- a/skeleton/SYSTEM/tg5040/paks/MinUI.pak/launch.sh +++ b/skeleton/SYSTEM/tg5040/paks/MinUI.pak/launch.sh @@ -26,6 +26,15 @@ export HOOKS_PATH="$USERDATA_PATH/.hooks" export DATETIME_PATH="$SHARED_USERDATA_PATH/datetime.txt" export HOME="$USERDATA_PATH" +# alsa-lib >= 1.1.2 guards each snd_pcm_t with a recursive lock. On the +# bluealsa sink that lock ends up held by the thread that opened the PCM and +# never released, so SDL's audio thread blocks inside libasound forever: no +# sound over bluetooth, and SDL_CloseAudioDevice() then deadlocks the game +# loop joining that thread when the headset disconnects. We never touch one +# PCM from two threads at once (SDL serialises that itself), so alsa's own +# locking buys us nothing - turn it off. +export LIBASOUND_THREAD_SAFE=0 + ####################################### if [ -f "/tmp/poweroff" ]; then diff --git a/skeleton/SYSTEM/tg5050/paks/MinUI.pak/launch.sh b/skeleton/SYSTEM/tg5050/paks/MinUI.pak/launch.sh index ec5a9af56..52fd3d762 100755 --- a/skeleton/SYSTEM/tg5050/paks/MinUI.pak/launch.sh +++ b/skeleton/SYSTEM/tg5050/paks/MinUI.pak/launch.sh @@ -26,6 +26,15 @@ export HOOKS_PATH="$USERDATA_PATH/.hooks" export DATETIME_PATH="$SHARED_USERDATA_PATH/datetime.txt" export HOME="$USERDATA_PATH" +# alsa-lib >= 1.1.2 guards each snd_pcm_t with a recursive lock. On the +# bluealsa sink that lock ends up held by the thread that opened the PCM and +# never released, so SDL's audio thread blocks inside libasound forever: no +# sound over bluetooth, and SDL_CloseAudioDevice() then deadlocks the game +# loop joining that thread when the headset disconnects. We never touch one +# PCM from two threads at once (SDL serialises that itself), so alsa's own +# locking buys us nothing - turn it off. +export LIBASOUND_THREAD_SAFE=0 + ####################################### if [ -f "/tmp/poweroff" ]; then diff --git a/workspace/all/minarch/ma_core.c b/workspace/all/minarch/ma_core.c index 3572cdbe7..d838049e0 100644 --- a/workspace/all/minarch/ma_core.c +++ b/workspace/all/minarch/ma_core.c @@ -152,10 +152,9 @@ void Core_reset(void) { Rewind_on_state_change(); } void Core_unload(void) { - // Disabling this is a dumb hack for bluetooth, we should really be using - // bluealsa with --keep-alive=-1 - but SDL wont reconnect the stream on next start. - // Reenable as soon as we have a more recent SDL available, if ever. - //SND_quit(); + // Audio teardown deliberately does not happen here: SND_quit() frees the + // sample buffer, and the core can still emit audio from unload_game(), + // which runs later in Core_quit(). It is done at the end of main(). } void Core_quit(void) { if (core.initialized) { diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index b3e156eda..30ac0d26e 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -367,10 +367,7 @@ int main(int argc , char* argv[]) { PWR_quit(); VIB_quit(); SND_removeDeviceWatcher(); - // Disabling this is a dumb hack for bluetooth, we should really be using - // bluealsa with --keep-alive=-1 - but SDL wont reconnect the stream on next start. - // Reenable as soon as we have a more recent SDL available, if ever. - //SND_quit(); + SND_quit(); PAD_quit(); GFX_quit(); Menu_waitScreenshot();