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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user