Fix audio timeout when changing buffer size/sample rate

Two bugs:

1. **Deadlock in set_audio_profile() (root cause of timeout)**
   jack_client.stop() was called BEFORE killing the JACK server.
   jack.Client.deactivate() is a synchronous call that blocks until
   the JACK RT callback thread acknowledges. When ALSA reconfigures
   (period/rate change), the RT thread can stall and deactivate()
   hangs permanently. Reversed order: kill JACK server first (via
   killall jackd) to force-kill the RT thread, then clean up the
   Python client. Same fix applied to device hot-swap path.

2. **Pipeline crash after period change**
   _CombFilter.buf and _AllpassFilter.buf were fixed-size arrays
   at BLOCK_SIZE (256). If JACK restarts with a different period
   (e.g. 512), self.buf[:] = block throws a shape mismatch in the
   RT callback -> audio dies permanently. Made internal buffers
   dynamically resize to match input block size.
This commit is contained in:
2026-06-16 17:15:22 -04:00
parent 0fa8bd387a
commit 2c4d7298ec
2 changed files with 24 additions and 9 deletions
+10 -7
View File
@@ -1480,17 +1480,18 @@ class WebServer:
target_profile["period"], target_profile["rate"],
)
# Stop JACK audio client first
jack_client = self.deps.jack_audio
if jack_client:
jack_client.stop()
# Update LATENCY_PROFILES with custom entry so latency_profile resolves it
LATENCY_PROFILES[effective_key] = target_profile
# ── Kill JACK server FIRST (breaks any client deadlock) ──
# This interrupts the audio callback thread, releasing any lock
# that jack_client.stop() would need.
# jack.Client.deactivate() is a synchronous JACK protocol call
# that blocks until the RT callback thread acknowledges. If the
# RT thread is stuck (ALSA reconfiguring, xruns), deactivate()
# hangs permanently. Kill the server first to force-kill the
# RT thread, then clean up the client.
# See also: JackAudioClient.stop() which calls deactivate().
try:
audio_sys.stop_jack()
except Exception:
@@ -1634,13 +1635,15 @@ class WebServer:
try:
audio_sys = self.deps.audio_system
jack_client = self.deps.jack_audio
if jack_client:
jack_client.stop()
# Kill JACK server FIRST to avoid deadlock on
# jack.Client.deactivate() (same pattern as profile switch)
if "input_device" in data:
audio_sys.config.input_device = str(data["input_device"])
if "output_device" in data:
audio_sys.config.output_device = str(data["output_device"])
audio_sys.stop_jack()
if jack_client:
jack_client.stop()
ok = audio_sys.start_jack(timeout=10)
if not ok:
logger.warning("JACK restart failed after device change — rolling back")