#include "HelperPluginHost.h" #include "Constants.h" #include #include #include #if JUCE_MAC #include #include #endif // Force helper process to become a proper GUI app on macOS static void makeGuiProcess() { #if JUCE_MAC // [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular] id nsApp = ((id(*)(id, SEL))objc_msgSend)((id)objc_getClass("NSApplication"), sel_registerName("sharedApplication")); ((void(*)(id, SEL, long))objc_msgSend)(nsApp, sel_registerName("setActivationPolicy:"), 0); // 0 = Regular ((void(*)(id, SEL, int))objc_msgSend)(nsApp, sel_registerName("activateIgnoringOtherApps:"), 1); #endif } // Custom window with working close button class PluginGuiWindow : public juce::DocumentWindow { public: PluginGuiWindow(const juce::String& name) : DocumentWindow(name, juce::Colour(0xff1a1a2e), DocumentWindow::allButtons, true) { // Use JUCE's own title bar — works regardless of macOS activation policy setUsingNativeTitleBar(false); setTitleBarHeight(28); } void closeButtonPressed() override { setVisible(false); } }; HelperPluginHost::HelperPluginHost() { #if JUCE_PLUGINHOST_VST3 formatManager.addFormat(new juce::VST3PluginFormat()); #endif #if JUCE_PLUGINHOST_AU formatManager.addFormat(new juce::AudioUnitPluginFormat()); #endif } HelperPluginHost::~HelperPluginHost() { stopProcessing(); hideGui(); unloadPlugin(); } bool HelperPluginHost::loadPlugin(const juce::String& path, double sampleRate, int blockSize) { unloadPlugin(); currentSampleRate = sampleRate; currentBlockSize = blockSize; juce::File pluginFile(path); if (!pluginFile.exists()) return false; juce::OwnedArray typesFound; for (int i = 0; i < formatManager.getNumFormats(); ++i) { auto* format = formatManager.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()) return false; juce::String errorMessage; plugin = formatManager.createPluginInstance(*typesFound[0], sampleRate, blockSize, errorMessage); if (plugin == nullptr) { DBG("HelperPluginHost: Failed to create plugin: " + errorMessage); return false; } plugin->enableAllBuses(); auto layout = juce::AudioProcessor::BusesLayout(); layout.inputBuses.add(juce::AudioChannelSet::stereo()); layout.outputBuses.add(juce::AudioChannelSet::stereo()); if (plugin->checkBusesLayoutSupported(layout)) plugin->setBusesLayout(layout); plugin->prepareToPlay(sampleRate, blockSize); DBG("HelperPluginHost: Loaded " + getPluginName() + " (" + juce::String(getParameterCount()) + " params)"); return true; } void HelperPluginHost::unloadPlugin() { hideGui(); if (plugin) { plugin->releaseResources(); plugin.reset(); } } juce::String HelperPluginHost::getPluginName() const { return plugin ? plugin->getName() : juce::String(); } int HelperPluginHost::getParameterCount() const { return plugin ? plugin->getParameters().size() : 0; } void HelperPluginHost::setSharedAudio(SharedAudioBuffer* shm, SharedSemaphore* inSem, SharedSemaphore* outSem) { sharedAudio = shm; inputSem = inSem; outputSem = outSem; } void HelperPluginHost::startProcessing() { if (processing.load()) return; processing.store(true); audioThread = std::thread([this]() { audioProcessingLoop(); }); } void HelperPluginHost::stopProcessing() { processing.store(false); if (inputSem) inputSem->signal(); if (audioThread.joinable()) audioThread.join(); } void HelperPluginHost::audioProcessingLoop() { struct sched_param param; param.sched_priority = sched_get_priority_max(SCHED_FIFO); pthread_setschedparam(pthread_self(), SCHED_FIFO, ¶m); while (processing.load()) { if (!inputSem->wait(100)) continue; if (!processing.load()) break; if (!plugin || !sharedAudio) continue; auto* header = sharedAudio->getHeader(); int numChannels = header->numChannels.load(); int blockSize = header->blockSize.load(); juce::AudioBuffer buffer(numChannels, blockSize); float* channelPtrs[2] = { buffer.getWritePointer(0), numChannels > 1 ? buffer.getWritePointer(1) : nullptr }; sharedAudio->readInput(channelPtrs, numChannels, blockSize); juce::MidiBuffer midi; plugin->processBlock(buffer, midi); const float* constPtrs[2] = { buffer.getReadPointer(0), numChannels > 1 ? buffer.getReadPointer(1) : nullptr }; sharedAudio->writeOutput(constPtrs, numChannels, blockSize); header->state.store(kStateOutputReady); outputSem->signal(); } } // --- Parameters --- std::vector HelperPluginHost::searchParams(const juce::String& keyword) const { std::vector results; if (!plugin) return results; auto& params = plugin->getParameters(); auto keywordLower = keyword.toLowerCase(); for (int i = 0; i < params.size(); ++i) { auto* param = params[i]; auto name = param->getName(128); if (name.toLowerCase().contains(keywordLower)) { ParamInfo info; info.index = i; info.name = name; info.value = param->getValue(); info.displayText = param->getText(param->getValue(), 64); if (auto* hosted = dynamic_cast(param)) info.id = hosted->getParameterID(); else info.id = juce::String(i); results.push_back(info); if (results.size() >= PluginBridgeConstants::kMaxParamResults) break; } } return results; } std::vector> HelperPluginHost::getParams(const std::vector& ids) const { std::vector> results; if (!plugin) return results; auto& params = plugin->getParameters(); for (int idx : ids) { if (idx >= 0 && idx < params.size()) results.push_back({idx, params[idx]->getValue()}); } return results; } bool HelperPluginHost::setParams(const std::map& values) { if (!plugin) return false; auto& params = plugin->getParameters(); for (auto& [idx, val] : values) { if (idx >= 0 && idx < params.size()) { auto* param = params[idx]; param->beginChangeGesture(); param->setValueNotifyingHost(juce::jlimit(0.0f, 1.0f, val)); param->endChangeGesture(); } } return true; } // --- GUI --- void HelperPluginHost::showGui() { if (!plugin || !plugin->hasEditor()) return; juce::MessageManager::callAsync([this]() { if (!plugin) return; // Activate as GUI process (macOS requires this for window interaction) makeGuiProcess(); // If window exists but hidden, just show it if (guiWindow) { guiWindow->setVisible(true); guiWindow->toFront(true); return; } auto* editor = plugin->createEditor(); if (!editor) return; guiWindow = std::make_unique(plugin->getName()); guiWindow->setContentOwned(editor, true); guiWindow->setResizable(true, true); guiWindow->centreWithSize(editor->getWidth(), editor->getHeight() + 28); // +28 for title bar guiWindow->setVisible(true); guiWindow->toFront(true); }); } void HelperPluginHost::hideGui() { if (guiWindow) { juce::MessageManager::callAsync([this]() { guiWindow.reset(); }); } } // --- State --- juce::String HelperPluginHost::getStateBase64() const { if (!plugin) return {}; juce::MemoryBlock stateData; plugin->getStateInformation(stateData); return stateData.toBase64Encoding(); } bool HelperPluginHost::setStateBase64(const juce::String& base64State) { if (!plugin) return false; juce::MemoryBlock stateData; if (!stateData.fromBase64Encoding(base64State)) return false; plugin->setStateInformation(stateData.getData(), (int)stateData.getSize()); return true; } void HelperPluginHost::configure(double sampleRate, int blockSize, int channels) { currentSampleRate = sampleRate; currentBlockSize = blockSize; currentChannels = channels; if (plugin) { plugin->releaseResources(); plugin->prepareToPlay(sampleRate, blockSize); } }