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:
2026-09-04 09:59:29 +07:00
parent 6a74631cba
commit 0d22e8b4d3
4 changed files with 276 additions and 0 deletions
+150
View File
@@ -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;