Phase 2: BuiltinFxChain C++ (8 DSP) + control ring SET_PARAM/REPORT_LATENCY
- native_bridge/src/BuiltinFxChain.{h,cpp}: 8 builtin DSP port 1:1 từ
Python _apply_builtin_fx_chain (eq/eqpro/compressor/limiter/exciter/rebalance)
và JS MASTER_MODULE_IO (imager/maximizer). Biquad RBJ DF2T, block-wise
stateful. createBuiltinFx trả nullptr cho gain/normalize (legacy).
- RenderFxJob: FxSlot thêm bfx, parse builtin 8 loại, runBuiltinSlotSafe SEH,
realtimeRunChain nhánh builtin, chainGen()/entryLatencies(), setParam
áp live lên bfx; Vst3Fx latencySamples từ getLatencySamples().
- RealtimeFxLoop: drain ctrl ring (SET_PARAM -> chain.setParam, drop cũ khi
đầy) + REPORT_LATENCY khi gen đổi (ipc->lat[] ring, lastGen).
- FxRealtimeIPC.h: header 18 u32 (72B), ctrl/lat slots 8, FxCtrlCmd 24B,
FxLatReport 8B, static_asserts.
- app/core/fx_realtime.py: mirror header 72B (ctrl_write/read, lat_write/read,
slots), set_param ghi ctrl ring qua _ctrl_enqueue_locked (guard shm),
get_latencies drain lat ring qua _lat_drain.
- tests/test_builtin_fx_golden.py: golden SNR C++ vs Python (8 cases, ngưỡng
30dB) + order test eq/compressor xen kẽ.
- TASKS_DAW_A.md: tick 2.1-2.9.
Test: 136 passed (4 fx_realtime_chain + 8 builtin golden + regression).
This commit is contained in:
@@ -0,0 +1,530 @@
|
||||
// native_bridge/src/BuiltinFxChain.cpp
|
||||
// 8 builtin DSP (PLAN_DAW_A.md Phase 2) — port 1:1 từ:
|
||||
// - Python app/core/render_engine.py `_apply_builtin_fx_chain` (6 loại:
|
||||
// eq, eqpro, compressor, limiter, exciter, rebalance)
|
||||
// - JS WebAudio MASTER_MODULE_IO (2 loại: imager, maximizer)
|
||||
// Biquad: RBJ cookbook, Direct Form II transposed (cùng công thức Python
|
||||
// `_rbj_*`). Block-wise, stateful giữa block — khớp lfilter full-file.
|
||||
#define _USE_MATH_DEFINES
|
||||
#include "BuiltinFxChain.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "sheredom_json.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// ── JSON helpers (mirror RenderFxJob.cpp anonymous-namespace versions) ──────
|
||||
const json_value_s* memberValue(const json_object_s* o, const char* name) {
|
||||
for (const json_object_element_s* e = o->start; e; e = e->next) {
|
||||
if (e->name && e->name->string && std::strcmp(e->name->string, name) == 0)
|
||||
return e->value;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
double memberNumber(const json_object_s* o, const char* name, double def) {
|
||||
const json_value_s* v = memberValue(o, name);
|
||||
if (v && v->type == json_type_number) return std::atof(static_cast<const json_number_s*>(v->payload)->number);
|
||||
return def;
|
||||
}
|
||||
bool memberBool(const json_object_s* o, const char* name, bool def) {
|
||||
const json_value_s* v = memberValue(o, name);
|
||||
if (v && v->type == json_type_true) return true;
|
||||
if (v && v->type == json_type_false) return false;
|
||||
return def;
|
||||
}
|
||||
std::string memberString(const json_object_s* o, const char* name, const std::string& def) {
|
||||
const json_value_s* v = memberValue(o, name);
|
||||
if (v && v->type == json_type_string && static_cast<const json_string_s*>(v->payload)->string)
|
||||
return std::string(static_cast<const json_string_s*>(v->payload)->string, static_cast<const json_string_s*>(v->payload)->string_size);
|
||||
return def;
|
||||
}
|
||||
|
||||
// ── Biquad (RBJ, Direct Form II transposed) ─────────────────────────────────
|
||||
class Biquad {
|
||||
public:
|
||||
void setCoeffs(double B0, double B1, double B2, double A1, double A2) {
|
||||
b0_ = B0; b1_ = B1; b2_ = B2; a1_ = A1; a2_ = A2;
|
||||
}
|
||||
// Reset state (đổi params → filter mới: state cũ vô nghĩa, tránh pop).
|
||||
void reset() { z1_[0] = z1_[1] = 0; z2_[0] = z2_[1] = 0; }
|
||||
float step(int ch, float x) {
|
||||
const double y = b0_ * x + z1_[ch];
|
||||
z1_[ch] = b1_ * x - a1_ * y + z2_[ch];
|
||||
z2_[ch] = b2_ * x - a2_ * y;
|
||||
return (float)y;
|
||||
}
|
||||
void process(float* L, float* R, uint32_t n) {
|
||||
for (uint32_t i = 0; i < n; ++i) { L[i] = step(0, L[i]); R[i] = step(1, R[i]); }
|
||||
}
|
||||
private:
|
||||
double b0_ = 1, b1_ = 0, b2_ = 0, a1_ = 0, a2_ = 0;
|
||||
double z1_[2] = {0, 0}, z2_[2] = {0, 0};
|
||||
};
|
||||
|
||||
// RBJ coefficient generators — công thức y hệt Python `_rbj_*`.
|
||||
struct RBJ { double b0, b1, b2, a1, a2; };
|
||||
RBJ rbjPeaking(double f0, double gdb, double q, double sr) {
|
||||
const double A = std::pow(10.0, gdb / 40.0);
|
||||
const double w0 = 2.0 * M_PI * f0 / sr;
|
||||
const double alpha = std::sin(w0) / (2.0 * q);
|
||||
const double cw = std::cos(w0);
|
||||
const double a0 = 1.0 + alpha / A;
|
||||
RBJ r;
|
||||
r.b0 = (1.0 + alpha * A) / a0;
|
||||
r.b1 = (-2.0 * cw) / a0;
|
||||
r.b2 = (1.0 - alpha * A) / a0;
|
||||
r.a1 = (-2.0 * cw) / a0;
|
||||
r.a2 = (1.0 - alpha / A) / a0;
|
||||
return r;
|
||||
}
|
||||
RBJ rbjShelf(double f0, double gdb, double q, double sr, bool low) {
|
||||
const double A = std::pow(10.0, gdb / 40.0);
|
||||
const double w0 = 2.0 * M_PI * f0 / sr;
|
||||
const double alpha = std::sin(w0) / (2.0 * q);
|
||||
const double cw = std::cos(w0);
|
||||
const double sA = 2.0 * std::sqrt(A) * alpha;
|
||||
RBJ r;
|
||||
if (low) {
|
||||
const double a0 = (A + 1) + (A - 1) * cw + sA;
|
||||
r.b0 = A * ((A + 1) - (A - 1) * cw + sA) / a0;
|
||||
r.b1 = 2.0 * A * ((A - 1) - (A + 1) * cw) / a0;
|
||||
r.b2 = A * ((A + 1) - (A - 1) * cw - sA) / a0;
|
||||
r.a1 = -2.0 * ((A - 1) + (A + 1) * cw) / a0;
|
||||
r.a2 = ((A + 1) + (A - 1) * cw - sA) / a0;
|
||||
} else {
|
||||
const double a0 = (A + 1) - (A - 1) * cw + sA;
|
||||
r.b0 = A * ((A + 1) + (A - 1) * cw + sA) / a0;
|
||||
r.b1 = -2.0 * A * ((A - 1) + (A + 1) * cw) / a0;
|
||||
r.b2 = A * ((A + 1) + (A - 1) * cw - sA) / a0;
|
||||
r.a1 = 2.0 * ((A - 1) - (A + 1) * cw) / a0;
|
||||
r.a2 = ((A + 1) - (A - 1) * cw - sA) / a0;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
RBJ rbjHighpass(double f0, double q, double sr) {
|
||||
const double w0 = 2.0 * M_PI * f0 / sr;
|
||||
const double alpha = std::sin(w0) / (2.0 * q);
|
||||
const double cw = std::cos(w0);
|
||||
const double a0 = 1.0 + alpha;
|
||||
RBJ r;
|
||||
r.b0 = ((1.0 + cw) / 2.0) / a0;
|
||||
r.b1 = (-(1.0 + cw)) / a0;
|
||||
r.b2 = ((1.0 + cw) / 2.0) / a0;
|
||||
r.a1 = (-2.0 * cw) / a0;
|
||||
r.a2 = (1.0 - alpha) / a0;
|
||||
return r;
|
||||
}
|
||||
RBJ rbjLowpass(double f0, double q, double sr) {
|
||||
const double w0 = 2.0 * M_PI * f0 / sr;
|
||||
const double alpha = std::sin(w0) / (2.0 * q);
|
||||
const double cw = std::cos(w0);
|
||||
const double a0 = 1.0 + alpha;
|
||||
RBJ r;
|
||||
r.b0 = ((1.0 - cw) / 2.0) / a0;
|
||||
r.b1 = (1.0 - cw) / a0;
|
||||
r.b2 = ((1.0 - cw) / 2.0) / a0;
|
||||
r.a1 = (-2.0 * cw) / a0;
|
||||
r.a2 = (1.0 - alpha) / a0;
|
||||
return r;
|
||||
}
|
||||
|
||||
// ── EQ 4-band (track 'eq'): lowshelf 100Hz, peaking 800Hz Q0.7, peaking
|
||||
// 3200Hz Q1.2, highshelf 10kHz — cùng thứ tự/đáp ứng Python `_apply_eq4`.
|
||||
class Eq4Fx : public BuiltinFx {
|
||||
public:
|
||||
Eq4Fx(const json_object_s* params, double sr) : sr_(sr) {
|
||||
static const double kF0[4] = {100, 800, 3200, 10000};
|
||||
static const double kQ[4] = {0.707, 0.7, 1.2, 0.707};
|
||||
static const bool kLow[4] = {true, false, false, false};
|
||||
for (int i = 0; i < 4; ++i) { f0_[i] = kF0[i]; q_[i] = kQ[i]; low_[i] = kLow[i]; }
|
||||
if (params) for (int i = 0; i < 4; ++i)
|
||||
gains_[i] = memberNumber(params, ("g" + std::to_string(i + 1)).c_str(), 0.0);
|
||||
rebuild();
|
||||
}
|
||||
void process(float* L, float* R, uint32_t n) override {
|
||||
for (int i = 0; i < 4; ++i) if (gains_[i] != 0.0) bands_[i].process(L, R, n);
|
||||
}
|
||||
bool setParam(const std::string& key, double value) override {
|
||||
if (key.size() == 2 && key[0] == 'g' && key[1] >= '1' && key[1] <= '4') {
|
||||
gains_[key[1] - '1'] = value;
|
||||
rebuild();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private:
|
||||
void rebuild() {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const RBJ r = low_[i] ? rbjShelf(f0_[i], gains_[i], q_[i], sr_, true)
|
||||
: (i == 3 ? rbjShelf(f0_[i], gains_[i], q_[i], sr_, false)
|
||||
: rbjPeaking(f0_[i], gains_[i], q_[i], sr_));
|
||||
bands_[i].setCoeffs(r.b0, r.b1, r.b2, r.a1, r.a2);
|
||||
bands_[i].reset();
|
||||
}
|
||||
}
|
||||
double sr_, gains_[4] = {0, 0, 0, 0}, f0_[4], q_[4];
|
||||
bool low_[4];
|
||||
Biquad bands_[4];
|
||||
};
|
||||
|
||||
// ── EQ Pro: RBJ per-band từ params.bands[] + amount — Python `_apply_eqpro`.
|
||||
class EqProFx : public BuiltinFx {
|
||||
public:
|
||||
EqProFx(const json_object_s* params, double sr) : sr_(sr) {
|
||||
if (params) {
|
||||
amount_ = memberNumber(params, "amount", 100.0) / 100.0;
|
||||
const json_value_s* bv = memberValue(params, "bands");
|
||||
if (bv && bv->type == json_type_array) {
|
||||
const json_array_s* arr = static_cast<const json_array_s*>(bv->payload);
|
||||
for (const json_array_element_s* el = arr->start; el; el = el->next) {
|
||||
if (!el->value || el->value->type != json_type_object) continue;
|
||||
const json_object_s* bo = static_cast<const json_object_s*>(el->value->payload);
|
||||
Band b;
|
||||
b.active = memberBool(bo, "active", true);
|
||||
b.gain = memberNumber(bo, "gain", 0.0);
|
||||
b.freq = memberNumber(bo, "freq", 1000.0);
|
||||
b.q = memberNumber(bo, "q", 1.0);
|
||||
b.type = memberString(bo, "type", "peaking");
|
||||
bands_.push_back(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
rebuild();
|
||||
}
|
||||
void process(float* L, float* R, uint32_t n) override {
|
||||
for (size_t i = 0; i < bqs_.size(); ++i) {
|
||||
if (!bands_[i].active || bands_[i].gain == 0.0) continue;
|
||||
bqs_[i].process(L, R, n);
|
||||
}
|
||||
}
|
||||
bool setParam(const std::string& key, double value) override {
|
||||
if (key == "amount") { amount_ = value / 100.0; rebuild(); return true; }
|
||||
return false;
|
||||
}
|
||||
private:
|
||||
struct Band { bool active = true; double gain = 0, freq = 1000, q = 1; std::string type; };
|
||||
void rebuild() {
|
||||
bqs_.clear();
|
||||
for (const auto& b : bands_) {
|
||||
const double g = b.gain * amount_;
|
||||
RBJ r;
|
||||
if (b.type == "lowshelf") r = rbjShelf(b.freq, g, b.q, sr_, true);
|
||||
else if (b.type == "highshelf") r = rbjShelf(b.freq, g, b.q, sr_, false);
|
||||
else if (b.type == "highpass") r = rbjHighpass(b.freq, b.q, sr_);
|
||||
else r = rbjPeaking(b.freq, g, b.q, sr_); // peaking/lowpass/notch/bandpass
|
||||
Biquad bq;
|
||||
bq.setCoeffs(r.b0, r.b1, r.b2, r.a1, r.a2);
|
||||
bqs_.push_back(bq);
|
||||
}
|
||||
}
|
||||
double sr_, amount_ = 1.0;
|
||||
std::vector<Band> bands_;
|
||||
std::vector<Biquad> bqs_;
|
||||
};
|
||||
|
||||
// ── Compressor: block-256 peak env, release 250ms — mirror Python 1:1.
|
||||
class CompressorFx : public BuiltinFx {
|
||||
public:
|
||||
explicit CompressorFx(const json_object_s* params, double sr) : sr_(sr) {
|
||||
if (params) {
|
||||
threshold_ = memberNumber(params, "threshold", -16.0);
|
||||
ratio_ = std::max(1.0, memberNumber(params, "ratio", 3.0));
|
||||
makeupDb_ = memberNumber(params, "makeup", 0.0);
|
||||
}
|
||||
rel_ = std::exp(-1.0 / (sr_ * 0.25));
|
||||
makeup_ = std::pow(10.0, makeupDb_ / 20.0);
|
||||
}
|
||||
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) peak = std::max(peak, std::fabs(x[pos + i]));
|
||||
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 * (1.0 - 1.0 / ratio_);
|
||||
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(1.0, value); return true; }
|
||||
if (key == "makeup") { makeupDb_ = value; makeup_ = std::pow(10.0, makeupDb_ / 20.0); return true; }
|
||||
return false;
|
||||
}
|
||||
private:
|
||||
double sr_, threshold_ = -16.0, ratio_ = 3.0, makeupDb_ = 0.0, rel_, makeup_ = 1.0;
|
||||
double env_[2] = {0, 0};
|
||||
};
|
||||
|
||||
// ── Limiter: tanh brickwall — 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));
|
||||
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_);
|
||||
}
|
||||
}
|
||||
bool setParam(const std::string& key, double value) override {
|
||||
if (key == "ceiling") { ceilingDb_ = std::min(0.0, value); rebuild(); 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_);
|
||||
}
|
||||
double ceilingDb_ = -1.0, k_, tanhK_;
|
||||
};
|
||||
|
||||
// ── Exciter: highpass 2kHz Q0.7 + tanh — mirror Python `_apply_exciter`.
|
||||
class ExciterFx : public BuiltinFx {
|
||||
public:
|
||||
explicit ExciterFx(const json_object_s* params, double sr) : sr_(sr) {
|
||||
if (params) drive_ = memberNumber(params, "drive", 40.0);
|
||||
wet_ = (drive_ / 100.0) * 0.6;
|
||||
const RBJ r = rbjHighpass(2000.0, 0.7, sr_);
|
||||
hp_.setCoeffs(r.b0, r.b1, r.b2, r.a1, r.a2);
|
||||
}
|
||||
void process(float* L, float* R, uint32_t n) override {
|
||||
for (uint32_t i = 0; i < n; ++i) {
|
||||
const float yl = hp_.step(0, L[i]);
|
||||
L[i] = L[i] + (float)(std::tanh(yl * 3.0) * wet_);
|
||||
const float yr = hp_.step(1, R[i]);
|
||||
R[i] = R[i] + (float)(std::tanh(yr * 3.0) * wet_);
|
||||
}
|
||||
}
|
||||
bool setParam(const std::string& key, double value) override {
|
||||
if (key == "drive") { drive_ = value; wet_ = (drive_ / 100.0) * 0.6; return true; }
|
||||
return false;
|
||||
}
|
||||
private:
|
||||
double sr_, drive_ = 40.0, wet_;
|
||||
Biquad hp_;
|
||||
};
|
||||
|
||||
// ── Rebalance: M/S crossfeed L'=a·L+b·R, R'=b·L+a·R — Python `_apply_rebalance`.
|
||||
class RebalanceFx : public BuiltinFx {
|
||||
public:
|
||||
explicit RebalanceFx(const json_object_s* params) {
|
||||
if (params) {
|
||||
mid_ = memberNumber(params, "mid", 0.0);
|
||||
side_ = memberNumber(params, "side", 0.0);
|
||||
}
|
||||
rebuild();
|
||||
}
|
||||
void process(float* L, float* R, uint32_t n) override {
|
||||
for (uint32_t i = 0; i < n; ++i) {
|
||||
const float l = L[i], r = R[i];
|
||||
L[i] = (float)(a_ * l + b_ * r);
|
||||
R[i] = (float)(b_ * l + a_ * r);
|
||||
}
|
||||
}
|
||||
bool setParam(const std::string& key, double value) override {
|
||||
if (key == "mid") { mid_ = value; rebuild(); return true; }
|
||||
if (key == "side") { side_ = value; rebuild(); return true; }
|
||||
return false;
|
||||
}
|
||||
private:
|
||||
void rebuild() {
|
||||
const double mid = std::pow(10.0, mid_ / 20.0);
|
||||
const double side = std::pow(10.0, side_ / 20.0);
|
||||
a_ = (mid + side) / 2.0;
|
||||
b_ = (mid - side) / 2.0;
|
||||
}
|
||||
double mid_ = 0, side_ = 0, a_ = 1, b_ = 0;
|
||||
};
|
||||
|
||||
// ── Imager: 4-band crossover (100/1000/6000Hz, WebAudio biquad Q=1) + M/S
|
||||
// width per band — JS MASTER_MODULE_IO imager (updateImagerBand).
|
||||
// Band1=LP100, Band2=HP100+LP1000, Band3=HP1000+LP6000, Band4=HP6000.
|
||||
class ImagerFx : public BuiltinFx {
|
||||
public:
|
||||
explicit ImagerFx(const json_object_s* params, double sr) : sr_(sr) {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
w_[i] = 100.0;
|
||||
if (params) w_[i] = memberNumber(params, ("w" + std::to_string(i + 1)).c_str(), 100.0);
|
||||
}
|
||||
// Crossover filter graph (mỗi band 1-2 biquad nối tiếp).
|
||||
// Mặc định: band 0/3 chỉ 1 stage; band 1/2 có 2 stage. Stage thừa bị
|
||||
// tắt (active=false) — `{}` init sẽ zero mọi field nên phải set lại.
|
||||
for (int b = 0; b < 4; ++b) for (int f = 0; f < 2; ++f)
|
||||
band_[b][f].active = true;
|
||||
const RBJ lp100 = rbjLowpass(100, 1.0, sr_), hp100 = rbjHighpass(100, 1.0, sr_);
|
||||
const RBJ lp1k = rbjLowpass(1000, 1.0, sr_), hp1k = rbjHighpass(1000, 1.0, sr_);
|
||||
const RBJ lp6k = rbjLowpass(6000, 1.0, sr_), hp6k = rbjHighpass(6000, 1.0, sr_);
|
||||
apply(lp100, band_[0][0]);
|
||||
apply(hp100, band_[1][0]); apply(lp1k, band_[1][1]);
|
||||
apply(hp1k, band_[2][0]); apply(lp6k, band_[2][1]);
|
||||
apply(hp6k, band_[3][0]);
|
||||
band_[0][1].active = false;
|
||||
band_[3][1].active = false;
|
||||
}
|
||||
void process(float* L, float* R, uint32_t n) override {
|
||||
// Biquad xử lý in-place trên bản sao band (không hủy input).
|
||||
std::vector<float> bl(n), br(n);
|
||||
std::vector<float> accL(n), accR(n);
|
||||
for (int b = 0; b < 4; ++b) {
|
||||
std::memcpy(bl.data(), L, n * sizeof(float));
|
||||
std::memcpy(br.data(), R, n * sizeof(float));
|
||||
for (int f = 0; f < 2 && band_[b][f].active; ++f) {
|
||||
band_[b][f].bq.process(bl.data(), br.data(), n);
|
||||
}
|
||||
const double width = std::max(0.0, std::min(200.0, w_[b]));
|
||||
const double g1 = (width + 100.0) / 200.0;
|
||||
const double g2 = (100.0 - width) / 200.0;
|
||||
for (uint32_t i = 0; i < n; ++i) {
|
||||
accL[i] += (float)(g1 * bl[i] + g2 * br[i]);
|
||||
accR[i] += (float)(g1 * br[i] + g2 * bl[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 {
|
||||
if (key.size() == 2 && key[0] == 'w' && key[1] >= '1' && key[1] <= '4') {
|
||||
w_[key[1] - '1'] = value;
|
||||
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);
|
||||
}
|
||||
double sr_, w_[4];
|
||||
BandFilter band_[4][2] = {}; // [band][stage]; stage 2 inactive → skip
|
||||
};
|
||||
|
||||
// ── 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`.
|
||||
class MaximizerFx : public BuiltinFx {
|
||||
public:
|
||||
explicit MaximizerFx(const json_object_s* params, double sr) : sr_(sr) {
|
||||
if (params) {
|
||||
boostDb_ = memberNumber(params, "boost_db", 0.0);
|
||||
softClip_ = memberNumber(params, "soft_clip", 0.0);
|
||||
upward_ = memberNumber(params, "upward", 0.0);
|
||||
ceilingDb_ = memberNumber(params, "ceiling_db", -0.1);
|
||||
}
|
||||
rebuild();
|
||||
att_ = std::exp(-1.0 / (sr_ * 0.01));
|
||||
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]);
|
||||
}
|
||||
}
|
||||
bool setParam(const std::string& key, double value) override {
|
||||
if (key == "boost_db") { boostDb_ = value; rebuild(); return true; }
|
||||
if (key == "soft_clip") { softClip_ = value; rebuild(); return true; }
|
||||
if (key == "upward") { upward_ = value; rebuild(); return true; }
|
||||
if (key == "ceiling_db") { ceilingDb_ = value; rebuild(); return true; }
|
||||
return false;
|
||||
}
|
||||
private:
|
||||
void rebuild() {
|
||||
boost_ = std::pow(10.0, std::max(-60.0, std::min(30.0, boostDb_)) / 20.0);
|
||||
const double p = std::max(0.0, std::min(100.0, softClip_));
|
||||
t_ = 1.0 - (p / 100.0) * 0.8;
|
||||
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);
|
||||
}
|
||||
float softClip(float x) const {
|
||||
const float ax = std::fabs(x);
|
||||
if (ax < (float)t_) return x;
|
||||
return (float)((x < 0 ? -1.0 : 1.0) * (t_ + inv_ * std::tanh((ax - t_) / inv_)));
|
||||
}
|
||||
// Soft-knee feedforward compressor (DynamicsCompressor-ish).
|
||||
float upwardComp(int ch, float x) {
|
||||
const float ax = std::fabs(x);
|
||||
if (ax > env_[ch]) env_[ch] = att_ * env_[ch] + (1.0 - att_) * ax;
|
||||
else env_[ch] = rel_ * env_[ch] + (1.0 - rel_) * ax;
|
||||
double db = 20.0 * std::log10(std::max(env_[ch], 1e-12));
|
||||
double y = db - (-30.0); // threshold -30dB
|
||||
double g = 1.0;
|
||||
if (2.0 * y > 10.0) { // beyond knee → slope 1/ratio
|
||||
g = std::pow(10.0, -y * (1.0 - 1.0 / 4.0) / 20.0);
|
||||
} else if (2.0 * y > -10.0) { // soft knee
|
||||
const double k = 10.0;
|
||||
g = std::pow(10.0, -(1.0 - 1.0 / 4.0) * (y + k / 2.0) * (y + k / 2.0) / (2.0 * k) / 20.0);
|
||||
}
|
||||
return (float)(g * x);
|
||||
}
|
||||
float processSample(int ch, float x) {
|
||||
const float xb = (float)(boost_ * x);
|
||||
const float dry = softClip(xb);
|
||||
const float wet = upwardGain_ * upwardComp(ch, xb);
|
||||
const float sum = dry + wet;
|
||||
return std::max(-(float)ceiling_, std::min((float)ceiling_, sum));
|
||||
}
|
||||
double sr_, boostDb_ = 0, softClip_ = 0, upward_ = 0, ceilingDb_ = -0.1;
|
||||
double boost_ = 1.0, t_ = 1.0, inv_ = 0.0, upwardGain_ = 0.0, ceiling_ = 0.9886;
|
||||
double att_, rel_;
|
||||
double env_[2] = {0, 0};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// ── BuiltinFxChain ──────────────────────────────────────────────────────────
|
||||
void BuiltinFxChain::add(std::unique_ptr<BuiltinFx> fx, bool bypass) {
|
||||
entries_.push_back(Entry{std::move(fx), bypass});
|
||||
}
|
||||
void BuiltinFxChain::process(float* L, float* R, uint32_t n) {
|
||||
for (auto& e : entries_) {
|
||||
if (e.bypass || !e.fx) continue;
|
||||
e.fx->process(L, R, n);
|
||||
}
|
||||
}
|
||||
bool BuiltinFxChain::setParam(int slot, const std::string& key, double value) {
|
||||
if (slot < 0 || (size_t)slot >= entries_.size()) return false;
|
||||
auto& e = entries_[(size_t)slot];
|
||||
return e.fx && e.fx->setParam(key, value);
|
||||
}
|
||||
|
||||
std::unique_ptr<BuiltinFx> createBuiltinFx(const std::string& id,
|
||||
const json_object_s* params,
|
||||
double sampleRate) {
|
||||
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 == "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);
|
||||
if (id == "maximizer") return std::make_unique<MaximizerFx>(params, sampleRate);
|
||||
return nullptr; // gain/normalize = legacy, xử lý riêng ở RenderFxJob
|
||||
}
|
||||
Reference in New Issue
Block a user