diff --git a/src/dsp/pipeline.py b/src/dsp/pipeline.py index e48b1fc..ce025de 100644 --- a/src/dsp/pipeline.py +++ b/src/dsp/pipeline.py @@ -352,6 +352,15 @@ class AudioPipeline: # load_preset() holds it only for the atomic swap (not during model I/O). self._lock = threading.Lock() + # ── DC-blocking filter state ─────────────────────────────────────── + # Single-pole high-pass at ~15Hz to remove DC offset and subsonic + # noise that can amplify into audible hum through NAM models. + # Filter: y[n] = alpha * (y[n-1] + x[n] - x[n-1]) + # where alpha = 1 / (1 + 2*pi*fc/sr) + self._dc_prev_x: float = 0.0 + self._dc_prev_y: float = 0.0 + self._dc_alpha: float = 1.0 / (1.0 + 2.0 * np.pi * 15.0 / 48000.0) + logger.info("Audio pipeline initialized (block=%d, sr=%d)", self._block_size, self._sample_rate) @@ -646,6 +655,21 @@ class AudioPipeline: ) buf = audio_in.copy() + # ── DC-blocking high-pass filter ──────────────────────────── + # Removes DC offset and subsonic noise (~15Hz corner) that can + # amplify into audible hum through high-gain NAM models. + # First-order HPF: y[n] = alpha * (y[n-1] + x[n] - x[n-1]) + alpha = self._dc_alpha + px = self._dc_prev_x + py = self._dc_prev_y + for i in range(len(buf)): + y = alpha * (py + buf[i] - px) + px = buf[i] + py = y + buf[i] = y + self._dc_prev_x = px + self._dc_prev_y = py + for idx, entry in enumerate(chain): if entry["bypass"] or not entry["enabled"]: continue @@ -683,6 +707,23 @@ class AudioPipeline: ch0 = audio_in[0, :].copy() ch1 = audio_in[1, :].copy() + # ── DC-blocking high-pass filter on both channels ─────────── + alpha = self._dc_alpha + px = self._dc_prev_x + py = self._dc_prev_y + for i in range(len(ch0)): + y = alpha * (py + ch0[i] - px) + px = ch0[i] + py = y + ch0[i] = y + for i in range(len(ch1)): + y = alpha * (py + ch1[i] - px) + px = ch1[i] + py = y + ch1[i] = y + self._dc_prev_x = px + self._dc_prev_y = py + # Update input VU level from the guitar input channel (ch0) in_rms = np.sqrt(np.mean(ch0 ** 2) + _EPS) self._input_level = ( @@ -3102,6 +3143,7 @@ class AudioPipeline: self._block_size = block_size self._sample_rate = sample_rate self._vu_alpha = np.exp(-block_size / (0.05 * sample_rate)) + self._dc_alpha = 1.0 / (1.0 + 2.0 * np.pi * 15.0 / sample_rate) # Clear DSP state — effects will reinit with new block/sample rate self._state.clear() self._coeffs.clear() diff --git a/src/web/server.py b/src/web/server.py index 5d3d069..7c22da2 100644 --- a/src/web/server.py +++ b/src/web/server.py @@ -1768,14 +1768,15 @@ class WebServer: # Clean stale client SHM before creating new JACK client. # The old process's jack.Client __del__ can recreate the # pi-multifx semaphore after stop_jack() cleaned it. + # IMPORTANT: Only target our client's semaphore, NOT the + # running JACK server's files (jack_sem.*_system, jack-shm-registry, + # jack_db-0/) — deleting those between start_jack() and + # jack_client.start() kills the JACK server socket and makes + # the client connection fail silently. try: from pathlib import Path - import shutil - for p in Path("/dev/shm").glob("jack*"): - if p.is_dir(): - shutil.rmtree(p, ignore_errors=True) - else: - p.unlink(missing_ok=True) + for p in Path("/dev/shm").glob("jack_sem.*.default_pi-multifx*"): + p.unlink(missing_ok=True) except Exception: pass