pluginbridge / Source /Helper /HelperPluginHost.cpp
RAM2118's picture
Fix GUI close: Use JUCE custom title bar (not native) + force app activation via ObjC runtime
75f7e51 verified
Raw History Blame
9.25 kB
#include "HelperPluginHost.h"
#include "Constants.h"
#include <juce_audio_basics/juce_audio_basics.h>
#include <pthread.h>
#include <sched.h>
#if JUCE_MAC
#include <objc/objc.h>
#include <objc/message.h>
#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<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 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, &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 (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 ---
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<PluginGuiWindow>(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);
}
}