RAM2118 commited on
Commit
225322d
Β·
verified Β·
1 Parent(s): 0a63fb2

Sprint 6: wire channelName, registry, and auto-port into Processor.cpp

Browse files

Changes from v0.5.3:
1. Constructor: McpServer(helper) β€” no port, auto-assigned
2. After start(): wire channelNameCallback + register to registry
3. Destructor: unregister from registry
4. New method: setChannelName()
5. getStateInformation: save channelName
6. setStateInformation: restore channelName"

Files changed (1) hide show
  1. Source/Plugin/PluginBridgeProcessor.cpp +142 -281
Source/Plugin/PluginBridgeProcessor.cpp CHANGED
@@ -2,6 +2,7 @@
2
  #include "PluginBridgeEditor.h"
3
  #include "IPCProtocol.h"
4
  #include "PBLog.h"
 
5
 
6
  #include <sys/wait.h>
7
  #include <spawn.h>
@@ -19,7 +20,7 @@ PluginBridgeProcessor::PluginBridgeProcessor()
19
  : AudioProcessor(BusesProperties()
20
  .withInput("Input", juce::AudioChannelSet::stereo(), true)
21
  .withOutput("Output", juce::AudioChannelSet::stereo(), true)),
22
- mcpServer(std::make_unique<McpServer>(PluginBridgeConstants::kDefaultMcpPort, helper))
23
  {
24
  PBLog::startSession("Processor");
25
  PBLOG("Processor", "Constructor β€” PluginBridgeProcessor created");
@@ -30,18 +31,15 @@ PluginBridgeProcessor::PluginBridgeProcessor()
30
  helper.onHelperCrashed = [this]()
31
  {
32
  PBLOG("Processor", "⚠️ Helper crashed β€” clearing plugin state");
33
-
34
  if (pluginPath.isNotEmpty())
35
  {
36
  PBLOG("Processor", "⚠️ Marking \"" + pluginName + "\" as UNSAFE (Helper crash)");
37
  safetyDB.setSafety(pluginPath, PluginSafetyDB::Safety::Unsafe);
38
  }
39
-
40
  pluginLoaded = false;
41
  pluginName.clear();
42
  pluginParamCount = 0;
43
  unloadInProcessPlugin();
44
- DBG("PluginBridgeProcessor: Helper crashed");
45
  };
46
 
47
  helper.onParamChanged = [this](int idx, float val)
@@ -60,9 +58,17 @@ PluginBridgeProcessor::PluginBridgeProcessor()
60
  });
61
  };
62
 
 
63
  mcpServer->getAnalysisCallback = [this]() { return analyser.getCompactAnalysis(); };
 
64
 
65
  mcpServer->start();
 
 
 
 
 
 
66
  startBackgroundScan();
67
  }
68
 
@@ -70,12 +76,31 @@ PluginBridgeProcessor::~PluginBridgeProcessor()
70
  {
71
  PBLOG("Processor", "Destructor β€” tearing down");
72
  stopBackgroundScan();
 
 
 
 
73
  mcpServer->stop();
74
  unloadInProcessPlugin();
75
  helper.killHelper();
76
  PBLOG("Processor", "Destructor β€” done");
77
  }
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  void PluginBridgeProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
80
  {
81
  currentSampleRate = sampleRate;
@@ -83,7 +108,6 @@ void PluginBridgeProcessor::prepareToPlay(double sampleRate, int samplesPerBlock
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();
@@ -119,7 +143,6 @@ void PluginBridgeProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce:
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);
@@ -127,7 +150,6 @@ void PluginBridgeProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce:
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();
@@ -166,7 +188,7 @@ juce::AudioProcessorEditor* PluginBridgeProcessor::createEditor()
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
  {
@@ -174,11 +196,7 @@ void PluginBridgeProcessor::loadInProcessPlugin(const juce::String& path)
174
  unloadInProcessPlugin();
175
 
176
  juce::File pluginFile(path);
177
- if (!pluginFile.exists())
178
- {
179
- PBLOG("Processor", "loadInProcessPlugin: file not found β€” abort");
180
- return;
181
- }
182
 
183
  juce::OwnedArray<juce::PluginDescription> typesFound;
184
  for (int i = 0; i < inProcessFormatManager.getNumFormats(); ++i)
@@ -186,31 +204,20 @@ void PluginBridgeProcessor::loadInProcessPlugin(const juce::String& path)
186
  auto* format = inProcessFormatManager.getFormat(i);
187
  bool isVST3 = pluginFile.getFileExtension() == ".vst3";
188
  bool isAU = pluginFile.getFileExtension() == ".component";
189
- if (isVST3 && format->getName() != "VST3") continue;
190
- if (isAU && format->getName() != "AudioUnit") continue;
191
  format->findAllTypesForFile(typesFound, pluginFile.getFullPathName());
192
  if (typesFound.size() > 0) break;
193
  }
194
 
195
- if (typesFound.isEmpty())
196
- {
197
- PBLOG("Processor", "loadInProcessPlugin: no types found");
198
- return;
199
- }
200
 
201
- PBLOG("Processor", "loadInProcessPlugin: creating instance \"" + typesFound[0]->name + "\"");
202
  juce::String err;
203
  inProcessPlugin = inProcessFormatManager.createPluginInstance(
204
  *typesFound[0], currentSampleRate, currentBlockSize, err);
205
 
206
- if (!inProcessPlugin)
207
- {
208
- PBLOG("Processor", "loadInProcessPlugin: createPluginInstance FAILED β€” " + err);
209
- DBG("PluginBridgeProcessor: in-process load failed: " + err);
210
- return;
211
- }
212
 
213
- // Configure buses
214
  inProcessPlugin->enableAllBuses();
215
  auto layout = juce::AudioProcessor::BusesLayout();
216
  layout.inputBuses.add(juce::AudioChannelSet::stereo());
@@ -218,11 +225,9 @@ void PluginBridgeProcessor::loadInProcessPlugin(const juce::String& path)
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,23 +244,17 @@ void PluginBridgeProcessor::loadInProcessPlugin(const juce::String& path)
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()
247
  {
248
  if (inProcessPlugin)
249
  {
250
- PBLOG("Processor", "unloadInProcessPlugin: removing \"" + inProcessPlugin->getName() + "\"");
251
  inProcessPlugin->removeListener(this);
252
  inProcessPlugin->releaseResources();
253
  inProcessPlugin.reset();
254
- PBLOG("Processor", "unloadInProcessPlugin: done");
255
- }
256
- else
257
- {
258
- PBLOG("Processor", "unloadInProcessPlugin: nothing to unload");
259
  }
260
  }
261
 
@@ -263,7 +262,6 @@ void PluginBridgeProcessor::audioProcessorParameterChanged(juce::AudioProcessor*
263
  {
264
  if (suppressParamSync.load()) return;
265
  if (!helper.isHelperRunning()) return;
266
-
267
  auto cmdStr = json({{"cmd","set_params"},{"values",{{std::to_string(paramIdx), value}}}}).dump();
268
  std::thread([this, cmdStr]() { helper.sendCommand(cmdStr); }).detach();
269
  }
@@ -272,21 +270,18 @@ void PluginBridgeProcessor::audioProcessorParameterChanged(juce::AudioProcessor*
272
 
273
  void PluginBridgeProcessor::loadPlugin(const juce::String& path)
274
  {
275
- PBLOG("Processor", "loadPlugin: " + juce::File(path).getFileName()
276
- + " (currently loaded: " + (pluginLoaded ? pluginName : "none") + ")");
277
 
278
  if (blocklist.isBlocked(path))
279
  {
280
- PBLOG("Processor", "loadPlugin: BLOCKED β€” skipping");
281
  if (onLoadFailed) onLoadFailed("Not compatible (crashed previously)");
282
  return;
283
  }
284
 
285
  auto safety = safetyDB.getSafety(path);
286
- PBLOG("Processor", juce::String("loadPlugin: safety = ")
287
- + (safety == PluginSafetyDB::Safety::Safe ? "SAFE"
288
- : safety == PluginSafetyDB::Safety::Unsafe ? "UNSAFE"
289
- : "UNKNOWN"));
290
 
291
  if (safety == PluginSafetyDB::Safety::Unknown)
292
  {
@@ -301,79 +296,59 @@ void PluginBridgeProcessor::loadPlugin(const juce::String& path)
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");
313
  if (!helper.spawnHelper())
314
  {
315
- PBLOG("Processor", "loadPlugin: ❌ spawnHelper FAILED");
316
  if (onLoadFailed) onLoadFailed("Failed to start helper process");
317
  return;
318
  }
319
- PBLOG("Processor", "loadPlugin: Helper spawned OK");
320
-
321
  json configCmd = {{"cmd","configure"},{"sampleRate",currentSampleRate},
322
  {"blockSize",currentBlockSize},{"channels",2}};
323
  helper.sendCommand(configCmd.dump());
324
  }
325
 
326
- PBLOG("Processor", "loadPlugin: sending load cmd to Helper...");
327
  json loadCmd = {{"cmd","load"},{"path",path.toStdString()},
328
  {"sampleRate",currentSampleRate},{"blockSize",currentBlockSize}};
329
  auto response = helper.sendCommand(loadCmd.dump());
330
- PBLOG("Processor", "loadPlugin: Helper response: " + juce::String(response));
331
 
332
  try
333
  {
334
  auto j = json::parse(response);
335
  if (j.value("ok", false))
336
  {
337
- pluginLoaded = true;
338
- pluginName = juce::String(j["name"].get<std::string>());
339
  pluginParamCount = j.value("params", 0);
340
- pluginPath = path;
341
- PBLOG("Processor", "loadPlugin: Helper loaded \"" + pluginName
342
- + "\" (" + juce::String(pluginParamCount) + " params)");
343
 
344
  if (safety == PluginSafetyDB::Safety::Safe)
345
  {
346
  juce::MessageManager::callAsync([this, path]()
347
  {
348
- PBLOG("Processor", "loadPlugin: [msg thread] calling loadInProcessPlugin");
349
  loadInProcessPlugin(path);
350
- PBLOG("Processor", "loadPlugin: [msg thread] firing onPluginLoaded");
351
  if (onPluginLoaded) onPluginLoaded();
352
  });
353
  }
354
  else
355
  {
356
- PBLOG("Processor", "loadPlugin: UNSAFE β€” skipping in-process load, audio-only mode");
357
  juce::MessageManager::callAsync([this]()
358
- {
359
- if (onPluginLoaded) onPluginLoaded();
360
- });
361
  }
362
  }
363
  else
364
  {
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")));
372
  }
373
  }
374
  catch (...)
375
  {
376
- PBLOG("Processor", "loadPlugin: ❌ exception parsing Helper response");
377
  pluginLoaded = false;
378
  pluginPath.clear();
379
  blocklist.addBlocked(path);
@@ -384,35 +359,25 @@ void PluginBridgeProcessor::loadPlugin(const juce::String& path)
384
  void PluginBridgeProcessor::unloadPlugin()
385
  {
386
  PBLOG("Processor", "unloadPlugin: \"" + pluginName + "\"");
387
-
388
- if (helper.isHelperRunning())
389
- helper.sendCommand("{\"cmd\":\"unload\"}");
390
-
391
  pluginLoaded = false;
392
  pluginName.clear();
393
  pluginPath.clear();
394
  pluginParamCount = 0;
395
-
396
  unloadInProcessPlugin();
397
- PBLOG("Processor", "unloadPlugin: done");
398
-
399
  if (onPluginUnloaded)
400
  juce::MessageManager::callAsync([this]() { if (onPluginUnloaded) onPluginUnloaded(); });
401
  }
402
 
403
- // ── Plugin Scanner ────────────────────────────────────────────────────────────
404
 
405
  static juce::String signalName(int sig)
406
  {
407
- switch (sig)
408
- {
409
- case SIGSEGV: return "SIGSEGV (segmentation fault)";
410
- case SIGABRT: return "SIGABRT (abort)";
411
- case SIGBUS: return "SIGBUS (bus error)";
412
- case SIGILL: return "SIGILL (illegal instruction)";
413
- case SIGFPE: return "SIGFPE (floating point exception)";
414
- case SIGKILL: return "SIGKILL (killed β€” timeout)";
415
- default: return "signal " + juce::String(sig);
416
  }
417
  }
418
 
@@ -422,23 +387,13 @@ static void writeScanLog(const juce::String& pluginPath, bool safe,
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");
427
- auto name = juce::File(pluginPath).getFileName();
428
-
429
  juce::String entry;
430
- entry << "────────────────────────────────────────\n"
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
 
@@ -449,98 +404,65 @@ bool PluginBridgeProcessor::runPluginScan(const juce::String& path)
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
- }
460
-
461
- if (!helperApp.exists())
462
- {
463
- writeScanLog(path, false, "Helper binary not found", {});
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
- {
489
- close(pipeFd[0]);
490
- writeScanLog(path, false, "posix_spawn failed: " + juce::String(strerror(spawnResult)), {});
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
- {
499
- pid_t result = waitpid(pid, &finalStatus, WNOHANG);
500
- if (result == pid) { timedOut = false; break; }
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
  {
526
  if (scanRunning.load()) return;
527
  scanRunning.store(true);
528
-
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); });
@@ -561,135 +483,75 @@ 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)
576
- {
577
- if (!spec.dir.isDirectory()) continue;
578
- auto files = spec.dir.findChildFiles(juce::File::findDirectories, false, spec.pattern);
579
- for (auto& f : files)
580
- {
581
- auto stem = f.getFileNameWithoutExtension().toStdString();
582
- if (seen.insert(stem).second) results.add(f);
583
- }
584
  }
585
-
586
  results.sort();
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";
689
-
690
- std::lock_guard<std::mutex> lock(manufacturerCacheMutex);
691
- manufacturerCache[pathKey] = mfr.toStdString();
692
- return mfr;
693
  }
694
 
695
  // ── State ─────────────────────────────────────────────────────────────────────
@@ -698,13 +560,12 @@ void PluginBridgeProcessor::getStateInformation(juce::MemoryBlock& destData)
698
  {
699
  juce::ValueTree state("PluginBridgeState");
700
  state.setProperty("pluginPath", pluginPath, nullptr);
 
701
 
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
 
@@ -717,14 +578,17 @@ void PluginBridgeProcessor::setStateInformation(const void* data, int sizeInByte
717
  auto state = juce::ValueTree::readFromData(data, (size_t)sizeInBytes);
718
  if (!state.isValid()) return;
719
 
 
 
 
 
720
  auto path = state.getProperty("pluginPath").toString();
721
  if (path.isEmpty()) return;
722
 
723
  loadPlugin(path);
724
 
725
  auto pluginState = state.getProperty("pluginState").toString();
726
- if (pluginState.isNotEmpty() && helper.isHelperRunning())
727
- {
728
  json cmd = {{"cmd","set_state"},{"state",pluginState.toStdString()}};
729
  helper.sendCommand(cmd.dump());
730
  }
@@ -735,7 +599,4 @@ void PluginBridgeProcessor::setStateInformation(const void* data, int sizeInByte
735
  bool PluginBridgeProcessor::isMcpServerRunning() const { return mcpServer && mcpServer->isRunning(); }
736
  int PluginBridgeProcessor::getMcpServerPort() const { return mcpServer ? mcpServer->getPort() : 0; }
737
 
738
- juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
739
- {
740
- return new PluginBridgeProcessor();
741
- }
 
2
  #include "PluginBridgeEditor.h"
3
  #include "IPCProtocol.h"
4
  #include "PBLog.h"
5
+ #include "InstanceRegistry.h"
6
 
7
  #include <sys/wait.h>
8
  #include <spawn.h>
 
20
  : AudioProcessor(BusesProperties()
21
  .withInput("Input", juce::AudioChannelSet::stereo(), true)
22
  .withOutput("Output", juce::AudioChannelSet::stereo(), true)),
23
+ mcpServer(std::make_unique<McpServer>(helper))
24
  {
25
  PBLog::startSession("Processor");
26
  PBLOG("Processor", "Constructor β€” PluginBridgeProcessor created");
 
31
  helper.onHelperCrashed = [this]()
32
  {
33
  PBLOG("Processor", "⚠️ Helper crashed β€” clearing plugin state");
 
34
  if (pluginPath.isNotEmpty())
35
  {
36
  PBLOG("Processor", "⚠️ Marking \"" + pluginName + "\" as UNSAFE (Helper crash)");
37
  safetyDB.setSafety(pluginPath, PluginSafetyDB::Safety::Unsafe);
38
  }
 
39
  pluginLoaded = false;
40
  pluginName.clear();
41
  pluginParamCount = 0;
42
  unloadInProcessPlugin();
 
43
  };
44
 
45
  helper.onParamChanged = [this](int idx, float val)
 
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);
71
+
72
  startBackgroundScan();
73
  }
74
 
 
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;
 
108
 
109
  analyser.prepareToPlay(sampleRate, samplesPerBlock);
110
 
 
111
  if (inProcessPlugin)
112
  {
113
  inProcessPlugin->releaseResources();
 
143
 
144
  if (!pluginLoaded) return;
145
 
 
146
  if (inProcessPlugin != nullptr)
147
  {
148
  inProcessPlugin->processBlock(buffer, midiMessages);
 
150
  return;
151
  }
152
 
 
153
  if (!helper.isHelperRunning()) return;
154
 
155
  auto& shm = helper.getSharedAudio();
 
188
  return new PluginBridgeEditor(*this);
189
  }
190
 
191
+ // ── In-process plugin ─────────────────────────────────────────────────────────
192
 
193
  void PluginBridgeProcessor::loadInProcessPlugin(const juce::String& path)
194
  {
 
196
  unloadInProcessPlugin();
197
 
198
  juce::File pluginFile(path);
199
+ if (!pluginFile.exists()) { PBLOG("Processor", "loadInProcessPlugin: file not found"); return; }
 
 
 
 
200
 
201
  juce::OwnedArray<juce::PluginDescription> typesFound;
202
  for (int i = 0; i < inProcessFormatManager.getNumFormats(); ++i)
 
204
  auto* format = inProcessFormatManager.getFormat(i);
205
  bool isVST3 = pluginFile.getFileExtension() == ".vst3";
206
  bool isAU = pluginFile.getFileExtension() == ".component";
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();
222
  auto layout = juce::AudioProcessor::BusesLayout();
223
  layout.inputBuses.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 j = json::parse(response);
 
244
  } catch (...) {}
245
 
246
  inProcessPlugin->addListener(this);
247
+ PBLOG("Processor", "loadInProcessPlugin: SUCCESS β€” \"" + inProcessPlugin->getName() + "\"");
 
248
  }
249
 
250
  void PluginBridgeProcessor::unloadInProcessPlugin()
251
  {
252
  if (inProcessPlugin)
253
  {
254
+ PBLOG("Processor", "unloadInProcessPlugin: " + inProcessPlugin->getName());
255
  inProcessPlugin->removeListener(this);
256
  inProcessPlugin->releaseResources();
257
  inProcessPlugin.reset();
 
 
 
 
 
258
  }
259
  }
260
 
 
262
  {
263
  if (suppressParamSync.load()) return;
264
  if (!helper.isHelperRunning()) return;
 
265
  auto cmdStr = json({{"cmd","set_params"},{"values",{{std::to_string(paramIdx), value}}}}).dump();
266
  std::thread([this, cmdStr]() { helper.sendCommand(cmdStr); }).detach();
267
  }
 
270
 
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
  {
 
296
  return;
297
  }
298
 
299
+ if (onPluginWillLoad) onPluginWillLoad();
 
 
 
 
300
 
301
  if (!helper.isHelperRunning())
302
  {
 
303
  if (!helper.spawnHelper())
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", false))
321
  {
322
+ pluginLoaded = true;
323
+ pluginName = juce::String(j["name"].get<std::string>());
324
  pluginParamCount = j.value("params", 0);
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
  juce::MessageManager::callAsync([this]()
339
+ { if (onPluginLoaded) onPluginLoaded(); });
 
 
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
  catch (...)
351
  {
 
352
  pluginLoaded = false;
353
  pluginPath.clear();
354
  blocklist.addBlocked(path);
 
359
  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
 
 
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
 
 
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
  int pipeFd[2];
414
+ if (pipe(pipeFd) != 0) return false;
415
  fcntl(pipeFd[0], F_SETFL, O_NONBLOCK);
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
+ WIFSIGNALED(status) ? signalName(WTERMSIG(status)) :
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); });
 
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
  { juce::File("/Library/Audio/Plug-Ins/VST3"), "*.vst3" },
489
+ { juce::File::getSpecialLocation(juce::File::userHomeDirectory).getChildFile("Library/Audio/Plug-Ins/VST3"), "*.vst3" },
 
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); }
503
 
504
+ // ── Manufacturer ──────────────────────────────────────────────────────────────
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 = ip.loadFileAsString();
534
+ mfr = cleanManufacturer(plistValue(t, "NSHumanReadableCopyright"));
535
+ if (mfr.isEmpty()) { auto is = plistValue(t, "CFBundleGetInfoString");
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 ─────────────────────────────────────────────────────────────────────
 
560
  {
561
  juce::ValueTree state("PluginBridgeState");
562
  state.setProperty("pluginPath", pluginPath, nullptr);
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
 
 
578
  auto state = juce::ValueTree::readFromData(data, (size_t)sizeInBytes);
579
  if (!state.isValid()) return;
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
  }
 
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(); }