// 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 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; } } // 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); 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; fmt.findAllTypesForFile(descs, juce::File(s.path).getFullPathName()); 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; } 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; }