fix: replace DC-blocking HPF with 60Hz biquad notch filter (hum fix)
CI / test (push) Has been cancelled

This commit is contained in:
2026-06-18 17:54:23 -04:00
parent 4ad50e3588
commit 4b694cc5f8
+58 -33
View File
@@ -352,14 +352,23 @@ 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)
# ── Mains hum notch filter state (60Hz notch) ───────────────────────
# Biquad notch filter to remove 60Hz mains hum without affecting
# guitar frequencies (lowest open string is 82Hz E2).
# Direct Form I state: two input delays (x1, x2) and two output delays (y1, y2)
self._notch_x1: float = 0.0
self._notch_x2: float = 0.0
self._notch_y1: float = 0.0
self._notch_y2: float = 0.0
self._notch_b0: float = 1.0
self._notch_b1: float = 0.0
self._notch_b2: float = 0.0
self._notch_a1: float = 0.0
self._notch_a2: float = 0.0
# Compute initial coefficients for 60Hz notch with Q=5
_b0, _b1, _b2, _a1, _a2 = _compute_notch_coeffs(60.0, 5.0, 48000.0)
self._notch_b0, self._notch_b1, self._notch_b2 = _b0, _b1, _b2
self._notch_a1, self._notch_a2 = _a1, _a2
logger.info("Audio pipeline initialized (block=%d, sr=%d)",
self._block_size, self._sample_rate)
@@ -655,20 +664,25 @@ 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
# ── 60Hz mains hum notch filter ────────────────────────────
# Biquad notch at 60Hz (Q=5) to remove mains hum without
# affecting guitar frequencies (lowest open string is 82Hz E2).
b0, b1, b2 = self._notch_b0, self._notch_b1, self._notch_b2
a1, a2 = self._notch_a1, self._notch_a2
x1, x2 = self._notch_x1, self._notch_x2
y1, y2 = self._notch_y1, self._notch_y2
for i in range(len(buf)):
y = alpha * (py + buf[i] - px)
px = buf[i]
py = y
x = buf[i]
y = b0*x + b1*x1 + b2*x2 - a1*y1 - a2*y2
x2 = x1
x1 = x
y2 = y1
y1 = y
buf[i] = y
self._dc_prev_x = px
self._dc_prev_y = py
self._notch_x1 = x1
self._notch_x2 = x2
self._notch_y1 = y1
self._notch_y2 = y2
for idx, entry in enumerate(chain):
if entry["bypass"] or not entry["enabled"]:
@@ -707,22 +721,27 @@ 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
# ── 60Hz mains hum notch filter on both channels ───────────
b0, b1, b2 = self._notch_b0, self._notch_b1, self._notch_b2
a1, a2 = self._notch_a1, self._notch_a2
# Apply notch to ch0
x1, x2 = self._notch_x1, self._notch_x2
y1, y2 = self._notch_y1, self._notch_y2
for i in range(len(ch0)):
y = alpha * (py + ch0[i] - px)
px = ch0[i]
py = y
x = ch0[i]
y = b0*x + b1*x1 + b2*x2 - a1*y1 - a2*y2
x2 = x1; x1 = x; y2 = y1; y1 = y
ch0[i] = y
# Apply notch to ch1 (continues from ch0's state)
for i in range(len(ch1)):
y = alpha * (py + ch1[i] - px)
px = ch1[i]
py = y
x = ch1[i]
y = b0*x + b1*x1 + b2*x2 - a1*y1 - a2*y2
x2 = x1; x1 = x; y2 = y1; y1 = y
ch1[i] = y
self._dc_prev_x = px
self._dc_prev_y = py
self._notch_x1 = x1
self._notch_x2 = x2
self._notch_y1 = y1
self._notch_y2 = y2
# Update input VU level from the guitar input channel (ch0)
in_rms = np.sqrt(np.mean(ch0 ** 2) + _EPS)
@@ -3143,7 +3162,13 @@ 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)
# Recompute 60Hz notch coefficients for new sample rate
_b0, _b1, _b2, _a1, _a2 = _compute_notch_coeffs(60.0, 5.0, sample_rate)
self._notch_b0, self._notch_b1, self._notch_b2 = _b0, _b1, _b2
self._notch_a1, self._notch_a2 = _a1, _a2
# Reset notch filter state to avoid pop on rate change
self._notch_x1 = self._notch_x2 = 0.0
self._notch_y1 = self._notch_y2 = 0.0
# Clear DSP state — effects will reinit with new block/sample rate
self._state.clear()
self._coeffs.clear()