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
+61 -3
View File
@@ -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