fix: replace DC-blocking HPF with 60Hz biquad notch filter (hum fix)
CI / test (push) Has been cancelled
CI / test (push) Has been cancelled
This commit is contained in:
+58
-33
@@ -352,14 +352,23 @@ class AudioPipeline:
|
|||||||
# load_preset() holds it only for the atomic swap (not during model I/O).
|
# load_preset() holds it only for the atomic swap (not during model I/O).
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
# ── DC-blocking filter state ───────────────────────────────────────
|
# ── Mains hum notch filter state (60Hz notch) ───────────────────────
|
||||||
# Single-pole high-pass at ~15Hz to remove DC offset and subsonic
|
# Biquad notch filter to remove 60Hz mains hum without affecting
|
||||||
# noise that can amplify into audible hum through NAM models.
|
# guitar frequencies (lowest open string is 82Hz E2).
|
||||||
# Filter: y[n] = alpha * (y[n-1] + x[n] - x[n-1])
|
# Direct Form I state: two input delays (x1, x2) and two output delays (y1, y2)
|
||||||
# where alpha = 1 / (1 + 2*pi*fc/sr)
|
self._notch_x1: float = 0.0
|
||||||
self._dc_prev_x: float = 0.0
|
self._notch_x2: float = 0.0
|
||||||
self._dc_prev_y: float = 0.0
|
self._notch_y1: float = 0.0
|
||||||
self._dc_alpha: float = 1.0 / (1.0 + 2.0 * np.pi * 15.0 / 48000.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)",
|
logger.info("Audio pipeline initialized (block=%d, sr=%d)",
|
||||||
self._block_size, self._sample_rate)
|
self._block_size, self._sample_rate)
|
||||||
@@ -655,20 +664,25 @@ class AudioPipeline:
|
|||||||
)
|
)
|
||||||
|
|
||||||
buf = audio_in.copy()
|
buf = audio_in.copy()
|
||||||
# ── DC-blocking high-pass filter ────────────────────────────
|
# ── 60Hz mains hum notch filter ─────────────────────────────
|
||||||
# Removes DC offset and subsonic noise (~15Hz corner) that can
|
# Biquad notch at 60Hz (Q=5) to remove mains hum without
|
||||||
# amplify into audible hum through high-gain NAM models.
|
# affecting guitar frequencies (lowest open string is 82Hz E2).
|
||||||
# First-order HPF: y[n] = alpha * (y[n-1] + x[n] - x[n-1])
|
b0, b1, b2 = self._notch_b0, self._notch_b1, self._notch_b2
|
||||||
alpha = self._dc_alpha
|
a1, a2 = self._notch_a1, self._notch_a2
|
||||||
px = self._dc_prev_x
|
x1, x2 = self._notch_x1, self._notch_x2
|
||||||
py = self._dc_prev_y
|
y1, y2 = self._notch_y1, self._notch_y2
|
||||||
for i in range(len(buf)):
|
for i in range(len(buf)):
|
||||||
y = alpha * (py + buf[i] - px)
|
x = buf[i]
|
||||||
px = buf[i]
|
y = b0*x + b1*x1 + b2*x2 - a1*y1 - a2*y2
|
||||||
py = y
|
x2 = x1
|
||||||
|
x1 = x
|
||||||
|
y2 = y1
|
||||||
|
y1 = y
|
||||||
buf[i] = y
|
buf[i] = y
|
||||||
self._dc_prev_x = px
|
self._notch_x1 = x1
|
||||||
self._dc_prev_y = py
|
self._notch_x2 = x2
|
||||||
|
self._notch_y1 = y1
|
||||||
|
self._notch_y2 = y2
|
||||||
|
|
||||||
for idx, entry in enumerate(chain):
|
for idx, entry in enumerate(chain):
|
||||||
if entry["bypass"] or not entry["enabled"]:
|
if entry["bypass"] or not entry["enabled"]:
|
||||||
@@ -707,22 +721,27 @@ class AudioPipeline:
|
|||||||
ch0 = audio_in[0, :].copy()
|
ch0 = audio_in[0, :].copy()
|
||||||
ch1 = audio_in[1, :].copy()
|
ch1 = audio_in[1, :].copy()
|
||||||
|
|
||||||
# ── DC-blocking high-pass filter on both channels ───────────
|
# ── 60Hz mains hum notch filter on both channels ────────────
|
||||||
alpha = self._dc_alpha
|
b0, b1, b2 = self._notch_b0, self._notch_b1, self._notch_b2
|
||||||
px = self._dc_prev_x
|
a1, a2 = self._notch_a1, self._notch_a2
|
||||||
py = self._dc_prev_y
|
# 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)):
|
for i in range(len(ch0)):
|
||||||
y = alpha * (py + ch0[i] - px)
|
x = ch0[i]
|
||||||
px = ch0[i]
|
y = b0*x + b1*x1 + b2*x2 - a1*y1 - a2*y2
|
||||||
py = y
|
x2 = x1; x1 = x; y2 = y1; y1 = y
|
||||||
ch0[i] = y
|
ch0[i] = y
|
||||||
|
# Apply notch to ch1 (continues from ch0's state)
|
||||||
for i in range(len(ch1)):
|
for i in range(len(ch1)):
|
||||||
y = alpha * (py + ch1[i] - px)
|
x = ch1[i]
|
||||||
px = ch1[i]
|
y = b0*x + b1*x1 + b2*x2 - a1*y1 - a2*y2
|
||||||
py = y
|
x2 = x1; x1 = x; y2 = y1; y1 = y
|
||||||
ch1[i] = y
|
ch1[i] = y
|
||||||
self._dc_prev_x = px
|
self._notch_x1 = x1
|
||||||
self._dc_prev_y = py
|
self._notch_x2 = x2
|
||||||
|
self._notch_y1 = y1
|
||||||
|
self._notch_y2 = y2
|
||||||
|
|
||||||
# Update input VU level from the guitar input channel (ch0)
|
# Update input VU level from the guitar input channel (ch0)
|
||||||
in_rms = np.sqrt(np.mean(ch0 ** 2) + _EPS)
|
in_rms = np.sqrt(np.mean(ch0 ** 2) + _EPS)
|
||||||
@@ -3143,7 +3162,13 @@ class AudioPipeline:
|
|||||||
self._block_size = block_size
|
self._block_size = block_size
|
||||||
self._sample_rate = sample_rate
|
self._sample_rate = sample_rate
|
||||||
self._vu_alpha = np.exp(-block_size / (0.05 * 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
|
# Clear DSP state — effects will reinit with new block/sample rate
|
||||||
self._state.clear()
|
self._state.clear()
|
||||||
self._coeffs.clear()
|
self._coeffs.clear()
|
||||||
|
|||||||
Reference in New Issue
Block a user