c084ac8ba7
- D17: wrap_info/wrap-skip, fx wrap xfade, playhead-derived scheduling - D18: solo stop -> NOTE_OFF all sounding timeline pitches + CC123/CC120 - D18b: track audible gain guard, tracksRef fallback - D18c: timelinePitchesRef per-track Set, kill at applyAllTrackMuteSolo - bridge ring _MAX 16, index v=202608290928 - plugins.py/vst_engine.py/fx_realtime.py/JuceFxEngine.cpp: VSTi load + Qin fixes (uncommitted 12:18)
367 lines
14 KiB
C++
367 lines
14 KiB
C++
// 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 <juce_audio_processors/juce_audio_processors.h>
|
|
#include "sheredom_json.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <iostream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
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<uint8_t>& 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<const json_string_s*>(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<const json_number_s*>(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<const json_number_s*>(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<DescCacheEntry> loadDescCache() {
|
|
std::vector<DescCacheEntry> 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<const json_array_s*>(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<const json_object_s*>(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<DescCacheEntry>& 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<float> buf;
|
|
uint32_t sampleRate = 0;
|
|
uint32_t blockSize = 0;
|
|
bool prepared = false;
|
|
std::vector<Slot> slots; // chain đã parse (thứ tự slot)
|
|
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;
|
|
};
|
|
|
|
JuceFxEngine::JuceFxEngine() : impl_(std::make_unique<Impl>()) {}
|
|
|
|
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<const json_array_s*>(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<const json_object_s*>(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<DescCacheEntry> 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<juce::PluginDescription> 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<uint8_t> 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<juce::AudioProcessor>(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<uint32_t> JuceFxEngine::entryLatencies() const {
|
|
return impl_->slotLatencies;
|
|
}
|