diff --git a/src/dsp/pipeline.py b/src/dsp/pipeline.py index 0ee1f6c..f4cc61e 100644 --- a/src/dsp/pipeline.py +++ b/src/dsp/pipeline.py @@ -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 diff --git a/src/web/server.py b/src/web/server.py index 43b9355..889efc4 100644 --- a/src/web/server.py +++ b/src/web/server.py @@ -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")