From c8c49b6b9c599e86ba4c809c97494616b5c1b5de Mon Sep 17 00:00:00 2001 From: locphamtran Date: Sun, 23 Aug 2026 07:59:33 +0700 Subject: [PATCH] =?UTF-8?q?Gap=204:=20MaximizerFx=20oversample=204x=20soft?= =?UTF-8?q?-clip=20+=20ceiling=20=E2=80=94=20zero-stuff=20+=20FIR33=20anti?= =?UTF-8?q?-alias=20+=20post-decimation=20clamp=20(gi=E1=BA=A3m=20aliasing?= =?UTF-8?q?=20fold-back,=20sample=20peak=20<=3D=20ceiling);=20test=5F61:?= =?UTF-8?q?=20impulse=20delay=208=20m=E1=BA=ABu,=20unity=20transparent,=20?= =?UTF-8?q?alias=209.1k=20-117dBFS,=20ceiling=20-1dB=20(24=20passed)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- native_bridge/src/BuiltinFxChain.cpp | 64 ++++++++++++++++++++-- tests/test_daw_phase5.py | 79 ++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 3 deletions(-) diff --git a/native_bridge/src/BuiltinFxChain.cpp b/native_bridge/src/BuiltinFxChain.cpp index 2f28f43..84b1d9e 100644 --- a/native_bridge/src/BuiltinFxChain.cpp +++ b/native_bridge/src/BuiltinFxChain.cpp @@ -558,10 +558,45 @@ private: BandFilter band_[4][2] = {}; // [band][stage]; stage 2 inactive → skip }; +// ── File-scope 33-tap FIR (dùng cho oversample Maximizer) — mirrors Fir33 +// private của LimiterFx; dùng chung thay vì refactor code D2 đã verify. +struct Fir33Shared { + double buf[33] = {0}; + int pos = 0; + double step(double x) { + buf[pos] = x; + pos = (pos + 1) % 33; + double y = 0.0; + for (int m = 0; m < 33; ++m) y += taps[m] * buf[(pos + 32 - m) % 33]; + return y; + } + void reset() { std::memset(buf, 0, sizeof(buf)); pos = 0; } + double taps[33]; +}; + +// 33-tap Hamming sinc, 2*fc = 0.25 (fc = 0.125 cycles/sample stream 4x), group +// delay 16, normalize DC = gain. +static void buildFir33Shared(double* taps, double gain) { + const double pi = 3.14159265358979323846; + const double fc2 = 0.25; + double sum = 0.0; + for (int i = 0; i < 33; ++i) { + const double t = i - 16.0; + const double sincv = (t == 0.0) ? 1.0 : std::sin(pi * fc2 * t) / (pi * fc2 * t); + const double w = 0.54 - 0.46 * std::cos(2.0 * pi * i / 32.0); + taps[i] = w * fc2 * sincv; + sum += taps[i]; + } + for (int i = 0; i < 33; ++i) taps[i] *= gain / sum; +} + // ── Maximizer: boost → soft-clip → (+upward comp) → ceiling clip — JS // MASTER_MODULE_IO maximizer. Upward compressor approximates WebAudio // DynamicsCompressor (soft-knee, attack/release) — `ponytail: port Chromium // algorithm chính xác nếu cần bit-parity; default maxUpward=0 → no-op`. +// Nonlinear (soft-clip + ceiling) chạy ở 4x (zero-stuff → FIR33 → clip → +// FIR33 → decimate) để giảm aliasing — mirror LimiterFx brickwall. +// ponytail: toggle oversample off khi realtime CPU kham không nổi. class MaximizerFx : public BuiltinFx { public: explicit MaximizerFx(const json_object_s* params, double sr) : sr_(sr) { @@ -576,9 +611,26 @@ public: rel_ = std::exp(-1.0 / (sr_ * 0.1)); } void process(float* L, float* R, uint32_t n) override { - for (uint32_t i = 0; i < n; ++i) { - L[i] = processSample(0, L[i]); - R[i] = processSample(1, R[i]); + for (uint32_t j = 0; j < n; ++j) { + float outL = 0.f, outR = 0.f; + for (int m = 0; m < 4; ++m) { + const double xv = (m == 0) ? (double)L[j] : 0.0; + const double y1 = fir1L_.step(xv); + const double yc = processSample(0, (float)y1); + const double yd = fir2L_.step(yc); + // clamp hậu decimation: FIR ringing sau clip có thể vượt ceiling + // ~0.2dB — sample peak phải ≤ ceiling (true peak xử lý ở export). + if (m == 0) outL = (float)std::max(-ceiling_, std::min(ceiling_, yd)); + } + L[j] = outL; + for (int m = 0; m < 4; ++m) { + const double xv = (m == 0) ? (double)R[j] : 0.0; + const double y1 = fir1R_.step(xv); + const double yc = processSample(1, (float)y1); + const double yd = fir2R_.step(yc); + if (m == 0) outR = (float)std::max(-ceiling_, std::min(ceiling_, yd)); + } + R[j] = outR; } } bool setParam(const std::string& key, double value) override { @@ -596,6 +648,11 @@ private: inv_ = 1.0 - t_; upwardGain_ = upward_ > 0 ? std::pow(10.0, std::max(0.0, std::min(30.0, upward_)) / 20.0) - 1.0 : 0.0; ceiling_ = std::pow(10.0, std::max(-60.0, std::min(0.0, ceilingDb_)) / 20.0); + buildFir33Shared(fir1L_.taps, 4.0); // upsampler: gain L=4 bù zero-stuff + buildFir33Shared(fir2L_.taps, 1.0); // downsampler: gain 1 + buildFir33Shared(fir1R_.taps, 4.0); + buildFir33Shared(fir2R_.taps, 1.0); + fir1L_.reset(); fir2L_.reset(); fir1R_.reset(); fir2R_.reset(); } float softClip(float x) const { const float ax = std::fabs(x); @@ -629,6 +686,7 @@ private: double boost_ = 1.0, t_ = 1.0, inv_ = 0.0, upwardGain_ = 0.0, ceiling_ = 0.9886; double att_, rel_; double env_[2] = {0, 0}; + Fir33Shared fir1L_, fir2L_, fir1R_, fir2R_; }; } // namespace diff --git a/tests/test_daw_phase5.py b/tests/test_daw_phase5.py index dabafe1..1997e91 100644 --- a/tests/test_daw_phase5.py +++ b/tests/test_daw_phase5.py @@ -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