#include "PluginBridgeProcessor.h" #include "PluginBridgeEditor.h" #include "IPCProtocol.h" #include "json.hpp" using json = nlohmann::json; PluginBridgeProcessor::PluginBridgeProcessor() : AudioProcessor(BusesProperties() .withInput("Input", juce::AudioChannelSet::stereo(), true) .withOutput("Output", juce::AudioChannelSet::stereo(), true)), mcpServer(PluginBridgeConstants::kDefaultMcpPort, helper) { helper.onHelperCrashed = [this]() { pluginLoaded = false; pluginName.clear(); pluginParamCount = 0; DBG("PluginBridgeProcessor: Helper crashed — plugin unloaded"); }; mcpServer.start(); } PluginBridgeProcessor::~PluginBridgeProcessor() { mcpServer.stop(); helper.killHelper(); } void PluginBridgeProcessor::prepareToPlay(double sampleRate, int samplesPerBlock) { currentSampleRate = sampleRate; currentBlockSize = 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& buffer, juce::MidiBuffer&) { juce::ScopedNoDenormals noDenormals; auto totalNumInputChannels = getTotalNumInputChannels(); auto totalNumOutputChannels = getTotalNumOutputChannels(); for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) buffer.clear(i, 0, buffer.getNumSamples()); if (!pluginLoaded || !helper.isHelperRunning()) return; auto& shm = helper.getSharedAudio(); if (!shm.isValid()) return; int numChannels = buffer.getNumChannels(); int 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); } } juce::AudioProcessorEditor* PluginBridgeProcessor::createEditor() { return new PluginBridgeEditor(*this); } void PluginBridgeProcessor::getStateInformation(juce::MemoryBlock& destData) { juce::ValueTree state("PluginBridgeState"); state.setProperty("pluginPath", pluginPath, nullptr); if (pluginLoaded && helper.isHelperRunning()) { auto response = helper.sendCommand("{\"cmd\":\"get_state\"}"); try { auto j = json::parse(response); if (j.value("ok", false)) state.setProperty("pluginState", juce::String(j["state"].get()), 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()) { auto path = state.getProperty("pluginPath").toString(); if (path.isNotEmpty()) { 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.isRunning(); } int PluginBridgeProcessor::getMcpServerPort() const { return mcpServer.getPort(); } void PluginBridgeProcessor::loadPlugin(const juce::String& path) { if (!helper.isHelperRunning()) { if (!helper.spawnHelper()) { DBG("PluginBridgeProcessor: Failed to spawn helper"); return; } json configCmd = {{"cmd", "configure"}, {"sampleRate", currentSampleRate}, {"blockSize", currentBlockSize}, {"channels", 2}}; helper.sendCommand(configCmd.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()); pluginParamCount = j.value("params", 0); pluginPath = path; DBG("PluginBridgeProcessor: Loaded " + pluginName + " (" + juce::String(pluginParamCount) + " params)"); } else { pluginLoaded = false; DBG("PluginBridgeProcessor: Failed to load: " + juce::String(j.value("error", "unknown"))); } } catch (...) { pluginLoaded = false; } } void PluginBridgeProcessor::unloadPlugin() { if (helper.isHelperRunning()) helper.sendCommand("{\"cmd\":\"unload\"}"); pluginLoaded = false; pluginName.clear(); pluginPath.clear(); pluginParamCount = 0; } void PluginBridgeProcessor::showPluginGui() { if (helper.isHelperRunning()) helper.sendCommand("{\"cmd\":\"show_gui\"}"); } void PluginBridgeProcessor::hidePluginGui() { if (helper.isHelperRunning()) helper.sendCommand("{\"cmd\":\"hide_gui\"}"); } juce::Array PluginBridgeProcessor::getInstalledPluginFiles() const { juce::Array results; juce::Array dirs = { juce::File("/Library/Audio/Plug-Ins/VST3"), juce::File::getSpecialLocation(juce::File::userHomeDirectory) .getChildFile("Library/Audio/Plug-Ins/VST3"), juce::File("/Library/Audio/Plug-Ins/Components"), juce::File::getSpecialLocation(juce::File::userHomeDirectory) .getChildFile("Library/Audio/Plug-Ins/Components") }; for (auto& dir : dirs) { if (dir.isDirectory()) { auto vst3s = dir.findChildFiles(juce::File::findDirectories, false, "*.vst3"); auto aus = dir.findChildFiles(juce::File::findDirectories, false, "*.component"); results.addArray(vst3s); results.addArray(aus); } } results.sort(); return results; } juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { return new PluginBridgeProcessor(); }