pluginbridge / Source /Helper /HelperPluginHost.mm
Sitar118
fix: replace CARemoteLayerServer with CAContext+CALayerHost for cross-process GUI
0548359
Raw
History Blame Contribute Delete
14 kB
#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; }
// --- 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, &param);
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 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); }
}