#include "HelperPluginHost.h" #include "Constants.h" #include "IPCProtocol.h" #include #include #include #if JUCE_MAC #import #import #include "PrivateCA.h" #endif 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: " + 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; } // --- Audio --- 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 (keyword.isEmpty() || 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 via CAContext + CALayerHost (cross-process layer sharing) --- void HelperPluginHost::showGui() { #if JUCE_MAC if (!plugin || !plugin->hasEditor()) return; if (editor) return; juce::MessageManager::callAsync([this]() { if (!plugin) return; editor.reset(plugin->createEditor()); if (!editor) return; guiWidth = editor->getWidth(); guiHeight = editor->getHeight(); if (guiWidth <= 0 || guiHeight <= 0) { guiWidth = 800; guiHeight = 600; } // Off-screen NSWindow positioned far off-screen so it's invisible to the user, // but orderFront: is required so the WindowServer composites its layer content. NSRect frame = NSMakeRect(-32000, -32000, guiWidth, guiHeight); NSWindow* window = [[NSWindow alloc] initWithContentRect:frame styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO]; [window setReleasedWhenClosed:NO]; [window setOpaque:NO]; [window setBackgroundColor:[NSColor clearColor]]; [[window contentView] setWantsLayer:YES]; // Embed JUCE editor NSView inside the off-screen window editor->addToDesktop(0, (void*)[window contentView]); if (auto* peer = editor->getPeer()) { NSView* editorView = (NSView*)peer->getNativeHandle(); [[window contentView] addSubview:editorView]; [editorView setFrame:[[window contentView] bounds]]; } // OrderFront so the WindowServer allocates a backing store and composites content. // The window is off-screen so the user never sees it. [window orderFront:nil]; guiWindow = (__bridge void*)window; // alloc/init already retains; no extra retain needed // CAContext wraps the content view's backing CALayer and exposes it to other // processes via a uint32_t contextId sent over our existing IPC socket. // We explicitly retain because we're not using ARC and this is a factory method. uint32_t cgsConn = CGSMainConnectionID(); CAContext* ctx = [[CAContext contextWithCGSConnection:cgsConn options:@{}] retain]; ctx.layer = [window contentView].layer; caContext = (__bridge void*)ctx; uint32_t ctxId = ctx.contextId; DBG("HelperPluginHost: GUI ready — contextId=" + juce::String(ctxId) + " size=" + juce::String(guiWidth) + "x" + juce::String(guiHeight)); if (onGuiCreated) onGuiCreated(juce::String(ctxId), guiWidth, guiHeight); }); #endif } void HelperPluginHost::hideGui() { #if JUCE_MAC juce::MessageManager::callAsync([this]() { editor.reset(); if (caContext) { CAContext* ctx = (__bridge CAContext*)caContext; ctx.layer = nil; [ctx release]; caContext = nullptr; } if (guiWindow) { NSWindow* window = (__bridge NSWindow*)guiWindow; [window orderOut:nil]; [window close]; [window release]; guiWindow = nullptr; } }); #endif } // --- Event Injection --- void HelperPluginHost::injectMouseDown(float x, float y, int button) { #if JUCE_MAC if (!guiWindow) return; NSWindow* window = (__bridge NSWindow*)guiWindow; NSEvent* event = [NSEvent mouseEventWithType:NSEventTypeLeftMouseDown location:NSMakePoint(x, guiHeight - y) modifierFlags:0 timestamp:[[NSProcessInfo processInfo] systemUptime] windowNumber:[window windowNumber] context:nil eventNumber:0 clickCount:1 pressure:1.0]; [NSApp postEvent:event atStart:NO]; #endif } void HelperPluginHost::injectMouseUp(float x, float y, int button) { #if JUCE_MAC if (!guiWindow) return; NSWindow* window = (__bridge NSWindow*)guiWindow; NSEvent* event = [NSEvent mouseEventWithType:NSEventTypeLeftMouseUp location:NSMakePoint(x, guiHeight - y) modifierFlags:0 timestamp:[[NSProcessInfo processInfo] systemUptime] windowNumber:[window windowNumber] context:nil eventNumber:0 clickCount:1 pressure:0.0]; [NSApp postEvent:event atStart:NO]; #endif } void HelperPluginHost::injectMouseMove(float x, float y) { #if JUCE_MAC if (!guiWindow) return; NSWindow* window = (__bridge NSWindow*)guiWindow; NSEvent* event = [NSEvent mouseEventWithType:NSEventTypeMouseMoved location:NSMakePoint(x, guiHeight - y) modifierFlags:0 timestamp:[[NSProcessInfo processInfo] systemUptime] windowNumber:[window windowNumber] context:nil eventNumber:0 clickCount:0 pressure:0.0]; [NSApp postEvent:event atStart:NO]; #endif } void HelperPluginHost::injectMouseDrag(float x, float y, int button) { #if JUCE_MAC if (!guiWindow) return; NSWindow* window = (__bridge NSWindow*)guiWindow; NSEvent* event = [NSEvent mouseEventWithType:NSEventTypeLeftMouseDragged location:NSMakePoint(x, guiHeight - y) modifierFlags:0 timestamp:[[NSProcessInfo processInfo] systemUptime] windowNumber:[window windowNumber] context:nil eventNumber:0 clickCount:0 pressure:1.0]; [NSApp postEvent:event atStart:NO]; #endif } void HelperPluginHost::injectMouseScroll(float x, float y, float dx, float dy) { #if JUCE_MAC if (!guiWindow) return; CGEventRef scrollEvent = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitPixel, 2, (int32_t)dy, (int32_t)dx); NSEvent* event = [NSEvent eventWithCGEvent:scrollEvent]; CFRelease(scrollEvent); [NSApp postEvent:event atStart:NO]; #endif } void HelperPluginHost::injectKeyDown(int keyCode, const juce::String& chars, int modifiers) { #if JUCE_MAC if (!guiWindow) return; NSWindow* window = (__bridge NSWindow*)guiWindow; NSString* nsChars = [NSString stringWithUTF8String:chars.toRawUTF8()]; NSEvent* event = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSZeroPoint modifierFlags:(NSEventModifierFlags)modifiers timestamp:[[NSProcessInfo processInfo] systemUptime] windowNumber:[window windowNumber] context:nil characters:nsChars charactersIgnoringModifiers:nsChars isARepeat:NO keyCode:(unsigned short)keyCode]; [NSApp postEvent:event atStart:NO]; #endif } void HelperPluginHost::injectKeyUp(int keyCode, const juce::String& chars, int modifiers) { #if JUCE_MAC if (!guiWindow) return; NSWindow* window = (__bridge NSWindow*)guiWindow; NSString* nsChars = [NSString stringWithUTF8String:chars.toRawUTF8()]; NSEvent* event = [NSEvent keyEventWithType:NSEventTypeKeyUp location:NSZeroPoint modifierFlags:(NSEventModifierFlags)modifiers timestamp:[[NSProcessInfo processInfo] systemUptime] windowNumber:[window windowNumber] context:nil characters:nsChars charactersIgnoringModifiers:nsChars isARepeat:NO keyCode:(unsigned short)keyCode]; [NSApp postEvent:event atStart:NO]; #endif } // --- 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); } }