Gap 10: multiband compressor + de-esser (LR4 crossover, UI, test_62)
This commit is contained in:
@@ -689,6 +689,169 @@ private:
|
||||
Fir33Shared fir1L_, fir2L_, fir1R_, fir2R_;
|
||||
};
|
||||
|
||||
// ── Multiband compressor: 3 band (low/mid/high) qua crossover biquad
|
||||
// (LP f1, HP f1+LP f2, HP f2 — mirror ImagerFx 4-band split), mỗi band nén
|
||||
// block-envelope độc lập (threshold/ratio/makeup) rồi cộng lại. Gap 10.
|
||||
class MultibandFx : public BuiltinFx {
|
||||
public:
|
||||
explicit MultibandFx(const json_object_s* params, double sr) : sr_(sr) {
|
||||
if (params) {
|
||||
lfCross_ = std::max(40.0, std::min(1000.0, memberNumber(params, "lf_cross", 200.0)));
|
||||
hfCross_ = std::max(1000.0, std::min(12000.0, memberNumber(params, "hf_cross", 4000.0)));
|
||||
thr_[0] = memberNumber(params, "low_thr", -24.0);
|
||||
rat_[0] = std::max(1.0, memberNumber(params, "low_ratio", 3.0));
|
||||
mk_[0] = memberNumber(params, "low_makeup", 0.0);
|
||||
thr_[1] = memberNumber(params, "mid_thr", -24.0);
|
||||
rat_[1] = std::max(1.0, memberNumber(params, "mid_ratio", 3.0));
|
||||
mk_[1] = memberNumber(params, "mid_makeup", 0.0);
|
||||
thr_[2] = memberNumber(params, "high_thr", -24.0);
|
||||
rat_[2] = std::max(1.0, memberNumber(params, "high_ratio", 3.0));
|
||||
mk_[2] = memberNumber(params, "high_makeup", 0.0);
|
||||
}
|
||||
rel_ = std::exp(-1.0 / (sr_ * 0.25));
|
||||
for (int b = 0; b < 3; ++b) mkLin_[b] = std::pow(10.0, mk_[b] / 20.0);
|
||||
rebuild();
|
||||
}
|
||||
void process(float* L, float* R, uint32_t n) override {
|
||||
const uint32_t block = 256;
|
||||
std::vector<float> bl(n), br(n);
|
||||
std::vector<float> accL(n), accR(n);
|
||||
for (int b = 0; b < 3; ++b) {
|
||||
std::memcpy(bl.data(), L, n * sizeof(float));
|
||||
std::memcpy(br.data(), R, n * sizeof(float));
|
||||
for (int f = 0; f < 4 && band_[b][f].active; ++f)
|
||||
band_[b][f].bq.process(bl.data(), br.data(), n);
|
||||
// block-envelope compressor per band (mirror CompressorFx)
|
||||
for (int c = 0; c < 2; ++c) {
|
||||
float* x = (c == 0) ? bl.data() : br.data();
|
||||
double env = env_[b][c];
|
||||
for (uint32_t pos = 0; pos < n; pos += block) {
|
||||
const uint32_t nb = std::min<uint32_t>(block, n - pos);
|
||||
float peak = 0.f;
|
||||
for (uint32_t i = 0; i < nb; ++i) peak = std::max(peak, std::fabs(x[pos + i]));
|
||||
env = std::max((double)peak, env * rel_);
|
||||
float g = (float)mkLin_[b];
|
||||
if (env > 1e-9) {
|
||||
const double db = 20.0 * std::log10(env);
|
||||
const double over = db - thr_[b];
|
||||
if (over > 0.0) {
|
||||
const double gdb = -over * (1.0 - 1.0 / rat_[b]);
|
||||
g = (float)(std::pow(10.0, gdb / 20.0) * mkLin_[b]);
|
||||
}
|
||||
}
|
||||
for (uint32_t i = 0; i < nb; ++i) x[pos + i] *= g;
|
||||
}
|
||||
env_[b][c] = env;
|
||||
}
|
||||
for (uint32_t i = 0; i < n; ++i) { accL[i] += bl[i]; accR[i] += br[i]; }
|
||||
}
|
||||
std::memcpy(L, accL.data(), n * sizeof(float));
|
||||
std::memcpy(R, accR.data(), n * sizeof(float));
|
||||
}
|
||||
bool setParam(const std::string& key, double value) override {
|
||||
const char* k = key.c_str();
|
||||
if (!std::strcmp(k, "lf_cross") || !std::strcmp(k, "hf_cross")) {
|
||||
if (!std::strcmp(k, "lf_cross")) lfCross_ = std::max(40.0, std::min(1000.0, value));
|
||||
else hfCross_ = std::max(1000.0, std::min(12000.0, value));
|
||||
rebuild();
|
||||
return true;
|
||||
}
|
||||
const char* prefix[] = {"low_", "mid_", "high_"};
|
||||
for (int b = 0; b < 3; ++b) {
|
||||
if (key == std::string(prefix[b]) + "thr") { thr_[b] = value; return true; }
|
||||
if (key == std::string(prefix[b]) + "ratio") { rat_[b] = std::max(1.0, value); return true; }
|
||||
if (key == std::string(prefix[b]) + "makeup") {
|
||||
mk_[b] = value; mkLin_[b] = std::pow(10.0, mk_[b] / 20.0); return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private:
|
||||
struct BandFilter { Biquad bq; bool active = true; };
|
||||
static void apply(const RBJ& r, BandFilter& f) {
|
||||
f.bq.setCoeffs(r.b0, r.b1, r.b2, r.a1, r.a2);
|
||||
f.active = true;
|
||||
}
|
||||
void rebuild() {
|
||||
for (int b = 0; b < 3; ++b) for (int f = 0; f < 4; ++f) band_[b][f].active = false;
|
||||
// LR4 cascade (two Butterworth Q=1/sqrt2 biquads per octave): LP^2 + HP^2
|
||||
// sums flat 0dB at every frequency (phase 0), so 3-band split is transparent.
|
||||
const RBJ lp1 = rbjLowpass(lfCross_, 0.7071, sr_), hp1 = rbjHighpass(lfCross_, 0.7071, sr_);
|
||||
const RBJ lp2 = rbjLowpass(hfCross_, 0.7071, sr_), hp2 = rbjHighpass(hfCross_, 0.7071, sr_);
|
||||
apply(lp1, band_[0][0]); apply(lp1, band_[0][1]); // low = LP(f1)^2
|
||||
apply(hp1, band_[1][0]); apply(hp1, band_[1][1]);
|
||||
apply(lp2, band_[1][2]); apply(lp2, band_[1][3]); // mid = HP(f1)^2 * LP(f2)^2
|
||||
apply(hp2, band_[2][0]); apply(hp2, band_[2][1]); // high = HP(f2)^2
|
||||
}
|
||||
double sr_, rel_;
|
||||
double lfCross_ = 200.0, hfCross_ = 4000.0;
|
||||
double thr_[3] = {-24, -24, -24}, rat_[3] = {3, 3, 3};
|
||||
double mk_[3] = {0, 0, 0}, mkLin_[3] = {1, 1, 1};
|
||||
double env_[3][2] = {{0, 0}, {0, 0}, {0, 0}};
|
||||
BandFilter band_[3][4] = {};
|
||||
};
|
||||
|
||||
// ── De-esser: detector highpass @freq (sibilance 5-8kHz) → envelope →
|
||||
// gain reduction (ratio = % max reduction) áp full band + makeup. Gap 10.
|
||||
class DeEssFx : public BuiltinFx {
|
||||
public:
|
||||
explicit DeEssFx(const json_object_s* params, double sr) : sr_(sr) {
|
||||
if (params) {
|
||||
threshold_ = memberNumber(params, "threshold", -30.0);
|
||||
ratio_ = std::max(0.0, std::min(100.0, memberNumber(params, "ratio", 40.0)));
|
||||
freq_ = std::max(2000.0, std::min(12000.0, memberNumber(params, "freq", 6000.0)));
|
||||
makeupDb_ = memberNumber(params, "makeup", 0.0);
|
||||
}
|
||||
rel_ = std::exp(-1.0 / (sr_ * 0.15));
|
||||
makeup_ = std::pow(10.0, makeupDb_ / 20.0);
|
||||
rebuild();
|
||||
}
|
||||
void process(float* L, float* R, uint32_t n) override {
|
||||
const uint32_t block = 256;
|
||||
float* chans[2] = {L, R};
|
||||
for (int c = 0; c < 2; ++c) {
|
||||
float* x = chans[c];
|
||||
double env = env_[c];
|
||||
for (uint32_t pos = 0; pos < n; pos += block) {
|
||||
const uint32_t nb = std::min<uint32_t>(block, n - pos);
|
||||
float peak = 0.f;
|
||||
for (uint32_t i = 0; i < nb; ++i) {
|
||||
const double d = hp_.step(c, x[pos + i]);
|
||||
peak = std::max(peak, (float)std::fabs(d));
|
||||
}
|
||||
env = std::max((double)peak, env * rel_);
|
||||
float g = (float)makeup_;
|
||||
if (env > 1e-9) {
|
||||
const double db = 20.0 * std::log10(env);
|
||||
const double over = db - threshold_;
|
||||
if (over > 0.0) {
|
||||
const double gdb = -over * (ratio_ / 100.0);
|
||||
g = (float)(std::pow(10.0, gdb / 20.0) * makeup_);
|
||||
}
|
||||
}
|
||||
for (uint32_t i = 0; i < nb; ++i) x[pos + i] *= g;
|
||||
}
|
||||
env_[c] = env;
|
||||
}
|
||||
}
|
||||
bool setParam(const std::string& key, double value) override {
|
||||
if (key == "threshold") { threshold_ = value; return true; }
|
||||
if (key == "ratio") { ratio_ = std::max(0.0, std::min(100.0, value)); return true; }
|
||||
if (key == "makeup") { makeupDb_ = value; makeup_ = std::pow(10.0, makeupDb_ / 20.0); return true; }
|
||||
if (key == "freq") { freq_ = std::max(2000.0, std::min(12000.0, value)); rebuild(); return true; }
|
||||
return false;
|
||||
}
|
||||
private:
|
||||
void rebuild() {
|
||||
const RBJ r = rbjHighpass(freq_, 0.7, sr_);
|
||||
hp_.setCoeffs(r.b0, r.b1, r.b2, r.a1, r.a2);
|
||||
}
|
||||
double sr_, threshold_ = -30.0, ratio_ = 40.0, freq_ = 6000.0, makeupDb_ = 0.0;
|
||||
double rel_, makeup_ = 1.0;
|
||||
double env_[2] = {0, 0};
|
||||
Biquad hp_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// ── BuiltinFxChain ──────────────────────────────────────────────────────────
|
||||
@@ -718,5 +881,7 @@ std::unique_ptr<BuiltinFx> createBuiltinFx(const std::string& id,
|
||||
if (id == "rebalance") return std::make_unique<RebalanceFx>(params);
|
||||
if (id == "imager") return std::make_unique<ImagerFx>(params, sampleRate);
|
||||
if (id == "maximizer") return std::make_unique<MaximizerFx>(params, sampleRate);
|
||||
if (id == "multiband") return std::make_unique<MultibandFx>(params, sampleRate);
|
||||
if (id == "deesser") return std::make_unique<DeEssFx>(params, sampleRate);
|
||||
return nullptr; // gain/normalize = legacy, xử lý riêng ở RenderFxJob
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user