diff --git a/native_bridge/juce_fx/CMakeLists.txt b/native_bridge/juce_fx/CMakeLists.txt index a6f3f8f..a531c11 100644 --- a/native_bridge/juce_fx/CMakeLists.txt +++ b/native_bridge/juce_fx/CMakeLists.txt @@ -28,6 +28,8 @@ target_link_libraries(juce_fx_bridge PRIVATE juce::juce_core juce::juce_audio_basics juce::juce_audio_processors + # M1 editor host: AudioProcessorEditor cần gui_basics (Component/MessageManager) + juce::juce_gui_basics ) # G2: VST3 hosting — JUCE mặc định JUCE_PLUGINHOST_VST3=0, bật để # VST3PluginFormat (findAllTypesForFile/createInstanceFromDescription/...) @@ -37,3 +39,25 @@ if(WIN32) # timeBeginPeriod (winmm) — như fx_vst_bridge target_link_libraries(juce_fx_bridge PRIVATE winmm) endif() + +# ── M1 selftest (editor API — KHÔNG ship): verify editor host trên instance +# DSP thật trước khi M2 attach GUI thread. Chạy thủ công: +# build_juce/Release/juce_fx_selftest.exe "" +add_executable(juce_fx_selftest + editor_selftest.cpp + JuceFxEngine.cpp +) +target_include_directories(juce_fx_selftest PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../include +) +target_link_libraries(juce_fx_selftest PRIVATE + juce::juce_core + juce::juce_audio_basics + juce::juce_audio_processors + juce::juce_gui_basics +) +target_compile_definitions(juce_fx_selftest PRIVATE JUCE_PLUGINHOST_VST3=1) +target_compile_definitions(juce_fx_selftest PRIVATE JUCE_MODAL_LOOPS_PERMITTED=1) +if(WIN32) + target_link_libraries(juce_fx_selftest PRIVATE winmm) +endif() diff --git a/native_bridge/juce_fx/JuceFxEngine.cpp b/native_bridge/juce_fx/JuceFxEngine.cpp index 07741ff..079e4f0 100644 --- a/native_bridge/juce_fx/JuceFxEngine.cpp +++ b/native_bridge/juce_fx/JuceFxEngine.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -189,13 +190,152 @@ struct JuceFxEngine::Impl { 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; + + // ── Editor host (M1): slot -> processor + editor (chỉ GUI thread đụng). + struct SlotEditor { + juce::AudioProcessor* proc = nullptr; // processor slot (không sở hữu) + std::unique_ptr editor; // sở hữu editor (GUI thread) + bool open = false; + std::string captured; // state capture gần nhất (raw) + }; + struct EditorCmd { + uint32_t slot = 0; + EditorAction action = EditorAction::Open; + int w = 0, h = 0; + }; + std::vector editors; // theo slot (dựng lại mỗi prepare) + std::vector editorQueue; // lệnh chờ (requestEditor) + mutable std::mutex editorMutex; // editors + captured + std::mutex queueMutex; // editorQueue }; JuceFxEngine::JuceFxEngine() : impl_(std::make_unique()) {} JuceFxEngine::~JuceFxEngine() { shutdown(); } +void JuceFxEngine::requestEditor(uint32_t slot, EditorAction action, int w, int h) { + // Chỉ xếp lệnh (thread-safe); validation + thực thi ở pumpEditorQueue() + // trên GUI thread — không log ở đây (có thể gọi từ thread khác). + std::lock_guard lk(impl_->queueMutex); + impl_->editorQueue.push_back({slot, action, w, h}); +} + +bool JuceFxEngine::pumpEditorQueue() { + std::vector cmds; + { + std::lock_guard lk(impl_->queueMutex); + if (impl_->editorQueue.empty()) return false; + cmds.swap(impl_->editorQueue); + } + bool any = false; + for (const auto& c : cmds) { + std::lock_guard lk(impl_->editorMutex); + if (c.slot >= impl_->editors.size()) { + std::cerr << "[JuceFxEngine] editor cmd slot " << c.slot + << " ngoài chain (" << impl_->editors.size() + << ") — bỏ qua" << std::endl; + continue; + } + auto& se = impl_->editors[c.slot]; + if (!se.proc) { + if (c.action == EditorAction::Open) + std::cerr << "[JuceFxEngine] slot " << c.slot + << " không có processor (bypass/fail) — không mở editor" + << std::endl; + continue; + } + switch (c.action) { + case EditorAction::Open: { + if (!se.editor) { + juce::AudioProcessorEditor* ed = se.proc->createEditorIfNeeded(); + if (!ed) { + std::cerr << "[JuceFxEngine] slot " << c.slot + << " createEditorIfNeeded null (plugin không có UI)" + << std::endl; + break; + } + se.editor.reset(ed); // engine sở hữu editor (M1) + se.open = true; + if (c.w > 0 && c.h > 0) se.editor->setSize(c.w, c.h); + std::cerr << "[JuceFxEngine] slot " << c.slot << " editor open " + << se.editor->getWidth() << "x" + << se.editor->getHeight() << std::endl; + } else if (c.w > 0 && c.h > 0) { + se.editor->setSize(c.w, c.h); + } + any = true; + break; + } + case EditorAction::Close: { + if (se.editor) { + se.editor.reset(); // ~editor -> processor->editorBeingDeleted + se.open = false; + std::cerr << "[JuceFxEngine] slot " << c.slot + << " editor closed" << std::endl; + any = true; + } + break; + } + case EditorAction::Show: + case EditorAction::Hide: { + // M1: editor chưa attach host window — setVisible chưa đủ. M2 + // (GUI thread + host window) sẽ xử lý show/hide thật. + if (se.editor) { + se.editor->setVisible(c.action == EditorAction::Show); + any = true; + } + break; + } + case EditorAction::Resize: { + if (se.editor && c.w > 0 && c.h > 0) { + se.editor->setSize(c.w, c.h); + any = true; + } + break; + } + case EditorAction::Capture: { + juce::MemoryBlock mb; + se.proc->getStateInformation(mb); + se.captured.assign(static_cast(mb.getData()), mb.getSize()); + std::cerr << "[JuceFxEngine] slot " << c.slot << " captured " + << se.captured.size() << " bytes" << std::endl; + any = true; + break; + } + } + } + return any; +} + +bool JuceFxEngine::isEditorOpen(uint32_t slot) const { + std::lock_guard lk(impl_->editorMutex); + if (slot >= impl_->editors.size()) return false; + return impl_->editors[slot].open; +} + +std::string JuceFxEngine::takeCapturedState(uint32_t slot) { + std::lock_guard lk(impl_->editorMutex); + if (slot >= impl_->editors.size()) return {}; + std::string s = std::move(impl_->editors[slot].captured); + impl_->editors[slot].captured.clear(); + return s; +} + void JuceFxEngine::shutdown() { + // Đóng mọi editor TRƯỚC khi clear graph — JUCE assert editor phải xoá + // trước processor. Editor ops ở đây chạy trên thread gọi shutdown; M1 + // chưa có GUI thread đồng thời nên an toàn. Khi M2 có GUI thread: chain + // đổi phải gửi ed.close trước khi re-prepare (M4/M5). + { + std::lock_guard lk(impl_->editorMutex); + for (auto& se : impl_->editors) { + if (se.editor) se.editor.reset(); // ~editor -> processor->editorBeingDeleted + se.proc = nullptr; + se.open = false; + se.captured.clear(); + } + impl_->editors.clear(); + } if (impl_->prepared) { impl_->graph.releaseResources(); impl_->graph.clear(); @@ -324,6 +464,16 @@ void JuceFxEngine::prepare(uint32_t sampleRate, uint32_t blockSize) { prev = node; } if (cacheDirty) saveDescCache(descCache); + + // Editor mapping: slot -> processor (Reaper-style — editor dùng ĐÚNG + // instance trong graph). Editors cũ đã đóng ở shutdown() khi graph đổi; + // chain không đổi (early-return trên) thì editors giữ nguyên + proc sống. + impl_->editors.clear(); + impl_->editors.resize(impl_->slots.size()); + for (size_t i = 0; i < impl_->pluginNodes.size(); ++i) + if (impl_->pluginNodes[i]) + impl_->editors[i].proc = impl_->pluginNodes[i]->getProcessor(); + for (int ch = 0; ch < 2; ++ch) { NCh src; src.nodeID = prev->nodeID; src.channelIndex = ch; NCh dst; dst.nodeID = outNode->nodeID; dst.channelIndex = ch; diff --git a/native_bridge/juce_fx/JuceFxEngine.h b/native_bridge/juce_fx/JuceFxEngine.h index b4b621e..7321081 100644 --- a/native_bridge/juce_fx/JuceFxEngine.h +++ b/native_bridge/juce_fx/JuceFxEngine.h @@ -28,6 +28,35 @@ public: uint32_t latencySamples() const; // Latency thật từng slot theo thứ tự chain (0 = skip/bypass/fail). std::vector entryLatencies() const; + + // ── Editor host (Phase 2 — Reaper-style): editor của CHÍNH instance DSP + // trong graph, không instance thứ 2 / không feeder. Audio thread + // process() KHÔNG bao giờ chạm editor; mọi thao tác đi qua queue và chỉ + // thực thi trong pumpEditorQueue() trên GUI thread (JUCE message thread). + enum class EditorAction { + Open, // tạo editor (createEditorIfNeeded) cho slot — giữ instance DSP + Close, // huỷ editor, GIỮ instance DSP + state (mở lại nhanh) + Show, // hiện editor (M2: host window) — chưa tác dụng khi chưa attach + Hide, // ẩn editor (M2: host window) + Resize, // đổi kích thước editor (M2: host window) + Capture // đọc state processor (getStateInformation) -> captured[slot] + }; + + // Thread-safe (gọi từ bất kỳ thread nào, kể cả ctrl-reader): xếp lệnh. + // w/h dùng cho Open/Resize (kích thước editor mong muốn). + void requestEditor(uint32_t slot, EditorAction action, int w = 0, int h = 0); + + // GUI thread (JUCE message thread): drain queue + thực thi lệnh editor. + // Trả true nếu xử lý >= 1 lệnh. Không gọi từ audio thread. + bool pumpEditorQueue(); + + // Thread-safe đọc: slot có editor đang mở không. + bool isEditorOpen(uint32_t slot) const; + + // Thread-safe: lấy + xoá state capture gần nhất của slot (rỗng nếu chưa). + // Raw bytes (chưa base64) — M3 encode khi đẩy về engine Python. + std::string takeCapturedState(uint32_t slot); + void shutdown(); private: diff --git a/native_bridge/juce_fx/editor_selftest.cpp b/native_bridge/juce_fx/editor_selftest.cpp new file mode 100644 index 0000000..fb41a25 --- /dev/null +++ b/native_bridge/juce_fx/editor_selftest.cpp @@ -0,0 +1,73 @@ +// native_bridge/juce_fx/editor_selftest.cpp +// M1 selftest (KHÔNG ship): verify editor host API trên instance DSP thật. +// juce_fx_selftest.exe "" +// Main thread = JUCE message thread (initialiseJuce_GUI). Plugin không có +// editor (createEditorIfNeeded null) → [FAIL] editor open — thử plugin khác. +#include "JuceFxEngine.h" + +#include +#include + +#include +#include + +static int failures = 0; + +static void check(bool ok, const char* what) { + std::printf("%s %s\n", ok ? "[PASS]" : "[FAIL]", what); + if (!ok) ++failures; +} + +int main(int argc, char** argv) { + if (argc < 2) { + std::fprintf(stderr, "usage: juce_fx_selftest \n"); + return 2; + } + // Escape backslash + quote cho JSON path. + std::string path(argv[1]), jpath; + jpath.reserve(path.size() + 8); + for (char c : path) { + if (c == '\\' || c == '"') jpath += '\\'; + jpath += c; + } + juce::initialiseJuce_GUI(); // main thread = message thread + + int rc = 1; + { + JuceFxEngine engine; + const std::string chain = + "[{\"type\":\"vst3\",\"path\":\"" + jpath + "\",\"bypass\":false}]"; + engine.setChain(chain); + engine.prepare(48000, 256); + + // Negative: slot ngoài chain — pump xử lý nhưng không có editor nào. + engine.requestEditor(9, JuceFxEngine::EditorAction::Open); + check(!engine.pumpEditorQueue(), "negative: slot 9 ngoài chain -> pump false"); + + // Open editor 400x300. + engine.requestEditor(0, JuceFxEngine::EditorAction::Open, 400, 300); + bool pumped = engine.pumpEditorQueue(); + // Pump message queue ngắn: editor constructor có thể tạo timer/callback. + juce::MessageManager::getInstance()->runDispatchLoopUntil(50); + check(pumped, "open: pumpEditorQueue true"); + check(engine.isEditorOpen(0), "open: isEditorOpen(0) true"); + + // Capture state — phải có dữ liệu nếu plugin đã nạp preset mặc định. + engine.requestEditor(0, JuceFxEngine::EditorAction::Capture); + check(engine.pumpEditorQueue(), "capture: pumpEditorQueue true"); + std::string cap = engine.takeCapturedState(0); + check(!cap.empty(), "capture: takeCapturedState non-empty"); + std::string cap2 = engine.takeCapturedState(0); + check(cap2.empty(), "capture: lần 2 rỗng (đã xoá)"); + + // Close — editor đóng, processor + state giữ nguyên. + engine.requestEditor(0, JuceFxEngine::EditorAction::Close); + check(engine.pumpEditorQueue(), "close: pumpEditorQueue true"); + check(!engine.isEditorOpen(0), "close: isEditorOpen(0) false"); + } // ~engine -> shutdown() (đóng editor trước graph) — trước shutdownJuce_GUI + + juce::shutdownJuce_GUI(); + rc = (failures == 0) ? 0 : 1; + std::printf(failures == 0 ? "SELFTEST OK\n" : "SELFTEST FAILED\n"); + return rc; +}