D2: LimiterFx brickwall — lookahead 1-5ms + oversample 4x + hard clip, UI toggle SOFT/BRICKWALL + LOOKAHEAD ms

This commit is contained in:
2026-08-23 07:23:52 +07:00
parent fe82b19557
commit 17e22bcf32
6 changed files with 202 additions and 32 deletions
+131 -14
View File
@@ -12,6 +12,7 @@
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <string>
#include <vector>
@@ -293,32 +294,148 @@ private:
double env_[2] = {0, 0};
};
// ── Limiter: tanh brickwall — mirror Python `_apply_limiter` / JS limNode.
// ── Limiter: soft = tanh brickwall (mặc định); brickwall = lookahead peak
// limiter + oversample 4x + hard clip — mirror Python `_apply_limiter` / JS limNode.
class LimiterFx : public BuiltinFx {
public:
explicit LimiterFx(const json_object_s* params) {
if (params) ceilingDb_ = std::min(0.0, memberNumber(params, "ceiling", -1.0));
explicit LimiterFx(const json_object_s* params, double sr) : sr_(sr) {
if (params) {
ceilingDb_ = std::min(0.0, memberNumber(params, "ceiling", -1.0));
const double mnum = memberNumber(params, "mode", 0.0);
const std::string mstr = memberString(params, "mode", "");
if (mstr == "brickwall" || mnum == 1.0) modeBrick_ = true;
setLookahead(memberNumber(params, "lookahead_ms", 2.0));
} else {
setLookahead(2.0);
}
rebuild();
}
void process(float* L, float* R, uint32_t n) override {
for (uint32_t i = 0; i < n; ++i) {
const float x = std::max(-1.f, std::min(1.f, L[i]));
L[i] = (float)(std::tanh((double)x * k_) / tanhK_);
const float y = std::max(-1.f, std::min(1.f, R[i]));
R[i] = (float)(std::tanh((double)y * k_) / tanhK_);
if (!modeBrick_) {
for (uint32_t i = 0; i < n; ++i) {
const float x = std::max(-1.f, std::min(1.f, L[i]));
L[i] = (float)(std::tanh((double)x * k_) / tanhK_);
const float y = std::max(-1.f, std::min(1.f, R[i]));
R[i] = (float)(std::tanh((double)y * k_) / tanhK_);
}
return;
}
// Stage 1: lookahead peak limiter (linked stereo). Ring 2L lưu input;
// deque giữ max |x| window [idx-L+1, idx]. Emit x[t-L] với t=idx-L+1,
// gain = ceiling/peak (smoothing: attack 1ms, release 150ms) — transient
// nằm trong window đủ L mẫu trước khi chính nó được emit nên bị bắt.
const int Lk = lookSamples_;
const int ringSize = 2 * Lk;
const int64_t start = (int64_t)totalIn_;
for (uint32_t j = 0; j < n; ++j) {
const int64_t idx = start + j;
const float xl = L[j], xr = R[j];
const float pl = xl < 0.f ? -xl : xl;
const float pr = xr < 0.f ? -xr : xr;
const double peak = std::max((double)pl, (double)pr);
ringL_[idx % ringSize] = xl;
ringR_[idx % ringSize] = xr;
while (!dq_.empty() && dq_.back().val <= peak) dq_.pop_back();
dq_.push_back({(int)idx, peak});
while (!dq_.empty() && dq_.front().idx < (int)idx - Lk + 1) dq_.pop_front();
const double curPeak = dq_.empty() ? 0.0 : dq_.front().val;
const double target = curPeak > ceilingLin_ ? ceilingLin_ / curPeak : 1.0;
gain_ += (target < gain_ ? attackCoef_ : releaseCoef_) * (target - gain_);
const int t = (int)idx - Lk + 1;
const int audioSlot = modPos(t - Lk, ringSize);
L[j] = (float)(ringL_[audioSlot] * gain_);
R[j] = (float)(ringR_[audioSlot] * gain_);
}
totalIn_ += n;
// Stage 2: oversample 4x (zero-stuff) → FIR33 (cutoff 0.25) → hard
// clip ceiling → FIR33 → decimate (k&3==0). Out[j]=y2[4j], delay 8.
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 = std::max(-ceilingLin_, std::min(ceilingLin_, y1));
if (m == 0) outL = (float)fir2L_.step(yc); else fir2L_.step(yc);
}
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 = std::max(-ceilingLin_, std::min(ceilingLin_, y1));
if (m == 0) outR = (float)fir2R_.step(yc); else fir2R_.step(yc);
}
R[j] = outR;
}
}
bool setParam(const std::string& key, double value) override {
if (key == "ceiling") { ceilingDb_ = std::min(0.0, value); rebuild(); return true; }
if (key == "mode") { modeBrick_ = (value == 1.0); return true; }
if (key == "lookahead_ms") { setLookahead(value); return true; }
return false;
}
private:
void rebuild() {
const double th = std::pow(10.0, ceilingDb_ / 20.0);
k_ = 1.0 / std::max(0.02, th);
tanhK_ = std::tanh(k_);
static int modPos(int a, int m) { int r = a % m; return r < 0 ? r + m : r; }
void setLookahead(double ms) {
lookaheadMs_ = std::min(5.0, std::max(1.0, ms));
lookSamples_ = std::max(1, (int)std::lround(sr_ * lookaheadMs_ / 1000.0));
ringL_.assign((size_t)(2 * lookSamples_), 0.0f);
ringR_.assign((size_t)(2 * lookSamples_), 0.0f);
dq_.clear();
totalIn_ = 0;
gain_ = 1.0;
}
double ceilingDb_ = -1.0, k_, tanhK_;
void rebuild() {
ceilingLin_ = std::pow(10.0, ceilingDb_ / 20.0);
const double th = std::max(0.02, ceilingLin_);
k_ = 1.0 / th;
tanhK_ = std::tanh(k_);
attackCoef_ = 1.0 - std::exp(-1.0 / (sr_ * 0.001));
releaseCoef_ = 1.0 - std::exp(-1.0 / (sr_ * 0.150));
buildFir(fir1L_.taps, 4.0); // upsampler: gain L=4 bù zero-stuff
buildFir(fir2L_.taps, 1.0); // downsampler: gain 1
buildFir(fir1R_.taps, 4.0);
buildFir(fir2R_.taps, 1.0);
fir1L_.reset(); fir2L_.reset(); fir1R_.reset(); fir2R_.reset();
}
// 33-tap Hamming-windowed sinc, 2*fc = 0.25 (fc = 0.125 cycles/sample của
// stream 4x = Nyquist gốc), group delay 16, normalize DC gain = gain.
static void buildFir(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;
}
struct Fir33 {
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];
};
double sr_;
double ceilingDb_ = -1.0, ceilingLin_ = 1.0, k_, tanhK_;
double lookaheadMs_ = 2.0;
int lookSamples_ = 0;
bool modeBrick_ = false;
uint64_t totalIn_ = 0;
double gain_ = 1.0, attackCoef_, releaseCoef_;
std::vector<float> ringL_, ringR_;
struct Pk { int idx; double val; };
std::deque<Pk> dq_;
Fir33 fir1L_, fir2L_, fir1R_, fir2R_;
};
// ── Exciter: highpass 2kHz Q0.7 + tanh — mirror Python `_apply_exciter`.
@@ -538,7 +655,7 @@ std::unique_ptr<BuiltinFx> createBuiltinFx(const std::string& id,
if (id == "eq") return std::make_unique<Eq4Fx>(params, sampleRate);
if (id == "eqpro") return std::make_unique<EqProFx>(params, sampleRate);
if (id == "compressor") return std::make_unique<CompressorFx>(params, sampleRate);
if (id == "limiter") return std::make_unique<LimiterFx>(params);
if (id == "limiter") return std::make_unique<LimiterFx>(params, sampleRate);
if (id == "exciter") return std::make_unique<ExciterFx>(params, sampleRate);
if (id == "rebalance") return std::make_unique<RebalanceFx>(params);
if (id == "imager") return std::make_unique<ImagerFx>(params, sampleRate);