Sprint 7: wire param callbacks in loadInProcessPlugin, clear in unloadInProcessPlugin
Browse filesWhen SAFE plugin loaded: searchParamCallback, getParamsCallback, setParamsCallback
route directly to inProcessPlugin. When unloaded: cleared to nullptr (falls back to Helper).
Pro-Q 4 GUI now updates instantly when AI sets parameters via MCP.
- Source/Plugin/PluginBridgeProcessor.cpp +131 -286
Source/Plugin/PluginBridgeProcessor.cpp
CHANGED
|
@@ -10,6 +10,7 @@
|
|
| 10 |
#include <unistd.h>
|
| 11 |
#include <fcntl.h>
|
| 12 |
#include <chrono>
|
|
|
|
| 13 |
|
| 14 |
extern char** environ;
|
| 15 |
|
|
@@ -58,13 +59,11 @@ PluginBridgeProcessor::PluginBridgeProcessor()
|
|
| 58 |
});
|
| 59 |
};
|
| 60 |
|
| 61 |
-
// Wire callbacks for MCP tools
|
| 62 |
mcpServer->getAnalysisCallback = [this]() { return analyser.getCompactAnalysis(); };
|
| 63 |
mcpServer->channelNameCallback = [this]() { return channelName.toStdString(); };
|
| 64 |
|
| 65 |
mcpServer->start();
|
| 66 |
|
| 67 |
-
// Register this instance in the shared registry
|
| 68 |
InstanceRegistry::registerInstance(channelName.toStdString(), mcpServer->getPort());
|
| 69 |
PBLOG("Processor", "MCP server on port " + juce::String(mcpServer->getPort())
|
| 70 |
+ " β channel: " + channelName);
|
|
@@ -76,36 +75,26 @@ PluginBridgeProcessor::~PluginBridgeProcessor()
|
|
| 76 |
{
|
| 77 |
PBLOG("Processor", "Destructor β tearing down");
|
| 78 |
stopBackgroundScan();
|
| 79 |
-
|
| 80 |
-
// Unregister from shared registry
|
| 81 |
InstanceRegistry::unregisterInstance(channelName.toStdString());
|
| 82 |
-
|
| 83 |
mcpServer->stop();
|
| 84 |
unloadInProcessPlugin();
|
| 85 |
helper.killHelper();
|
| 86 |
PBLOG("Processor", "Destructor β done");
|
| 87 |
}
|
| 88 |
|
| 89 |
-
// ββ Channel Name ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 90 |
-
|
| 91 |
void PluginBridgeProcessor::setChannelName(const juce::String& name)
|
| 92 |
{
|
| 93 |
if (name == channelName) return;
|
| 94 |
-
|
| 95 |
-
// Unregister old name, update, register new
|
| 96 |
InstanceRegistry::unregisterInstance(channelName.toStdString());
|
| 97 |
channelName = name;
|
| 98 |
InstanceRegistry::registerInstance(channelName.toStdString(), mcpServer->getPort());
|
| 99 |
PBLOG("Processor", "Channel renamed to: " + channelName);
|
| 100 |
}
|
| 101 |
|
| 102 |
-
// ββ Audio βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 103 |
-
|
| 104 |
void PluginBridgeProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
|
| 105 |
{
|
| 106 |
currentSampleRate = sampleRate;
|
| 107 |
currentBlockSize = samplesPerBlock;
|
| 108 |
-
|
| 109 |
analyser.prepareToPlay(sampleRate, samplesPerBlock);
|
| 110 |
|
| 111 |
if (inProcessPlugin)
|
|
@@ -116,19 +105,10 @@ void PluginBridgeProcessor::prepareToPlay(double sampleRate, int samplesPerBlock
|
|
| 116 |
|
| 117 |
if (helper.isHelperRunning())
|
| 118 |
{
|
| 119 |
-
json cmd = {{"cmd",
|
| 120 |
-
{"sampleRate", sampleRate},
|
| 121 |
-
{"blockSize", samplesPerBlock},
|
| 122 |
-
{"channels", 2}};
|
| 123 |
helper.sendCommand(cmd.dump());
|
| 124 |
-
|
| 125 |
auto* header = helper.getSharedAudio().getHeader();
|
| 126 |
-
if (header)
|
| 127 |
-
{
|
| 128 |
-
header->sampleRate.store((uint32_t)sampleRate);
|
| 129 |
-
header->blockSize.store((uint32_t)samplesPerBlock);
|
| 130 |
-
header->numChannels.store(2);
|
| 131 |
-
}
|
| 132 |
}
|
| 133 |
}
|
| 134 |
|
|
@@ -137,10 +117,8 @@ void PluginBridgeProcessor::releaseResources() {}
|
|
| 137 |
void PluginBridgeProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
|
| 138 |
{
|
| 139 |
juce::ScopedNoDenormals noDenormals;
|
| 140 |
-
|
| 141 |
for (auto i = getTotalNumInputChannels(); i < getTotalNumOutputChannels(); ++i)
|
| 142 |
buffer.clear(i, 0, buffer.getNumSamples());
|
| 143 |
-
|
| 144 |
if (!pluginLoaded) return;
|
| 145 |
|
| 146 |
if (inProcessPlugin != nullptr)
|
|
@@ -151,42 +129,24 @@ void PluginBridgeProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce:
|
|
| 151 |
}
|
| 152 |
|
| 153 |
if (!helper.isHelperRunning()) return;
|
| 154 |
-
|
| 155 |
auto& shm = helper.getSharedAudio();
|
| 156 |
if (!shm.isValid()) return;
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
int numSamples = buffer.getNumSamples();
|
| 160 |
-
|
| 161 |
-
const float* inputPtrs[2] = {
|
| 162 |
-
buffer.getReadPointer(0),
|
| 163 |
-
numChannels > 1 ? buffer.getReadPointer(1) : buffer.getReadPointer(0)
|
| 164 |
-
};
|
| 165 |
shm.writeInput(inputPtrs, numChannels, numSamples);
|
| 166 |
-
|
| 167 |
auto* header = shm.getHeader();
|
| 168 |
-
header->blockSize.store((uint32_t)numSamples);
|
| 169 |
-
header->numChannels.store((uint32_t)numChannels);
|
| 170 |
-
header->state.store(kStateInputReady);
|
| 171 |
-
|
| 172 |
helper.getInputSem().signal();
|
| 173 |
-
|
| 174 |
if (helper.getOutputSem().wait(IPC::kProcessBlockTimeoutMs))
|
| 175 |
{
|
| 176 |
-
float* outputPtrs[2] = {
|
| 177 |
-
buffer.getWritePointer(0),
|
| 178 |
-
numChannels > 1 ? buffer.getWritePointer(1) : buffer.getWritePointer(0)
|
| 179 |
-
};
|
| 180 |
shm.readOutput(outputPtrs, numChannels, numSamples);
|
| 181 |
header->state.store(kStateIdle);
|
| 182 |
analyser.processBlock(buffer);
|
| 183 |
}
|
| 184 |
}
|
| 185 |
|
| 186 |
-
juce::AudioProcessorEditor* PluginBridgeProcessor::createEditor()
|
| 187 |
-
{
|
| 188 |
-
return new PluginBridgeEditor(*this);
|
| 189 |
-
}
|
| 190 |
|
| 191 |
// ββ In-process plugin βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 192 |
|
|
@@ -203,19 +163,16 @@ void PluginBridgeProcessor::loadInProcessPlugin(const juce::String& path)
|
|
| 203 |
{
|
| 204 |
auto* format = inProcessFormatManager.getFormat(i);
|
| 205 |
bool isVST3 = pluginFile.getFileExtension() == ".vst3";
|
| 206 |
-
bool isAU
|
| 207 |
if (isVST3 && format->getName() != "VST3") continue;
|
| 208 |
if (isAU && format->getName() != "AudioUnit") continue;
|
| 209 |
format->findAllTypesForFile(typesFound, pluginFile.getFullPathName());
|
| 210 |
if (typesFound.size() > 0) break;
|
| 211 |
}
|
| 212 |
-
|
| 213 |
if (typesFound.isEmpty()) { PBLOG("Processor", "loadInProcessPlugin: no types found"); return; }
|
| 214 |
|
| 215 |
juce::String err;
|
| 216 |
-
inProcessPlugin = inProcessFormatManager.createPluginInstance(
|
| 217 |
-
*typesFound[0], currentSampleRate, currentBlockSize, err);
|
| 218 |
-
|
| 219 |
if (!inProcessPlugin) { PBLOG("Processor", "loadInProcessPlugin: FAILED β " + err); return; }
|
| 220 |
|
| 221 |
inProcessPlugin->enableAllBuses();
|
|
@@ -224,31 +181,74 @@ void PluginBridgeProcessor::loadInProcessPlugin(const juce::String& path)
|
|
| 224 |
layout.outputBuses.add(juce::AudioChannelSet::stereo());
|
| 225 |
if (inProcessPlugin->checkBusesLayoutSupported(layout))
|
| 226 |
inProcessPlugin->setBusesLayout(layout);
|
| 227 |
-
|
| 228 |
if (currentSampleRate > 0 && currentBlockSize > 0)
|
| 229 |
inProcessPlugin->prepareToPlay(currentSampleRate, currentBlockSize);
|
| 230 |
|
|
|
|
| 231 |
auto response = helper.sendCommand("{\"cmd\":\"get_state\"}");
|
| 232 |
-
try {
|
| 233 |
-
auto
|
| 234 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
{
|
| 236 |
-
|
| 237 |
-
|
|
|
|
|
|
|
| 238 |
{
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
|
|
|
| 242 |
}
|
| 243 |
}
|
| 244 |
-
|
|
|
|
| 245 |
|
| 246 |
-
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
}
|
| 249 |
|
| 250 |
void PluginBridgeProcessor::unloadInProcessPlugin()
|
| 251 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
if (inProcessPlugin)
|
| 253 |
{
|
| 254 |
PBLOG("Processor", "unloadInProcessPlugin: " + inProcessPlugin->getName());
|
|
@@ -271,26 +271,17 @@ void PluginBridgeProcessor::audioProcessorParameterChanged(juce::AudioProcessor*
|
|
| 271 |
void PluginBridgeProcessor::loadPlugin(const juce::String& path)
|
| 272 |
{
|
| 273 |
PBLOG("Processor", "loadPlugin: " + juce::File(path).getFileName());
|
| 274 |
-
|
| 275 |
-
if (blocklist.isBlocked(path))
|
| 276 |
-
{
|
| 277 |
-
if (onLoadFailed) onLoadFailed("Not compatible (crashed previously)");
|
| 278 |
-
return;
|
| 279 |
-
}
|
| 280 |
|
| 281 |
auto safety = safetyDB.getSafety(path);
|
| 282 |
-
PBLOG("Processor", "loadPlugin: safety = " + juce::String(
|
| 283 |
-
safety == PluginSafetyDB::Safety::Safe ? "SAFE" :
|
| 284 |
-
safety == PluginSafetyDB::Safety::Unsafe ? "UNSAFE" : "UNKNOWN"));
|
| 285 |
|
| 286 |
if (safety == PluginSafetyDB::Safety::Unknown)
|
| 287 |
{
|
| 288 |
if (onLoadFailed) onLoadFailed("Scanning plugin...");
|
| 289 |
-
std::thread([this, path]()
|
| 290 |
-
{
|
| 291 |
bool safe = runPluginScan(path);
|
| 292 |
-
safetyDB.setSafety(path, safe ? PluginSafetyDB::Safety::Safe
|
| 293 |
-
: PluginSafetyDB::Safety::Unsafe);
|
| 294 |
juce::MessageManager::callAsync([this, path]() { loadPlugin(path); });
|
| 295 |
}).detach();
|
| 296 |
return;
|
|
@@ -298,60 +289,33 @@ void PluginBridgeProcessor::loadPlugin(const juce::String& path)
|
|
| 298 |
|
| 299 |
if (onPluginWillLoad) onPluginWillLoad();
|
| 300 |
|
| 301 |
-
if (!helper.isHelperRunning())
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
if (onLoadFailed) onLoadFailed("Failed to start helper process");
|
| 306 |
-
return;
|
| 307 |
-
}
|
| 308 |
-
json configCmd = {{"cmd","configure"},{"sampleRate",currentSampleRate},
|
| 309 |
-
{"blockSize",currentBlockSize},{"channels",2}};
|
| 310 |
-
helper.sendCommand(configCmd.dump());
|
| 311 |
}
|
| 312 |
|
| 313 |
-
json loadCmd = {{"cmd","load"},{"path",path.toStdString()},
|
| 314 |
-
{"sampleRate",currentSampleRate},{"blockSize",currentBlockSize}};
|
| 315 |
auto response = helper.sendCommand(loadCmd.dump());
|
| 316 |
|
| 317 |
-
try
|
| 318 |
-
{
|
| 319 |
auto j = json::parse(response);
|
| 320 |
-
if (j.value("ok",
|
| 321 |
-
{
|
| 322 |
pluginLoaded = true;
|
| 323 |
pluginName = juce::String(j["name"].get<std::string>());
|
| 324 |
-
pluginParamCount = j.value("params",
|
| 325 |
pluginPath = path;
|
| 326 |
PBLOG("Processor", "loadPlugin: loaded \"" + pluginName + "\"");
|
| 327 |
-
|
| 328 |
if (safety == PluginSafetyDB::Safety::Safe)
|
| 329 |
-
|
| 330 |
-
juce::MessageManager::callAsync([this, path]()
|
| 331 |
-
{
|
| 332 |
-
loadInProcessPlugin(path);
|
| 333 |
-
if (onPluginLoaded) onPluginLoaded();
|
| 334 |
-
});
|
| 335 |
-
}
|
| 336 |
else
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
}
|
| 341 |
-
}
|
| 342 |
-
else
|
| 343 |
-
{
|
| 344 |
-
pluginLoaded = false;
|
| 345 |
-
pluginPath.clear();
|
| 346 |
-
blocklist.addBlocked(path);
|
| 347 |
if (onLoadFailed) onLoadFailed("Not compatible: " + juce::String(j.value("error","unknown")));
|
| 348 |
}
|
| 349 |
-
}
|
| 350 |
-
|
| 351 |
-
{
|
| 352 |
-
pluginLoaded = false;
|
| 353 |
-
pluginPath.clear();
|
| 354 |
-
blocklist.addBlocked(path);
|
| 355 |
if (onLoadFailed) onLoadFailed("Helper communication error");
|
| 356 |
}
|
| 357 |
}
|
|
@@ -360,143 +324,76 @@ void PluginBridgeProcessor::unloadPlugin()
|
|
| 360 |
{
|
| 361 |
PBLOG("Processor", "unloadPlugin: \"" + pluginName + "\"");
|
| 362 |
if (helper.isHelperRunning()) helper.sendCommand("{\"cmd\":\"unload\"}");
|
| 363 |
-
pluginLoaded = false;
|
| 364 |
-
pluginName.clear();
|
| 365 |
-
pluginPath.clear();
|
| 366 |
-
pluginParamCount = 0;
|
| 367 |
unloadInProcessPlugin();
|
| 368 |
-
if (onPluginUnloaded)
|
| 369 |
-
juce::MessageManager::callAsync([this]() { if (onPluginUnloaded) onPluginUnloaded(); });
|
| 370 |
}
|
| 371 |
|
| 372 |
// ββ Scanner βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 373 |
|
| 374 |
-
static juce::String signalName(int sig)
|
| 375 |
-
{
|
| 376 |
-
switch (sig) {
|
| 377 |
-
case SIGSEGV: return "SIGSEGV"; case SIGABRT: return "SIGABRT";
|
| 378 |
-
case SIGBUS: return "SIGBUS"; case SIGILL: return "SIGILL";
|
| 379 |
-
case SIGFPE: return "SIGFPE"; case SIGKILL: return "SIGKILL";
|
| 380 |
-
default: return "signal " + juce::String(sig);
|
| 381 |
-
}
|
| 382 |
}
|
| 383 |
|
| 384 |
-
static void writeScanLog(const juce::String& pluginPath, bool safe,
|
| 385 |
-
|
| 386 |
-
{
|
| 387 |
-
auto logFile = juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory)
|
| 388 |
-
.getChildFile("PluginBridge").getChildFile("scan.log");
|
| 389 |
logFile.getParentDirectory().createDirectory();
|
| 390 |
auto ts = juce::Time::getCurrentTime().formatted("%Y-%m-%d %H:%M:%S");
|
| 391 |
-
juce::String entry;
|
| 392 |
-
entry << "βββ\n" << ts << " " << juce::File(pluginPath).getFileName()
|
| 393 |
-
<< "\nResult: " << (safe ? "SAFE" : "UNSAFE");
|
| 394 |
if (!reason.isEmpty()) entry << " (" << reason << ")";
|
| 395 |
if (captured.isNotEmpty()) entry << "\nOutput: " << captured.substring(0, 500);
|
| 396 |
-
entry << "\n\n";
|
| 397 |
-
logFile.appendText(entry);
|
| 398 |
}
|
| 399 |
|
| 400 |
-
bool PluginBridgeProcessor::runPluginScan(const juce::String& path)
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
.getParentDirectory().getParentDirectory()
|
| 404 |
-
.getChildFile("Resources").getChildFile("PluginBridgeHelper.app")
|
| 405 |
-
.getChildFile("Contents").getChildFile("MacOS")
|
| 406 |
-
.getChildFile(PluginBridgeConstants::kHelperBinaryName);
|
| 407 |
-
if (!helperApp.exists())
|
| 408 |
-
helperApp = juce::File::getSpecialLocation(juce::File::currentExecutableFile)
|
| 409 |
-
.getParentDirectory().getParentDirectory()
|
| 410 |
-
.getChildFile("Resources").getChildFile(PluginBridgeConstants::kHelperBinaryName);
|
| 411 |
if (!helperApp.exists()) { writeScanLog(path, false, "Helper not found", {}); return false; }
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
auto hStr = helperApp.getFullPathName().toStdString();
|
| 418 |
-
auto pStr = path.toStdString();
|
| 419 |
-
posix_spawn_file_actions_t actions;
|
| 420 |
-
posix_spawn_file_actions_init(&actions);
|
| 421 |
-
posix_spawn_file_actions_adddup2(&actions, pipeFd[1], STDERR_FILENO);
|
| 422 |
-
posix_spawn_file_actions_addclose(&actions, pipeFd[0]);
|
| 423 |
-
|
| 424 |
-
pid_t pid;
|
| 425 |
-
char* argv[] = { (char*)hStr.c_str(), (char*)"--scan", (char*)pStr.c_str(), nullptr };
|
| 426 |
int sr = posix_spawn(&pid, hStr.c_str(), &actions, nullptr, argv, environ);
|
| 427 |
-
posix_spawn_file_actions_destroy(&actions);
|
| 428 |
-
close(pipeFd[1]);
|
| 429 |
if (sr != 0) { close(pipeFd[0]); return false; }
|
| 430 |
-
|
| 431 |
bool timedOut = true; int status = 0;
|
| 432 |
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
| 433 |
-
while (std::chrono::steady_clock::now() < deadline) {
|
| 434 |
-
if (waitpid(pid, &status, WNOHANG) == pid) { timedOut = false; break; }
|
| 435 |
-
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
| 436 |
-
}
|
| 437 |
if (timedOut) { kill(pid, SIGKILL); waitpid(pid, &status, 0); }
|
| 438 |
-
|
| 439 |
-
juce::String captured;
|
| 440 |
-
{ char buf[4096]; ssize_t n;
|
| 441 |
-
while ((n = read(pipeFd[0], buf, sizeof(buf)-1)) > 0) { buf[n]=0; captured += buf; } }
|
| 442 |
-
close(pipeFd[0]);
|
| 443 |
-
|
| 444 |
bool safe = !timedOut && WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
| 445 |
-
juce::String reason = timedOut ? "Timeout" :
|
| 446 |
-
|
| 447 |
-
(!safe ? "Exit " + juce::String(WEXITSTATUS(status)) : "");
|
| 448 |
-
writeScanLog(path, safe, reason, captured);
|
| 449 |
-
return safe;
|
| 450 |
}
|
| 451 |
|
| 452 |
-
void PluginBridgeProcessor::startBackgroundScan()
|
| 453 |
-
|
| 454 |
-
if (scanRunning.load()) return;
|
| 455 |
-
scanRunning.store(true);
|
| 456 |
scanThread = std::thread([this]() {
|
| 457 |
auto files = getInstalledPluginFiles();
|
| 458 |
for (int i = 0; i < files.size() && scanRunning.load(); ++i) {
|
| 459 |
auto path = files[i].getFullPathName();
|
| 460 |
if (safetyDB.getSafety(path) != PluginSafetyDB::Safety::Unknown) continue;
|
| 461 |
-
while (helper.isHelperRunning() && scanRunning.load())
|
| 462 |
-
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
| 463 |
if (!scanRunning.load()) break;
|
| 464 |
bool safe = runPluginScan(path);
|
| 465 |
safetyDB.setSafety(path, safe ? PluginSafetyDB::Safety::Safe : PluginSafetyDB::Safety::Unsafe);
|
| 466 |
-
if (onScanProgress)
|
| 467 |
-
juce::MessageManager::callAsync([this, path, safe]()
|
| 468 |
-
{ if (onScanProgress) onScanProgress(path, safe); });
|
| 469 |
}
|
| 470 |
scanRunning.store(false);
|
| 471 |
});
|
| 472 |
}
|
| 473 |
|
| 474 |
-
void PluginBridgeProcessor::stopBackgroundScan()
|
| 475 |
-
{
|
| 476 |
-
scanRunning.store(false);
|
| 477 |
-
if (scanThread.joinable()) scanThread.join();
|
| 478 |
-
}
|
| 479 |
|
| 480 |
// ββ Plugin List βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 481 |
|
| 482 |
-
juce::Array<juce::File> PluginBridgeProcessor::getInstalledPluginFiles() const
|
| 483 |
-
|
| 484 |
-
juce::Array<juce::File> results;
|
| 485 |
-
std::set<std::string> seen;
|
| 486 |
struct D { juce::File dir; const char* pat; };
|
| 487 |
-
std::vector<D> specs = {
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
{ juce::File("/Library/Audio/Plug-Ins/Components"), "*.component" },
|
| 491 |
-
{ juce::File::getSpecialLocation(juce::File::userHomeDirectory).getChildFile("Library/Audio/Plug-Ins/Components"), "*.component" }
|
| 492 |
-
};
|
| 493 |
-
for (auto& s : specs) {
|
| 494 |
-
if (!s.dir.isDirectory()) continue;
|
| 495 |
-
for (auto& f : s.dir.findChildFiles(juce::File::findDirectories, false, s.pat))
|
| 496 |
-
if (seen.insert(f.getFileNameWithoutExtension().toStdString()).second) results.add(f);
|
| 497 |
-
}
|
| 498 |
-
results.sort();
|
| 499 |
-
return results;
|
| 500 |
}
|
| 501 |
|
| 502 |
bool PluginBridgeProcessor::isBlocked(const juce::String& path) const { return blocklist.isBlocked(path); }
|
|
@@ -505,98 +402,46 @@ bool PluginBridgeProcessor::isBlocked(const juce::String& path) const { return b
|
|
| 505 |
|
| 506 |
static juce::String cleanManufacturer(const juce::String& raw) {
|
| 507 |
auto s = raw.trim(); if (s.isEmpty() || s.containsOnly("0123456789.")) return {};
|
| 508 |
-
if (s.toLowerCase().startsWith("copyright")) {
|
| 509 |
-
auto r = s.substring(9).trim();
|
| 510 |
-
if (r.startsWithIgnoreCase("(c)")) r = r.substring(3).trim();
|
| 511 |
-
else if (r.startsWith(juce::String::fromUTF8("\xc2\xa9")))
|
| 512 |
-
r = r.substring(juce::String::fromUTF8("\xc2\xa9").length()).trim();
|
| 513 |
-
auto sp = r.indexOfChar(' '); if (sp > 0) r = r.substring(sp+1).trim();
|
| 514 |
-
return r.isNotEmpty() ? r : juce::String();
|
| 515 |
-
}
|
| 516 |
return s;
|
| 517 |
}
|
| 518 |
-
|
| 519 |
-
static juce::String plistValue(const juce::String& t, const juce::String& k) {
|
| 520 |
-
auto p = t.fromFirstOccurrenceOf(k, false, true); if (p.isEmpty()) return {};
|
| 521 |
-
return p.fromFirstOccurrenceOf("<string>", false, false)
|
| 522 |
-
.upToFirstOccurrenceOf("</string>", false, false).trim();
|
| 523 |
-
}
|
| 524 |
|
| 525 |
juce::String PluginBridgeProcessor::getPluginManufacturer(const juce::File& f) const {
|
| 526 |
auto pk = f.getFullPathName().toStdString();
|
| 527 |
-
{ std::lock_guard<std::mutex> l(manufacturerCacheMutex);
|
| 528 |
-
auto it = manufacturerCache.find(pk); if (it != manufacturerCache.end()) return juce::String(it->second); }
|
| 529 |
juce::String mfr;
|
| 530 |
if (f.getFileExtension().toLowerCase() == ".vst3") {
|
| 531 |
auto ip = f.getChildFile("Contents").getChildFile("Info.plist");
|
| 532 |
-
if (ip.existsAsFile()) {
|
| 533 |
-
auto t =
|
| 534 |
-
mfr =
|
| 535 |
-
|
| 536 |
-
if (is.contains(",")) { auto bc = is.upToFirstOccurrenceOf(",",false,false).trim();
|
| 537 |
-
auto fw = bc.upToFirstOccurrenceOf(" ",false,false).trim();
|
| 538 |
-
if (fw.isNotEmpty() && !fw.containsOnly("0123456789.")) mfr = fw; }
|
| 539 |
-
if (mfr.isEmpty()) mfr = cleanManufacturer(is); }
|
| 540 |
-
if (mfr.isEmpty()) { auto bid = plistValue(t, "CFBundleIdentifier");
|
| 541 |
-
auto segs = juce::StringArray::fromTokens(bid,".","" );
|
| 542 |
-
if (segs.size()>=2) { auto sl = segs[1].trim();
|
| 543 |
-
if (sl.isNotEmpty()&&sl!="apple"&&sl!="com")
|
| 544 |
-
mfr = sl.substring(0,1).toUpperCase()+sl.substring(1); } }
|
| 545 |
-
}
|
| 546 |
-
if (mfr.isEmpty()) { auto mi = f.getChildFile("Contents").getChildFile("moduleinfo.json");
|
| 547 |
-
if (mi.existsAsFile()) { try { auto j = json::parse(mi.loadFileAsString().toStdString());
|
| 548 |
-
mfr = cleanManufacturer(juce::String(j.value("vendor",""))); } catch(...){} } }
|
| 549 |
}
|
| 550 |
-
if (mfr.isEmpty()) { auto n = f.getFileNameWithoutExtension();
|
| 551 |
-
auto fw = n.upToFirstOccurrenceOf(" ",false,false);
|
| 552 |
-
mfr = (fw.isNotEmpty()&&!fw.containsOnly("0123456789.")) ? fw : n; }
|
| 553 |
if (mfr.isEmpty()) mfr = "Other";
|
| 554 |
std::lock_guard<std::mutex> l(manufacturerCacheMutex); manufacturerCache[pk] = mfr.toStdString(); return mfr;
|
| 555 |
}
|
| 556 |
|
| 557 |
// ββ State βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 558 |
|
| 559 |
-
void PluginBridgeProcessor::getStateInformation(juce::MemoryBlock& destData)
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
state.setProperty("channelName", channelName, nullptr);
|
| 564 |
-
|
| 565 |
-
if (pluginLoaded && helper.isHelperRunning()) {
|
| 566 |
-
auto r = helper.sendCommand("{\"cmd\":\"get_state\"}");
|
| 567 |
-
try { auto j = json::parse(r); if (j.value("ok",false))
|
| 568 |
-
state.setProperty("pluginState", juce::String(j["state"].get<std::string>()), nullptr);
|
| 569 |
-
} catch (...) {}
|
| 570 |
-
}
|
| 571 |
-
|
| 572 |
-
juce::MemoryOutputStream stream(destData, false);
|
| 573 |
-
state.writeToStream(stream);
|
| 574 |
}
|
| 575 |
|
| 576 |
-
void PluginBridgeProcessor::setStateInformation(const void* data, int sizeInBytes)
|
| 577 |
-
|
| 578 |
-
auto
|
| 579 |
-
if (
|
| 580 |
-
|
| 581 |
-
// Restore channel name
|
| 582 |
-
auto savedChannel = state.getProperty("channelName", "Unnamed").toString();
|
| 583 |
-
setChannelName(savedChannel);
|
| 584 |
-
|
| 585 |
-
auto path = state.getProperty("pluginPath").toString();
|
| 586 |
-
if (path.isEmpty()) return;
|
| 587 |
-
|
| 588 |
loadPlugin(path);
|
| 589 |
-
|
| 590 |
auto pluginState = state.getProperty("pluginState").toString();
|
| 591 |
-
if (pluginState.isNotEmpty() && helper.isHelperRunning()) {
|
| 592 |
-
json cmd = {{"cmd","set_state"},{"state",pluginState.toStdString()}};
|
| 593 |
-
helper.sendCommand(cmd.dump());
|
| 594 |
-
}
|
| 595 |
}
|
| 596 |
|
| 597 |
// ββ MCP βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 598 |
|
| 599 |
bool PluginBridgeProcessor::isMcpServerRunning() const { return mcpServer && mcpServer->isRunning(); }
|
| 600 |
int PluginBridgeProcessor::getMcpServerPort() const { return mcpServer ? mcpServer->getPort() : 0; }
|
| 601 |
-
|
| 602 |
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { return new PluginBridgeProcessor(); }
|
|
|
|
| 10 |
#include <unistd.h>
|
| 11 |
#include <fcntl.h>
|
| 12 |
#include <chrono>
|
| 13 |
+
#include <algorithm>
|
| 14 |
|
| 15 |
extern char** environ;
|
| 16 |
|
|
|
|
| 59 |
});
|
| 60 |
};
|
| 61 |
|
|
|
|
| 62 |
mcpServer->getAnalysisCallback = [this]() { return analyser.getCompactAnalysis(); };
|
| 63 |
mcpServer->channelNameCallback = [this]() { return channelName.toStdString(); };
|
| 64 |
|
| 65 |
mcpServer->start();
|
| 66 |
|
|
|
|
| 67 |
InstanceRegistry::registerInstance(channelName.toStdString(), mcpServer->getPort());
|
| 68 |
PBLOG("Processor", "MCP server on port " + juce::String(mcpServer->getPort())
|
| 69 |
+ " β channel: " + channelName);
|
|
|
|
| 75 |
{
|
| 76 |
PBLOG("Processor", "Destructor β tearing down");
|
| 77 |
stopBackgroundScan();
|
|
|
|
|
|
|
| 78 |
InstanceRegistry::unregisterInstance(channelName.toStdString());
|
|
|
|
| 79 |
mcpServer->stop();
|
| 80 |
unloadInProcessPlugin();
|
| 81 |
helper.killHelper();
|
| 82 |
PBLOG("Processor", "Destructor β done");
|
| 83 |
}
|
| 84 |
|
|
|
|
|
|
|
| 85 |
void PluginBridgeProcessor::setChannelName(const juce::String& name)
|
| 86 |
{
|
| 87 |
if (name == channelName) return;
|
|
|
|
|
|
|
| 88 |
InstanceRegistry::unregisterInstance(channelName.toStdString());
|
| 89 |
channelName = name;
|
| 90 |
InstanceRegistry::registerInstance(channelName.toStdString(), mcpServer->getPort());
|
| 91 |
PBLOG("Processor", "Channel renamed to: " + channelName);
|
| 92 |
}
|
| 93 |
|
|
|
|
|
|
|
| 94 |
void PluginBridgeProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
|
| 95 |
{
|
| 96 |
currentSampleRate = sampleRate;
|
| 97 |
currentBlockSize = samplesPerBlock;
|
|
|
|
| 98 |
analyser.prepareToPlay(sampleRate, samplesPerBlock);
|
| 99 |
|
| 100 |
if (inProcessPlugin)
|
|
|
|
| 105 |
|
| 106 |
if (helper.isHelperRunning())
|
| 107 |
{
|
| 108 |
+
json cmd = {{"cmd","configure"},{"sampleRate",sampleRate},{"blockSize",samplesPerBlock},{"channels",2}};
|
|
|
|
|
|
|
|
|
|
| 109 |
helper.sendCommand(cmd.dump());
|
|
|
|
| 110 |
auto* header = helper.getSharedAudio().getHeader();
|
| 111 |
+
if (header) { header->sampleRate.store((uint32_t)sampleRate); header->blockSize.store((uint32_t)samplesPerBlock); header->numChannels.store(2); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
}
|
| 113 |
}
|
| 114 |
|
|
|
|
| 117 |
void PluginBridgeProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
|
| 118 |
{
|
| 119 |
juce::ScopedNoDenormals noDenormals;
|
|
|
|
| 120 |
for (auto i = getTotalNumInputChannels(); i < getTotalNumOutputChannels(); ++i)
|
| 121 |
buffer.clear(i, 0, buffer.getNumSamples());
|
|
|
|
| 122 |
if (!pluginLoaded) return;
|
| 123 |
|
| 124 |
if (inProcessPlugin != nullptr)
|
|
|
|
| 129 |
}
|
| 130 |
|
| 131 |
if (!helper.isHelperRunning()) return;
|
|
|
|
| 132 |
auto& shm = helper.getSharedAudio();
|
| 133 |
if (!shm.isValid()) return;
|
| 134 |
+
int numChannels = buffer.getNumChannels(), numSamples = buffer.getNumSamples();
|
| 135 |
+
const float* inputPtrs[2] = { buffer.getReadPointer(0), numChannels > 1 ? buffer.getReadPointer(1) : buffer.getReadPointer(0) };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
shm.writeInput(inputPtrs, numChannels, numSamples);
|
|
|
|
| 137 |
auto* header = shm.getHeader();
|
| 138 |
+
header->blockSize.store((uint32_t)numSamples); header->numChannels.store((uint32_t)numChannels); header->state.store(kStateInputReady);
|
|
|
|
|
|
|
|
|
|
| 139 |
helper.getInputSem().signal();
|
|
|
|
| 140 |
if (helper.getOutputSem().wait(IPC::kProcessBlockTimeoutMs))
|
| 141 |
{
|
| 142 |
+
float* outputPtrs[2] = { buffer.getWritePointer(0), numChannels > 1 ? buffer.getWritePointer(1) : buffer.getWritePointer(0) };
|
|
|
|
|
|
|
|
|
|
| 143 |
shm.readOutput(outputPtrs, numChannels, numSamples);
|
| 144 |
header->state.store(kStateIdle);
|
| 145 |
analyser.processBlock(buffer);
|
| 146 |
}
|
| 147 |
}
|
| 148 |
|
| 149 |
+
juce::AudioProcessorEditor* PluginBridgeProcessor::createEditor() { return new PluginBridgeEditor(*this); }
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
// ββ In-process plugin βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 152 |
|
|
|
|
| 163 |
{
|
| 164 |
auto* format = inProcessFormatManager.getFormat(i);
|
| 165 |
bool isVST3 = pluginFile.getFileExtension() == ".vst3";
|
| 166 |
+
bool isAU = pluginFile.getFileExtension() == ".component";
|
| 167 |
if (isVST3 && format->getName() != "VST3") continue;
|
| 168 |
if (isAU && format->getName() != "AudioUnit") continue;
|
| 169 |
format->findAllTypesForFile(typesFound, pluginFile.getFullPathName());
|
| 170 |
if (typesFound.size() > 0) break;
|
| 171 |
}
|
|
|
|
| 172 |
if (typesFound.isEmpty()) { PBLOG("Processor", "loadInProcessPlugin: no types found"); return; }
|
| 173 |
|
| 174 |
juce::String err;
|
| 175 |
+
inProcessPlugin = inProcessFormatManager.createPluginInstance(*typesFound[0], currentSampleRate, currentBlockSize, err);
|
|
|
|
|
|
|
| 176 |
if (!inProcessPlugin) { PBLOG("Processor", "loadInProcessPlugin: FAILED β " + err); return; }
|
| 177 |
|
| 178 |
inProcessPlugin->enableAllBuses();
|
|
|
|
| 181 |
layout.outputBuses.add(juce::AudioChannelSet::stereo());
|
| 182 |
if (inProcessPlugin->checkBusesLayoutSupported(layout))
|
| 183 |
inProcessPlugin->setBusesLayout(layout);
|
|
|
|
| 184 |
if (currentSampleRate > 0 && currentBlockSize > 0)
|
| 185 |
inProcessPlugin->prepareToPlay(currentSampleRate, currentBlockSize);
|
| 186 |
|
| 187 |
+
// Sync state from Helper
|
| 188 |
auto response = helper.sendCommand("{\"cmd\":\"get_state\"}");
|
| 189 |
+
try { auto j = json::parse(response); if (j.value("ok",false)) {
|
| 190 |
+
auto stateB64 = juce::String(j["state"].get<std::string>());
|
| 191 |
+
if (stateB64.isNotEmpty()) { juce::MemoryBlock d; if (d.fromBase64Encoding(stateB64))
|
| 192 |
+
inProcessPlugin->setStateInformation(d.getData(), (int)d.getSize()); }
|
| 193 |
+
}} catch (...) {}
|
| 194 |
+
|
| 195 |
+
inProcessPlugin->addListener(this);
|
| 196 |
+
|
| 197 |
+
// ββ Wire MCP param callbacks to in-process plugin βββββββββββββββββββββββββ
|
| 198 |
+
mcpServer->searchParamCallback = [this](const std::string& keyword) -> std::string {
|
| 199 |
+
if (!inProcessPlugin) return "[]";
|
| 200 |
+
auto& params = inProcessPlugin->getParameters();
|
| 201 |
+
json results = json::array();
|
| 202 |
+
std::string kw = keyword;
|
| 203 |
+
std::transform(kw.begin(), kw.end(), kw.begin(), ::tolower);
|
| 204 |
+
for (int i = 0; i < params.size(); ++i)
|
| 205 |
{
|
| 206 |
+
std::string name = params[i]->getName(128).toStdString();
|
| 207 |
+
std::string nameLow = name;
|
| 208 |
+
std::transform(nameLow.begin(), nameLow.end(), nameLow.begin(), ::tolower);
|
| 209 |
+
if (kw.empty() || nameLow.find(kw) != std::string::npos)
|
| 210 |
{
|
| 211 |
+
results.push_back({{"i", i}, {"name", name},
|
| 212 |
+
{"value", std::round(params[i]->getValue() * 1000.0f) / 1000.0f},
|
| 213 |
+
{"display", params[i]->getText(params[i]->getValue(), 64).toStdString()}});
|
| 214 |
+
if (results.size() >= 50) break;
|
| 215 |
}
|
| 216 |
}
|
| 217 |
+
return results.dump();
|
| 218 |
+
};
|
| 219 |
|
| 220 |
+
mcpServer->getParamsCallback = [this](const std::vector<int>& ids) -> std::string {
|
| 221 |
+
if (!inProcessPlugin) return "{}";
|
| 222 |
+
auto& params = inProcessPlugin->getParameters();
|
| 223 |
+
json values = json::object();
|
| 224 |
+
for (int idx : ids)
|
| 225 |
+
if (idx >= 0 && idx < params.size())
|
| 226 |
+
values[std::to_string(idx)] = std::round(params[idx]->getValue() * 1000.0f) / 1000.0f;
|
| 227 |
+
return values.dump();
|
| 228 |
+
};
|
| 229 |
+
|
| 230 |
+
mcpServer->setParamsCallback = [this](const std::map<int,float>& values) -> bool {
|
| 231 |
+
if (!inProcessPlugin) return false;
|
| 232 |
+
juce::MessageManager::callAsync([this, values]() {
|
| 233 |
+
if (!inProcessPlugin) return;
|
| 234 |
+
auto& p = inProcessPlugin->getParameters();
|
| 235 |
+
for (auto& [idx, val] : values)
|
| 236 |
+
if (idx >= 0 && idx < p.size())
|
| 237 |
+
p[idx]->setValueNotifyingHost(juce::jlimit(0.0f, 1.0f, val));
|
| 238 |
+
});
|
| 239 |
+
return true;
|
| 240 |
+
};
|
| 241 |
+
|
| 242 |
+
PBLOG("Processor", "loadInProcessPlugin: SUCCESS β \"" + inProcessPlugin->getName() + "\" + MCP param callbacks wired");
|
| 243 |
}
|
| 244 |
|
| 245 |
void PluginBridgeProcessor::unloadInProcessPlugin()
|
| 246 |
{
|
| 247 |
+
// Clear MCP param callbacks BEFORE destroying plugin
|
| 248 |
+
mcpServer->searchParamCallback = nullptr;
|
| 249 |
+
mcpServer->getParamsCallback = nullptr;
|
| 250 |
+
mcpServer->setParamsCallback = nullptr;
|
| 251 |
+
|
| 252 |
if (inProcessPlugin)
|
| 253 |
{
|
| 254 |
PBLOG("Processor", "unloadInProcessPlugin: " + inProcessPlugin->getName());
|
|
|
|
| 271 |
void PluginBridgeProcessor::loadPlugin(const juce::String& path)
|
| 272 |
{
|
| 273 |
PBLOG("Processor", "loadPlugin: " + juce::File(path).getFileName());
|
| 274 |
+
if (blocklist.isBlocked(path)) { if (onLoadFailed) onLoadFailed("Not compatible (crashed previously)"); return; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
auto safety = safetyDB.getSafety(path);
|
| 277 |
+
PBLOG("Processor", "loadPlugin: safety = " + juce::String(safety == PluginSafetyDB::Safety::Safe ? "SAFE" : safety == PluginSafetyDB::Safety::Unsafe ? "UNSAFE" : "UNKNOWN"));
|
|
|
|
|
|
|
| 278 |
|
| 279 |
if (safety == PluginSafetyDB::Safety::Unknown)
|
| 280 |
{
|
| 281 |
if (onLoadFailed) onLoadFailed("Scanning plugin...");
|
| 282 |
+
std::thread([this, path]() {
|
|
|
|
| 283 |
bool safe = runPluginScan(path);
|
| 284 |
+
safetyDB.setSafety(path, safe ? PluginSafetyDB::Safety::Safe : PluginSafetyDB::Safety::Unsafe);
|
|
|
|
| 285 |
juce::MessageManager::callAsync([this, path]() { loadPlugin(path); });
|
| 286 |
}).detach();
|
| 287 |
return;
|
|
|
|
| 289 |
|
| 290 |
if (onPluginWillLoad) onPluginWillLoad();
|
| 291 |
|
| 292 |
+
if (!helper.isHelperRunning()) {
|
| 293 |
+
if (!helper.spawnHelper()) { if (onLoadFailed) onLoadFailed("Failed to start helper"); return; }
|
| 294 |
+
json cfg = {{"cmd","configure"},{"sampleRate",currentSampleRate},{"blockSize",currentBlockSize},{"channels",2}};
|
| 295 |
+
helper.sendCommand(cfg.dump());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
}
|
| 297 |
|
| 298 |
+
json loadCmd = {{"cmd","load"},{"path",path.toStdString()},{"sampleRate",currentSampleRate},{"blockSize",currentBlockSize}};
|
|
|
|
| 299 |
auto response = helper.sendCommand(loadCmd.dump());
|
| 300 |
|
| 301 |
+
try {
|
|
|
|
| 302 |
auto j = json::parse(response);
|
| 303 |
+
if (j.value("ok",false)) {
|
|
|
|
| 304 |
pluginLoaded = true;
|
| 305 |
pluginName = juce::String(j["name"].get<std::string>());
|
| 306 |
+
pluginParamCount = j.value("params",0);
|
| 307 |
pluginPath = path;
|
| 308 |
PBLOG("Processor", "loadPlugin: loaded \"" + pluginName + "\"");
|
|
|
|
| 309 |
if (safety == PluginSafetyDB::Safety::Safe)
|
| 310 |
+
juce::MessageManager::callAsync([this, path]() { loadInProcessPlugin(path); if (onPluginLoaded) onPluginLoaded(); });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
else
|
| 312 |
+
juce::MessageManager::callAsync([this]() { if (onPluginLoaded) onPluginLoaded(); });
|
| 313 |
+
} else {
|
| 314 |
+
pluginLoaded = false; pluginPath.clear(); blocklist.addBlocked(path);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
if (onLoadFailed) onLoadFailed("Not compatible: " + juce::String(j.value("error","unknown")));
|
| 316 |
}
|
| 317 |
+
} catch (...) {
|
| 318 |
+
pluginLoaded = false; pluginPath.clear(); blocklist.addBlocked(path);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
if (onLoadFailed) onLoadFailed("Helper communication error");
|
| 320 |
}
|
| 321 |
}
|
|
|
|
| 324 |
{
|
| 325 |
PBLOG("Processor", "unloadPlugin: \"" + pluginName + "\"");
|
| 326 |
if (helper.isHelperRunning()) helper.sendCommand("{\"cmd\":\"unload\"}");
|
| 327 |
+
pluginLoaded = false; pluginName.clear(); pluginPath.clear(); pluginParamCount = 0;
|
|
|
|
|
|
|
|
|
|
| 328 |
unloadInProcessPlugin();
|
| 329 |
+
if (onPluginUnloaded) juce::MessageManager::callAsync([this]() { if (onPluginUnloaded) onPluginUnloaded(); });
|
|
|
|
| 330 |
}
|
| 331 |
|
| 332 |
// ββ Scanner βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 333 |
|
| 334 |
+
static juce::String signalName(int sig) {
|
| 335 |
+
switch (sig) { case SIGSEGV: return "SIGSEGV"; case SIGABRT: return "SIGABRT"; case SIGBUS: return "SIGBUS"; case SIGILL: return "SIGILL"; case SIGFPE: return "SIGFPE"; case SIGKILL: return "SIGKILL"; default: return "signal " + juce::String(sig); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
}
|
| 337 |
|
| 338 |
+
static void writeScanLog(const juce::String& pluginPath, bool safe, const juce::String& reason, const juce::String& captured) {
|
| 339 |
+
auto logFile = juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory).getChildFile("PluginBridge").getChildFile("scan.log");
|
|
|
|
|
|
|
|
|
|
| 340 |
logFile.getParentDirectory().createDirectory();
|
| 341 |
auto ts = juce::Time::getCurrentTime().formatted("%Y-%m-%d %H:%M:%S");
|
| 342 |
+
juce::String entry; entry << "βββ\n" << ts << " " << juce::File(pluginPath).getFileName() << "\nResult: " << (safe ? "SAFE" : "UNSAFE");
|
|
|
|
|
|
|
| 343 |
if (!reason.isEmpty()) entry << " (" << reason << ")";
|
| 344 |
if (captured.isNotEmpty()) entry << "\nOutput: " << captured.substring(0, 500);
|
| 345 |
+
entry << "\n\n"; logFile.appendText(entry);
|
|
|
|
| 346 |
}
|
| 347 |
|
| 348 |
+
bool PluginBridgeProcessor::runPluginScan(const juce::String& path) {
|
| 349 |
+
auto helperApp = juce::File::getSpecialLocation(juce::File::currentExecutableFile).getParentDirectory().getParentDirectory().getChildFile("Resources").getChildFile("PluginBridgeHelper.app").getChildFile("Contents").getChildFile("MacOS").getChildFile(PluginBridgeConstants::kHelperBinaryName);
|
| 350 |
+
if (!helperApp.exists()) helperApp = juce::File::getSpecialLocation(juce::File::currentExecutableFile).getParentDirectory().getParentDirectory().getChildFile("Resources").getChildFile(PluginBridgeConstants::kHelperBinaryName);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
if (!helperApp.exists()) { writeScanLog(path, false, "Helper not found", {}); return false; }
|
| 352 |
+
int pipeFd[2]; if (pipe(pipeFd) != 0) return false; fcntl(pipeFd[0], F_SETFL, O_NONBLOCK);
|
| 353 |
+
auto hStr = helperApp.getFullPathName().toStdString(); auto pStr = path.toStdString();
|
| 354 |
+
posix_spawn_file_actions_t actions; posix_spawn_file_actions_init(&actions);
|
| 355 |
+
posix_spawn_file_actions_adddup2(&actions, pipeFd[1], STDERR_FILENO); posix_spawn_file_actions_addclose(&actions, pipeFd[0]);
|
| 356 |
+
pid_t pid; char* argv[] = { (char*)hStr.c_str(), (char*)"--scan", (char*)pStr.c_str(), nullptr };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
int sr = posix_spawn(&pid, hStr.c_str(), &actions, nullptr, argv, environ);
|
| 358 |
+
posix_spawn_file_actions_destroy(&actions); close(pipeFd[1]);
|
|
|
|
| 359 |
if (sr != 0) { close(pipeFd[0]); return false; }
|
|
|
|
| 360 |
bool timedOut = true; int status = 0;
|
| 361 |
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
| 362 |
+
while (std::chrono::steady_clock::now() < deadline) { if (waitpid(pid, &status, WNOHANG) == pid) { timedOut = false; break; } std::this_thread::sleep_for(std::chrono::milliseconds(100)); }
|
|
|
|
|
|
|
|
|
|
| 363 |
if (timedOut) { kill(pid, SIGKILL); waitpid(pid, &status, 0); }
|
| 364 |
+
juce::String captured; { char buf[4096]; ssize_t n; while ((n = read(pipeFd[0], buf, sizeof(buf)-1)) > 0) { buf[n]=0; captured += buf; } } close(pipeFd[0]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
bool safe = !timedOut && WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
| 366 |
+
juce::String reason = timedOut ? "Timeout" : WIFSIGNALED(status) ? signalName(WTERMSIG(status)) : (!safe ? "Exit " + juce::String(WEXITSTATUS(status)) : "");
|
| 367 |
+
writeScanLog(path, safe, reason, captured); return safe;
|
|
|
|
|
|
|
|
|
|
| 368 |
}
|
| 369 |
|
| 370 |
+
void PluginBridgeProcessor::startBackgroundScan() {
|
| 371 |
+
if (scanRunning.load()) return; scanRunning.store(true);
|
|
|
|
|
|
|
| 372 |
scanThread = std::thread([this]() {
|
| 373 |
auto files = getInstalledPluginFiles();
|
| 374 |
for (int i = 0; i < files.size() && scanRunning.load(); ++i) {
|
| 375 |
auto path = files[i].getFullPathName();
|
| 376 |
if (safetyDB.getSafety(path) != PluginSafetyDB::Safety::Unknown) continue;
|
| 377 |
+
while (helper.isHelperRunning() && scanRunning.load()) std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
|
|
|
| 378 |
if (!scanRunning.load()) break;
|
| 379 |
bool safe = runPluginScan(path);
|
| 380 |
safetyDB.setSafety(path, safe ? PluginSafetyDB::Safety::Safe : PluginSafetyDB::Safety::Unsafe);
|
| 381 |
+
if (onScanProgress) juce::MessageManager::callAsync([this, path, safe]() { if (onScanProgress) onScanProgress(path, safe); });
|
|
|
|
|
|
|
| 382 |
}
|
| 383 |
scanRunning.store(false);
|
| 384 |
});
|
| 385 |
}
|
| 386 |
|
| 387 |
+
void PluginBridgeProcessor::stopBackgroundScan() { scanRunning.store(false); if (scanThread.joinable()) scanThread.join(); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
|
| 389 |
// ββ Plugin List βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 390 |
|
| 391 |
+
juce::Array<juce::File> PluginBridgeProcessor::getInstalledPluginFiles() const {
|
| 392 |
+
juce::Array<juce::File> results; std::set<std::string> seen;
|
|
|
|
|
|
|
| 393 |
struct D { juce::File dir; const char* pat; };
|
| 394 |
+
std::vector<D> specs = { { juce::File("/Library/Audio/Plug-Ins/VST3"), "*.vst3" }, { juce::File::getSpecialLocation(juce::File::userHomeDirectory).getChildFile("Library/Audio/Plug-Ins/VST3"), "*.vst3" }, { juce::File("/Library/Audio/Plug-Ins/Components"), "*.component" }, { juce::File::getSpecialLocation(juce::File::userHomeDirectory).getChildFile("Library/Audio/Plug-Ins/Components"), "*.component" } };
|
| 395 |
+
for (auto& s : specs) { if (!s.dir.isDirectory()) continue; for (auto& f : s.dir.findChildFiles(juce::File::findDirectories, false, s.pat)) if (seen.insert(f.getFileNameWithoutExtension().toStdString()).second) results.add(f); }
|
| 396 |
+
results.sort(); return results;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
}
|
| 398 |
|
| 399 |
bool PluginBridgeProcessor::isBlocked(const juce::String& path) const { return blocklist.isBlocked(path); }
|
|
|
|
| 402 |
|
| 403 |
static juce::String cleanManufacturer(const juce::String& raw) {
|
| 404 |
auto s = raw.trim(); if (s.isEmpty() || s.containsOnly("0123456789.")) return {};
|
| 405 |
+
if (s.toLowerCase().startsWith("copyright")) { auto r = s.substring(9).trim(); if (r.startsWithIgnoreCase("(c)")) r = r.substring(3).trim(); else if (r.startsWith(juce::String::fromUTF8("\xc2\xa9"))) r = r.substring(juce::String::fromUTF8("\xc2\xa9").length()).trim(); auto sp = r.indexOfChar(' '); if (sp > 0) r = r.substring(sp+1).trim(); return r.isNotEmpty() ? r : juce::String(); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
return s;
|
| 407 |
}
|
| 408 |
+
static juce::String plistValue(const juce::String& t, const juce::String& k) { auto p = t.fromFirstOccurrenceOf(k, false, true); if (p.isEmpty()) return {}; return p.fromFirstOccurrenceOf("<string>", false, false).upToFirstOccurrenceOf("</string>", false, false).trim(); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
|
| 410 |
juce::String PluginBridgeProcessor::getPluginManufacturer(const juce::File& f) const {
|
| 411 |
auto pk = f.getFullPathName().toStdString();
|
| 412 |
+
{ std::lock_guard<std::mutex> l(manufacturerCacheMutex); auto it = manufacturerCache.find(pk); if (it != manufacturerCache.end()) return juce::String(it->second); }
|
|
|
|
| 413 |
juce::String mfr;
|
| 414 |
if (f.getFileExtension().toLowerCase() == ".vst3") {
|
| 415 |
auto ip = f.getChildFile("Contents").getChildFile("Info.plist");
|
| 416 |
+
if (ip.existsAsFile()) { auto t = ip.loadFileAsString(); mfr = cleanManufacturer(plistValue(t, "NSHumanReadableCopyright"));
|
| 417 |
+
if (mfr.isEmpty()) { auto is = plistValue(t, "CFBundleGetInfoString"); if (is.contains(",")) { auto bc = is.upToFirstOccurrenceOf(",",false,false).trim(); auto fw = bc.upToFirstOccurrenceOf(" ",false,false).trim(); if (fw.isNotEmpty() && !fw.containsOnly("0123456789.")) mfr = fw; } if (mfr.isEmpty()) mfr = cleanManufacturer(is); }
|
| 418 |
+
if (mfr.isEmpty()) { auto bid = plistValue(t, "CFBundleIdentifier"); auto segs = juce::StringArray::fromTokens(bid,".",""); if (segs.size()>=2) { auto sl = segs[1].trim(); if (sl.isNotEmpty()&&sl!="apple"&&sl!="com") mfr = sl.substring(0,1).toUpperCase()+sl.substring(1); } } }
|
| 419 |
+
if (mfr.isEmpty()) { auto mi = f.getChildFile("Contents").getChildFile("moduleinfo.json"); if (mi.existsAsFile()) { try { auto j = json::parse(mi.loadFileAsString().toStdString()); mfr = cleanManufacturer(juce::String(j.value("vendor",""))); } catch(...){} } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
}
|
| 421 |
+
if (mfr.isEmpty()) { auto n = f.getFileNameWithoutExtension(); auto fw = n.upToFirstOccurrenceOf(" ",false,false); mfr = (fw.isNotEmpty()&&!fw.containsOnly("0123456789.")) ? fw : n; }
|
|
|
|
|
|
|
| 422 |
if (mfr.isEmpty()) mfr = "Other";
|
| 423 |
std::lock_guard<std::mutex> l(manufacturerCacheMutex); manufacturerCache[pk] = mfr.toStdString(); return mfr;
|
| 424 |
}
|
| 425 |
|
| 426 |
// ββ State βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 427 |
|
| 428 |
+
void PluginBridgeProcessor::getStateInformation(juce::MemoryBlock& destData) {
|
| 429 |
+
juce::ValueTree state("PluginBridgeState"); state.setProperty("pluginPath", pluginPath, nullptr); state.setProperty("channelName", channelName, nullptr);
|
| 430 |
+
if (pluginLoaded && helper.isHelperRunning()) { auto r = helper.sendCommand("{\"cmd\":\"get_state\"}"); try { auto j = json::parse(r); if (j.value("ok",false)) state.setProperty("pluginState", juce::String(j["state"].get<std::string>()), nullptr); } catch (...) {} }
|
| 431 |
+
juce::MemoryOutputStream stream(destData, false); state.writeToStream(stream);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
}
|
| 433 |
|
| 434 |
+
void PluginBridgeProcessor::setStateInformation(const void* data, int sizeInBytes) {
|
| 435 |
+
auto state = juce::ValueTree::readFromData(data, (size_t)sizeInBytes); if (!state.isValid()) return;
|
| 436 |
+
auto savedChannel = state.getProperty("channelName", "Unnamed").toString(); setChannelName(savedChannel);
|
| 437 |
+
auto path = state.getProperty("pluginPath").toString(); if (path.isEmpty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
loadPlugin(path);
|
|
|
|
| 439 |
auto pluginState = state.getProperty("pluginState").toString();
|
| 440 |
+
if (pluginState.isNotEmpty() && helper.isHelperRunning()) { json cmd = {{"cmd","set_state"},{"state",pluginState.toStdString()}}; helper.sendCommand(cmd.dump()); }
|
|
|
|
|
|
|
|
|
|
| 441 |
}
|
| 442 |
|
| 443 |
// ββ MCP βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 444 |
|
| 445 |
bool PluginBridgeProcessor::isMcpServerRunning() const { return mcpServer && mcpServer->isRunning(); }
|
| 446 |
int PluginBridgeProcessor::getMcpServerPort() const { return mcpServer ? mcpServer->getPort() : 0; }
|
|
|
|
| 447 |
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { return new PluginBridgeProcessor(); }
|