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:
2026-08-22 19:55:08 +07:00
parent fd790c1dc6
commit 1390ad545b
10 changed files with 980 additions and 29 deletions
+9 -9
View File
@@ -12,15 +12,15 @@ Trạng thái: `[x]` hoàn thành, `[ ]` chưa. Cập nhật sau mỗi phase.
- [x] 1.7 Pytest: chain hợp nhất → job.json đúng schema; SET_PARAM/REPORT_LATENCY round-trip.
## Phase 2 — Bridge C++: BuiltinFxChain (8 DSP)
- [ ] 2.1 File `native_bridge/src/BuiltinFxChain.{h,cpp}` — khung chain serial + SEH guard.
- [ ] 2.2 Port EQ 4-band (eq) từ Python `_apply_eq4`.
- [ ] 2.3 Port EQ Pro (eqpro) RBJ 8-band từ Python `_apply_eqpro`.
- [ ] 2.4 Port compressor/limiter từ Python.
- [ ] 2.5 Port exciter/rebalance từ Python.
- [ ] 2.6 Port imager/maximizer từ JS WebAudio (MASTER_MODULE_IO) — Python không có.
- [ ] 2.7 `RealtimeFxChain::buildChain` + RenderFxJob: nhận builtin slot → chạy BuiltinFxChain xen kẽ VST3 đúng thứ tự.
- [ ] 2.8 SET_PARAM/REPORT_LATENCY trong bridge (SHM control ring).
- [ ] 2.9 Golden test: C++ vs Python `_apply_builtin_fx_chain` (SNR/diff ngưỡng) + order test xen kẽ.
- [x] 2.1 File `native_bridge/src/BuiltinFxChain.{h,cpp}` — khung chain serial + SEH guard.
- [x] 2.2 Port EQ 4-band (eq) từ Python `_apply_eq4`.
- [x] 2.3 Port EQ Pro (eqpro) RBJ 8-band từ Python `_apply_eqpro`.
- [x] 2.4 Port compressor/limiter từ Python.
- [x] 2.5 Port exciter/rebalance từ Python.
- [x] 2.6 Port imager/maximizer từ JS WebAudio (MASTER_MODULE_IO) — Python không có.
- [x] 2.7 `RealtimeFxChain::buildChain` + RenderFxJob: nhận builtin slot → chạy BuiltinFxChain xen kẽ VST3 đúng thứ tự.
- [x] 2.8 SET_PARAM/REPORT_LATENCY trong bridge (SHM control ring).
- [x] 2.9 Golden test: C++ vs Python `_apply_builtin_fx_chain` (SNR/diff ngưỡng) + order test xen kẽ.
## Phase 3 — Engine Python: render + realtime chain hợp nhất
- [ ] 3.1 `render_engine.py`: track/master đọc chain hợp nhất; Python DSP giữ làm fallback (RENDER_ENGINE≠bridge).
+71 -4
View File
@@ -4,9 +4,12 @@ spawn fx_vst_bridge --realtime-fx, đẩy/kéo block audio qua SHM ring.
Layout mirror chính xác native_bridge/include/FxRealtimeIPC.h (mọi field
u32/float 4-byte, không padding):
header 12 u32 (48B): magic state sampleRate blockSize running heartbeat
header 18 u32 (72B): magic state sampleRate blockSize running heartbeat
inWrite inRead inSlots outWrite outRead outSlots
ctrlWrite ctrlRead ctrlSlots latWrite latRead latSlots
inL[4][256] inR[4][256] outL[8][256] outR[8][256]
ctrl[8] (FxCtrlCmd 24B) — SET_PARAM ring (engine -> bridge)
lat[8] (FxLatReport 8B) — REPORT_LATENCY ring (bridge -> engine)
Luồng dữ liệu: browser → WS → write_input() → [bridge xử lý] → read_output()
→ WS → browser. Block = FXRT_BLOCK=256 stereo float32 = 2048 bytes.
@@ -29,6 +32,8 @@ FXRT_MAGIC = 0x46585254
FXRT_BLOCK = 256
FXRT_IN_SLOTS = 4
FXRT_OUT_SLOTS = 8
FXRT_CTRL_SLOTS = 8
FXRT_LAT_SLOTS = 8
FXRT_STATE_STARTING = 0
FXRT_STATE_READY = 1
FXRT_STATE_ERROR = 2
@@ -48,15 +53,38 @@ class FxRTHeader(ctypes.Structure):
("out_write", ctypes.c_uint32),
("out_read", ctypes.c_uint32),
("out_slots", ctypes.c_uint32),
("ctrl_write", ctypes.c_uint32),
("ctrl_read", ctypes.c_uint32),
("ctrl_slots", ctypes.c_uint32),
("lat_write", ctypes.c_uint32),
("lat_read", ctypes.c_uint32),
("lat_slots", ctypes.c_uint32),
]
HEADER_SIZE = ctypes.sizeof(FxRTHeader) # 48
IN_L_OFF = HEADER_SIZE # 48
class FxCtrlCmd(ctypes.Structure):
_fields_ = [
("slot", ctypes.c_uint32),
("key", ctypes.c_char * 16),
("value", ctypes.c_float),
]
class FxLatReport(ctypes.Structure):
_fields_ = [
("slot", ctypes.c_uint32),
("samples", ctypes.c_uint32),
]
HEADER_SIZE = ctypes.sizeof(FxRTHeader) # 72
IN_L_OFF = HEADER_SIZE # 72
IN_R_OFF = IN_L_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
OUT_L_OFF = IN_R_OFF + FXRT_IN_SLOTS * FXRT_BLOCK * 4
OUT_R_OFF = OUT_L_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
TOTAL_SIZE = OUT_R_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
CTRL_OFF = OUT_R_OFF + FXRT_OUT_SLOTS * FXRT_BLOCK * 4
LAT_OFF = CTRL_OFF + FXRT_CTRL_SLOTS * ctypes.sizeof(FxCtrlCmd)
TOTAL_SIZE = LAT_OFF + FXRT_LAT_SLOTS * ctypes.sizeof(FxLatReport)
class FxRealtimeSession:
@@ -81,6 +109,8 @@ class FxRealtimeSession:
self.h.running = 1
self.h.in_slots = FXRT_IN_SLOTS
self.h.out_slots = FXRT_OUT_SLOTS
self.h.ctrl_slots = FXRT_CTRL_SLOTS
self.h.lat_slots = FXRT_LAT_SLOTS
self._np = np.frombuffer(self.shm.buf, dtype=np.float32)
self._lock = threading.Lock()
self.proc = None
@@ -225,6 +255,28 @@ class FxRealtimeSession:
def set_param(self, slot, key, value):
with self._lock:
self.pending_params.setdefault(slot, {})[str(key)] = value
# Phase 2.8: đẩy vào SHM control ring (bridge drain mỗi
# iteration). pending_params giữ làm fallback — fake test session
# (object.__new__) không có shm; drain_params vẫn hoạt động.
if getattr(self, "shm", None) is not None:
try:
self._ctrl_enqueue_locked(slot, str(key), value)
except Exception:
pass
def _ctrl_enqueue_locked(self, slot, key, value):
h = self.h
kb = key.encode("utf-8")[:15]
cmds = (FxCtrlCmd * FXRT_CTRL_SLOTS).from_buffer(self.shm.buf, CTRL_OFF)
while True:
if h.ctrl_write - h.ctrl_read < h.ctrl_slots:
cs = h.ctrl_write & (h.ctrl_slots - 1)
cmds[cs].slot = int(slot) & 0xFFFFFFFF
cmds[cs].key = kb + b"\00" * (16 - len(kb))
cmds[cs].value = float(value)
h.ctrl_write += 1
return
h.ctrl_read += 1 # ring đầy → drop lệnh cũ nhất
def report_latency(self, slot, samples):
with self._lock:
@@ -239,8 +291,23 @@ class FxRealtimeSession:
def get_latencies(self):
with self._lock:
self._lat_drain()
return dict(self.latencies)
def _lat_drain(self):
shm = getattr(self, "shm", None)
if shm is None:
return
try:
h = self.h
lats = (FxLatReport * FXRT_LAT_SLOTS).from_buffer(shm.buf, LAT_OFF)
while h.lat_read < h.lat_write:
ls = h.lat_read & (h.lat_slots - 1)
self.latencies[int(lats[ls].slot)] = int(lats[ls].samples)
h.lat_read += 1
except Exception:
pass
# Registry toàn tiến trình (desktop app 1 user).
_SESSIONS = {}
+1
View File
@@ -106,6 +106,7 @@ add_executable(fx_vst_bridge
src/main_fx.cpp
src/RenderFxJob.cpp
src/RealtimeFxLoop.cpp
src/BuiltinFxChain.cpp
src/FxGuiServer.cpp
)
if(VST3_SDK_TARGET)
+47
View File
@@ -0,0 +1,47 @@
// native_bridge/include/BuiltinFxChain.h
// Builtin DSP chain (PLAN_DAW_A.md Phase 2): 8 custom FX được port từ Python
// (render_engine.py `_apply_builtin_fx_chain`) + JS WebAudio (imager/maximizer,
// MASTER_MODULE_IO). Serial, in-place, stereo — engine xử lý TOÀN chain; chạy
// xen kẽ VST3 đúng thứ tự UI trong RenderFxJob (offline) + RealtimeFxChain.
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
// Forward: sheredom JSON object (params). Khai báo để factory dùng.
struct json_object_s;
// Một FX builtin — stateful giữa các block (biquad z1/z2, env compressor...).
class BuiltinFx {
public:
virtual ~BuiltinFx() = default;
// Xử lý 1 block stereo in-place (L/R song song, interleaved-wise).
virtual void process(float* L, float* R, uint32_t n) = 0;
// SET_PARAM (SHM control ring, Phase 2.8): cập nhật tham số live.
// Trả false nếu slot không có param này (gọi từ realtime loop thread,
// cùng thread process → không cần lock).
virtual bool setParam(const std::string& key, double value) { (void)key; (void)value; return false; }
};
// Chain serial các BuiltinFx — giữ NGUYÊN thứ tự thêm vào.
class BuiltinFxChain {
public:
explicit BuiltinFxChain(double sampleRate) : sampleRate_(sampleRate) {}
void add(std::unique_ptr<BuiltinFx> fx, bool bypass);
// Chạy toàn chain serial, in-place.
void process(float* L, float* R, uint32_t n);
bool setParam(int slot, const std::string& key, double value);
size_t size() const { return entries_.size(); }
private:
struct Entry { std::unique_ptr<BuiltinFx> fx; bool bypass = false; };
std::vector<Entry> entries_;
double sampleRate_;
};
// Factory: id ∈ eq|eqpro|imager|maximizer|compressor|limiter|exciter|rebalance
// |gain|normalize (gain/normalize legacy giữ ở RenderFxJob — factory trả
// nullptr cho 2 loại này). params = JSON object (sheredom), có thể null.
std::unique_ptr<BuiltinFx> createBuiltinFx(const std::string& id,
const json_object_s* params,
double sampleRate);
+26 -1
View File
@@ -10,11 +10,13 @@
#define FXRT_BLOCK 256
#define FXRT_IN_SLOTS 4 // engine -> bridge
#define FXRT_OUT_SLOTS 8 // bridge -> engine
#define FXRT_CTRL_SLOTS 8 // SET_PARAM commands (engine -> bridge)
#define FXRT_LAT_SLOTS 8 // REPORT_LATENCY reports (bridge -> engine)
#define FXRT_STATE_STARTING 0
#define FXRT_STATE_READY 1
#define FXRT_STATE_ERROR 2
// Header 48 bytes (12 u32) — giữ thứ tự khớp fx_realtime.py.
// Header 72 bytes (18 u32) — giữ thứ tự khớp fx_realtime.py.
struct FxRealtimeHeader {
volatile uint32_t magic;
volatile uint32_t state;
@@ -28,12 +30,35 @@ struct FxRealtimeHeader {
volatile uint32_t outWrite; // producer: bridge
volatile uint32_t outRead; // consumer: engine
uint32_t outSlots;
volatile uint32_t ctrlWrite; // producer: engine (SET_PARAM ring)
volatile uint32_t ctrlRead; // consumer: bridge
uint32_t ctrlSlots;
volatile uint32_t latWrite; // producer: bridge (REPORT_LATENCY ring)
volatile uint32_t latRead; // consumer: engine
uint32_t latSlots;
};
// SET_PARAM: {slot_idx, key[16], value} — 24 bytes.
struct FxCtrlCmd {
volatile uint32_t slot;
char key[16];
volatile float value;
};
static_assert(sizeof(FxCtrlCmd) == 24, "FxCtrlCmd must be 24 bytes");
// REPORT_LATENCY: {slot_idx, samples} — 8 bytes.
struct FxLatReport {
volatile uint32_t slot;
volatile uint32_t samples;
};
static_assert(sizeof(FxLatReport) == 8, "FxLatReport must be 8 bytes");
struct FxRealtimeIPC {
FxRealtimeHeader h;
float inL[FXRT_IN_SLOTS][FXRT_BLOCK];
float inR[FXRT_IN_SLOTS][FXRT_BLOCK];
float outL[FXRT_OUT_SLOTS][FXRT_BLOCK];
float outR[FXRT_OUT_SLOTS][FXRT_BLOCK];
FxCtrlCmd ctrl[FXRT_CTRL_SLOTS];
FxLatReport lat[FXRT_LAT_SLOTS];
};
+10
View File
@@ -5,6 +5,7 @@
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
// Offline FX render mode (Phase 0 of PLAN_MASTERBUS_FX_RACK_VST.md):
// `daw_vst_bridge --render-fx <job.json> --in <input.wav> --out <output.wav>`
@@ -72,6 +73,15 @@ public:
// JSON: [{"path":"...","bypass":true|false},...] — chain entries replace
// the previous chain atomically. May be called from any thread.
void setChain(const std::string& json, double sampleRate, int32_t blockSize);
// SET_PARAM (Phase 2.8): áp dụng live vào builtin slot (slot index theo
// chain JSON). Gọi từ loop thread.
void setParam(int slot, const std::string& key, double value);
// Phase 2.8 (SHM control ring): chain generation — tăng mỗi lần worker
// swap chain; RealtimeFxLoop so gen → báo lại latency qua lat ring.
uint64_t chainGen();
// Per-slot latency samples (thứ tự khớp chain JSON): builtin = 0 (IIR
// zero-latency), VST3 = getLatencySamples (đọc lúc load) — PDC Phase 3.
std::vector<uint32_t> entryLatencies();
// Audio thread only: run the chain in-place over one stereo block.
void process(float* inL, float* inR, uint32_t n);
void shutdown();
+530
View File
@@ -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
}
+30
View File
@@ -166,11 +166,41 @@ int run_realtime_fx_loop(const std::string& jobPath, const std::string& shmName,
uint32_t inMask = ipc->h.inSlots - 1;
uint32_t outMask = ipc->h.outSlots - 1;
uint64_t processed = 0;
uint64_t lastGen = 0; // chain generation đã báo latency
while (ipc->h.running) {
if (parentPid && !parentAlive(parentPid)) {
std::cerr << "[RealtimeFxLoop] parent gone — exiting" << std::endl;
break;
}
// SET_PARAM ring (engine -> bridge, Phase 2.8): drain mỗi iteration —
// áp dụng vào chain TRƯỚC block kế tiếp. Ring đầy → drop lệnh cũ.
while (ipc->h.ctrlRead < ipc->h.ctrlWrite) {
const uint32_t cs = ipc->h.ctrlRead & (FXRT_CTRL_SLOTS - 1);
const FxCtrlCmd& cmd = ipc->ctrl[cs];
const char* kend = std::find(cmd.key, cmd.key + sizeof(cmd.key), '\0');
chain.setParam((int)cmd.slot,
std::string(cmd.key, (size_t)(kend - cmd.key)),
(double)cmd.value);
MemoryBarrier();
ipc->h.ctrlRead++;
}
// REPORT_LATENCY (bridge -> engine): chain swap (gen đổi) → báo lại
// latency từng slot (builtin=0; VST3=getLatencySamples). Ring 8 —
// engine drain định kỳ, đầy thì overwrite (drop cũ).
const uint64_t gen = chain.chainGen();
if (gen != lastGen) {
const std::vector<uint32_t> lats = chain.entryLatencies();
for (size_t i = 0; i < lats.size(); ++i) {
const uint32_t ls = ipc->h.latWrite & (FXRT_LAT_SLOTS - 1);
ipc->lat[ls].slot = (uint32_t)i;
ipc->lat[ls].samples = lats[i];
MemoryBarrier();
ipc->h.latWrite++;
}
lastGen = gen;
std::cerr << "[RealtimeFxLoop] latency reported: " << lats.size()
<< " slot(s)" << std::endl;
}
const uint32_t avail = ipc->h.inWrite - ipc->h.inRead;
if (avail == 0) { sleepMs(1); continue; }
const uint32_t slot = ipc->h.inRead & inMask;
+117 -15
View File
@@ -38,8 +38,10 @@
#include <windows.h>
#endif
#include "FxRealtimeIPC.h"
#include "BuiltinFxChain.h"
#include <algorithm>
#include <atomic>
#include <cctype>
#include <cmath>
#include <cstdio>
@@ -382,6 +384,7 @@ struct Vst3FxState {
HostComponentHandlerFx componentHandler;
int32 inputChannels = 2;
int32 outputChannels = 2;
int32 latencySamples = 0; // PDC (REPORT_LATENCY, Phase 2.8)
bool controllerIsComponent = false;
Steinberg::IPlugFrame* plugFrame = nullptr; // owned; freed in closeEditor
IPtr<IPlugView> view; // editor view while GUI open
@@ -517,6 +520,8 @@ bool vst3FxLoadInner(Vst3FxState* s, const std::string& path, double sampleRate,
std::cerr << "[RenderFx] STEP setActive OK" << std::endl;
processor->setProcessing(true);
std::cerr << "[RenderFx] STEP setProcessing OK" << std::endl;
s->latencySamples = processor->getLatencySamples();
std::cerr << "[RenderFx] STEP getLatencySamples = " << s->latencySamples << std::endl;
if (!s->processData.prepare(*component, maxBlockSize, kSample32)) {
err = "processData.prepare failed"; return false;
}
@@ -850,12 +855,22 @@ public:
#endif
}
bool loaded() const { return state_ != nullptr; }
// Plugin-reported latency samples (PDC) — 0 nếu plugin không báo.
int32 latencySamples() const {
#ifndef HAVE_VST3SDK
return 0;
#else
return state_ ? static_cast<Vst3FxState*>(state_)->latencySamples : 0;
#endif
}
private:
void* state_ = nullptr;
};
// One FX slot from the job. builtin id: "gain" | "normalize".
// One FX slot from the job. builtin id: "gain" | "normalize" (legacy) hoặc 8
// DSP (PLAN_DAW_A Phase 2) — id ∈ eq|eqpro|imager|maximizer|compressor|limiter
// |exciter|rebalance → bfx (BuiltinFxChain).
struct FxSlot {
bool vst = false; // true = VST3, false = builtin
std::string path; // vst only
@@ -865,6 +880,7 @@ struct FxSlot {
double db = 0.0; // gain param
double peak = 0.95; // normalize param
std::unique_ptr<Vst3Fx> fx;
std::unique_ptr<BuiltinFx> bfx; // 8 DSP builtin (Phase 2)
};
} // namespace
@@ -887,6 +903,22 @@ static bool runFxSlotSafe(const FxSlot& sl, const float* inL, const float* inR,
#endif
}
// SEH-guarded builtin DSP slot (in-place). Frame chỉ raw pointers.
static bool runBuiltinSlotSafe(BuiltinFx* fx, float* L, float* R, uint32_t n,
uint32_t* crashCode) {
#ifdef _WIN32
__try {
fx->process(L, R, n);
return true;
} __except (*crashCode = (uint32_t)GetExceptionCode(), EXCEPTION_EXECUTE_HANDLER) {
return false;
}
#else
fx->process(L, R, n);
return true;
#endif
}
int run_render_fx_job(const std::string& jobPath, const std::string& inPath,
const std::string& outPath) {
#ifdef _WIN32
@@ -951,11 +983,18 @@ int run_render_fx_job(const std::string& jobPath, const std::string& inPath,
} else if (type == "builtin") {
sl.vst = false;
sl.builtinId = memberString(o, "id", "");
const json_object_s* po = nullptr;
const json_value_s* pv = memberValue(o, "params");
if (pv && pv->type == json_type_object) {
const json_object_s* po = static_cast<const json_object_s*>(pv->payload);
double d = sl.db; memberNumber(po, "db", d); sl.db = d;
double pk = sl.peak; memberNumber(po, "peak", pk); sl.peak = pk;
if (pv && pv->type == json_type_object)
po = static_cast<const json_object_s*>(pv->payload);
double d = sl.db; if (po) memberNumber(po, "db", d); sl.db = d;
double pk = sl.peak; if (po) memberNumber(po, "peak", pk); sl.peak = pk;
if (sl.builtinId != "gain" && sl.builtinId != "normalize") {
// 8 DSP (PLAN_DAW_A Phase 2) — khớp Python _apply_builtin_fx_chain.
sl.bfx = createBuiltinFx(sl.builtinId, po, srD);
if (!sl.bfx) {
fail("unknown builtin id: " + sl.builtinId); return rc;
}
}
} else {
fail("unknown fx_chain slot type: " + type); return rc;
@@ -1004,6 +1043,14 @@ int run_render_fx_job(const std::string& jobPath, const std::string& inPath,
+ std::to_string(crashCode) + ")"); return rc; }
std::memcpy(in.L.data() + pos, L0.data(), n * sizeof(float));
std::memcpy(in.R.data() + pos, R0.data(), n * sizeof(float));
} else if (sl.bfx) {
// Builtin DSP (8 loại) — in-place, xen kẽ VST3 đúng thứ tự UI.
bool ok = runBuiltinSlotSafe(sl.bfx.get(), in.L.data() + pos,
in.R.data() + pos, n, &crashCode);
if (!ok) {
rc = 3; fail("builtin DSP crashed (SEH code=0x"
+ std::to_string(crashCode) + ")"); return rc;
}
} else {
for (uint32_t i = 0; i < n; ++i) {
in.L[pos + i] = curL[i] * (float)gainLin;
@@ -1058,32 +1105,38 @@ int run_render_fx_job(const std::string& jobPath, const std::string& inPath,
namespace {
// SEH frame holds only raw pointers / trivials (C2712 — no unwinding locals).
static void realtimeRunChain(Vst3Fx** fxs, const bool* bypass, uint32_t count,
float* inL, float* inR, uint32_t n) {
static void realtimeRunChain(Vst3Fx** vst, BuiltinFx** bfx, const bool* bypass,
uint32_t count, float* inL, float* inR, uint32_t n) {
#ifdef _WIN32
__try {
for (uint32_t i = 0; i < count; ++i) {
if (!fxs[i]) continue;
if (bypass && bypass[i]) continue;
fxs[i]->processAudio(inL, inR, inL, inR, n); // in-place safe
if (vst[i]) vst[i]->processAudio(inL, inR, inL, inR, n); // in-place safe
else if (bfx[i]) bfx[i]->process(inL, inR, n); // builtin in-place
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
std::cerr << "[RealtimeFx] plugin crashed in process — chain bypassed" << std::endl;
std::cerr << "[RealtimeFx] chain crashed in process — slot bypassed" << std::endl;
}
#else
(void)fxs; (void)bypass; (void)count; (void)inL; (void)inR; (void)n;
(void)vst; (void)bfx; (void)bypass; (void)count; (void)inL; (void)inR; (void)n;
#endif
}
} // namespace
struct RealtimeFxChain::Impl {
struct Entry { std::shared_ptr<Vst3Fx> fx; bool bypass = false; };
// VST3 (fx) hoặc builtin DSP (bfx) — 1 trong 2; xen kẽ đúng thứ tự UI.
struct Entry {
std::shared_ptr<Vst3Fx> fx;
std::unique_ptr<BuiltinFx> bfx;
bool bypass = false;
};
struct Chain { std::vector<Entry> entries; };
std::mutex mutex_; // guards chain_ / retired_
std::shared_ptr<Chain> chain_;
std::shared_ptr<Chain> retired_; // dtor deferred to the worker thread
std::atomic<uint64_t> gen_{0}; // chain generation (Phase 2.8)
std::mutex qmutex_; // guards q_ / quit_
std::condition_variable qcv_;
@@ -1140,16 +1193,45 @@ void RealtimeFxChain::process(float* inL, float* inR, uint32_t n) {
}
if (!c || c->entries.empty()) return;
constexpr uint32_t kMaxSlots = 16; // v1 cap
Vst3Fx* fxs[kMaxSlots];
Vst3Fx* vst[kMaxSlots];
BuiltinFx* bfx[kMaxSlots];
bool bypass[kMaxSlots];
uint32_t count = 0;
for (const auto& e : c->entries) {
if (count >= kMaxSlots) break;
fxs[count] = e.fx.get();
vst[count] = e.fx.get();
bfx[count] = e.bfx.get();
bypass[count] = e.bypass;
++count;
}
realtimeRunChain(fxs, bypass, count, inL, inR, n);
realtimeRunChain(vst, bfx, bypass, count, inL, inR, n);
}
void RealtimeFxChain::setParam(int slot, const std::string& key, double value) {
// SET_PARAM (SHM control ring, Phase 2.8): áp dụng live vào builtin slot.
// VST3 param automation qua VST3 API — ponytail: them khi co nhu cau.
if (!impl_) return;
std::shared_ptr<Impl::Chain> c;
{ std::lock_guard<std::mutex> lk(impl_->mutex_); c = impl_->chain_; }
if (!c || slot < 0 || (size_t)slot >= c->entries.size()) return;
auto& e = c->entries[(size_t)slot];
if (e.bfx) e.bfx->setParam(key, value);
}
uint64_t RealtimeFxChain::chainGen() {
return impl_ ? impl_->gen_.load(std::memory_order_acquire) : 0;
}
std::vector<uint32_t> RealtimeFxChain::entryLatencies() {
std::vector<uint32_t> out;
if (!impl_) return out;
std::shared_ptr<Impl::Chain> c;
{ std::lock_guard<std::mutex> lk(impl_->mutex_); c = impl_->chain_; }
if (!c) return out;
out.reserve(c->entries.size());
for (const auto& e : c->entries)
out.push_back(e.fx ? (uint32_t)e.fx->latencySamples() : 0u);
return out;
}
std::shared_ptr<RealtimeFxChain::Impl::Chain> RealtimeFxChain::Impl::buildChain(const std::string& json) {
@@ -1166,6 +1248,25 @@ std::shared_ptr<RealtimeFxChain::Impl::Chain> RealtimeFxChain::Impl::buildChain(
for (const json_array_element_s* e = arr->start; e; e = e->next) {
if (!e->value || e->value->type != json_type_object) continue;
const json_object_s* o = static_cast<const json_object_s*>(e->value->payload);
const std::string type = memberString(o, "type", "");
if (type == "builtin") {
// Builtin DSP (Phase 2): id ∈ 8 loại — xen kẽ VST3 đúng thứ tự.
const std::string id = memberString(o, "id", "");
if (id.empty()) continue;
const json_object_s* po = nullptr;
const json_value_s* pv = memberValue(o, "params");
if (pv && pv->type == json_type_object)
po = static_cast<const json_object_s*>(pv->payload);
Entry ent;
ent.bypass = memberBool(o, "bypass", false);
ent.bfx = createBuiltinFx(id, po, sampleRate_);
if (!ent.bfx) {
std::cerr << "[RealtimeFx] unknown builtin id — slot skipped: " << id << std::endl;
continue;
}
chain->entries.push_back(std::move(ent));
continue;
}
const std::string path = memberString(o, "path", "");
if (path.empty()) continue;
Entry ent;
@@ -1213,6 +1314,7 @@ void RealtimeFxChain::Impl::workerLoop() {
retired_ = std::move(chain_); // old chain destroyed on THIS thread
chain_ = std::move(next);
}
gen_.fetch_add(1, std::memory_order_release); // loop báo lại latency
}
#ifdef _WIN32
CoUninitialize();
+139
View File
@@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
"""Golden test PLAN_DAW_A.md 2.9: C++ BuiltinFxChain vs Python
_apply_builtin_fx_chain (render_engine.py) — SNR per-chain, kể cả order test.
Skip nếu thiếu fx_vst_bridge.exe (build/Release) hoặc scipy.
"""
import io, json, math, os, subprocess, sys, tempfile, wave
import numpy as np
import pytest
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, REPO)
from app.core.render_engine import _apply_builtin_fx_chain # noqa: E402
BRIDGE_CANDIDATES = [
os.environ.get("SF_FX_BRIDGE_PATH", ""),
os.path.join(REPO, "native_bridge", "build", "Release", "fx_vst_bridge.exe"),
os.path.join(REPO, "install", "fx_vst_bridge.exe"),
]
def _find_bridge():
for c in BRIDGE_CANDIDATES:
if c and os.path.isfile(c):
return c
return None
@pytest.fixture(scope="module")
def bridge():
b = _find_bridge()
if not b:
pytest.skip("fx_vst_bridge.exe not found (native_bridge/build/Release)")
return b
def _write_wav(path, data, sr):
data = np.clip(data, -1.0, 1.0)
# data shape (2, n) → interleave (n, 2) trước khi ghi
pcm = (data.T * 32767).astype(np.int16)
with wave.open(path, "wb") as w:
w.setnchannels(2)
w.setsampwidth(2)
w.setframerate(sr)
w.writeframes(pcm.tobytes())
def _read_wav(path):
import soundfile as sf
data, sr = sf.read(path, dtype="float32", always_2d=True)
return data.T, sr
def _render_bridge(bridge, wav_in, sr, chain):
job = {"sample_rate": sr, "block_size": 512, "fx_chain": chain}
with tempfile.TemporaryDirectory() as td:
job_path = os.path.join(td, "job.json")
out_path = os.path.join(td, "out.wav")
with io.open(job_path, "w", encoding="utf-8") as f:
json.dump(job, f)
r = subprocess.run([bridge, "--render-fx", job_path, "--in", wav_in,
"--out", out_path],
capture_output=True, text=True, timeout=120)
assert r.returncode == 0, f"bridge rc={r.returncode}: {r.stdout} {r.stderr}"
assert os.path.isfile(out_path), f"no out wav: {r.stdout} {r.stderr}"
return _read_wav(out_path)[0]
def _snr_db(ref, got):
ref = ref.astype(np.float64)
got = got.astype(np.float64)
if ref.shape != got.shape:
raise AssertionError(f"shape mismatch {ref.shape} vs {got.shape}")
n = min(ref.shape[1], got.shape[1])
if n == 0:
return math.inf
ref, got = ref[:, :n], got[:, :n]
denom = float(np.sum((ref - got) ** 2))
if denom < 1e-12:
return math.inf
return 10.0 * math.log10(float(np.sum(ref ** 2)) / denom)
def _run_case(bridge, sr, seed, chain):
rng = np.random.default_rng(seed)
n = sr // 4
t = np.arange(n) / sr
sig = (0.25 * np.sin(2 * np.pi * 220 * t)
+ 0.12 * np.sin(2 * np.pi * 1100 * t)
+ 0.06 * np.sin(2 * np.pi * 5000 * t))
noise = 0.01 * rng.standard_normal(n)
audio = np.stack([sig + noise, 0.9 * sig + 0.02 * rng.standard_normal(n)])
audio = audio.astype(np.float32)
with tempfile.TemporaryDirectory() as td:
wav_in = os.path.join(td, "in.wav")
_write_wav(wav_in, audio, sr)
got = _render_bridge(bridge, wav_in, sr, chain)
# Python reference dùng schema cũ {type: eq} — convert từ job schema mới
# {type: builtin, id: eq} (bridge nhận schema mới, spec Phase 2).
py_chain = [{"type": s.get("id", s.get("type")), "params": s.get("params") or {}}
for s in chain]
ref = _apply_builtin_fx_chain(audio.copy(), py_chain, sr)
snr = _snr_db(ref, got)
return snr
def _b(id, params):
return {"type": "builtin", "id": id, "params": params or {}}
CHAINS = {
"eq": [_b("eq", {"g1": 3.0, "g2": -2.0, "g3": 1.5, "g4": 2.0})],
"eqpro": [_b("eqpro", {"amount": 80, "bands": [
{"active": True, "type": "lowshelf", "freq": 120, "gain": 2.5, "q": 0.8},
{"active": True, "type": "peaking", "freq": 900, "gain": -3.0, "q": 1.2},
{"active": True, "type": "highpass", "freq": 60, "gain": 0.0, "q": 0.7},
{"active": True, "type": "highshelf", "freq": 8000, "gain": 1.8, "q": 0.7},
]})],
"compressor": [_b("compressor", {"threshold": -20.0, "ratio": 4.0, "makeup": 2.0})],
"limiter": [_b("limiter", {"ceiling": -3.0})],
"exciter": [_b("exciter", {"drive": 60.0})],
"rebalance": [_b("rebalance", {"mid": 2.0, "side": -1.5})],
"order_ec": [_b("eq", {"g1": 2.0, "g3": -1.0}),
_b("compressor", {"threshold": -18.0, "ratio": 3.0})],
"order_ce": [_b("compressor", {"threshold": -18.0, "ratio": 3.0}),
_b("eq", {"g1": 2.0, "g3": -1.0})],
}
MIN_SNR = 30.0
@pytest.mark.parametrize("name", sorted(CHAINS))
def test_builtin_golden(bridge, name):
snr = _run_case(bridge, 44100, seed=1234, chain=CHAINS[name])
assert snr >= MIN_SNR, f"{name}: SNR {snr:.1f} dB < {MIN_SNR}"