Download Source/Helper/HelperPluginHost.mm from RAM2118/pluginbridge: direct link, hf CLI and curl.
- Browser
- Download file 14 kB
-
https://huggingface.co/RAM2118/pluginbridge/resolve/main/Source/Helper/HelperPluginHost.mm
- Command line
-
hf download hf://RAM2118/pluginbridge/Source/Helper/HelperPluginHost.mm
-
curl -L -o HelperPluginHost.mm https://huggingface.co/RAM2118/pluginbridge/resolve/main/Source/Helper/HelperPluginHost.mm
14 kB
| HelperPluginHost::HelperPluginHost() | |
| { | |
| formatManager.addFormat(new juce::VST3PluginFormat()); | |
| formatManager.addFormat(new juce::AudioUnitPluginFormat()); | |
| } | |
| 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<juce::PluginDescription> 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<float> 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::ParamInfo> HelperPluginHost::searchParams(const juce::String& keyword) const | |
| { | |
| std::vector<ParamInfo> 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<juce::AudioPluginInstance::HostedParameter*>(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<std::pair<int, float>> HelperPluginHost::getParams(const std::vector<int>& ids) const | |
| { | |
| std::vector<std::pair<int, float>> 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<int, float>& 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 (!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); | |
| }); | |
| } | |
| void HelperPluginHost::hideGui() | |
| { | |
| 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; | |
| } | |
| }); | |
| } | |
| // --- Event Injection --- | |
| void HelperPluginHost::injectMouseDown(float x, float y, int button) | |
| { | |
| 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]; | |
| } | |
| void HelperPluginHost::injectMouseUp(float x, float y, int button) | |
| { | |
| 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]; | |
| } | |
| void HelperPluginHost::injectMouseMove(float x, float y) | |
| { | |
| 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]; | |
| } | |
| void HelperPluginHost::injectMouseDrag(float x, float y, int button) | |
| { | |
| 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]; | |
| } | |
| void HelperPluginHost::injectMouseScroll(float x, float y, float dx, float dy) | |
| { | |
| 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]; | |
| } | |
| void HelperPluginHost::injectKeyDown(int keyCode, const juce::String& chars, int modifiers) | |
| { | |
| 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]; | |
| } | |
| void HelperPluginHost::injectKeyUp(int keyCode, const juce::String& chars, int modifiers) | |
| { | |
| 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]; | |
| } | |
| // --- 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); } | |
| } | |