Sprint 5: route audio through in-process plugin for SAFE plugins
Browse filesNEXT PUSH:
TASK: Pro-Q 4 spectrum analyzer fix β audio now flows through in-process instance
FILES CHANGING:
- PluginBridgeProcessor.cpp (processBlock routing + prepareToPlay in loadInProcessPlugin)
FILES NOT TOUCHING:
- PluginBridgeEditor.mm β
- PluginPickerComponent.mm β
- HelperPluginHost.mm β
- HelperConnection.cpp β
NEW FILES: none
APIS USED:
- inProcessPlugin->processBlock() β verified (AudioProcessor inheritance)
- inProcessPlugin->prepareToPlay() β verified safe (only SAFE plugins reach here)
Changes:
1. loadInProcessPlugin(): added prepareToPlay(currentSampleRate, currentBlockSize)
after setBusesLayout(), before state restore + addListener()
2. processBlock(): named MidiBuffer param, added in-process audio path:
if inProcessPlugin exists β processBlock directly (skips Helper shm)
else β existing Helper shm path unchanged
3. AudioAnalyser runs on the output regardless of which path is used"
|
@@ -31,8 +31,6 @@ PluginBridgeProcessor::PluginBridgeProcessor()
|
|
| 31 |
{
|
| 32 |
PBLOG("Processor", "β οΈ Helper crashed β clearing plugin state");
|
| 33 |
|
| 34 |
-
// Mark the crashing plugin as unsafe so it loads audio-only next time
|
| 35 |
-
// instead of respawning a Helper that will crash again.
|
| 36 |
if (pluginPath.isNotEmpty())
|
| 37 |
{
|
| 38 |
PBLOG("Processor", "β οΈ Marking \"" + pluginName + "\" as UNSAFE (Helper crash)");
|
|
@@ -62,7 +60,6 @@ PluginBridgeProcessor::PluginBridgeProcessor()
|
|
| 62 |
});
|
| 63 |
};
|
| 64 |
|
| 65 |
-
// Wire audio analysis to MCP get_analysis tool
|
| 66 |
mcpServer->getAnalysisCallback = [this]() { return analyser.getCompactAnalysis(); };
|
| 67 |
|
| 68 |
mcpServer->start();
|
|
@@ -86,6 +83,13 @@ void PluginBridgeProcessor::prepareToPlay(double sampleRate, int samplesPerBlock
|
|
| 86 |
|
| 87 |
analyser.prepareToPlay(sampleRate, samplesPerBlock);
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
if (helper.isHelperRunning())
|
| 90 |
{
|
| 91 |
json cmd = {{"cmd", "configure"},
|
|
@@ -106,14 +110,25 @@ void PluginBridgeProcessor::prepareToPlay(double sampleRate, int samplesPerBlock
|
|
| 106 |
|
| 107 |
void PluginBridgeProcessor::releaseResources() {}
|
| 108 |
|
| 109 |
-
void PluginBridgeProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer&)
|
| 110 |
{
|
| 111 |
juce::ScopedNoDenormals noDenormals;
|
| 112 |
|
| 113 |
for (auto i = getTotalNumInputChannels(); i < getTotalNumOutputChannels(); ++i)
|
| 114 |
buffer.clear(i, 0, buffer.getNumSamples());
|
| 115 |
|
| 116 |
-
if (!pluginLoaded
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
auto& shm = helper.getSharedAudio();
|
| 119 |
if (!shm.isValid()) return;
|
|
@@ -151,7 +166,7 @@ juce::AudioProcessorEditor* PluginBridgeProcessor::createEditor()
|
|
| 151 |
return new PluginBridgeEditor(*this);
|
| 152 |
}
|
| 153 |
|
| 154 |
-
// ββ In-process plugin (GUI
|
| 155 |
|
| 156 |
void PluginBridgeProcessor::loadInProcessPlugin(const juce::String& path)
|
| 157 |
{
|
|
@@ -195,11 +210,7 @@ void PluginBridgeProcessor::loadInProcessPlugin(const juce::String& path)
|
|
| 195 |
return;
|
| 196 |
}
|
| 197 |
|
| 198 |
-
//
|
| 199 |
-
// We do NOT call prepareToPlay() β the in-process plugin is GUI-only and never
|
| 200 |
-
// processes audio. Calling prepareToPlay() causes some plugins (iZotope Neutron,
|
| 201 |
-
// inter-plugin comms plugins) to spin up internal audio threads that then race
|
| 202 |
-
// with unloadInProcessPlugin() and crash the audio thread.
|
| 203 |
inProcessPlugin->enableAllBuses();
|
| 204 |
auto layout = juce::AudioProcessor::BusesLayout();
|
| 205 |
layout.inputBuses.add(juce::AudioChannelSet::stereo());
|
|
@@ -207,6 +218,11 @@ void PluginBridgeProcessor::loadInProcessPlugin(const juce::String& path)
|
|
| 207 |
if (inProcessPlugin->checkBusesLayoutSupported(layout))
|
| 208 |
inProcessPlugin->setBusesLayout(layout);
|
| 209 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
auto response = helper.sendCommand("{\"cmd\":\"get_state\"}");
|
| 211 |
try {
|
| 212 |
auto j = json::parse(response);
|
|
@@ -223,8 +239,8 @@ void PluginBridgeProcessor::loadInProcessPlugin(const juce::String& path)
|
|
| 223 |
} catch (...) {}
|
| 224 |
|
| 225 |
inProcessPlugin->addListener(this);
|
| 226 |
-
PBLOG("Processor", "loadInProcessPlugin: SUCCESS β \"" + inProcessPlugin->getName() + "\"");
|
| 227 |
-
DBG("PluginBridgeProcessor: in-process plugin loaded
|
| 228 |
}
|
| 229 |
|
| 230 |
void PluginBridgeProcessor::unloadInProcessPlugin()
|
|
@@ -266,7 +282,6 @@ void PluginBridgeProcessor::loadPlugin(const juce::String& path)
|
|
| 266 |
return;
|
| 267 |
}
|
| 268 |
|
| 269 |
-
// ββ Safety check ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 270 |
auto safety = safetyDB.getSafety(path);
|
| 271 |
PBLOG("Processor", juce::String("loadPlugin: safety = ")
|
| 272 |
+ (safety == PluginSafetyDB::Safety::Safe ? "SAFE"
|
|
@@ -286,15 +301,12 @@ void PluginBridgeProcessor::loadPlugin(const juce::String& path)
|
|
| 286 |
return;
|
| 287 |
}
|
| 288 |
|
| 289 |
-
// ββ Notify editor to detach BEFORE we tear down the old plugin ββββββββββββ
|
| 290 |
-
// This prevents use-after-free when switching between plugins.
|
| 291 |
if (onPluginWillLoad)
|
| 292 |
{
|
| 293 |
PBLOG("Processor", "loadPlugin: firing onPluginWillLoad β editor will detach old GUI");
|
| 294 |
onPluginWillLoad();
|
| 295 |
}
|
| 296 |
|
| 297 |
-
// ββ Start Helper ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 298 |
if (!helper.isHelperRunning())
|
| 299 |
{
|
| 300 |
PBLOG("Processor", "loadPlugin: Helper not running β spawning");
|
|
@@ -353,7 +365,7 @@ void PluginBridgeProcessor::loadPlugin(const juce::String& path)
|
|
| 353 |
PBLOG("Processor", "loadPlugin: β Helper refused load: "
|
| 354 |
+ juce::String(j.value("error", "unknown")));
|
| 355 |
pluginLoaded = false;
|
| 356 |
-
pluginPath.clear();
|
| 357 |
blocklist.addBlocked(path);
|
| 358 |
if (onLoadFailed)
|
| 359 |
onLoadFailed("Not compatible: " + juce::String(j.value("error", "unknown")));
|
|
@@ -363,7 +375,7 @@ void PluginBridgeProcessor::loadPlugin(const juce::String& path)
|
|
| 363 |
{
|
| 364 |
PBLOG("Processor", "loadPlugin: β exception parsing Helper response");
|
| 365 |
pluginLoaded = false;
|
| 366 |
-
pluginPath.clear();
|
| 367 |
blocklist.addBlocked(path);
|
| 368 |
if (onLoadFailed) onLoadFailed("Helper communication error");
|
| 369 |
}
|
|
@@ -389,9 +401,6 @@ void PluginBridgeProcessor::unloadPlugin()
|
|
| 389 |
}
|
| 390 |
|
| 391 |
// ββ Plugin Scanner ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 392 |
-
// Spawns Helper with --scan <path> in an isolated subprocess.
|
| 393 |
-
// stderr is piped back so we capture every step the scanner logged.
|
| 394 |
-
// If the plugin crashes the subprocess, waitpid() catches WTERMSIG. Ableton is safe.
|
| 395 |
|
| 396 |
static juce::String signalName(int sig)
|
| 397 |
{
|
|
@@ -407,14 +416,11 @@ static juce::String signalName(int sig)
|
|
| 407 |
}
|
| 408 |
}
|
| 409 |
|
| 410 |
-
static void writeScanLog(const juce::String& pluginPath,
|
| 411 |
-
|
| 412 |
-
const juce::String& reason,
|
| 413 |
-
const juce::String& captured)
|
| 414 |
{
|
| 415 |
auto logFile = juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory)
|
| 416 |
-
.getChildFile("PluginBridge")
|
| 417 |
-
.getChildFile("scan.log");
|
| 418 |
logFile.getParentDirectory().createDirectory();
|
| 419 |
|
| 420 |
auto ts = juce::Time::getCurrentTime().formatted("%Y-%m-%d %H:%M:%S");
|
|
@@ -425,39 +431,29 @@ static void writeScanLog(const juce::String& pluginPath,
|
|
| 425 |
<< ts << " " << name << "\n"
|
| 426 |
<< "Path : " << pluginPath << "\n"
|
| 427 |
<< "Result : " << (safe ? "SAFE β" : "UNSAFE β") << "\n";
|
| 428 |
-
|
| 429 |
-
if (!reason.isEmpty())
|
| 430 |
-
entry << "Reason : " << reason << "\n";
|
| 431 |
-
|
| 432 |
if (captured.isNotEmpty())
|
| 433 |
{
|
| 434 |
entry << "Scanner output:\n";
|
| 435 |
for (auto& line : juce::StringArray::fromLines(captured))
|
| 436 |
-
if (line.isNotEmpty())
|
| 437 |
-
entry << " " << line << "\n";
|
| 438 |
}
|
| 439 |
-
|
| 440 |
entry << "\n";
|
| 441 |
logFile.appendText(entry);
|
| 442 |
}
|
| 443 |
|
| 444 |
bool PluginBridgeProcessor::runPluginScan(const juce::String& path)
|
| 445 |
{
|
| 446 |
-
// Resolve Helper binary (same logic as HelperConnection::spawnHelper)
|
| 447 |
auto helperApp = juce::File::getSpecialLocation(juce::File::currentExecutableFile)
|
| 448 |
-
.getParentDirectory()
|
| 449 |
-
.
|
| 450 |
-
.getChildFile("
|
| 451 |
-
.getChildFile("PluginBridgeHelper.app")
|
| 452 |
-
.getChildFile("Contents")
|
| 453 |
-
.getChildFile("MacOS")
|
| 454 |
.getChildFile(PluginBridgeConstants::kHelperBinaryName);
|
| 455 |
|
| 456 |
if (!helperApp.exists())
|
| 457 |
{
|
| 458 |
helperApp = juce::File::getSpecialLocation(juce::File::currentExecutableFile)
|
| 459 |
-
.getParentDirectory()
|
| 460 |
-
.getParentDirectory()
|
| 461 |
.getChildFile("Resources")
|
| 462 |
.getChildFile(PluginBridgeConstants::kHelperBinaryName);
|
| 463 |
}
|
|
@@ -468,36 +464,25 @@ bool PluginBridgeProcessor::runPluginScan(const juce::String& path)
|
|
| 468 |
return false;
|
| 469 |
}
|
| 470 |
|
| 471 |
-
// Create pipe to capture scanner's stderr
|
| 472 |
int pipeFd[2];
|
| 473 |
-
if (pipe(pipeFd) != 0)
|
| 474 |
-
{
|
| 475 |
-
writeScanLog(path, false, "pipe() failed", {});
|
| 476 |
-
return false;
|
| 477 |
-
}
|
| 478 |
-
// Make read end non-blocking so we can drain it safely
|
| 479 |
fcntl(pipeFd[0], F_SETFL, O_NONBLOCK);
|
| 480 |
|
| 481 |
auto helperPathStr = helperApp.getFullPathName().toStdString();
|
| 482 |
auto pluginPathStr = path.toStdString();
|
| 483 |
|
| 484 |
-
// Redirect child stderr β pipe write end
|
| 485 |
posix_spawn_file_actions_t actions;
|
| 486 |
posix_spawn_file_actions_init(&actions);
|
| 487 |
posix_spawn_file_actions_adddup2(&actions, pipeFd[1], STDERR_FILENO);
|
| 488 |
posix_spawn_file_actions_addclose(&actions, pipeFd[0]);
|
| 489 |
|
| 490 |
pid_t pid;
|
| 491 |
-
char* argv[] = {
|
| 492 |
-
|
| 493 |
-
(char*)"--scan",
|
| 494 |
-
(char*)pluginPathStr.c_str(),
|
| 495 |
-
nullptr
|
| 496 |
-
};
|
| 497 |
|
| 498 |
int spawnResult = posix_spawn(&pid, helperPathStr.c_str(), &actions, nullptr, argv, environ);
|
| 499 |
posix_spawn_file_actions_destroy(&actions);
|
| 500 |
-
close(pipeFd[1]);
|
| 501 |
|
| 502 |
if (spawnResult != 0)
|
| 503 |
{
|
|
@@ -506,10 +491,8 @@ bool PluginBridgeProcessor::runPluginScan(const juce::String& path)
|
|
| 506 |
return false;
|
| 507 |
}
|
| 508 |
|
| 509 |
-
// Poll with 10s timeout
|
| 510 |
bool timedOut = true;
|
| 511 |
-
int
|
| 512 |
-
|
| 513 |
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
| 514 |
while (std::chrono::steady_clock::now() < deadline)
|
| 515 |
{
|
|
@@ -518,54 +501,25 @@ bool PluginBridgeProcessor::runPluginScan(const juce::String& path)
|
|
| 518 |
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
| 519 |
}
|
| 520 |
|
| 521 |
-
if (timedOut)
|
| 522 |
-
{
|
| 523 |
-
kill(pid, SIGKILL);
|
| 524 |
-
waitpid(pid, &finalStatus, 0);
|
| 525 |
-
}
|
| 526 |
|
| 527 |
-
// Drain pipe β read whatever the scanner managed to write before dying
|
| 528 |
juce::String captured;
|
| 529 |
-
{
|
| 530 |
-
|
| 531 |
-
ssize_t n;
|
| 532 |
-
while ((n = read(pipeFd[0], buf, sizeof(buf) - 1)) > 0)
|
| 533 |
-
{
|
| 534 |
-
buf[n] = '\0';
|
| 535 |
-
captured += buf;
|
| 536 |
-
}
|
| 537 |
-
}
|
| 538 |
close(pipeFd[0]);
|
| 539 |
|
| 540 |
-
// Interpret result
|
| 541 |
bool safe = false;
|
| 542 |
juce::String reason;
|
| 543 |
-
|
| 544 |
-
if (
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
}
|
| 548 |
-
else if (WIFEXITED(finalStatus))
|
| 549 |
-
{
|
| 550 |
-
int code = WEXITSTATUS(finalStatus);
|
| 551 |
-
safe = (code == 0);
|
| 552 |
-
reason = safe ? "" : "Exited with code " + juce::String(code);
|
| 553 |
-
}
|
| 554 |
-
else if (WIFSIGNALED(finalStatus))
|
| 555 |
-
{
|
| 556 |
-
reason = "Crashed: " + signalName(WTERMSIG(finalStatus));
|
| 557 |
-
}
|
| 558 |
|
| 559 |
writeScanLog(path, safe, reason, captured);
|
| 560 |
-
DBG("runPluginScan: " + juce::File(path).getFileName()
|
| 561 |
-
+ (safe ? " β SAFE" : " β UNSAFE") + (reason.isEmpty() ? "" : " (" + reason + ")"));
|
| 562 |
-
|
| 563 |
return safe;
|
| 564 |
}
|
| 565 |
|
| 566 |
// ββ Background Scanner ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 567 |
-
// Scans all installed plugins whose safety is Unknown, one at a time.
|
| 568 |
-
// Results are stored in safetyDB and broadcast via onScanProgress.
|
| 569 |
|
| 570 |
void PluginBridgeProcessor::startBackgroundScan()
|
| 571 |
{
|
|
@@ -575,45 +529,30 @@ void PluginBridgeProcessor::startBackgroundScan()
|
|
| 575 |
scanThread = std::thread([this]()
|
| 576 |
{
|
| 577 |
auto files = getInstalledPluginFiles();
|
| 578 |
-
|
| 579 |
for (int i = 0; i < files.size() && scanRunning.load(); ++i)
|
| 580 |
{
|
| 581 |
auto path = files[i].getFullPathName();
|
| 582 |
-
if (safetyDB.getSafety(path) != PluginSafetyDB::Safety::Unknown)
|
| 583 |
-
continue;
|
| 584 |
|
| 585 |
-
// CRITICAL: wait while the Helper is actively running a plugin.
|
| 586 |
-
// The scan subprocess and the live Helper share the same shared-memory
|
| 587 |
-
// name. Running both simultaneously causes the scan to crash β
|
| 588 |
-
// valid plugins get wrongly stamped UNSAFE.
|
| 589 |
while (helper.isHelperRunning() && scanRunning.load())
|
| 590 |
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
| 591 |
-
|
| 592 |
if (!scanRunning.load()) break;
|
| 593 |
|
| 594 |
bool safe = runPluginScan(path);
|
| 595 |
safetyDB.setSafety(path, safe ? PluginSafetyDB::Safety::Safe
|
| 596 |
: PluginSafetyDB::Safety::Unsafe);
|
| 597 |
-
|
| 598 |
if (onScanProgress)
|
| 599 |
-
{
|
| 600 |
juce::MessageManager::callAsync([this, path, safe]()
|
| 601 |
-
|
| 602 |
-
if (onScanProgress) onScanProgress(path, safe);
|
| 603 |
-
});
|
| 604 |
-
}
|
| 605 |
}
|
| 606 |
-
|
| 607 |
scanRunning.store(false);
|
| 608 |
-
DBG("PluginBridgeProcessor: background scan complete");
|
| 609 |
});
|
| 610 |
}
|
| 611 |
|
| 612 |
void PluginBridgeProcessor::stopBackgroundScan()
|
| 613 |
{
|
| 614 |
scanRunning.store(false);
|
| 615 |
-
if (scanThread.joinable())
|
| 616 |
-
scanThread.join();
|
| 617 |
}
|
| 618 |
|
| 619 |
// ββ Plugin List βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -621,19 +560,16 @@ void PluginBridgeProcessor::stopBackgroundScan()
|
|
| 621 |
juce::Array<juce::File> PluginBridgeProcessor::getInstalledPluginFiles() const
|
| 622 |
{
|
| 623 |
juce::Array<juce::File> results;
|
| 624 |
-
std::set<std::string> seen;
|
| 625 |
|
| 626 |
-
// Priority order: system VST3 β user VST3 β system AU β user AU.
|
| 627 |
-
// System paths match the existing safety DB entries, so VST3 is preferred
|
| 628 |
-
// and same-named AU duplicates are automatically skipped.
|
| 629 |
struct DirSpec { juce::File dir; const char* pattern; };
|
| 630 |
std::vector<DirSpec> specs = {
|
| 631 |
-
{ juce::File("/Library/Audio/Plug-Ins/VST3"),
|
| 632 |
{ juce::File::getSpecialLocation(juce::File::userHomeDirectory)
|
| 633 |
-
.getChildFile("Library/Audio/Plug-Ins/VST3"),
|
| 634 |
-
{ juce::File("/Library/Audio/Plug-Ins/Components"),
|
| 635 |
{ juce::File::getSpecialLocation(juce::File::userHomeDirectory)
|
| 636 |
-
.getChildFile("Library/Audio/Plug-Ins/Components"),
|
| 637 |
};
|
| 638 |
|
| 639 |
for (auto& spec : specs)
|
|
@@ -643,8 +579,7 @@ juce::Array<juce::File> PluginBridgeProcessor::getInstalledPluginFiles() const
|
|
| 643 |
for (auto& f : files)
|
| 644 |
{
|
| 645 |
auto stem = f.getFileNameWithoutExtension().toStdString();
|
| 646 |
-
if (seen.insert(stem).second)
|
| 647 |
-
results.add(f);
|
| 648 |
}
|
| 649 |
}
|
| 650 |
|
|
@@ -652,149 +587,102 @@ juce::Array<juce::File> PluginBridgeProcessor::getInstalledPluginFiles() const
|
|
| 652 |
return results;
|
| 653 |
}
|
| 654 |
|
| 655 |
-
bool PluginBridgeProcessor::isBlocked(const juce::String& path) const
|
| 656 |
-
{
|
| 657 |
-
return blocklist.isBlocked(path);
|
| 658 |
-
}
|
| 659 |
|
| 660 |
// ββ Manufacturer name βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 661 |
-
// Fast: reads VST3 Info.plist (no plugin loading). Cached after first call.
|
| 662 |
|
| 663 |
-
// Strip copyright preamble and return the actual company name.
|
| 664 |
-
// Handles: "Copyright Β© 2002-2025 FabFilter" β "FabFilter"
|
| 665 |
-
// "Copyright (C) 2017-2021 Soundtheory" β "Soundtheory"
|
| 666 |
-
// "16.7.33.200" (version as vendor) β "" (trigger fallback)
|
| 667 |
static juce::String cleanManufacturer(const juce::String& raw)
|
| 668 |
{
|
| 669 |
auto s = raw.trim();
|
| 670 |
if (s.isEmpty()) return {};
|
| 671 |
-
|
| 672 |
-
// Pure version number used as vendor (e.g. "16.7.33.200") β reject
|
| 673 |
if (s.containsOnly("0123456789.")) return {};
|
| 674 |
|
| 675 |
auto lower = s.toLowerCase();
|
| 676 |
if (lower.startsWith("copyright"))
|
| 677 |
{
|
| 678 |
-
// Skip the word "Copyright"
|
| 679 |
auto rest = s.substring(9).trim();
|
| 680 |
-
|
| 681 |
-
if (rest.startsWithIgnoreCase("(c)"))
|
| 682 |
-
rest = rest.substring(3).trim();
|
| 683 |
-
// Skip "Β©" (UTF-8 0xC2 0xA9, or the Unicode character directly)
|
| 684 |
else if (rest.startsWith(juce::String::fromUTF8("\xc2\xa9")))
|
| 685 |
rest = rest.substring(juce::String::fromUTF8("\xc2\xa9").length()).trim();
|
| 686 |
else if (rest.startsWith(juce::CharPointer_UTF8("\xc2\xa9")))
|
| 687 |
rest = rest.substring(1).trim();
|
| 688 |
-
// Skip year token(s): "2025" or "2002-2025"
|
| 689 |
auto spacePos = rest.indexOfChar(' ');
|
| 690 |
-
if (spacePos > 0)
|
| 691 |
-
rest = rest.substring(spacePos + 1).trim();
|
| 692 |
if (rest.isNotEmpty()) return rest;
|
| 693 |
return {};
|
| 694 |
}
|
| 695 |
-
|
| 696 |
return s;
|
| 697 |
}
|
| 698 |
|
| 699 |
-
// Extract the plist <string> value for a given key from raw plist XML text.
|
| 700 |
static juce::String plistValue(const juce::String& text, const juce::String& key)
|
| 701 |
{
|
| 702 |
auto pos = text.fromFirstOccurrenceOf(key, false, true);
|
| 703 |
if (pos.isEmpty()) return {};
|
| 704 |
return pos.fromFirstOccurrenceOf("<string>", false, false)
|
| 705 |
-
.upToFirstOccurrenceOf("</string>", false, false)
|
| 706 |
-
.trim();
|
| 707 |
}
|
| 708 |
|
| 709 |
juce::String PluginBridgeProcessor::getPluginManufacturer(const juce::File& f) const
|
| 710 |
{
|
| 711 |
auto pathKey = f.getFullPathName().toStdString();
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
auto it = manufacturerCache.find(pathKey);
|
| 716 |
-
if (it != manufacturerCache.end())
|
| 717 |
-
return juce::String(it->second);
|
| 718 |
-
}
|
| 719 |
|
| 720 |
juce::String mfr;
|
| 721 |
|
| 722 |
-
// ββ VST3: Info.plist fields (no plugin loading needed) βββββββββββββββββββ
|
| 723 |
if (f.getFileExtension().toLowerCase() == ".vst3")
|
| 724 |
{
|
| 725 |
auto infoPlist = f.getChildFile("Contents").getChildFile("Info.plist");
|
| 726 |
if (infoPlist.existsAsFile())
|
| 727 |
{
|
| 728 |
auto text = infoPlist.loadFileAsString();
|
| 729 |
-
|
| 730 |
-
// 1) NSHumanReadableCopyright β "Copyright Β© 2002-2025 FabFilter" β "FabFilter"
|
| 731 |
auto copyright = plistValue(text, "NSHumanReadableCopyright");
|
| 732 |
mfr = cleanManufacturer(copyright);
|
| 733 |
|
| 734 |
-
// 2) CFBundleGetInfoString β "FabFilter Pro-Q 4.02, Copyright Β© β¦"
|
| 735 |
-
// Take the part before the comma (= "FabFilter Pro-Q 4.02"),
|
| 736 |
-
// then its first space-delimited word (= "FabFilter")
|
| 737 |
if (mfr.isEmpty())
|
| 738 |
{
|
| 739 |
auto infoStr = plistValue(text, "CFBundleGetInfoString");
|
| 740 |
if (infoStr.contains(","))
|
| 741 |
{
|
| 742 |
auto beforeComma = infoStr.upToFirstOccurrenceOf(",", false, false).trim();
|
| 743 |
-
// First word of "FabFilter Pro-Q 4.02" is "FabFilter"
|
| 744 |
auto first = beforeComma.upToFirstOccurrenceOf(" ", false, false).trim();
|
| 745 |
-
if (first.isNotEmpty() && !first.containsOnly("0123456789."))
|
| 746 |
-
mfr = first;
|
| 747 |
}
|
| 748 |
-
if (mfr.isEmpty())
|
| 749 |
-
mfr = cleanManufacturer(infoStr);
|
| 750 |
}
|
| 751 |
|
| 752 |
-
// 3) CFBundleIdentifier β "com.fabfilter.Pro-Q.4" β second segment
|
| 753 |
if (mfr.isEmpty())
|
| 754 |
{
|
| 755 |
auto bid = plistValue(text, "CFBundleIdentifier");
|
| 756 |
-
// "com.fabfilter.Pro-Q.4" β segments: com | fabfilter | β¦
|
| 757 |
auto segs = juce::StringArray::fromTokens(bid, ".", "");
|
| 758 |
if (segs.size() >= 2)
|
| 759 |
{
|
| 760 |
-
// segment[1] is usually the company slug β capitalise it
|
| 761 |
auto slug = segs[1].trim();
|
| 762 |
if (slug.isNotEmpty() && slug != "apple" && slug != "com")
|
| 763 |
-
{
|
| 764 |
-
slug = slug.substring(0, 1).toUpperCase() + slug.substring(1);
|
| 765 |
-
mfr = slug;
|
| 766 |
-
}
|
| 767 |
}
|
| 768 |
}
|
| 769 |
}
|
| 770 |
|
| 771 |
-
// 4) moduleinfo.json "vendor" (VST3 SDK 3.7+) β clean copyright if needed
|
| 772 |
if (mfr.isEmpty())
|
| 773 |
{
|
| 774 |
auto moduleInfo = f.getChildFile("Contents").getChildFile("moduleinfo.json");
|
| 775 |
if (moduleInfo.existsAsFile())
|
| 776 |
{
|
| 777 |
-
try
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
auto vendor = cleanManufacturer(juce::String(j.value("vendor", "")));
|
| 781 |
-
if (vendor.isNotEmpty()) mfr = vendor;
|
| 782 |
-
}
|
| 783 |
-
catch (...) {}
|
| 784 |
}
|
| 785 |
}
|
| 786 |
}
|
| 787 |
|
| 788 |
-
// ββ Filename heuristic (AU or VST3 fallback) ββββββββββββββββββββββββββββββ
|
| 789 |
-
// "FabFilter Pro-Q 4" β "FabFilter", "ValhallaDelay" β "ValhallaDelay"
|
| 790 |
if (mfr.isEmpty())
|
| 791 |
{
|
| 792 |
auto name = f.getFileNameWithoutExtension();
|
| 793 |
auto first = name.upToFirstOccurrenceOf(" ", false, false);
|
| 794 |
-
if (first.isNotEmpty() && !first.containsOnly("0123456789."))
|
| 795 |
-
|
| 796 |
-
else
|
| 797 |
-
mfr = name;
|
| 798 |
}
|
| 799 |
|
| 800 |
if (mfr.isEmpty()) mfr = "Other";
|
|
@@ -814,11 +702,9 @@ void PluginBridgeProcessor::getStateInformation(juce::MemoryBlock& destData)
|
|
| 814 |
if (pluginLoaded && helper.isHelperRunning())
|
| 815 |
{
|
| 816 |
auto response = helper.sendCommand("{\"cmd\":\"get_state\"}");
|
| 817 |
-
try {
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
state.setProperty("pluginState",
|
| 821 |
-
juce::String(j["state"].get<std::string>()), nullptr);
|
| 822 |
} catch (...) {}
|
| 823 |
}
|
| 824 |
|
|
|
|
| 31 |
{
|
| 32 |
PBLOG("Processor", "β οΈ Helper crashed β clearing plugin state");
|
| 33 |
|
|
|
|
|
|
|
| 34 |
if (pluginPath.isNotEmpty())
|
| 35 |
{
|
| 36 |
PBLOG("Processor", "β οΈ Marking \"" + pluginName + "\" as UNSAFE (Helper crash)");
|
|
|
|
| 60 |
});
|
| 61 |
};
|
| 62 |
|
|
|
|
| 63 |
mcpServer->getAnalysisCallback = [this]() { return analyser.getCompactAnalysis(); };
|
| 64 |
|
| 65 |
mcpServer->start();
|
|
|
|
| 83 |
|
| 84 |
analyser.prepareToPlay(sampleRate, samplesPerBlock);
|
| 85 |
|
| 86 |
+
// If in-process plugin is already loaded, update its sample rate/block size too
|
| 87 |
+
if (inProcessPlugin)
|
| 88 |
+
{
|
| 89 |
+
inProcessPlugin->releaseResources();
|
| 90 |
+
inProcessPlugin->prepareToPlay(sampleRate, samplesPerBlock);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
if (helper.isHelperRunning())
|
| 94 |
{
|
| 95 |
json cmd = {{"cmd", "configure"},
|
|
|
|
| 110 |
|
| 111 |
void PluginBridgeProcessor::releaseResources() {}
|
| 112 |
|
| 113 |
+
void PluginBridgeProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
|
| 114 |
{
|
| 115 |
juce::ScopedNoDenormals noDenormals;
|
| 116 |
|
| 117 |
for (auto i = getTotalNumInputChannels(); i < getTotalNumOutputChannels(); ++i)
|
| 118 |
buffer.clear(i, 0, buffer.getNumSamples());
|
| 119 |
|
| 120 |
+
if (!pluginLoaded) return;
|
| 121 |
+
|
| 122 |
+
// ββ SAFE plugin path: audio goes through in-process instance ββββββββββββββ
|
| 123 |
+
if (inProcessPlugin != nullptr)
|
| 124 |
+
{
|
| 125 |
+
inProcessPlugin->processBlock(buffer, midiMessages);
|
| 126 |
+
analyser.processBlock(buffer);
|
| 127 |
+
return;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
// ββ UNSAFE plugin path: audio goes through Helper via shared memory βββββββ
|
| 131 |
+
if (!helper.isHelperRunning()) return;
|
| 132 |
|
| 133 |
auto& shm = helper.getSharedAudio();
|
| 134 |
if (!shm.isValid()) return;
|
|
|
|
| 166 |
return new PluginBridgeEditor(*this);
|
| 167 |
}
|
| 168 |
|
| 169 |
+
// ββ In-process plugin (now handles BOTH audio + GUI for SAFE plugins) βββββββββ
|
| 170 |
|
| 171 |
void PluginBridgeProcessor::loadInProcessPlugin(const juce::String& path)
|
| 172 |
{
|
|
|
|
| 210 |
return;
|
| 211 |
}
|
| 212 |
|
| 213 |
+
// Configure buses
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
inProcessPlugin->enableAllBuses();
|
| 215 |
auto layout = juce::AudioProcessor::BusesLayout();
|
| 216 |
layout.inputBuses.add(juce::AudioChannelSet::stereo());
|
|
|
|
| 218 |
if (inProcessPlugin->checkBusesLayoutSupported(layout))
|
| 219 |
inProcessPlugin->setBusesLayout(layout);
|
| 220 |
|
| 221 |
+
// Prepare for audio processing β safe for SAFE plugins (unsafe ones never reach here)
|
| 222 |
+
if (currentSampleRate > 0 && currentBlockSize > 0)
|
| 223 |
+
inProcessPlugin->prepareToPlay(currentSampleRate, currentBlockSize);
|
| 224 |
+
|
| 225 |
+
// Sync state from Helper (if it has state loaded already)
|
| 226 |
auto response = helper.sendCommand("{\"cmd\":\"get_state\"}");
|
| 227 |
try {
|
| 228 |
auto j = json::parse(response);
|
|
|
|
| 239 |
} catch (...) {}
|
| 240 |
|
| 241 |
inProcessPlugin->addListener(this);
|
| 242 |
+
PBLOG("Processor", "loadInProcessPlugin: SUCCESS β \"" + inProcessPlugin->getName() + "\" (audio+GUI)");
|
| 243 |
+
DBG("PluginBridgeProcessor: in-process plugin loaded: " + inProcessPlugin->getName());
|
| 244 |
}
|
| 245 |
|
| 246 |
void PluginBridgeProcessor::unloadInProcessPlugin()
|
|
|
|
| 282 |
return;
|
| 283 |
}
|
| 284 |
|
|
|
|
| 285 |
auto safety = safetyDB.getSafety(path);
|
| 286 |
PBLOG("Processor", juce::String("loadPlugin: safety = ")
|
| 287 |
+ (safety == PluginSafetyDB::Safety::Safe ? "SAFE"
|
|
|
|
| 301 |
return;
|
| 302 |
}
|
| 303 |
|
|
|
|
|
|
|
| 304 |
if (onPluginWillLoad)
|
| 305 |
{
|
| 306 |
PBLOG("Processor", "loadPlugin: firing onPluginWillLoad β editor will detach old GUI");
|
| 307 |
onPluginWillLoad();
|
| 308 |
}
|
| 309 |
|
|
|
|
| 310 |
if (!helper.isHelperRunning())
|
| 311 |
{
|
| 312 |
PBLOG("Processor", "loadPlugin: Helper not running β spawning");
|
|
|
|
| 365 |
PBLOG("Processor", "loadPlugin: β Helper refused load: "
|
| 366 |
+ juce::String(j.value("error", "unknown")));
|
| 367 |
pluginLoaded = false;
|
| 368 |
+
pluginPath.clear();
|
| 369 |
blocklist.addBlocked(path);
|
| 370 |
if (onLoadFailed)
|
| 371 |
onLoadFailed("Not compatible: " + juce::String(j.value("error", "unknown")));
|
|
|
|
| 375 |
{
|
| 376 |
PBLOG("Processor", "loadPlugin: β exception parsing Helper response");
|
| 377 |
pluginLoaded = false;
|
| 378 |
+
pluginPath.clear();
|
| 379 |
blocklist.addBlocked(path);
|
| 380 |
if (onLoadFailed) onLoadFailed("Helper communication error");
|
| 381 |
}
|
|
|
|
| 401 |
}
|
| 402 |
|
| 403 |
// ββ Plugin Scanner ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
| 404 |
|
| 405 |
static juce::String signalName(int sig)
|
| 406 |
{
|
|
|
|
| 416 |
}
|
| 417 |
}
|
| 418 |
|
| 419 |
+
static void writeScanLog(const juce::String& pluginPath, bool safe,
|
| 420 |
+
const juce::String& reason, const juce::String& captured)
|
|
|
|
|
|
|
| 421 |
{
|
| 422 |
auto logFile = juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory)
|
| 423 |
+
.getChildFile("PluginBridge").getChildFile("scan.log");
|
|
|
|
| 424 |
logFile.getParentDirectory().createDirectory();
|
| 425 |
|
| 426 |
auto ts = juce::Time::getCurrentTime().formatted("%Y-%m-%d %H:%M:%S");
|
|
|
|
| 431 |
<< ts << " " << name << "\n"
|
| 432 |
<< "Path : " << pluginPath << "\n"
|
| 433 |
<< "Result : " << (safe ? "SAFE β" : "UNSAFE β") << "\n";
|
| 434 |
+
if (!reason.isEmpty()) entry << "Reason : " << reason << "\n";
|
|
|
|
|
|
|
|
|
|
| 435 |
if (captured.isNotEmpty())
|
| 436 |
{
|
| 437 |
entry << "Scanner output:\n";
|
| 438 |
for (auto& line : juce::StringArray::fromLines(captured))
|
| 439 |
+
if (line.isNotEmpty()) entry << " " << line << "\n";
|
|
|
|
| 440 |
}
|
|
|
|
| 441 |
entry << "\n";
|
| 442 |
logFile.appendText(entry);
|
| 443 |
}
|
| 444 |
|
| 445 |
bool PluginBridgeProcessor::runPluginScan(const juce::String& path)
|
| 446 |
{
|
|
|
|
| 447 |
auto helperApp = juce::File::getSpecialLocation(juce::File::currentExecutableFile)
|
| 448 |
+
.getParentDirectory().getParentDirectory()
|
| 449 |
+
.getChildFile("Resources").getChildFile("PluginBridgeHelper.app")
|
| 450 |
+
.getChildFile("Contents").getChildFile("MacOS")
|
|
|
|
|
|
|
|
|
|
| 451 |
.getChildFile(PluginBridgeConstants::kHelperBinaryName);
|
| 452 |
|
| 453 |
if (!helperApp.exists())
|
| 454 |
{
|
| 455 |
helperApp = juce::File::getSpecialLocation(juce::File::currentExecutableFile)
|
| 456 |
+
.getParentDirectory().getParentDirectory()
|
|
|
|
| 457 |
.getChildFile("Resources")
|
| 458 |
.getChildFile(PluginBridgeConstants::kHelperBinaryName);
|
| 459 |
}
|
|
|
|
| 464 |
return false;
|
| 465 |
}
|
| 466 |
|
|
|
|
| 467 |
int pipeFd[2];
|
| 468 |
+
if (pipe(pipeFd) != 0) { writeScanLog(path, false, "pipe() failed", {}); return false; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
fcntl(pipeFd[0], F_SETFL, O_NONBLOCK);
|
| 470 |
|
| 471 |
auto helperPathStr = helperApp.getFullPathName().toStdString();
|
| 472 |
auto pluginPathStr = path.toStdString();
|
| 473 |
|
|
|
|
| 474 |
posix_spawn_file_actions_t actions;
|
| 475 |
posix_spawn_file_actions_init(&actions);
|
| 476 |
posix_spawn_file_actions_adddup2(&actions, pipeFd[1], STDERR_FILENO);
|
| 477 |
posix_spawn_file_actions_addclose(&actions, pipeFd[0]);
|
| 478 |
|
| 479 |
pid_t pid;
|
| 480 |
+
char* argv[] = { (char*)helperPathStr.c_str(), (char*)"--scan",
|
| 481 |
+
(char*)pluginPathStr.c_str(), nullptr };
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
|
| 483 |
int spawnResult = posix_spawn(&pid, helperPathStr.c_str(), &actions, nullptr, argv, environ);
|
| 484 |
posix_spawn_file_actions_destroy(&actions);
|
| 485 |
+
close(pipeFd[1]);
|
| 486 |
|
| 487 |
if (spawnResult != 0)
|
| 488 |
{
|
|
|
|
| 491 |
return false;
|
| 492 |
}
|
| 493 |
|
|
|
|
| 494 |
bool timedOut = true;
|
| 495 |
+
int finalStatus = 0;
|
|
|
|
| 496 |
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
| 497 |
while (std::chrono::steady_clock::now() < deadline)
|
| 498 |
{
|
|
|
|
| 501 |
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
| 502 |
}
|
| 503 |
|
| 504 |
+
if (timedOut) { kill(pid, SIGKILL); waitpid(pid, &finalStatus, 0); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
|
|
|
|
| 506 |
juce::String captured;
|
| 507 |
+
{ char buf[8192]; ssize_t n;
|
| 508 |
+
while ((n = read(pipeFd[0], buf, sizeof(buf)-1)) > 0) { buf[n]='\0'; captured += buf; } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
close(pipeFd[0]);
|
| 510 |
|
|
|
|
| 511 |
bool safe = false;
|
| 512 |
juce::String reason;
|
| 513 |
+
if (timedOut) reason = "Timed out after 10s";
|
| 514 |
+
else if (WIFEXITED(finalStatus)) { int code = WEXITSTATUS(finalStatus); safe = (code==0);
|
| 515 |
+
reason = safe ? "" : "Exited with code " + juce::String(code); }
|
| 516 |
+
else if (WIFSIGNALED(finalStatus)) reason = "Crashed: " + signalName(WTERMSIG(finalStatus));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
|
| 518 |
writeScanLog(path, safe, reason, captured);
|
|
|
|
|
|
|
|
|
|
| 519 |
return safe;
|
| 520 |
}
|
| 521 |
|
| 522 |
// ββ Background Scanner ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
| 523 |
|
| 524 |
void PluginBridgeProcessor::startBackgroundScan()
|
| 525 |
{
|
|
|
|
| 529 |
scanThread = std::thread([this]()
|
| 530 |
{
|
| 531 |
auto files = getInstalledPluginFiles();
|
|
|
|
| 532 |
for (int i = 0; i < files.size() && scanRunning.load(); ++i)
|
| 533 |
{
|
| 534 |
auto path = files[i].getFullPathName();
|
| 535 |
+
if (safetyDB.getSafety(path) != PluginSafetyDB::Safety::Unknown) continue;
|
|
|
|
| 536 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
while (helper.isHelperRunning() && scanRunning.load())
|
| 538 |
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
|
|
|
| 539 |
if (!scanRunning.load()) break;
|
| 540 |
|
| 541 |
bool safe = runPluginScan(path);
|
| 542 |
safetyDB.setSafety(path, safe ? PluginSafetyDB::Safety::Safe
|
| 543 |
: PluginSafetyDB::Safety::Unsafe);
|
|
|
|
| 544 |
if (onScanProgress)
|
|
|
|
| 545 |
juce::MessageManager::callAsync([this, path, safe]()
|
| 546 |
+
{ if (onScanProgress) onScanProgress(path, safe); });
|
|
|
|
|
|
|
|
|
|
| 547 |
}
|
|
|
|
| 548 |
scanRunning.store(false);
|
|
|
|
| 549 |
});
|
| 550 |
}
|
| 551 |
|
| 552 |
void PluginBridgeProcessor::stopBackgroundScan()
|
| 553 |
{
|
| 554 |
scanRunning.store(false);
|
| 555 |
+
if (scanThread.joinable()) scanThread.join();
|
|
|
|
| 556 |
}
|
| 557 |
|
| 558 |
// ββ Plugin List βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 560 |
juce::Array<juce::File> PluginBridgeProcessor::getInstalledPluginFiles() const
|
| 561 |
{
|
| 562 |
juce::Array<juce::File> results;
|
| 563 |
+
std::set<std::string> seen;
|
| 564 |
|
|
|
|
|
|
|
|
|
|
| 565 |
struct DirSpec { juce::File dir; const char* pattern; };
|
| 566 |
std::vector<DirSpec> specs = {
|
| 567 |
+
{ juce::File("/Library/Audio/Plug-Ins/VST3"), "*.vst3" },
|
| 568 |
{ juce::File::getSpecialLocation(juce::File::userHomeDirectory)
|
| 569 |
+
.getChildFile("Library/Audio/Plug-Ins/VST3"), "*.vst3" },
|
| 570 |
+
{ juce::File("/Library/Audio/Plug-Ins/Components"), "*.component" },
|
| 571 |
{ juce::File::getSpecialLocation(juce::File::userHomeDirectory)
|
| 572 |
+
.getChildFile("Library/Audio/Plug-Ins/Components"), "*.component" }
|
| 573 |
};
|
| 574 |
|
| 575 |
for (auto& spec : specs)
|
|
|
|
| 579 |
for (auto& f : files)
|
| 580 |
{
|
| 581 |
auto stem = f.getFileNameWithoutExtension().toStdString();
|
| 582 |
+
if (seen.insert(stem).second) results.add(f);
|
|
|
|
| 583 |
}
|
| 584 |
}
|
| 585 |
|
|
|
|
| 587 |
return results;
|
| 588 |
}
|
| 589 |
|
| 590 |
+
bool PluginBridgeProcessor::isBlocked(const juce::String& path) const { return blocklist.isBlocked(path); }
|
|
|
|
|
|
|
|
|
|
| 591 |
|
| 592 |
// ββ Manufacturer name βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 593 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 594 |
static juce::String cleanManufacturer(const juce::String& raw)
|
| 595 |
{
|
| 596 |
auto s = raw.trim();
|
| 597 |
if (s.isEmpty()) return {};
|
|
|
|
|
|
|
| 598 |
if (s.containsOnly("0123456789.")) return {};
|
| 599 |
|
| 600 |
auto lower = s.toLowerCase();
|
| 601 |
if (lower.startsWith("copyright"))
|
| 602 |
{
|
|
|
|
| 603 |
auto rest = s.substring(9).trim();
|
| 604 |
+
if (rest.startsWithIgnoreCase("(c)")) rest = rest.substring(3).trim();
|
|
|
|
|
|
|
|
|
|
| 605 |
else if (rest.startsWith(juce::String::fromUTF8("\xc2\xa9")))
|
| 606 |
rest = rest.substring(juce::String::fromUTF8("\xc2\xa9").length()).trim();
|
| 607 |
else if (rest.startsWith(juce::CharPointer_UTF8("\xc2\xa9")))
|
| 608 |
rest = rest.substring(1).trim();
|
|
|
|
| 609 |
auto spacePos = rest.indexOfChar(' ');
|
| 610 |
+
if (spacePos > 0) rest = rest.substring(spacePos + 1).trim();
|
|
|
|
| 611 |
if (rest.isNotEmpty()) return rest;
|
| 612 |
return {};
|
| 613 |
}
|
|
|
|
| 614 |
return s;
|
| 615 |
}
|
| 616 |
|
|
|
|
| 617 |
static juce::String plistValue(const juce::String& text, const juce::String& key)
|
| 618 |
{
|
| 619 |
auto pos = text.fromFirstOccurrenceOf(key, false, true);
|
| 620 |
if (pos.isEmpty()) return {};
|
| 621 |
return pos.fromFirstOccurrenceOf("<string>", false, false)
|
| 622 |
+
.upToFirstOccurrenceOf("</string>", false, false).trim();
|
|
|
|
| 623 |
}
|
| 624 |
|
| 625 |
juce::String PluginBridgeProcessor::getPluginManufacturer(const juce::File& f) const
|
| 626 |
{
|
| 627 |
auto pathKey = f.getFullPathName().toStdString();
|
| 628 |
+
{ std::lock_guard<std::mutex> lock(manufacturerCacheMutex);
|
| 629 |
+
auto it = manufacturerCache.find(pathKey);
|
| 630 |
+
if (it != manufacturerCache.end()) return juce::String(it->second); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 631 |
|
| 632 |
juce::String mfr;
|
| 633 |
|
|
|
|
| 634 |
if (f.getFileExtension().toLowerCase() == ".vst3")
|
| 635 |
{
|
| 636 |
auto infoPlist = f.getChildFile("Contents").getChildFile("Info.plist");
|
| 637 |
if (infoPlist.existsAsFile())
|
| 638 |
{
|
| 639 |
auto text = infoPlist.loadFileAsString();
|
|
|
|
|
|
|
| 640 |
auto copyright = plistValue(text, "NSHumanReadableCopyright");
|
| 641 |
mfr = cleanManufacturer(copyright);
|
| 642 |
|
|
|
|
|
|
|
|
|
|
| 643 |
if (mfr.isEmpty())
|
| 644 |
{
|
| 645 |
auto infoStr = plistValue(text, "CFBundleGetInfoString");
|
| 646 |
if (infoStr.contains(","))
|
| 647 |
{
|
| 648 |
auto beforeComma = infoStr.upToFirstOccurrenceOf(",", false, false).trim();
|
|
|
|
| 649 |
auto first = beforeComma.upToFirstOccurrenceOf(" ", false, false).trim();
|
| 650 |
+
if (first.isNotEmpty() && !first.containsOnly("0123456789.")) mfr = first;
|
|
|
|
| 651 |
}
|
| 652 |
+
if (mfr.isEmpty()) mfr = cleanManufacturer(infoStr);
|
|
|
|
| 653 |
}
|
| 654 |
|
|
|
|
| 655 |
if (mfr.isEmpty())
|
| 656 |
{
|
| 657 |
auto bid = plistValue(text, "CFBundleIdentifier");
|
|
|
|
| 658 |
auto segs = juce::StringArray::fromTokens(bid, ".", "");
|
| 659 |
if (segs.size() >= 2)
|
| 660 |
{
|
|
|
|
| 661 |
auto slug = segs[1].trim();
|
| 662 |
if (slug.isNotEmpty() && slug != "apple" && slug != "com")
|
| 663 |
+
{ slug = slug.substring(0,1).toUpperCase() + slug.substring(1); mfr = slug; }
|
|
|
|
|
|
|
|
|
|
| 664 |
}
|
| 665 |
}
|
| 666 |
}
|
| 667 |
|
|
|
|
| 668 |
if (mfr.isEmpty())
|
| 669 |
{
|
| 670 |
auto moduleInfo = f.getChildFile("Contents").getChildFile("moduleinfo.json");
|
| 671 |
if (moduleInfo.existsAsFile())
|
| 672 |
{
|
| 673 |
+
try { auto j = json::parse(moduleInfo.loadFileAsString().toStdString());
|
| 674 |
+
auto vendor = cleanManufacturer(juce::String(j.value("vendor","")));
|
| 675 |
+
if (vendor.isNotEmpty()) mfr = vendor; } catch (...) {}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 676 |
}
|
| 677 |
}
|
| 678 |
}
|
| 679 |
|
|
|
|
|
|
|
| 680 |
if (mfr.isEmpty())
|
| 681 |
{
|
| 682 |
auto name = f.getFileNameWithoutExtension();
|
| 683 |
auto first = name.upToFirstOccurrenceOf(" ", false, false);
|
| 684 |
+
if (first.isNotEmpty() && !first.containsOnly("0123456789.")) mfr = first;
|
| 685 |
+
else mfr = name;
|
|
|
|
|
|
|
| 686 |
}
|
| 687 |
|
| 688 |
if (mfr.isEmpty()) mfr = "Other";
|
|
|
|
| 702 |
if (pluginLoaded && helper.isHelperRunning())
|
| 703 |
{
|
| 704 |
auto response = helper.sendCommand("{\"cmd\":\"get_state\"}");
|
| 705 |
+
try { auto j = json::parse(response);
|
| 706 |
+
if (j.value("ok",false))
|
| 707 |
+
state.setProperty("pluginState", juce::String(j["state"].get<std::string>()), nullptr);
|
|
|
|
|
|
|
| 708 |
} catch (...) {}
|
| 709 |
}
|
| 710 |
|