M1: engine editor host API (Reaper-style, editor cua CHINH instance DSP trong graph) + selftest
JuceFxEngine: requestEditor/pumpEditorQueue/isEditorOpen/takeCapturedState, queue thread-safe, thuc thi tren GUI thread; shutdown() dong editor truoc graph.clear(); prepare() map slot->processor. juce_fx_bridge link juce_gui_basics; target juce_fx_selftest (khong ship) verify open/close/capture voi VST3 that — PASS tren Focusrite fast-compressor_x64. Tien de M2 (GUI thread + host window).
This commit is contained in:
@@ -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 "<path.vst3>"
|
||||
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()
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -189,13 +190,152 @@ struct JuceFxEngine::Impl {
|
||||
std::vector<juce::AudioProcessorGraph::Node::Ptr> pluginNodes; // node theo slot (null = bỏ qua)
|
||||
std::vector<uint32_t> 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<juce::AudioProcessorEditor> 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<SlotEditor> editors; // theo slot (dựng lại mỗi prepare)
|
||||
std::vector<EditorCmd> editorQueue; // lệnh chờ (requestEditor)
|
||||
mutable std::mutex editorMutex; // editors + captured
|
||||
std::mutex queueMutex; // editorQueue
|
||||
};
|
||||
|
||||
JuceFxEngine::JuceFxEngine() : impl_(std::make_unique<Impl>()) {}
|
||||
|
||||
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<std::mutex> lk(impl_->queueMutex);
|
||||
impl_->editorQueue.push_back({slot, action, w, h});
|
||||
}
|
||||
|
||||
bool JuceFxEngine::pumpEditorQueue() {
|
||||
std::vector<Impl::EditorCmd> cmds;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(impl_->queueMutex);
|
||||
if (impl_->editorQueue.empty()) return false;
|
||||
cmds.swap(impl_->editorQueue);
|
||||
}
|
||||
bool any = false;
|
||||
for (const auto& c : cmds) {
|
||||
std::lock_guard<std::mutex> 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<const char*>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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;
|
||||
|
||||
@@ -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<uint32_t> 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:
|
||||
|
||||
@@ -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 "<path.vst3>"
|
||||
// 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 <juce_audio_processors/juce_audio_processors.h>
|
||||
#include <juce_gui_basics/juce_gui_basics.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
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 <path.vst3>\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;
|
||||
}
|
||||
Reference in New Issue
Block a user