Gap 4: MaximizerFx oversample 4x soft-clip + ceiling — zero-stuff + FIR33 anti-alias + post-decimation clamp (giảm aliasing fold-back, sample peak <= ceiling); test_61: impulse delay 8 mẫu, unity transparent, alias 9.1k -117dBFS, ceiling -1dB (24 passed)

This commit is contained in:
2026-08-23 07:59:33 +07:00
parent 5d37c0683b
commit c8c49b6b9c
2 changed files with 140 additions and 3 deletions
+79
View File
@@ -528,6 +528,85 @@ def test_59_limiter_brickwall_oversample(fx_bridge):
pk_low = float(np.max(np.abs(out_low)))
assert 0.05 <= pk_low <= 0.15, f"low-level peak {pk_low:.4f} lech unity"
def test_61_maximizer_oversample(fx_bridge):
"""Gap 4: MaximizerFx oversample 4x — impulse delay 8 mẫu (2×FIR33 group
delay 16@4x / 4) chứng minh oversample active; sine -20dB boost 0 trong
suốt (unity); 5kHz soft_clip 100: harmonic 3 (15k) in-band giữ > -40dBFS
còn alias spur 9.1k (fold cua harmonic 7 = 35k) bi FIR33 anti-alias chặn
< -50dBFS; hot sine bám ceiling -1dB."""
def _bin_db(x, f):
x0 = x[0].astype(np.float64)
n = len(x0)
win = np.hanning(n)
X = np.fft.rfft((x0 - np.mean(x0)) * win)
freq = np.fft.rfftfreq(n, 1.0 / SR)
i = int(np.argmin(np.abs(freq - f)))
amp = np.abs(X[i]) * 2.0 / np.sum(win)
return 20.0 * np.log10(amp + 1e-12)
mx = [{"type": "builtin", "id": "maximizer",
"params": {"boost_db": 0.0, "soft_clip": 0.0, "upward": 0.0,
"ceiling_db": -1.0}}]
n = 8192
t = np.arange(n) / SR
# 1) impulse → delay 8 (oversample FIR chain active), gain ~1
imp = np.zeros(n, dtype=np.float32)
imp[512] = 1.0
audio = np.stack([imp, imp]).astype(np.float32)
with tempfile.TemporaryDirectory() as td:
wav_in = os.path.join(td, "in.wav")
_write_wav(wav_in, audio, SR)
out = _render_bridge(fx_bridge, wav_in, SR, mx, block=512)
argmax = int(np.argmax(np.abs(out[0])))
assert argmax == 512 + 8, f"impulse delay {argmax - 512} (expect 8)"
pk = float(np.max(np.abs(out[0])))
assert 0.8 <= pk <= 1.2, f"impulse peak {pk:.4f} (expect ~1.0)"
# 2) trong suốt: -20dBFS 1kHz qua maximizer gain 0 → unity
sine = (0.1 * np.sin(2 * np.pi * 1000.0 * t)).astype(np.float32)
audio = np.stack([sine, sine])
with tempfile.TemporaryDirectory() as td:
wav_in = os.path.join(td, "in.wav")
_write_wav(wav_in, audio, SR)
out2 = _render_bridge(fx_bridge, wav_in, SR, mx, block=512)
pk2 = float(np.max(np.abs(out2[0])))
assert 0.09 <= pk2 <= 0.11, f"transparent peak {pk2:.4f} (expect 0.1)"
# 3) soft_clip 100: 15k in-band giữ, alias 9.1k bị chặn
mx_hot = [{"type": "builtin", "id": "maximizer",
"params": {"boost_db": 0.0, "soft_clip": 100.0, "upward": 0.0,
"ceiling_db": -1.0}}]
sine5 = (0.7 * np.sin(2 * np.pi * 5000.0 * t)).astype(np.float32)
audio = np.stack([sine5, sine5])
with tempfile.TemporaryDirectory() as td:
wav_in = os.path.join(td, "in.wav")
_write_wav(wav_in, audio, SR)
out3 = _render_bridge(fx_bridge, wav_in, SR, mx_hot, block=512)
assert out3.shape == audio.shape, "length giu nguyen"
h3 = _bin_db(out3, 15000.0)
alias = _bin_db(out3, 9100.0)
print(f"maximizer: h3@15k={h3:.1f}dBFS alias@9.1k={alias:.1f}dBFS")
assert h3 > -40.0, f"3rd harmonic 15k {h3:.1f}dBFS qua thap"
assert alias < -50.0, f"alias spur 9.1k {alias:.1f}dBFS (oversample phai chan fold 35k)"
# 4) hot sine bám ceiling -1dB
ceil_lin = 10.0 ** (-1.0 / 20.0)
sine_hot = (0.9 * np.sin(2 * np.pi * 1000.0 * t)).astype(np.float32)
audio = np.stack([sine_hot, sine_hot])
mx_boost = [{"type": "builtin", "id": "maximizer",
"params": {"boost_db": 18.0, "soft_clip": 100.0, "upward": 0.0,
"ceiling_db": -1.0}}]
with tempfile.TemporaryDirectory() as td:
wav_in = os.path.join(td, "in.wav")
_write_wav(wav_in, audio, SR)
out4 = _render_bridge(fx_bridge, wav_in, SR, mx_boost, block=512)
pk4 = float(np.max(np.abs(out4[0])))
assert pk4 <= ceil_lin + 0.005, f"ceiling peak {pk4:.4f} > {ceil_lin:.4f}"
assert pk4 >= 0.7, f"ceiling peak {pk4:.4f} qua thap"
# ── 5.C Realtime VST3 setParam live (PLAN_MASTER_ENHANCE C) ───────────────
def test_60_vst3_setparam_live(fx_bridge, adelay, monkeypatch):
"""set_param runtime cho VST3: chain [vst3-delay, builtin limiter], đổi