| #include "PluginBridgeProcessor.h" |
| #include "PluginBridgeEditor.h" |
| #include "IPCProtocol.h" |
| #include "PBLog.h" |
| #include "InstanceRegistry.h" |
|
|
| #include <sys/wait.h> |
| #include <spawn.h> |
| #include <signal.h> |
| #include <unistd.h> |
| #include <fcntl.h> |
| #include <chrono> |
| #include <algorithm> |
|
|
| extern char** environ; |
|
|
| #include "json.hpp" |
| using json = nlohmann::json; |
|
|
| PluginBridgeProcessor::PluginBridgeProcessor() |
| : AudioProcessor(BusesProperties() |
| .withInput("Input", juce::AudioChannelSet::stereo(), true) |
| .withOutput("Output", juce::AudioChannelSet::stereo(), true)), |
| mcpServer(std::make_unique<McpServer>(helper)) |
| { |
| PBLog::startSession("Processor"); |
| PBLOG("Processor", "Constructor β PluginBridgeProcessor created"); |
|
|
| inProcessFormatManager.addFormat(new juce::VST3PluginFormat()); |
| inProcessFormatManager.addFormat(new juce::AudioUnitPluginFormat()); |
|
|
| helper.onHelperCrashed = [this]() |
| { |
| PBLOG("Processor", "β οΈ Helper crashed β clearing plugin state"); |
| if (pluginPath.isNotEmpty()) |
| { |
| PBLOG("Processor", "β οΈ Marking \"" + pluginName + "\" as UNSAFE (Helper crash)"); |
| safetyDB.setSafety(pluginPath, PluginSafetyDB::Safety::Unsafe); |
| } |
| pluginLoaded = false; |
| pluginName.clear(); |
| pluginParamCount = 0; |
| unloadInProcessPlugin(); |
| }; |
|
|
| helper.onParamChanged = [this](int idx, float val) |
| { |
| if (!inProcessPlugin) return; |
| auto& params = inProcessPlugin->getParameters(); |
| if (idx < 0 || idx >= params.size()) return; |
| suppressParamSync.store(true); |
| juce::MessageManager::callAsync([this, idx, val]() |
| { |
| if (!inProcessPlugin) return; |
| auto& p = inProcessPlugin->getParameters(); |
| if (idx < p.size()) |
| p[idx]->setValueNotifyingHost(juce::jlimit(0.0f, 1.0f, val)); |
| suppressParamSync.store(false); |
| }); |
| }; |
|
|
| mcpServer->getAnalysisCallback = [this]() { return analyser.getCompactAnalysis(); }; |
| mcpServer->channelNameCallback = [this]() { return channelName.toStdString(); }; |
|
|
| mcpServer->start(); |
|
|
| InstanceRegistry::registerInstance(channelName.toStdString(), mcpServer->getPort()); |
| PBLOG("Processor", "MCP server on port " + juce::String(mcpServer->getPort()) |
| + " β channel: " + channelName); |
|
|
| startBackgroundScan(); |
| } |
|
|
| PluginBridgeProcessor::~PluginBridgeProcessor() |
| { |
| PBLOG("Processor", "Destructor β tearing down"); |
| stopBackgroundScan(); |
| InstanceRegistry::unregisterInstance(channelName.toStdString()); |
| mcpServer->stop(); |
| unloadInProcessPlugin(); |
| helper.killHelper(); |
| PBLOG("Processor", "Destructor β done"); |
| } |
|
|
| void PluginBridgeProcessor::setChannelName(const juce::String& name) |
| { |
| if (name == channelName) return; |
| InstanceRegistry::unregisterInstance(channelName.toStdString()); |
| channelName = name; |
| InstanceRegistry::registerInstance(channelName.toStdString(), mcpServer->getPort()); |
| PBLOG("Processor", "Channel renamed to: " + channelName); |
| } |
|
|
| void PluginBridgeProcessor::prepareToPlay(double sampleRate, int samplesPerBlock) |
| { |
| currentSampleRate = sampleRate; |
| currentBlockSize = samplesPerBlock; |
| analyser.prepareToPlay(sampleRate, samplesPerBlock); |
|
|
| if (inProcessPlugin) |
| { |
| inProcessPlugin->releaseResources(); |
| inProcessPlugin->prepareToPlay(sampleRate, samplesPerBlock); |
| } |
|
|
| if (helper.isHelperRunning()) |
| { |
| json cmd = {{"cmd","configure"},{"sampleRate",sampleRate},{"blockSize",samplesPerBlock},{"channels",2}}; |
| helper.sendCommand(cmd.dump()); |
| auto* header = helper.getSharedAudio().getHeader(); |
| if (header) { header->sampleRate.store((uint32_t)sampleRate); header->blockSize.store((uint32_t)samplesPerBlock); header->numChannels.store(2); } |
| } |
| } |
|
|
| void PluginBridgeProcessor::releaseResources() {} |
|
|
| void PluginBridgeProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) |
| { |
| juce::ScopedNoDenormals noDenormals; |
| for (auto i = getTotalNumInputChannels(); i < getTotalNumOutputChannels(); ++i) |
| buffer.clear(i, 0, buffer.getNumSamples()); |
| if (!pluginLoaded) return; |
|
|
| if (inProcessPlugin != nullptr) |
| { |
| inProcessPlugin->processBlock(buffer, midiMessages); |
| analyser.processBlock(buffer); |
| return; |
| } |
|
|
| if (!helper.isHelperRunning()) return; |
| auto& shm = helper.getSharedAudio(); |
| if (!shm.isValid()) return; |
| int numChannels = buffer.getNumChannels(), numSamples = buffer.getNumSamples(); |
| const float* inputPtrs[2] = { buffer.getReadPointer(0), numChannels > 1 ? buffer.getReadPointer(1) : buffer.getReadPointer(0) }; |
| shm.writeInput(inputPtrs, numChannels, numSamples); |
| auto* header = shm.getHeader(); |
| header->blockSize.store((uint32_t)numSamples); header->numChannels.store((uint32_t)numChannels); header->state.store(kStateInputReady); |
| helper.getInputSem().signal(); |
| if (helper.getOutputSem().wait(IPC::kProcessBlockTimeoutMs)) |
| { |
| float* outputPtrs[2] = { buffer.getWritePointer(0), numChannels > 1 ? buffer.getWritePointer(1) : buffer.getWritePointer(0) }; |
| shm.readOutput(outputPtrs, numChannels, numSamples); |
| header->state.store(kStateIdle); |
| analyser.processBlock(buffer); |
| } |
| } |
|
|
| juce::AudioProcessorEditor* PluginBridgeProcessor::createEditor() { return new PluginBridgeEditor(*this); } |
|
|
| |
|
|
| void PluginBridgeProcessor::loadInProcessPlugin(const juce::String& path) |
| { |
| PBLOG("Processor", "loadInProcessPlugin: " + juce::File(path).getFileName()); |
| unloadInProcessPlugin(); |
|
|
| juce::File pluginFile(path); |
| if (!pluginFile.exists()) { PBLOG("Processor", "loadInProcessPlugin: file not found"); return; } |
|
|
| juce::OwnedArray<juce::PluginDescription> typesFound; |
| for (int i = 0; i < inProcessFormatManager.getNumFormats(); ++i) |
| { |
| auto* format = inProcessFormatManager.getFormat(i); |
| bool isVST3 = pluginFile.getFileExtension() == ".vst3"; |
| bool isAU = pluginFile.getFileExtension() == ".component"; |
| if (isVST3 && format->getName() != "VST3") continue; |
| if (isAU && format->getName() != "AudioUnit") continue; |
| format->findAllTypesForFile(typesFound, pluginFile.getFullPathName()); |
| if (typesFound.size() > 0) break; |
| } |
| if (typesFound.isEmpty()) { PBLOG("Processor", "loadInProcessPlugin: no types found"); return; } |
|
|
| juce::String err; |
| inProcessPlugin = inProcessFormatManager.createPluginInstance(*typesFound[0], currentSampleRate, currentBlockSize, err); |
| if (!inProcessPlugin) { PBLOG("Processor", "loadInProcessPlugin: FAILED β " + err); return; } |
|
|
| inProcessPlugin->enableAllBuses(); |
| auto layout = juce::AudioProcessor::BusesLayout(); |
| layout.inputBuses.add(juce::AudioChannelSet::stereo()); |
| layout.outputBuses.add(juce::AudioChannelSet::stereo()); |
| if (inProcessPlugin->checkBusesLayoutSupported(layout)) |
| inProcessPlugin->setBusesLayout(layout); |
| if (currentSampleRate > 0 && currentBlockSize > 0) |
| inProcessPlugin->prepareToPlay(currentSampleRate, currentBlockSize); |
|
|
| |
| auto response = helper.sendCommand("{\"cmd\":\"get_state\"}"); |
| try { auto j = json::parse(response); if (j.value("ok",false)) { |
| auto stateB64 = juce::String(j["state"].get<std::string>()); |
| if (stateB64.isNotEmpty()) { juce::MemoryBlock d; if (d.fromBase64Encoding(stateB64)) |
| inProcessPlugin->setStateInformation(d.getData(), (int)d.getSize()); } |
| }} catch (...) {} |
|
|
| inProcessPlugin->addListener(this); |
|
|
| |
| mcpServer->searchParamCallback = [this](const std::string& keyword) -> std::string { |
| if (!inProcessPlugin) return "[]"; |
| auto& params = inProcessPlugin->getParameters(); |
| json results = json::array(); |
| std::string kw = keyword; |
| std::transform(kw.begin(), kw.end(), kw.begin(), ::tolower); |
| for (int i = 0; i < params.size(); ++i) |
| { |
| std::string name = params[i]->getName(128).toStdString(); |
| std::string nameLow = name; |
| std::transform(nameLow.begin(), nameLow.end(), nameLow.begin(), ::tolower); |
| if (kw.empty() || nameLow.find(kw) != std::string::npos) |
| { |
| results.push_back({{"i", i}, {"name", name}, |
| {"value", std::round(params[i]->getValue() * 1000.0f) / 1000.0f}, |
| {"display", params[i]->getText(params[i]->getValue(), 64).toStdString()}}); |
| if (results.size() >= 50) break; |
| } |
| } |
| return results.dump(); |
| }; |
|
|
| mcpServer->getParamsCallback = [this](const std::vector<int>& ids) -> std::string { |
| if (!inProcessPlugin) return "{}"; |
| auto& params = inProcessPlugin->getParameters(); |
| json values = json::object(); |
| for (int idx : ids) |
| if (idx >= 0 && idx < params.size()) |
| values[std::to_string(idx)] = std::round(params[idx]->getValue() * 1000.0f) / 1000.0f; |
| return values.dump(); |
| }; |
|
|
| mcpServer->setParamsCallback = [this](const std::map<int,float>& values) -> bool { |
| if (!inProcessPlugin) return false; |
| juce::MessageManager::callAsync([this, values]() { |
| if (!inProcessPlugin) return; |
| auto& p = inProcessPlugin->getParameters(); |
| for (auto& [idx, val] : values) |
| if (idx >= 0 && idx < p.size()) |
| p[idx]->setValueNotifyingHost(juce::jlimit(0.0f, 1.0f, val)); |
| }); |
| return true; |
| }; |
|
|
| PBLOG("Processor", "loadInProcessPlugin: SUCCESS β \"" + inProcessPlugin->getName() + "\" + MCP param callbacks wired"); |
| } |
|
|
| void PluginBridgeProcessor::unloadInProcessPlugin() |
| { |
| |
| mcpServer->searchParamCallback = nullptr; |
| mcpServer->getParamsCallback = nullptr; |
| mcpServer->setParamsCallback = nullptr; |
|
|
| if (inProcessPlugin) |
| { |
| PBLOG("Processor", "unloadInProcessPlugin: " + inProcessPlugin->getName()); |
| inProcessPlugin->removeListener(this); |
| inProcessPlugin->releaseResources(); |
| inProcessPlugin.reset(); |
| } |
| } |
|
|
| void PluginBridgeProcessor::audioProcessorParameterChanged(juce::AudioProcessor*, int paramIdx, float value) |
| { |
| if (suppressParamSync.load()) return; |
| if (!helper.isHelperRunning()) return; |
| auto cmdStr = json({{"cmd","set_params"},{"values",{{std::to_string(paramIdx), value}}}}).dump(); |
| std::thread([this, cmdStr]() { helper.sendCommand(cmdStr); }).detach(); |
| } |
|
|
| |
|
|
| void PluginBridgeProcessor::loadPlugin(const juce::String& path) |
| { |
| PBLOG("Processor", "loadPlugin: " + juce::File(path).getFileName()); |
| if (blocklist.isBlocked(path)) { if (onLoadFailed) onLoadFailed("Not compatible (crashed previously)"); return; } |
|
|
| auto safety = safetyDB.getSafety(path); |
| PBLOG("Processor", "loadPlugin: safety = " + juce::String(safety == PluginSafetyDB::Safety::Safe ? "SAFE" : safety == PluginSafetyDB::Safety::Unsafe ? "UNSAFE" : "UNKNOWN")); |
|
|
| if (safety == PluginSafetyDB::Safety::Unknown) |
| { |
| if (onLoadFailed) onLoadFailed("Scanning plugin..."); |
| std::thread([this, path]() { |
| bool safe = runPluginScan(path); |
| safetyDB.setSafety(path, safe ? PluginSafetyDB::Safety::Safe : PluginSafetyDB::Safety::Unsafe); |
| juce::MessageManager::callAsync([this, path]() { loadPlugin(path); }); |
| }).detach(); |
| return; |
| } |
|
|
| if (onPluginWillLoad) onPluginWillLoad(); |
|
|
| if (!helper.isHelperRunning()) { |
| if (!helper.spawnHelper()) { if (onLoadFailed) onLoadFailed("Failed to start helper"); return; } |
| json cfg = {{"cmd","configure"},{"sampleRate",currentSampleRate},{"blockSize",currentBlockSize},{"channels",2}}; |
| helper.sendCommand(cfg.dump()); |
| } |
|
|
| json loadCmd = {{"cmd","load"},{"path",path.toStdString()},{"sampleRate",currentSampleRate},{"blockSize",currentBlockSize}}; |
| auto response = helper.sendCommand(loadCmd.dump()); |
|
|
| try { |
| auto j = json::parse(response); |
| if (j.value("ok",false)) { |
| pluginLoaded = true; |
| pluginName = juce::String(j["name"].get<std::string>()); |
| pluginParamCount = j.value("params",0); |
| pluginPath = path; |
| PBLOG("Processor", "loadPlugin: loaded \"" + pluginName + "\""); |
| if (safety == PluginSafetyDB::Safety::Safe) |
| juce::MessageManager::callAsync([this, path]() { loadInProcessPlugin(path); if (onPluginLoaded) onPluginLoaded(); }); |
| else |
| juce::MessageManager::callAsync([this]() { if (onPluginLoaded) onPluginLoaded(); }); |
| } else { |
| pluginLoaded = false; pluginPath.clear(); blocklist.addBlocked(path); |
| if (onLoadFailed) onLoadFailed("Not compatible: " + juce::String(j.value("error","unknown"))); |
| } |
| } catch (...) { |
| pluginLoaded = false; pluginPath.clear(); blocklist.addBlocked(path); |
| if (onLoadFailed) onLoadFailed("Helper communication error"); |
| } |
| } |
|
|
| void PluginBridgeProcessor::unloadPlugin() |
| { |
| PBLOG("Processor", "unloadPlugin: \"" + pluginName + "\""); |
| if (helper.isHelperRunning()) helper.sendCommand("{\"cmd\":\"unload\"}"); |
| pluginLoaded = false; pluginName.clear(); pluginPath.clear(); pluginParamCount = 0; |
| unloadInProcessPlugin(); |
| if (onPluginUnloaded) juce::MessageManager::callAsync([this]() { if (onPluginUnloaded) onPluginUnloaded(); }); |
| } |
|
|
| |
|
|
| static juce::String signalName(int sig) { |
| switch (sig) { case SIGSEGV: return "SIGSEGV"; case SIGABRT: return "SIGABRT"; case SIGBUS: return "SIGBUS"; case SIGILL: return "SIGILL"; case SIGFPE: return "SIGFPE"; case SIGKILL: return "SIGKILL"; default: return "signal " + juce::String(sig); } |
| } |
|
|
| static void writeScanLog(const juce::String& pluginPath, bool safe, const juce::String& reason, const juce::String& captured) { |
| auto logFile = juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory).getChildFile("PluginBridge").getChildFile("scan.log"); |
| logFile.getParentDirectory().createDirectory(); |
| auto ts = juce::Time::getCurrentTime().formatted("%Y-%m-%d %H:%M:%S"); |
| juce::String entry; entry << "βββ\n" << ts << " " << juce::File(pluginPath).getFileName() << "\nResult: " << (safe ? "SAFE" : "UNSAFE"); |
| if (!reason.isEmpty()) entry << " (" << reason << ")"; |
| if (captured.isNotEmpty()) entry << "\nOutput: " << captured.substring(0, 500); |
| entry << "\n\n"; logFile.appendText(entry); |
| } |
|
|
| bool PluginBridgeProcessor::runPluginScan(const juce::String& path) { |
| auto helperApp = juce::File::getSpecialLocation(juce::File::currentExecutableFile).getParentDirectory().getParentDirectory().getChildFile("Resources").getChildFile("PluginBridgeHelper.app").getChildFile("Contents").getChildFile("MacOS").getChildFile(PluginBridgeConstants::kHelperBinaryName); |
| if (!helperApp.exists()) helperApp = juce::File::getSpecialLocation(juce::File::currentExecutableFile).getParentDirectory().getParentDirectory().getChildFile("Resources").getChildFile(PluginBridgeConstants::kHelperBinaryName); |
| if (!helperApp.exists()) { writeScanLog(path, false, "Helper not found", {}); return false; } |
| int pipeFd[2]; if (pipe(pipeFd) != 0) return false; fcntl(pipeFd[0], F_SETFL, O_NONBLOCK); |
| auto hStr = helperApp.getFullPathName().toStdString(); auto pStr = path.toStdString(); |
| posix_spawn_file_actions_t actions; posix_spawn_file_actions_init(&actions); |
| posix_spawn_file_actions_adddup2(&actions, pipeFd[1], STDERR_FILENO); posix_spawn_file_actions_addclose(&actions, pipeFd[0]); |
| pid_t pid; char* argv[] = { (char*)hStr.c_str(), (char*)"--scan", (char*)pStr.c_str(), nullptr }; |
| int sr = posix_spawn(&pid, hStr.c_str(), &actions, nullptr, argv, environ); |
| posix_spawn_file_actions_destroy(&actions); close(pipeFd[1]); |
| if (sr != 0) { close(pipeFd[0]); return false; } |
| bool timedOut = true; int status = 0; |
| auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); |
| while (std::chrono::steady_clock::now() < deadline) { if (waitpid(pid, &status, WNOHANG) == pid) { timedOut = false; break; } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } |
| if (timedOut) { kill(pid, SIGKILL); waitpid(pid, &status, 0); } |
| juce::String captured; { char buf[4096]; ssize_t n; while ((n = read(pipeFd[0], buf, sizeof(buf)-1)) > 0) { buf[n]=0; captured += buf; } } close(pipeFd[0]); |
| bool safe = !timedOut && WIFEXITED(status) && WEXITSTATUS(status) == 0; |
| juce::String reason = timedOut ? "Timeout" : WIFSIGNALED(status) ? signalName(WTERMSIG(status)) : (!safe ? "Exit " + juce::String(WEXITSTATUS(status)) : ""); |
| writeScanLog(path, safe, reason, captured); return safe; |
| } |
|
|
| void PluginBridgeProcessor::startBackgroundScan() { |
| if (scanRunning.load()) return; scanRunning.store(true); |
| scanThread = std::thread([this]() { |
| auto files = getInstalledPluginFiles(); |
| for (int i = 0; i < files.size() && scanRunning.load(); ++i) { |
| auto path = files[i].getFullPathName(); |
| if (safetyDB.getSafety(path) != PluginSafetyDB::Safety::Unknown) continue; |
| while (helper.isHelperRunning() && scanRunning.load()) std::this_thread::sleep_for(std::chrono::milliseconds(500)); |
| if (!scanRunning.load()) break; |
| bool safe = runPluginScan(path); |
| safetyDB.setSafety(path, safe ? PluginSafetyDB::Safety::Safe : PluginSafetyDB::Safety::Unsafe); |
| if (onScanProgress) juce::MessageManager::callAsync([this, path, safe]() { if (onScanProgress) onScanProgress(path, safe); }); |
| } |
| scanRunning.store(false); |
| }); |
| } |
|
|
| void PluginBridgeProcessor::stopBackgroundScan() { scanRunning.store(false); if (scanThread.joinable()) scanThread.join(); } |
|
|
| |
|
|
| juce::Array<juce::File> PluginBridgeProcessor::getInstalledPluginFiles() const { |
| juce::Array<juce::File> results; std::set<std::string> seen; |
| struct D { juce::File dir; const char* pat; }; |
| std::vector<D> specs = { { juce::File("/Library/Audio/Plug-Ins/VST3"), "*.vst3" }, { juce::File::getSpecialLocation(juce::File::userHomeDirectory).getChildFile("Library/Audio/Plug-Ins/VST3"), "*.vst3" }, { juce::File("/Library/Audio/Plug-Ins/Components"), "*.component" }, { juce::File::getSpecialLocation(juce::File::userHomeDirectory).getChildFile("Library/Audio/Plug-Ins/Components"), "*.component" } }; |
| for (auto& s : specs) { if (!s.dir.isDirectory()) continue; for (auto& f : s.dir.findChildFiles(juce::File::findDirectories, false, s.pat)) if (seen.insert(f.getFileNameWithoutExtension().toStdString()).second) results.add(f); } |
| results.sort(); return results; |
| } |
|
|
| bool PluginBridgeProcessor::isBlocked(const juce::String& path) const { return blocklist.isBlocked(path); } |
|
|
| |
|
|
| static juce::String cleanManufacturer(const juce::String& raw) { |
| auto s = raw.trim(); if (s.isEmpty() || s.containsOnly("0123456789.")) return {}; |
| if (s.toLowerCase().startsWith("copyright")) { auto r = s.substring(9).trim(); if (r.startsWithIgnoreCase("(c)")) r = r.substring(3).trim(); else if (r.startsWith(juce::String::fromUTF8("\xc2\xa9"))) r = r.substring(juce::String::fromUTF8("\xc2\xa9").length()).trim(); auto sp = r.indexOfChar(' '); if (sp > 0) r = r.substring(sp+1).trim(); return r.isNotEmpty() ? r : juce::String(); } |
| return s; |
| } |
| static juce::String plistValue(const juce::String& t, const juce::String& k) { auto p = t.fromFirstOccurrenceOf(k, false, true); if (p.isEmpty()) return {}; return p.fromFirstOccurrenceOf("<string>", false, false).upToFirstOccurrenceOf("</string>", false, false).trim(); } |
|
|
| juce::String PluginBridgeProcessor::getPluginManufacturer(const juce::File& f) const { |
| auto pk = f.getFullPathName().toStdString(); |
| { std::lock_guard<std::mutex> l(manufacturerCacheMutex); auto it = manufacturerCache.find(pk); if (it != manufacturerCache.end()) return juce::String(it->second); } |
| juce::String mfr; |
| if (f.getFileExtension().toLowerCase() == ".vst3") { |
| auto ip = f.getChildFile("Contents").getChildFile("Info.plist"); |
| if (ip.existsAsFile()) { auto t = ip.loadFileAsString(); mfr = cleanManufacturer(plistValue(t, "NSHumanReadableCopyright")); |
| if (mfr.isEmpty()) { auto is = plistValue(t, "CFBundleGetInfoString"); if (is.contains(",")) { auto bc = is.upToFirstOccurrenceOf(",",false,false).trim(); auto fw = bc.upToFirstOccurrenceOf(" ",false,false).trim(); if (fw.isNotEmpty() && !fw.containsOnly("0123456789.")) mfr = fw; } if (mfr.isEmpty()) mfr = cleanManufacturer(is); } |
| if (mfr.isEmpty()) { auto bid = plistValue(t, "CFBundleIdentifier"); auto segs = juce::StringArray::fromTokens(bid,".",""); if (segs.size()>=2) { auto sl = segs[1].trim(); if (sl.isNotEmpty()&&sl!="apple"&&sl!="com") mfr = sl.substring(0,1).toUpperCase()+sl.substring(1); } } } |
| if (mfr.isEmpty()) { auto mi = f.getChildFile("Contents").getChildFile("moduleinfo.json"); if (mi.existsAsFile()) { try { auto j = json::parse(mi.loadFileAsString().toStdString()); mfr = cleanManufacturer(juce::String(j.value("vendor",""))); } catch(...){} } } |
| } |
| if (mfr.isEmpty()) { auto n = f.getFileNameWithoutExtension(); auto fw = n.upToFirstOccurrenceOf(" ",false,false); mfr = (fw.isNotEmpty()&&!fw.containsOnly("0123456789.")) ? fw : n; } |
| if (mfr.isEmpty()) mfr = "Other"; |
| std::lock_guard<std::mutex> l(manufacturerCacheMutex); manufacturerCache[pk] = mfr.toStdString(); return mfr; |
| } |
|
|
| |
|
|
| void PluginBridgeProcessor::getStateInformation(juce::MemoryBlock& destData) { |
| juce::ValueTree state("PluginBridgeState"); state.setProperty("pluginPath", pluginPath, nullptr); state.setProperty("channelName", channelName, nullptr); |
| if (pluginLoaded && helper.isHelperRunning()) { auto r = helper.sendCommand("{\"cmd\":\"get_state\"}"); try { auto j = json::parse(r); if (j.value("ok",false)) state.setProperty("pluginState", juce::String(j["state"].get<std::string>()), nullptr); } catch (...) {} } |
| juce::MemoryOutputStream stream(destData, false); state.writeToStream(stream); |
| } |
|
|
| void PluginBridgeProcessor::setStateInformation(const void* data, int sizeInBytes) { |
| auto state = juce::ValueTree::readFromData(data, (size_t)sizeInBytes); if (!state.isValid()) return; |
| auto savedChannel = state.getProperty("channelName", "Unnamed").toString(); setChannelName(savedChannel); |
| auto path = state.getProperty("pluginPath").toString(); if (path.isEmpty()) return; |
| loadPlugin(path); |
| auto pluginState = state.getProperty("pluginState").toString(); |
| if (pluginState.isNotEmpty() && helper.isHelperRunning()) { json cmd = {{"cmd","set_state"},{"state",pluginState.toStdString()}}; helper.sendCommand(cmd.dump()); } |
| } |
|
|
| |
|
|
| bool PluginBridgeProcessor::isMcpServerRunning() const { return mcpServer && mcpServer->isRunning(); } |
| int PluginBridgeProcessor::getMcpServerPort() const { return mcpServer ? mcpServer->getPort() : 0; } |
| juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { return new PluginBridgeProcessor(); } |
|
|