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
+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();