fix(fx): realtime VST ngắt quãng khi mở MASTERING PANEL + PWR master không ảnh hưởng âm; pacing WS input/output, gate fxRt theo masterConnected, worklet cap 32

This commit is contained in:
2026-08-21 21:48:56 +07:00
parent 4e3f2fd08a
commit bb6658e03d
17 changed files with 1813 additions and 142 deletions
+2 -3
View File
@@ -100,13 +100,12 @@ else()
endif()
# ── fx_vst_bridge: VST FX host (split from daw_vst_bridge). Windows-only:
# one-shot modes --render-fx / --scan / --open-fx-gui / --fx-gui. No SHM
# realtime loop (the live master FX chain stays in daw_vst_bridge because
# it processes the instrument mix in-process). ──
# --render-fx (offline) + --realtime-fx (SHM realtime cloud-FX loop). ──
if(WIN32)
add_executable(fx_vst_bridge
src/main_fx.cpp
src/RenderFxJob.cpp
src/RealtimeFxLoop.cpp
src/FxGuiServer.cpp
)
if(VST3_SDK_TARGET)
+39
View File
@@ -0,0 +1,39 @@
// native_bridge/include/FxRealtimeIPC.h
// SHM layout cho fx_vst_bridge --realtime-fx (VST FX xử lý realtime, cloud-FX
// loop): engine (Python) tạo mapping, bridge mở và chạy vòng lặp audio.
// Tất cả field u32/float (4-byte) — KHÔNG padding — mirror chính xác bằng
// ctypes trong app/core/fx_realtime.py.
#pragma once
#include <cstdint>
#define FXRT_MAGIC 0x46585254u // "FXRT"
#define FXRT_BLOCK 256
#define FXRT_IN_SLOTS 4 // engine -> bridge
#define FXRT_OUT_SLOTS 8 // 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.
struct FxRealtimeHeader {
volatile uint32_t magic;
volatile uint32_t state;
uint32_t sampleRate;
uint32_t blockSize;
volatile uint32_t running; // engine set 0 → bridge thoát
volatile uint32_t heartbeat; // bridge tăng mỗi 100ms (watchdog)
volatile uint32_t inWrite; // producer: engine
volatile uint32_t inRead; // consumer: bridge
uint32_t inSlots;
volatile uint32_t outWrite; // producer: bridge
volatile uint32_t outRead; // consumer: engine
uint32_t outSlots;
};
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];
};
+5
View File
@@ -81,3 +81,8 @@ private:
};
#endif // RENDER_FX_JOB_H
// Realtime SHM FX loop (fx_vst_bridge --realtime-fx): engine (Python) tạo
// mapping FxRealtimeIPC, bridge mở, đọc input ring, xử lý qua RealtimeFxChain,
// ghi output ring. Thoát khi running==0 hoặc parent chết.
int run_realtime_fx_loop(const std::string& jobPath, const std::string& shmName,
uint32_t parentPid);
+194
View File
@@ -0,0 +1,194 @@
// native_bridge/src/RealtimeFxLoop.cpp
// fx_vst_bridge --realtime-fx <job.json> --shm <name> [--parent <pid>]
// Vòng lặp SHM realtime: engine (Python) tạo mapping FxRealtimeIPC, bridge mở,
// đọc block input ring (engine->bridge), chạy qua RealtimeFxChain (VST3 FX,
// SEH-guarded, in-place), ghi block output ring (bridge->engine). Heartbeat
// 100ms riêng (watchdog). Thoát khi running==0 hoặc parent chết.
//
// job.json: { "sample_rate": 48000, "block_size": 256, "fx_chain": [ ... ] }
// fx_chain: mảng {type,path,name,preset_b64,bypass} — RealtimeFxChain::setChain
// nhận JSON mảng {path,bypass,preset_b64} (type/name bỏ qua).
#include "RenderFxJob.h"
#include "FxRealtimeIPC.h"
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstring>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
#include <thread>
namespace {
struct ShmView {
void* map = nullptr;
void* view = nullptr;
};
ShmView* openShm(const std::string& name, size_t size) {
int wlen = MultiByteToWideChar(CP_UTF8, 0, name.c_str(), -1, nullptr, 0);
std::wstring wname(wlen, L'\0');
MultiByteToWideChar(CP_UTF8, 0, name.c_str(), -1, &wname[0], wlen);
HANDLE map = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, wname.c_str());
if (!map) return nullptr;
void* view = MapViewOfFile(map, FILE_MAP_ALL_ACCESS, 0, 0, size);
if (!view) { CloseHandle(map); return nullptr; }
ShmView* v = new ShmView();
v->map = map;
v->view = view;
return v;
}
void closeShm(ShmView* v) {
if (!v) return;
if (v->view) UnmapViewOfFile(v->view);
if (v->map) CloseHandle((HANDLE)v->map);
delete v;
}
static bool parentAlive(uint32_t pid) {
if (pid == 0) return true;
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (!h) return false;
CloseHandle(h);
return true;
}
static void sleepMs(uint32_t ms) { Sleep(ms); }
std::string readFile(const std::string& path) {
std::ifstream f(path, std::ios::binary);
return std::string((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
}
// Tách giá trị số khỏi JSON flat: "key":<num> hoặc "key": <num>
double jsonNumber(const std::string& s, const std::string& key, double def) {
std::string k = "\"" + key + "\"";
size_t p = s.find(k);
if (p == std::string::npos) return def;
p += k.size();
while (p < s.size() && (s[p] == ':' || s[p] == ' ' || s[p] == '\t' ||
s[p] == '\r' || s[p] == '\n'))
++p;
size_t e = p;
while (e < s.size() && (std::isdigit((unsigned char)s[e]) || s[e] == '.' ||
s[e] == '-' || s[e] == '+' || s[e] == 'e' ||
s[e] == 'E'))
++e;
if (e == p) return def;
try { return std::stod(s.substr(p, e - p)); } catch (...) { return def; }
}
// Cắt substring mảng JSON "fx_chain":[...] (base64 không chứa [ ] nên depth
// scan an toàn). Trả rỗng nếu không có.
std::string extractChainJson(const std::string& s) {
std::string k = "\"fx_chain\"";
size_t p = s.find(k);
if (p == std::string::npos) return "";
p = s.find('[', p);
if (p == std::string::npos) return "";
size_t depth = 0, i = p;
for (; i < s.size(); ++i) {
if (s[i] == '[') ++depth;
else if (s[i] == ']') { --depth; if (depth == 0) break; }
}
if (i >= s.size()) return "";
return s.substr(p, i - p + 1);
}
} // namespace
int run_realtime_fx_loop(const std::string& jobPath, const std::string& shmName,
uint32_t parentPid) {
std::cerr << "[RealtimeFxLoop] start job=" << jobPath << " shm=" << shmName
<< " parent=" << parentPid << std::endl;
const std::string job = readFile(jobPath);
if (job.empty()) {
std::cerr << "[RealtimeFxLoop] cannot read job file" << std::endl;
return 1;
}
const double srD = jsonNumber(job, "sample_rate", 44100.0);
const uint32_t sampleRate = (uint32_t)srD;
const double blkD = jsonNumber(job, "block_size", (double)FXRT_BLOCK);
const uint32_t block = (uint32_t)std::max<double>(32.0, std::min<double>(blkD, (double)FXRT_BLOCK));
const std::string chainJson = extractChainJson(job);
if (chainJson.empty()) {
std::cerr << "[RealtimeFxLoop] no fx_chain in job (empty chain = passthrough)" << std::endl;
}
RealtimeFxChain chain;
chain.setChain(chainJson, sampleRate, block);
ShmView* v = openShm(shmName, sizeof(FxRealtimeIPC));
if (!v) {
std::cerr << "[RealtimeFxLoop] cannot open SHM: " << shmName
<< " (engine phải tạo trước)" << std::endl;
return 2;
}
auto* ipc = static_cast<FxRealtimeIPC*>(v->view);
// Chờ engine khởi tạo header (magic) — tối đa 2s.
for (int i = 0; i < 200 && ipc->h.magic != FXRT_MAGIC; ++i) sleepMs(10);
if (ipc->h.magic != FXRT_MAGIC) {
std::cerr << "[RealtimeFxLoop] SHM magic mismatch (engine chưa init?)" << std::endl;
closeShm(v);
return 2;
}
if (ipc->h.inSlots != FXRT_IN_SLOTS || ipc->h.outSlots != FXRT_OUT_SLOTS) {
std::cerr << "[RealtimeFxLoop] slot count mismatch" << std::endl;
closeShm(v);
return 2;
}
ipc->h.state = FXRT_STATE_READY;
std::cerr << "[RealtimeFxLoop] ready sr=" << sampleRate << " block=" << block
<< " chain=" << (chainJson.empty() ? "(empty)" : "loaded") << std::endl;
// Heartbeat thread riêng — watchdog của engine (loop chậm không = chết).
std::thread hb([&]() {
while (ipc->h.running && ipc->h.state == FXRT_STATE_READY) {
ipc->h.heartbeat++;
sleepMs(100);
}
});
const uint32_t n = block;
uint32_t inMask = ipc->h.inSlots - 1;
uint32_t outMask = ipc->h.outSlots - 1;
uint64_t processed = 0;
while (ipc->h.running) {
if (parentPid && !parentAlive(parentPid)) {
std::cerr << "[RealtimeFxLoop] parent gone — exiting" << std::endl;
break;
}
const uint32_t avail = ipc->h.inWrite - ipc->h.inRead;
if (avail == 0) { sleepMs(1); continue; }
const uint32_t slot = ipc->h.inRead & inMask;
float L[FXRT_BLOCK], R[FXRT_BLOCK];
std::memcpy(L, ipc->inL[slot], n * sizeof(float));
std::memcpy(R, ipc->inR[slot], n * sizeof(float));
ipc->h.inRead++; // consume
chain.process(L, R, n); // SEH-guarded; chain rỗng = passthrough
const uint32_t oslot = ipc->h.outWrite & outMask;
std::memcpy(ipc->outL[oslot], L, n * sizeof(float));
std::memcpy(ipc->outR[oslot], R, n * sizeof(float));
MemoryBarrier(); // dữ liệu trước index (đúng thứ tự trên ARM64)
ipc->h.outWrite++; // publish
++processed;
}
ipc->h.state = FXRT_STATE_STARTING; // đã dừng
hb.join();
closeShm(v);
std::cerr << "[RealtimeFxLoop] exit processed=" << processed << std::endl;
return 0;
}
+24 -4
View File
@@ -1,12 +1,15 @@
// native_bridge/src/main_fx.cpp
// Entry point of fx_vst_bridge.exe — VST FX host bridge (task: split FX out
// of daw_vst_bridge). Windows only. One-shot modes only:
// --render-fx <job.json> --in <input.wav> --out <output.wav>
// of daw_vst_bridge). Windows only. Modes:
// --render-fx <job.json> --in <input.wav> --out <output.wav> (offline)
// --realtime-fx <job.json> --shm <name> [--parent <pid>] (SHM realtime)
// --scan <dir>
// --open-fx-gui <job.json>
// --fx-gui <job.json>
// No SHM realtime loop: the realtime master FX chain (control type 6) stays
// in daw_vst_bridge because it processes the live instrument mix in-process.
// SHM realtime loop (--realtime-fx): cloud-FX loop cho VST3 trong master FX
// chain — browser gửi mix qua engine → SHM input ring → bridge xử lý (cùng
// RealtimeFxChain như daw_vst_bridge master chain) → SHM output ring →
// engine → browser. Xem FxRealtimeIPC.h + RealtimeFxLoop.cpp.
#include "RenderFxJob.h"
#ifdef _WIN32
@@ -113,6 +116,8 @@ int main(int argc, char* argv[]) {
std::string scanDir;
std::string openFxGuiJob;
std::string fxGuiServerJob;
std::string realtimeFxJob, realtimeShm;
uint32_t realtimeParent = 0;
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--render-fx") == 0 && i + 1 < argc)
renderFxJob = argv[i + 1];
@@ -126,6 +131,20 @@ int main(int argc, char* argv[]) {
openFxGuiJob = argv[i + 1];
else if (std::strcmp(argv[i], "--fx-gui") == 0 && i + 1 < argc)
fxGuiServerJob = argv[i + 1];
else if (std::strcmp(argv[i], "--realtime-fx") == 0 && i + 1 < argc)
realtimeFxJob = argv[i + 1];
else if (std::strcmp(argv[i], "--shm") == 0 && i + 1 < argc)
realtimeShm = argv[i + 1];
else if (std::strcmp(argv[i], "--parent") == 0 && i + 1 < argc)
realtimeParent = (uint32_t)std::strtoul(argv[i + 1], nullptr, 10);
}
if (!realtimeFxJob.empty()) {
if (realtimeShm.empty()) {
std::cerr << "[RealtimeFxLoop] --realtime-fx requires --shm <name>"
<< std::endl;
return 1;
}
return run_realtime_fx_loop(realtimeFxJob, realtimeShm, realtimeParent);
}
if (!renderFxJob.empty()) {
if (renderFxIn.empty() || renderFxOut.empty()) {
@@ -141,6 +160,7 @@ int main(int argc, char* argv[]) {
std::cerr << "fx_vst_bridge: no mode. Usage:\n"
" --render-fx <job.json> --in <input.wav> --out <output.wav>\n"
" --realtime-fx <job.json> --shm <name> [--parent <pid>]\n"
" --scan <dir>\n"
" --open-fx-gui <job.json>\n"
" --fx-gui <job.json>" << std::endl;