// 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 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; }; JuceFxEngine::JuceFxEngine() : impl_(std::make_unique()) {} JuceFxEngine::~JuceFxEngine() { shutdown(); } void JuceFxEngine::shutdown() { 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" << 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; } if (cacheDirty) saveDescCache(descCache); 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; }