// 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 #include #include #include #include #include #include #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(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(v->payload)->string) return std::string(static_cast(v->payload)->string, static_cast(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(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(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; } if (key.compare(0, 4, "band") == 0) { const size_t us = key.find('_', 4); if (us != std::string::npos && us > 4) { const int idx = std::atoi(key.c_str() + 4); const std::string field = key.substr(us + 1); if (idx >= 0 && idx < (int)bands_.size()) { Band& b = bands_[idx]; if (field == "freq") b.freq = value; else if (field == "gain") b.gain = value; else if (field == "q") b.q = value; else if (field == "active") b.active = (value != 0.0); else return false; 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 bands_; std::vector 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(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: 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, 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 { 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: 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; } 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 ringL_, ringR_; struct Pk { int idx; double val; }; std::deque dq_; Fir33 fir1L_, fir2L_, fir1R_, fir2R_; }; // ── 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 bl(n), br(n); std::vector 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 }; // ── 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) { 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 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 { 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); 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); 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}; 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 bl(n), br(n); std::vector 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(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(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 ────────────────────────────────────────────────────────── void BuiltinFxChain::add(std::unique_ptr 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 createBuiltinFx(const std::string& id, const json_object_s* params, double sampleRate) { if (id == "eq") return std::make_unique(params, sampleRate); if (id == "eqpro") return std::make_unique(params, sampleRate); if (id == "compressor") return std::make_unique(params, sampleRate); if (id == "limiter") return std::make_unique(params, sampleRate); if (id == "exciter") return std::make_unique(params, sampleRate); if (id == "rebalance") return std::make_unique(params); if (id == "imager") return std::make_unique(params, sampleRate); if (id == "maximizer") return std::make_unique(params, sampleRate); if (id == "multiband") return std::make_unique(params, sampleRate); if (id == "deesser") return std::make_unique(params, sampleRate); return nullptr; // gain/normalize = legacy, xử lý riêng ở RenderFxJob }