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:
+14
-2
@@ -228,7 +228,7 @@ class _DelayLine:
|
||||
class _CombFilter:
|
||||
"""Comb filter for Schroeder reverb."""
|
||||
|
||||
__slots__ = ("delay", "feedback", "damping", "damp_filt", "buf")
|
||||
__slots__ = ("delay", "feedback", "damping", "damp_filt", "buf", "_buf_size")
|
||||
|
||||
def __init__(self, delay_samples: int):
|
||||
line_len = max(BLOCK_SIZE * 2, delay_samples + 1)
|
||||
@@ -237,8 +237,14 @@ class _CombFilter:
|
||||
self.damping: float = 0.5 # low-pass damping coefficient
|
||||
self.damp_filt: float = 0.0 # state variable for damping
|
||||
self.buf = np.zeros(BLOCK_SIZE, dtype=np.float32)
|
||||
self._buf_size = BLOCK_SIZE
|
||||
|
||||
def process(self, block: np.ndarray) -> np.ndarray:
|
||||
# Resize internal buffer if block size changed (e.g. JACK period switch)
|
||||
n = len(block)
|
||||
if n != self._buf_size:
|
||||
self.buf = np.zeros(n, dtype=np.float32)
|
||||
self._buf_size = n
|
||||
self.buf[:] = block
|
||||
# Write with feedback: out[n] = in[n] + feedback * damped_delayed
|
||||
delayed = self.delay.add_to_block(self.buf, self.delay.max_len - 1, self.feedback)
|
||||
@@ -255,15 +261,21 @@ class _CombFilter:
|
||||
class _AllpassFilter:
|
||||
"""Allpass filter for Schroeder reverb."""
|
||||
|
||||
__slots__ = ("delay", "gain", "buf")
|
||||
__slots__ = ("delay", "gain", "buf", "_buf_size")
|
||||
|
||||
def __init__(self, delay_samples: int):
|
||||
line_len = max(BLOCK_SIZE * 2, delay_samples + 1)
|
||||
self.delay = _DelayLine(line_len)
|
||||
self.gain: float = 0.5
|
||||
self.buf = np.zeros(BLOCK_SIZE, dtype=np.float32)
|
||||
self._buf_size = BLOCK_SIZE
|
||||
|
||||
def process(self, block: np.ndarray) -> np.ndarray:
|
||||
# Resize internal buffer if block size changed (e.g. JACK period switch)
|
||||
n = len(block)
|
||||
if n != self._buf_size:
|
||||
self.buf = np.zeros(n, dtype=np.float32)
|
||||
self._buf_size = n
|
||||
# out[n] = -gain * in[n] + delay[n - D] + gain * delay_output[n - D]
|
||||
# Standard allpass: out = -g * in + delayed + g * delayed_out
|
||||
# But block-wise: read delayed, write in + g * delayed, output = -g * in + delayed
|
||||
|
||||
+10
-7
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user