| #include "HelperPluginHost.h" |
| #include "Constants.h" |
| #include "IPCProtocol.h" |
| #include <juce_audio_basics/juce_audio_basics.h> |
| #include <pthread.h> |
| #include <sched.h> |
|
|
| #if JUCE_MAC |
| #import <Cocoa/Cocoa.h> |
| #import <QuartzCore/QuartzCore.h> |
| #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<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; } |
|
|
| |
|
|
| 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(); |
| } |
| } |
|
|
| |
|
|
| 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; |
| } |
|
|
| |
|
|
| 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; } |
|
|
| |
| |
| 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]; |
|
|
| |
| 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]]; |
| } |
|
|
| |
| |
| [window orderFront:nil]; |
| guiWindow = (__bridge void*)window; |
|
|
| |
| |
| |
| 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 |
| } |
|
|
| |
|
|
| 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 |
| } |
|
|
| |
|
|
| 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); } |
| } |
|
|