RAM2118 commited on
Commit
4d61c1f
Β·
verified Β·
1 Parent(s): 6f6ce59

Sprint 7: wire param callbacks in loadInProcessPlugin, clear in unloadInProcessPlugin

Browse files

When 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.

Files changed (1) hide show
  1. 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", "configure"},
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
- int numChannels = buffer.getNumChannels();
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 = 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();
@@ -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 j = json::parse(response);
234
- if (j.value("ok", false))
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  {
236
- auto stateB64 = juce::String(j["state"].get<std::string>());
237
- if (stateB64.isNotEmpty())
 
 
238
  {
239
- juce::MemoryBlock stateData;
240
- if (stateData.fromBase64Encoding(stateB64))
241
- inProcessPlugin->setStateInformation(stateData.getData(), (int)stateData.getSize());
 
242
  }
243
  }
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());
@@ -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
- 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);
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
- const juce::String& reason, const juce::String& captured)
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
- auto helperApp = juce::File::getSpecialLocation(juce::File::currentExecutableFile)
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
- 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); });
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
- { 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); }
@@ -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 = 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 ─────────────────────────────────────────────────────────────────────
558
 
559
- void PluginBridgeProcessor::getStateInformation(juce::MemoryBlock& destData)
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
-
572
- juce::MemoryOutputStream stream(destData, false);
573
- state.writeToStream(stream);
574
  }
575
 
576
- void PluginBridgeProcessor::setStateInformation(const void* data, int sizeInBytes)
577
- {
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
- }
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(); }