From 92c6bb9587ba580c877d2e7f1d0d1cb1c650a85a Mon Sep 17 00:00:00 2001 From: Shawn Date: Thu, 18 Jun 2026 17:29:52 -0400 Subject: [PATCH] fix: stop bt-a2dp-jack during profile changes to prevent JACK race The bt-a2dp-jack systemd unit runs an independent JACK instance for Bluetooth audio. When stop_jack() kills jackd, bt-a2dp-jack restarts it within 5 seconds with default parameters. By the time start_jack() runs in the profile handler, _jack_is_running() already returns True for the bt-a2dp JACK, causing start_jack() to return immediately without configuring our period/rate. The pedal's JACK client then fails with server_error (0x21) because the running JACK doesn't match. Fix: - stop_jack() temporarily stops bt-a2dp-jack before killing jackd - Profile handler restarts bt-a2dp-jack after JACK is configured - Rollback path also restarts bt-a2dp-jack --- src/system/audio.py | 9 +++++++++ src/web/server.py | 16 ++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/system/audio.py b/src/system/audio.py index af4c39e..7288b44 100644 --- a/src/system/audio.py +++ b/src/system/audio.py @@ -371,6 +371,15 @@ class AudioSystem: def stop_jack(self) -> None: """Gracefully stop the JACK server.""" + # Stop the Bluetooth A2DP bridge first — it restarts jackd + # independently and can race with our profile changes. + try: + subprocess.run( + ["systemctl", "stop", "bt-a2dp-jack"], + capture_output=True, timeout=5, + ) + except Exception: + pass try: subprocess.run( ["killall", "jackd"], diff --git a/src/web/server.py b/src/web/server.py index 7c3720d..5d3d069 100644 --- a/src/web/server.py +++ b/src/web/server.py @@ -1739,6 +1739,12 @@ class WebServer: jack_client.start() except Exception: pass + # Restart bt-a2dp bridge even on rollback so Bluetooth audio recovers + try: + import subprocess + subprocess.run(["systemctl", "start", "bt-a2dp-jack"], capture_output=True, timeout=5) + except Exception: + pass raise HTTPException( status_code=500, detail=f"Failed to start JACK with period={target_profile['period']}, rate={target_profile['rate']} — rolled back", @@ -1787,6 +1793,16 @@ class WebServer: except Exception as e: logger.warning("connect_fx_ports() failed after profile switch: %s", e) + # Restart Bluetooth A2DP bridge so it can connect to our JACK + try: + import subprocess + subprocess.run( + ["systemctl", "start", "bt-a2dp-jack"], + capture_output=True, timeout=5, + ) + except Exception: + pass + # ── Persist to config.yaml ────────────────────────────── if self.deps.config is not None: audio_cfg = self.deps.config.setdefault("audio", {})