// 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 #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; } int64_t jInt64(const json_object_s* o, const char* key, int64_t def) { const json_object_element_s* m = jMember(o, key); if (m && m->value && m->value->type == json_type_number) { const auto* n = static_cast(m->value->payload); return (int64_t)std::strtoll(n->number, nullptr, 10); } return def; } uint32_t jUInt32(const json_object_s* o, const char* key, uint32_t def) { const json_object_element_s* m = jMember(o, key); if (m && m->value && m->value->type == json_type_number) { const auto* n = static_cast(m->value->payload); return (uint32_t)std::strtoul(n->number, nullptr, 10); } return def; } std::string jsonEscape(const std::string& s) { std::string out; out.reserve(s.size()); for (char c : s) { if (c == '\\' || c == '"') { out += '\\'; out += c; } else out += c; } return out; } // --- VST3 description cache ------------------------------------------------- // findAllTypesForFile() slow path load DLL + quét factory + instantiate // component (hàng trăm ms/plugin mỗi session). createInstanceFromDescription // chỉ cần {fileOrIdentifier, name, uniqueId, deprecatedUid} — // findClassMatchingDescription so name+uid trên class info, KHÔNG instantiate. // Cache JSON theo path+mtime: hit → dựng desc trực tiếp, bỏ qua scan. struct DescCacheEntry { std::string path; int64_t mtime = 0; std::string name; uint32_t uniqueId = 0; uint32_t deprecatedUid = 0; }; std::string descCachePath() { return juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory) .getChildFile("SonicForgeDAW") .getChildFile("juce_vst3_desc_cache.json") .getFullPathName() .toStdString(); } std::vector loadDescCache() { std::vector out; const juce::File f(descCachePath()); if (!f.existsAsFile()) return out; const juce::String content = f.loadFileAsString(); json_value_s* root = json_parse(content.toRawUTF8(), content.getNumBytesAsUTF8()); 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); DescCacheEntry e; e.path = jString(o, "path", ""); e.mtime = jInt64(o, "mtime", 0); e.name = jString(o, "name", ""); e.uniqueId = jUInt32(o, "unique_id", 0); e.deprecatedUid = jUInt32(o, "deprecated_uid", 0); if (!e.path.empty() && !e.name.empty()) out.push_back(std::move(e)); } } if (root) free(root); return out; } void saveDescCache(const std::vector& cache) { std::string json = "[\n"; for (size_t i = 0; i < cache.size(); ++i) { const auto& e = cache[i]; if (i) json += ",\n"; json += " {\"path\":\"" + jsonEscape(e.path) + "\",\"mtime\":" + std::to_string(e.mtime) + ",\"name\":\"" + jsonEscape(e.name) + "\",\"unique_id\":" + std::to_string(e.uniqueId) + ",\"deprecated_uid\":" + std::to_string(e.deprecatedUid) + "}"; } json += "\n]\n"; try { const juce::File f(descCachePath()); f.getParentDirectory().createDirectory(); f.replaceWithText(juce::String(json)); } catch (...) { // Cache lỗi không được làm hỏng prepare() — lần sau scan lại. } } } // 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; // ── 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; bool visible = true; // M2: trạng thái Show/Hide (GuiHost sync window) 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 // M14: retry Open khi editor chưa mở (lần Open trước fail/trễ). struct RetryOpen { uint32_t slot = 0; int w = 0, h = 0; uint64_t dueMs = 0; int tries = 0; }; std::vector retryOpens; // M14 GUI-thread-only }; 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; { // M14: retry drain cần chạy kể cả khi queue rỗng (retry pending) // — bỏ early-return, chỉ đổi queue nếu có lệnh. std::lock_guard lk(impl_->queueMutex); if (!impl_->editorQueue.empty()) cmds.swap(impl_->editorQueue); } bool any = false; // M14: đồng hồ + retry-open helpers (GUI-thread-only). const auto nowMs = []() -> uint64_t { return static_cast(std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()).count()); }; const auto retryClearSlot = [this](uint32_t slot) { auto& v = impl_->retryOpens; for (auto it = v.begin(); it != v.end(); ++it) { if (it->slot == slot) { v.erase(it); return; } } }; const auto retrySchedule = [this, &nowMs](uint32_t slot, int w, int h) { for (const auto& r : impl_->retryOpens) if (r.slot == slot) return; // không xếp chồng impl_->retryOpens.push_back({slot, w, h, nowMs() + 1000, 0}); }; 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; retrySchedule(c.slot, c.w, c.h); // M14: thử lại sau break; } se.editor.reset(ed); // engine sở hữu editor (M1) se.open = true; se.visible = true; // M2: editor mở = hiển thị (GuiHost mở window) 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; retryClearSlot(c.slot); // M14: mở thành công = hết retry } else { // Đã mở — Show lại (Reaper: editor còn sống, mở window lại). se.visible = true; se.editor->setVisible(true); if (c.w > 0 && c.h > 0) se.editor->setSize(c.w, c.h); retryClearSlot(c.slot); // M14 std::cerr << "[JuceFxEngine] slot " << c.slot << " editor ALREADY open — re-show" << std::endl; } 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; } retryClearSlot(c.slot); // M14: đóng = huỷ retry mở dang dở break; } case EditorAction::Show: case EditorAction::Hide: { // M2: GuiHost đồng bộ window theo flag visible (editor setVisible // cho nội dung). Show = hiện window, Hide = ẩn (giữ editor+instance). const bool vis = (c.action == EditorAction::Show); se.visible = vis; if (se.editor) se.editor->setVisible(vis); 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; } } } // M14: Drain retry-open — editor chưa mở do lần Open trước fail/trễ. { auto& rv = impl_->retryOpens; for (auto it = rv.begin(); it != rv.end();) { bool gone; { std::lock_guard lk(impl_->editorMutex); gone = (it->slot >= impl_->editors.size()) || !impl_->editors[it->slot].proc || impl_->editors[it->slot].open; } if (gone || it->tries >= 2) { it = rv.erase(it); continue; } if (nowMs() >= it->dueMs) { std::cerr << "[JuceFxEngine] M14 retry open slot " << it->slot << " lần " << (it->tries + 1) << std::endl; { std::lock_guard lk(impl_->queueMutex); impl_->editorQueue.push_back({it->slot, EditorAction::Open, it->w, it->h}); } it->tries++; it->dueMs = nowMs() + 1000; } ++it; } } 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; } bool JuceFxEngine::guiBusy() const { { std::lock_guard lk(impl_->queueMutex); if (!impl_->editorQueue.empty()) return true; } { std::lock_guard lk(impl_->editorMutex); for (const auto& se : impl_->editors) if (se.open) return true; } return !impl_->retryOpens.empty(); // GUI-thread-only — chỉ GUI thread gọi } 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::editorHandle(uint32_t slot) { std::lock_guard lk(impl_->editorMutex); if (slot >= impl_->editors.size()) return nullptr; return impl_->editors[slot].editor.get(); } uint32_t JuceFxEngine::slotCount() const { std::lock_guard lk(impl_->editorMutex); return (uint32_t)impl_->editors.size(); } bool JuceFxEngine::hasOpenEditor() const { std::lock_guard lk(impl_->editorMutex); for (const auto& se : impl_->editors) if (se.open) return true; return false; } void JuceFxEngine::closeAllEditors() { // Gom slot đang mở (editorMutex) rồi enqueue Close (queueMutex) — gọi từ // main thread trước khi prepare()/shutdown() khi GUI thread đang chạy. std::vector open; { std::lock_guard lk(impl_->editorMutex); for (size_t i = 0; i < impl_->editors.size(); ++i) if (impl_->editors[i].open) open.push_back((uint32_t)i); } { std::lock_guard lk(impl_->queueMutex); for (uint32_t s : open) impl_->editorQueue.push_back({s, EditorAction::Close, 0, 0}); } } bool JuceFxEngine::isEditorVisible(uint32_t slot) const { std::lock_guard lk(impl_->editorMutex); if (slot >= impl_->editors.size()) return false; return impl_->editors[slot].open && impl_->editors[slot].visible; } 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.visible = false; se.captured.clear(); } impl_->editors.clear(); } impl_->retryOpens.clear(); // M14 if (impl_->prepared) { impl_->graph.releaseResources(); impl_->graph.clear(); impl_->prepared = false; 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) { if (impl_->prepared && impl_->sampleRate == sampleRate && impl_->blockSize == blockSize) return; // không đổi — giữ graph shutdown(); impl_->sampleRate = sampleRate; impl_->blockSize = blockSize; impl_->graph.setPlayConfigDetails(2, 2, (double)sampleRate, (int)blockSize); auto& g = impl_->graph; const auto inNode = g.addNode(std::make_unique< juce::AudioProcessorGraph::AudioGraphIOProcessor>( juce::AudioProcessorGraph::AudioGraphIOProcessor::audioInputNode)); const auto outNode = g.addNode(std::make_unique< juce::AudioProcessorGraph::AudioGraphIOProcessor>( 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); static std::vector descCache = loadDescCache(); bool cacheDirty = false; 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; const juce::File pluginFile(s.path); const int64_t mtime = pluginFile.getLastModificationTime().toMilliseconds(); const DescCacheEntry* hit = nullptr; for (const auto& e : descCache) if (e.path == s.path && e.mtime == mtime) { hit = &e; break; } if (hit) { // Cache hit: dựng desc (name+uid) — không scan factory, không // instantiate 2 lần như findAllTypesForFile slow path. auto* d = new juce::PluginDescription(); d->fileOrIdentifier = juce::String(hit->path); d->name = juce::String(hit->name); d->uniqueId = hit->uniqueId; d->deprecatedUid = hit->deprecatedUid; d->pluginFormatName = "VST3"; descs.add(d); } else { fmt.findAllTypesForFile(descs, pluginFile.getFullPathName()); if (!descs.isEmpty()) { const auto* d = descs.getFirst(); DescCacheEntry e; e.path = s.path; e.mtime = mtime; e.name = d->name.toStdString(); e.uniqueId = d->uniqueId; e.deprecatedUid = d->deprecatedUid; descCache.push_back(std::move(e)); cacheDirty = true; } } 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 (vstpreset)" << std::endl; else { // M13: preset_b64 tu editor capture (getStateInformation) la // JUCE VST3 state raw (4B len + "VstW" + payload), KHONG // phai file .vstpreset -> setStateFromVSTPresetFile luon FAIL // -> plugin mo default moi lan spawn (preset da luu mat). // Ap thang setStateInformation (doi xung capture - cung wrapper). inst->setStateInformation(mb.getData(), (int)mb.getSize()); std::cerr << "[JuceFxEngine] slot " << i << " preset applied (state)" << 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; } 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. // Lock editorMutex: GUI thread (GuiHost tick reconcile) có thể đang đọc. { std::lock_guard lk(impl_->editorMutex); 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; 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; } void JuceFxEngine::process(float* inL, float* inR, uint32_t n) { if (!impl_->prepared || n == 0) return; auto& b = impl_->buf; b.setSize(2, (int)n, false, false, true); b.copyFrom(0, 0, inL, (int)n); b.copyFrom(1, 0, inR, (int)n); juce::MidiBuffer midi; impl_->graph.processBlock(b, midi); // 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); std::copy(outR, outR + n, inR); } uint32_t JuceFxEngine::latencySamples() const { return impl_->prepared ? impl_->totalLatency : 0u; } std::vector JuceFxEngine::entryLatencies() const { return impl_->slotLatencies; }