diff --git a/src/system/audio.py b/src/system/audio.py index 78bab91..9ab037f 100644 --- a/src/system/audio.py +++ b/src/system/audio.py @@ -294,8 +294,9 @@ class AudioSystem: # IMPORTANT: On JACK 1.9.22 there is no /dev/shm/jack_default_0_0 # socket. Instead, verify via jack_lsp which tests actual IPC. if _jack_is_running(): - # Verify JACK is actually accepting connections - if _jack_is_operational(): + # Verify JACK is actually accepting connections AND matches + # our device (ignore rogue jackd -T from bt-a2dp's alsa_in) + if _jack_is_operational() and _jack_matches_config(self.config.output_device): logger.info("JACK already running and operational — reusing") return True # Process exists but can't connect — likely a zombie that @@ -356,7 +357,7 @@ class AudioSystem: # Stabilization check — JACK can start briefly then crash # if the ALSA device is flaky. Wait 1.5s with retries. time.sleep(0.5) - if _jack_is_operational(): + if _jack_is_operational() and _jack_matches_config(self.config.output_device): # Auto-connect ports if enabled if self.config.auto_connect: self.connect_fx_ports() @@ -388,6 +389,16 @@ class AudioSystem: ) except Exception: pass + # Kill alsa_in — bt-a2dp's bridge uses it, and when JACK dies + # alsa_in auto-spawns a rogue jackd -T -ndefault that satisfies + # _jack_is_operational() but connects as the wrong server. + try: + subprocess.run( + ["killall", "alsa_in"], + capture_output=True, timeout=3, + ) + except Exception: + pass try: subprocess.run( ["killall", "jackd"], @@ -879,6 +890,35 @@ def _jack_is_operational() -> bool: return False +def _jack_matches_config(cmdline_match: str = "") -> bool: + """Check if the running jackd command matches expected args. + + On systems with bt-a2dp-jack, ``alsa_in`` can auto-spawn a rogue + ``jackd -T -ndefault`` that satisfies ``_jack_is_operational()`` + but uses the wrong server name/ALSA device. Only return True if + the running jackd's ``/proc/*/cmdline`` contains the expected + device string. + + Args: + cmdline_match: String to match in the jackd cmdline + (e.g. ``hw:USB,0`` or a period value). + """ + if not cmdline_match: + return True # no filter — just return operational status + try: + import glob as _glob + for proc_dir in _glob.glob("/proc/[0-9]*"): + try: + cmd = (Path(proc_dir) / "cmdline").read_bytes().decode("utf-8", errors="replace").replace("\0", " ") + if "jackd" in cmd and cmdline_match in cmd: + return True + except (OSError, PermissionError): + continue + return False + except Exception: + return True # on error, don't block — rely on operational check + + # ═══════════════════════════════════════════════════════════════════ # JACK audio client (real-time I/O with pipeline) # ═══════════════════════════════════════════════════════════════════