D2: LimiterFx brickwall — lookahead 1-5ms + oversample 4x + hard clip, UI toggle SOFT/BRICKWALL + LOOKAHEAD ms
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user