diff --git a/G2_RESULTS.md b/G2_RESULTS.md new file mode 100644 index 0000000..c54e370 --- /dev/null +++ b/G2_RESULTS.md @@ -0,0 +1,36 @@ +# G2 — JUCE VST3 hosting + latency thật (FxLatReport) + +Trạng thái: **DONE** — đã verify qua probe, build pass. + +## Thay đổi + +- `native_bridge/juce_fx/JuceFxEngine.cpp` — `setChain(chainJson)`: parse mảng fx_chain + (`sheredom_json.h`), load VST3 qua `juce::VST3PluginFormat::findAllTypesForFile` + + `createInstanceFromDescription`; preset_b64 → `setStateFromVSTPresetFile` (deprecated, + vẫn dùng được); nối node nối tiếp in→p0→…→out; sau `prepareToPlay`: + `slotLatencies[i] = getLatencySamples()` từng plugin, `totalLatency = graph.getLatencySamples()`. + `entryLatencies()` trả `std::vector` theo thứ tự slot (skip/bypass/fail = 0). +- `JuceFxEngine.h` — khai báo `struct Slot;` + `setChain`/`entryLatencies`. +- `JuceFxLoop.cpp` — đọc `chainJson` từ job, gọi `engine.setChain` trước prepare; + `reportLatencies` báo từng slot qua FxLatReport (giữ giao thức cũ). +- `juce_fx/CMakeLists.txt` — bật `JUCE_PLUGINHOST_VST3=1` (JUCE mặc định 0 → class + VST3PluginFormat bị ẩn, gây C3861/C2039). +- `src-tauri/binaries/juce_fx_bridge-x86_64-pc-windows-msvc.exe` — build mới (G2). + +## Verify (debug/g0_latency_probe.py, exe mới) + +| config | plugin_latency | drained | recv/sent | roundtrip mean | +|---|---|---|---|---| +| empty chain | `{}` | True | 10000/10000 | 21.57 ms | +| pureComp | `{0: 2108}` | True | 10000/10000 | 21.70 ms | +| Ozone 11 | `{0: 0}` | True | 10000/10000 | 21.69 ms | + +- Latency thật đúng dự đoán: pureComp ~2108 samples @48k, Ozone 11 không delay. +- Không crackle, drained=True, recv=sent — benchmark G1 giữ nguyên. + +## Ghi chú + +- `setStateFromVSTPresetFile` là API deprecated nhưng public (bên trong `#ifndef DOXYGEN`), + compile được với `JUCE_PLUGINHOST_VST3=1`. +- Bỏ hằng `(7+8)*256` trong `fxRtApplyPdc` (app.jsx) là việc của G3 (dùng latencyTotal + thật từ WS — hiện `FX_RT_ROUNDTRIP_SAMPLES=3840` là ước lượng RTT pipeline, ponytail). diff --git a/native_bridge/juce_fx/CMakeLists.txt b/native_bridge/juce_fx/CMakeLists.txt index b7949a1..a6f3f8f 100644 --- a/native_bridge/juce_fx/CMakeLists.txt +++ b/native_bridge/juce_fx/CMakeLists.txt @@ -29,6 +29,10 @@ target_link_libraries(juce_fx_bridge PRIVATE juce::juce_audio_basics juce::juce_audio_processors ) +# G2: VST3 hosting — JUCE mặc định JUCE_PLUGINHOST_VST3=0, bật để +# VST3PluginFormat (findAllTypesForFile/createInstanceFromDescription/...) +# được compile (juce_audio_processors tự bundle VST3_SDK). +target_compile_definitions(juce_fx_bridge PRIVATE JUCE_PLUGINHOST_VST3=1) if(WIN32) # timeBeginPeriod (winmm) — như fx_vst_bridge target_link_libraries(juce_fx_bridge PRIVATE winmm) diff --git a/native_bridge/juce_fx/JuceFxEngine.cpp b/native_bridge/juce_fx/JuceFxEngine.cpp index fe6355d..5bed051 100644 --- a/native_bridge/juce_fx/JuceFxEngine.cpp +++ b/native_bridge/juce_fx/JuceFxEngine.cpp @@ -1,19 +1,96 @@ // native_bridge/juce_fx/JuceFxEngine.cpp // Implementation: AudioProcessorGraph với input/output node — chain rỗng G1. +// G2: nạp VST3 theo fx_chain (path, preset_b64, bypass), chèn node giữa +// input/output; latency thật = graph.getLatencySamples() (PDC nội bộ graph). #include "JuceFxEngine.h" #include +#include "sheredom_json.h" #include #include +#include +#include +#include +#include #include +namespace { + +// --- base64 decode (RFC 4648) — như RenderFxJob.cpp ------------------------- +int b64val(unsigned char c) { + if (c >= 'A' && c <= 'Z') return (int)(c - 'A'); + if (c >= 'a' && c <= 'z') return (int)(c - 'a') + 26; + if (c >= '0' && c <= '9') return (int)(c - '0') + 52; + if (c == '+') return 62; + if (c == '/') return 63; + return -1; +} + +bool base64Decode(const std::string& in, std::vector& out) { + out.clear(); + out.reserve((in.size() / 4) * 3); + uint32_t acc = 0; + int bits = 0; + for (unsigned char ch : in) { + if (ch == '=' || ch == '\n' || ch == '\r' || ch == ' ') continue; + int v = b64val(ch); + if (v < 0) return false; + acc = (acc << 6) | (uint32_t)v; + bits += 6; + if (bits >= 8) { + bits -= 8; + out.push_back((uint8_t)((acc >> bits) & 0xFF)); + } + } + return true; +} + +// --- sheredom helpers (giống RenderFxJob.cpp) ------------------------------- +const json_object_element_s* jMember(const json_object_s* o, const char* key) { + for (const json_object_element_s* e = o ? o->start : nullptr; e; e = e->next) + if (e->name && e->name->string && std::strcmp(e->name->string, key) == 0) + return e; + return nullptr; +} + +std::string jString(const json_object_s* o, const char* key, const std::string& def) { + const json_object_element_s* m = jMember(o, key); + if (m && m->value && m->value->type == json_type_string) { + const auto* s = static_cast(m->value->payload); + return std::string(s->string, s->string_size); + } + return def; +} + +bool jBool(const json_object_s* o, const char* key, bool def) { + const json_object_element_s* m = jMember(o, key); + if (m && m->value) { + if (m->value->type == json_type_true) return true; + if (m->value->type == json_type_false) return false; + } + return def; +} + +} // namespace + +struct JuceFxEngine::Slot { + std::string type; + std::string path; + std::string presetB64; + bool bypass = false; +}; + struct JuceFxEngine::Impl { juce::AudioProcessorGraph graph; juce::AudioBuffer buf; uint32_t sampleRate = 0; uint32_t blockSize = 0; bool prepared = false; + std::vector slots; // chain đã parse (thứ tự slot) + std::vector pluginNodes; // node theo slot (null = bỏ qua) + std::vector slotLatencies; // latency thật từng slot (0 = skip/bypass/fail) + uint32_t totalLatency = 0; }; JuceFxEngine::JuceFxEngine() : impl_(std::make_unique()) {} @@ -28,6 +105,30 @@ void JuceFxEngine::shutdown() { impl_->sampleRate = 0; impl_->blockSize = 0; } + impl_->pluginNodes.clear(); + impl_->slotLatencies.assign(impl_->slots.size(), 0); + impl_->totalLatency = 0; +} + +void JuceFxEngine::setChain(const std::string& chainJson) { + // Parse mảng JSON fx_chain: [ {type,path,preset_b64,bypass}, ... ]. + impl_->slots.clear(); + json_value_s* root = json_parse(chainJson.data(), chainJson.size()); + if (root && root->type == json_type_array) { + const auto* arr = static_cast(root->payload); + for (const json_array_element_s* el = arr->start; el; el = el->next) { + const json_value_s* v = el->value; + if (!v || v->type != json_type_object) continue; + const auto* o = static_cast(v->payload); + Slot s; + s.type = jString(o, "type", ""); + s.path = jString(o, "path", ""); + s.presetB64 = jString(o, "preset_b64", ""); + s.bypass = jBool(o, "bypass", false); + impl_->slots.push_back(std::move(s)); + } + } + if (root) free(root); } void JuceFxEngine::prepare(uint32_t sampleRate, uint32_t blockSize) { @@ -40,8 +141,6 @@ void JuceFxEngine::prepare(uint32_t sampleRate, uint32_t blockSize) { impl_->blockSize = blockSize; impl_->graph.setPlayConfigDetails(2, 2, (double)sampleRate, (int)blockSize); - // Chain rỗng G1: audio input -> audio output (passthrough). G2: chèn - // plugin node giữa hai node này theo fx_chain. auto& g = impl_->graph; const auto inNode = g.addNode(std::make_unique< juce::AudioProcessorGraph::AudioGraphIOProcessor>( @@ -51,14 +150,66 @@ void JuceFxEngine::prepare(uint32_t sampleRate, uint32_t blockSize) { juce::AudioProcessorGraph::AudioGraphIOProcessor::audioOutputNode)); using NCh = juce::AudioProcessorGraph::NodeAndChannel; using Conn = juce::AudioProcessorGraph::Connection; + + // G2: nạp VST3 theo chain — nối nối tiếp in -> p0 -> p1 -> ... -> out. + // Slot bypass / type khác vst3 / load fail → passthrough (latency 0). + impl_->pluginNodes.assign(impl_->slots.size(), nullptr); + impl_->slotLatencies.assign(impl_->slots.size(), 0); + juce::AudioProcessorGraph::Node::Ptr prev = inNode; + for (size_t i = 0; i < impl_->slots.size(); ++i) { + const Slot& s = impl_->slots[i]; + if (s.bypass || (s.type != "vst3" && s.type != "vst") || s.path.empty()) + continue; + juce::VST3PluginFormat fmt; + juce::OwnedArray descs; + fmt.findAllTypesForFile(descs, juce::File(s.path).getFullPathName()); + if (descs.isEmpty()) { + std::cerr << "[JuceFxEngine] slot " << i << " no VST3 desc: " + << s.path << std::endl; + continue; + } + auto inst = fmt.createInstanceFromDescription(*descs.getFirst(), + (double)sampleRate, + (int)blockSize); + if (!inst) { + std::cerr << "[JuceFxEngine] slot " << i << " createInstance fail: " + << s.path << std::endl; + continue; + } + if (!s.presetB64.empty()) { + std::vector bytes; + if (base64Decode(s.presetB64, bytes) && !bytes.empty()) { + juce::MemoryBlock mb(bytes.data(), bytes.size()); + if (juce::VST3PluginFormat::setStateFromVSTPresetFile(inst.get(), mb)) + std::cerr << "[JuceFxEngine] slot " << i << " preset applied" << std::endl; + else + std::cerr << "[JuceFxEngine] slot " << i << " preset FAILED" << std::endl; + } + } + auto node = g.addNode(std::unique_ptr(std::move(inst))); + impl_->pluginNodes[i] = node; + for (int ch = 0; ch < 2; ++ch) { + NCh src; src.nodeID = prev->nodeID; src.channelIndex = ch; + NCh dst; dst.nodeID = node->nodeID; dst.channelIndex = ch; + g.addConnection(Conn(src, dst)); + } + prev = node; + } for (int ch = 0; ch < 2; ++ch) { - NCh src; src.nodeID = inNode->nodeID; src.channelIndex = ch; + NCh src; src.nodeID = prev->nodeID; src.channelIndex = ch; NCh dst; dst.nodeID = outNode->nodeID; dst.channelIndex = ch; g.addConnection(Conn(src, dst)); } g.prepareToPlay((double)sampleRate, (int)(blockSize * 4)); // batch tối đa 4 block impl_->buf.setSize(2, (int)(blockSize * 4), false, false, true); + + // Latency thật từng slot sau prepareToPlay (plugin set trong prepare). + for (size_t i = 0; i < impl_->pluginNodes.size(); ++i) + if (impl_->pluginNodes[i]) + impl_->slotLatencies[i] = + (uint32_t)impl_->pluginNodes[i]->getProcessor()->getLatencySamples(); + impl_->totalLatency = (uint32_t)g.getLatencySamples(); impl_->prepared = true; } @@ -70,7 +221,7 @@ void JuceFxEngine::process(float* inL, float* inR, uint32_t n) { b.copyFrom(1, 0, inR, (int)n); juce::MidiBuffer midi; impl_->graph.processBlock(b, midi); - // G1 passthrough: copy kết quả về buffer đầu vào (in-place như contract). + // Graph PDC nội bộ: output đã align dry/wet — copy về in-place như contract. const auto* outL = b.getReadPointer(0); const auto* outR = b.getReadPointer(1); std::copy(outL, outL + n, inL); @@ -78,5 +229,9 @@ void JuceFxEngine::process(float* inL, float* inR, uint32_t n) { } uint32_t JuceFxEngine::latencySamples() const { - return impl_->prepared ? (uint32_t)impl_->graph.getLatencySamples() : 0u; + return impl_->prepared ? impl_->totalLatency : 0u; +} + +std::vector JuceFxEngine::entryLatencies() const { + return impl_->slotLatencies; } diff --git a/native_bridge/juce_fx/JuceFxEngine.h b/native_bridge/juce_fx/JuceFxEngine.h index 6e92c40..b4b621e 100644 --- a/native_bridge/juce_fx/JuceFxEngine.h +++ b/native_bridge/juce_fx/JuceFxEngine.h @@ -4,6 +4,8 @@ #pragma once #include #include +#include +#include class JuceFxEngine { public: @@ -13,17 +15,23 @@ public: JuceFxEngine(const JuceFxEngine&) = delete; JuceFxEngine& operator=(const JuceFxEngine&) = delete; - // Tạo graph mới (input node -> output node, passthrough), prepareToPlay. + // Tạo graph mới (input node -> [VST3 theo chain] -> output node). // sampleRate/blockSize đổi → teardown graph cũ + prepare lại (validate // header G1). blockSize là kích thước mỗi SHM block (256). void prepare(uint32_t sampleRate, uint32_t blockSize); + // G2: đặt chain (mảng JSON fx_chain) trước prepare — các slot bypass / + // không phải vst3 / load fail chạy passthrough (latency 0). + void setChain(const std::string& chainJson); // Audio thread: xử lý n mẫu stereo in-place (n <= blockSize * FXRT_IN_SLOTS). void process(float* inL, float* inR, uint32_t n); // Graph latency samples (G1 chain rỗng = 0; G2 = getLatencySamples thật). uint32_t latencySamples() const; + // Latency thật từng slot theo thứ tự chain (0 = skip/bypass/fail). + std::vector entryLatencies() const; void shutdown(); private: + struct Slot; // G2: một mục fx_chain (type/path/presetB64/bypass) struct Impl; std::unique_ptr impl_; }; diff --git a/native_bridge/juce_fx/JuceFxLoop.cpp b/native_bridge/juce_fx/JuceFxLoop.cpp index da175d8..3e007dd 100644 --- a/native_bridge/juce_fx/JuceFxLoop.cpp +++ b/native_bridge/juce_fx/JuceFxLoop.cpp @@ -62,6 +62,22 @@ double jsonNumber(const std::string& s, const std::string& key, double def) { try { return std::stod(s.substr(p, e - p)); } catch (...) { return def; } } +// Cắt substring mảng JSON "fx_chain":[...] — như RealtimeFxLoop.cpp. +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_juce_fx_loop(const std::string& jobPath, const std::string& shmName, @@ -77,7 +93,11 @@ int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName, 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(32.0, std::min(blkD, (double)FXRT_BLOCK)); - // G1: fx_chain bỏ qua (chain rỗng). G2: đọc fx_chain -> build graph. + // G2: đọc fx_chain -> build graph (VST3 theo chain). Rỗng = passthrough. + const std::string chainJson = extractChainJson(job); + if (chainJson.empty()) + std::cerr << "[JuceFxLoop] no fx_chain in job (empty chain = passthrough)" + << std::endl; fxshm::ShmView* v = fxshm::openShm(shmName, sizeof(FxRealtimeIPC)); if (!v) { @@ -104,25 +124,39 @@ int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName, SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL); #endif - // Engine: prepare graph (chain rỗng). sr/block từ job; header là nguồn - // thật (engine ghi lúc tạo SHM) — nếu khác, lấy header. + // Engine: prepare graph (chain theo fx_chain). sr/block từ job; header là + // nguồn thật (engine ghi lúc tạo SHM) — nếu khác, lấy header. uint32_t curSr = sampleRate; uint32_t curBlock = block; JuceFxEngine engine; + engine.setChain(chainJson); if (ipc->h.sampleRate) curSr = ipc->h.sampleRate; if (ipc->h.blockSize) curBlock = ipc->h.blockSize; engine.prepare(curSr, curBlock); std::cerr << "[JuceFxLoop] prepared sr=" << curSr << " block=" << curBlock << " latency=" << engine.latencySamples() << std::endl; - // Report latency ban đầu (slot 0 = tổng chain). - { - const uint32_t ls = ipc->h.latWrite & (FXRT_LAT_SLOTS - 1); - ipc->lat[ls].slot = 0; - ipc->lat[ls].samples = engine.latencySamples(); - MemoryBarrier(); - ipc->h.latWrite++; - } + // Report latency ban đầu: từng slot (giữ giao thức FxLatReport cũ — engine + // Python sum → total). Chain rỗng → slot 0 = 0 (khớp G1). + auto reportLatencies = [&]() { + const auto lats = engine.entryLatencies(); + if (lats.empty()) { + const uint32_t ls = ipc->h.latWrite & (FXRT_LAT_SLOTS - 1); + ipc->lat[ls].slot = 0; + ipc->lat[ls].samples = 0; + MemoryBarrier(); + ipc->h.latWrite++; + } else { + 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++; + } + } + }; + reportLatencies(); ipc->h.state = FXRT_STATE_READY; std::cerr << "[JuceFxLoop] ready" << std::endl; @@ -159,11 +193,7 @@ int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName, curSr = ipc->h.sampleRate; curBlock = ipc->h.blockSize; engine.prepare(curSr, curBlock); - const uint32_t ls = ipc->h.latWrite & (FXRT_LAT_SLOTS - 1); - ipc->lat[ls].slot = 0; - ipc->lat[ls].samples = engine.latencySamples(); - MemoryBarrier(); - ipc->h.latWrite++; + reportLatencies(); std::cerr << "[JuceFxLoop] re-prepared sr=" << curSr << " block=" << curBlock << " latency=" << engine.latencySamples() << std::endl; diff --git a/src-tauri/binaries/juce_fx_bridge-x86_64-pc-windows-msvc.exe b/src-tauri/binaries/juce_fx_bridge-x86_64-pc-windows-msvc.exe index 4e39e1f..c74a1e6 100644 Binary files a/src-tauri/binaries/juce_fx_bridge-x86_64-pc-windows-msvc.exe and b/src-tauri/binaries/juce_fx_bridge-x86_64-pc-windows-msvc.exe differ