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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user