Sitar118 Claude Sonnet 4.6 commited on
Commit
fdc4e41
·
1 Parent(s): df843ce

fix: restore v0.5.2 architecture + wire AudioAnalyser correctly

Browse files

- Restored PluginBridgeEditor.mm to v0.5.2 (in-process createEditor, TreeView picker)
- Restored PluginBridgeProcessor.cpp to v0.5.2 (loadInProcessPlugin, safety scan, deduplication, manufacturer names, PBLOG)
- Added PluginPickerComponent.mm to CMakeLists.txt (was never registered)
- Wired AudioAnalyser into v0.5.2 processor (prepareToPlay, processBlock, getAnalysisCallback)
- Fixed AudioAnalyser: FFT is now a stored member, not created per audio block
- Updated MACHINE-CONTEXT.md: full scope rules, workflow, architecture lock, dead-end list

Root cause of regression: ML Intern rewrote Editor+Processor to add AudioAnalyser,
accidentally reverting to pre-v0.5.2 CALayerHost architecture (FAILED-APPROACHES #10).
MACHINE-CONTEXT.md now has explicit scope rules to prevent this.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

CMakeLists.txt CHANGED
@@ -33,6 +33,7 @@ target_sources(PluginBridge
33
  PRIVATE
34
  Source/Plugin/PluginBridgeProcessor.cpp
35
  Source/Plugin/PluginBridgeEditor.mm
 
36
  Source/Plugin/HelperConnection.cpp
37
  Source/Plugin/McpServer.cpp
38
  )
 
33
  PRIVATE
34
  Source/Plugin/PluginBridgeProcessor.cpp
35
  Source/Plugin/PluginBridgeEditor.mm
36
+ Source/Plugin/PluginPickerComponent.mm
37
  Source/Plugin/HelperConnection.cpp
38
  Source/Plugin/McpServer.cpp
39
  )
MACHINE-CONTEXT.md CHANGED
@@ -186,18 +186,143 @@ cat ~/Library/PluginBridge/safety_db.json | python3 -m json.tool | head -30
186
 
187
  ---
188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  ## WORKFLOW
190
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  ```
192
  1. ML Intern wants to use [API/approach]
193
  2. ML Intern asks: "Claude Code — verify [API] works. Fill MACHINE-CONTEXT.md"
194
  3. Claude Code: writes test, runs it, fills verified API table, pushes
195
- 4. ML Intern: reads this file, writes code based on FACTS not assumptions
196
- 5. ML Intern: pushes code to HF repo
197
  6. Claude Code: pulls, builds, tests, updates BUILD STATE + RUNTIME STATE sections
198
  7. If error: Claude Code fills ERROR REPORT section with full diagnostic
199
  8. ML Intern reads diagnostic, writes fix based on FACTS
200
- 9. Repeat until working
201
  ```
202
 
203
  ---
@@ -207,8 +332,8 @@ cat ~/Library/PluginBridge/safety_db.json | python3 -m json.tool | head -30
207
  - **Never assume an API works** — always ask Claude Code to verify first via this file
208
  - **Never use deprecated macOS APIs** without Claude Code confirming they still function
209
  - **ObjC++ (.mm) files** — only use if Claude Code confirms the ObjC API is available in current SDK
210
- - **Window management on macOS** — always ask Claude Code to test the specific approach (fullscreen is tricky)
211
  - **JUCE version matters** — APIs change between JUCE 7 and 8. Claude Code can check `JUCE/modules/` headers
212
  - **Never push more than ONE conceptual change at a time** — if it breaks, you know exactly what caused it
213
  - **If you don't know why something failed, DON'T GUESS** — ask Claude Code to investigate and fill the error report
214
  - **CAContext/CALayerHost is a dead end** — already tried, documented in FAILED-APPROACHES #10. Do not revisit.
 
 
186
 
187
  ---
188
 
189
+ ## WHO DOES WHAT — READ THIS FIRST
190
+
191
+ ```
192
+ ┌─────────────────────────────────────────────────────────────────┐
193
+ │ ML INTERN (HuggingFace) CLAUDE CODE (local Mac) │
194
+ │ ───────────────────────── ────────────────────────── │
195
+ │ Writes code Runs code │
196
+ │ Pushes to HF repo Pulls, builds, tests │
197
+ │ Cannot run anything Fills this file with facts │
198
+ │ Cannot see the Mac Is ML Intern's eyes + hands │
199
+ │ Cannot check if it compiles Reports errors with full diag │
200
+ └─────────────────────────────────────────────────────────────────┘
201
+ ```
202
+
203
+ **The ML Intern has NO Mac access. It cannot:**
204
+ - Run a build to see if code compiles
205
+ - Check if an API exists in the local JUCE version
206
+ - Verify a macOS behavior
207
+ - See what's currently on screen or in the logs
208
+ - Know if a file was committed or is only local
209
+
210
+ **Claude Code is the ML Intern's eyes and hands on the Mac.**
211
+ Every question about "does X work on this machine?" must go through Claude Code.
212
+
213
+ ---
214
+
215
  ## WORKFLOW
216
 
217
+ ```
218
+ 1. ML Intern is given a task (e.g. "add audio analysis to MCP")
219
+ 2. If the task touches a new API or macOS behavior:
220
+ ML Intern asks Claude Code: "verify [API]. Fill MACHINE-CONTEXT.md"
221
+ Claude Code tests it, writes result here, pushes
222
+ ML Intern reads this file → writes code based on FACTS
223
+ 3. ML Intern writes ONLY the files needed for the task
224
+ (see SCOPE RULES below — do NOT touch other files)
225
+ 4. ML Intern pushes to HF repo
226
+ 5. Claude Code pulls, builds, tests
227
+ 6. If error: Claude Code fills ERROR REPORT section — full diagnostic
228
+ 7. ML Intern reads diagnostic → fixes based on FACTS only
229
+ 8. Repeat from step 4 until Claude Code confirms: ✅ working
230
+ ```
231
+
232
+ ---
233
+
234
+ ## SCOPE RULES — CRITICAL
235
+
236
+ **The single most important rule:**
237
+ > Only modify files that are directly required for the task you were given.
238
+ > If a file is not listed in the task, do NOT touch it.
239
+
240
+ **What "touching" includes:**
241
+ - Rewriting the file
242
+ - Removing code from it
243
+ - Replacing its architecture
244
+ - Removing logging/debug calls
245
+
246
+ ### Concrete examples of scope violations that broke the project:
247
+
248
+ | Task given | Files that should be touched | Files that must NOT be touched |
249
+ |---|---|---|
250
+ | "Add AudioAnalyser to MCP" | `AudioAnalyser.h`, `McpServer.cpp/.h`, `PluginBridgeProcessor.h/.cpp` (add 3 lines) | `PluginBridgeEditor.mm`, `PluginPickerComponent.mm`, `CMakeLists.txt` |
251
+ | "Fix manufacturer names" | `PluginBridgeProcessor.cpp` (`getPluginManufacturer`) | Everything else |
252
+ | "Fix picker duplicates" | `PluginBridgeProcessor.cpp` (`getInstalledPluginFiles`) | Everything else |
253
+
254
+ **When in doubt: ask Claude Code which files to touch. Do not guess.**
255
+
256
+ ---
257
+
258
+ ## DO NOT REWRITE WORKING FILES
259
+
260
+ If a file is currently working (confirmed by Claude Code in RUNTIME STATE), do not rewrite it.
261
+ Work around it, extend it, or add to it. Never replace it.
262
+
263
+ Files that are locked unless explicitly told to change them:
264
+ - `Source/Plugin/PluginBridgeEditor.mm` — in-process GUI embedding, took 3 sprints to get right
265
+ - `Source/Plugin/PluginPickerComponent.mm` — Ableton-style TreeView picker
266
+ - `Source/Helper/HelperPluginHost.mm` — Helper audio routing + process lifecycle
267
+ - `Source/Plugin/HelperConnection.cpp` — IPC + shared memory protocol
268
+
269
+ ---
270
+
271
+ ## DO NOT REMOVE LOGGING
272
+
273
+ `PBLOG(...)` calls in the processor and editor are how Claude Code diagnoses runtime errors.
274
+ **Never remove PBLOG calls.** If you don't understand a log line, leave it.
275
+
276
+ ---
277
+
278
+ ## WHAT TO DO WHEN YOU ARE UNSURE
279
+
280
+ Stop. Write a message to Claude Code in this format:
281
+
282
+ ```
283
+ QUESTION FOR CLAUDE CODE:
284
+ Task: [what you're trying to do]
285
+ Uncertainty: [what you don't know]
286
+ Files you plan to touch: [list]
287
+ Please verify and update MACHINE-CONTEXT.md before I proceed.
288
+ ```
289
+
290
+ Claude Code will test it on the Mac and write the answer here.
291
+
292
+ ---
293
+
294
+ ## ARCHITECTURE — DO NOT CHANGE WITHOUT DISCUSSION
295
+
296
+ Current architecture is v0.5.2 — hybrid safety-scan:
297
+
298
+ ```
299
+ Safe plugin → in-process load → createEditor() → GUI embedded in Ableton window ✅
300
+ Unsafe plugin → Helper-only → audio routing only, no GUI (MCP still works) ✅
301
+ ```
302
+
303
+ This took 4 sprints to reach. The following are DEAD ENDS — do not revisit:
304
+
305
+ | Approach | Why it failed | Documented in |
306
+ |---|---|---|
307
+ | CAContext + CALayerHost | Ableton process isolation blocks cross-process compositor | FAILED-APPROACHES #10 |
308
+ | Floating window from Helper | Opens in separate Space in fullscreen Ableton | FAILED-APPROACHES #1 |
309
+ | PluginDirectoryScanner | Executes plugin code during scan → crashes | FAILED-APPROACHES #5 |
310
+ | Helper-only load (no in-process) | Plugin GUI never appears — no createEditor() called | This session (2026-05-17) |
311
+
312
+ ---
313
+
314
+ ## WORKFLOW (detailed steps)
315
+
316
  ```
317
  1. ML Intern wants to use [API/approach]
318
  2. ML Intern asks: "Claude Code — verify [API] works. Fill MACHINE-CONTEXT.md"
319
  3. Claude Code: writes test, runs it, fills verified API table, pushes
320
+ 4. ML Intern: reads this file, writes ONLY the required files
321
+ 5. ML Intern: pushes to HF repo
322
  6. Claude Code: pulls, builds, tests, updates BUILD STATE + RUNTIME STATE sections
323
  7. If error: Claude Code fills ERROR REPORT section with full diagnostic
324
  8. ML Intern reads diagnostic, writes fix based on FACTS
325
+ 9. Repeat until Claude Code confirms ✅ working
326
  ```
327
 
328
  ---
 
332
  - **Never assume an API works** — always ask Claude Code to verify first via this file
333
  - **Never use deprecated macOS APIs** without Claude Code confirming they still function
334
  - **ObjC++ (.mm) files** — only use if Claude Code confirms the ObjC API is available in current SDK
 
335
  - **JUCE version matters** — APIs change between JUCE 7 and 8. Claude Code can check `JUCE/modules/` headers
336
  - **Never push more than ONE conceptual change at a time** — if it breaks, you know exactly what caused it
337
  - **If you don't know why something failed, DON'T GUESS** — ask Claude Code to investigate and fill the error report
338
  - **CAContext/CALayerHost is a dead end** — already tried, documented in FAILED-APPROACHES #10. Do not revisit.
339
+ - **If you add a new .cpp or .mm file, tell Claude Code** — it needs to add it to CMakeLists.txt. You cannot do this yourself (you can't verify the build).
Source/Plugin/AudioAnalyser.h CHANGED
@@ -170,7 +170,7 @@ private:
170
  for (int i = 0; i < kFFTSize; ++i)
171
  fftData[i] = windowed[i];
172
 
173
- juce::dsp::FFT fft(kFFTOrder);
174
  fft.performFrequencyOnlyForwardTransform(fftData.data());
175
 
176
  // Now fftData[0..kFFTSize/2] contains magnitudes
@@ -240,6 +240,9 @@ private:
240
  double currentSampleRate = 44100.0;
241
  int currentBlockSize = 512;
242
 
 
 
 
243
  // FFT ring buffer
244
  std::array<float, kFFTSize> fftBuffer{};
245
  int fftWritePos = 0;
 
170
  for (int i = 0; i < kFFTSize; ++i)
171
  fftData[i] = windowed[i];
172
 
173
+ // fft is a stored member — no heap allocation in audio thread
174
  fft.performFrequencyOnlyForwardTransform(fftData.data());
175
 
176
  // Now fftData[0..kFFTSize/2] contains magnitudes
 
240
  double currentSampleRate = 44100.0;
241
  int currentBlockSize = 512;
242
 
243
+ // FFT — constructed once, reused per block (no allocation in audio thread)
244
+ juce::dsp::FFT fft{kFFTOrder};
245
+
246
  // FFT ring buffer
247
  std::array<float, kFFTSize> fftBuffer{};
248
  int fftWritePos = 0;
Source/Plugin/PluginBridgeEditor.h CHANGED
@@ -3,6 +3,7 @@
3
  #include <juce_audio_processors/juce_audio_processors.h>
4
  #include <juce_gui_basics/juce_gui_basics.h>
5
  #include "PluginBridgeProcessor.h"
 
6
 
7
  class PluginBridgeEditor : public juce::AudioProcessorEditor, private juce::Timer
8
  {
@@ -13,19 +14,14 @@ public:
13
  void paint(juce::Graphics& g) override;
14
  void resized() override;
15
 
16
- // Mouse forwarding to helper
17
- void mouseDown(const juce::MouseEvent& e) override;
18
- void mouseUp(const juce::MouseEvent& e) override;
19
- void mouseMove(const juce::MouseEvent& e) override;
20
- void mouseDrag(const juce::MouseEvent& e) override;
21
- void mouseWheelMove(const juce::MouseEvent& e, const juce::MouseWheelDetails& w) override;
22
-
23
  private:
24
  void timerCallback() override;
25
  void showPluginMenu();
26
- void connectRemoteLayer();
27
- void disconnectRemoteLayer();
28
- void sendMouseEvent(const juce::String& type, float x, float y, int button = 0);
 
 
29
 
30
  PluginBridgeProcessor& processorRef;
31
 
@@ -35,16 +31,22 @@ private:
35
  juce::Label pluginStatusLabel;
36
  juce::TextButton selectButton{"Select Plugin"};
37
 
38
- // Remote layer state (ObjC pointers stored as void*)
39
- void* remoteLayerClient = nullptr; // CARemoteLayerClient*
40
- bool layerConnected = false;
 
 
 
 
 
41
 
42
- // State
43
- bool showingError = false;
44
 
45
- static constexpr int kTopBarHeight = 32;
46
- static constexpr int kDefaultWidth = 400;
47
- static constexpr int kDefaultHeight = 50;
 
48
 
49
  JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PluginBridgeEditor)
50
  };
 
3
  #include <juce_audio_processors/juce_audio_processors.h>
4
  #include <juce_gui_basics/juce_gui_basics.h>
5
  #include "PluginBridgeProcessor.h"
6
+ #include "PluginPickerComponent.h"
7
 
8
  class PluginBridgeEditor : public juce::AudioProcessorEditor, private juce::Timer
9
  {
 
14
  void paint(juce::Graphics& g) override;
15
  void resized() override;
16
 
 
 
 
 
 
 
 
17
  private:
18
  void timerCallback() override;
19
  void showPluginMenu();
20
+ void hidePicker();
21
+ void attachPluginEditor();
22
+ void detachPluginEditor();
23
+ void showAudioOnlyUI();
24
+ void hideAudioOnlyUI();
25
 
26
  PluginBridgeProcessor& processorRef;
27
 
 
31
  juce::Label pluginStatusLabel;
32
  juce::TextButton selectButton{"Select Plugin"};
33
 
34
+ // In-process plugin editor (null for unsafe plugins)
35
+ std::unique_ptr<juce::AudioProcessorEditor> pluginEditor;
36
+
37
+ // Plugin browser — shown inline when user hits Select Plugin
38
+ std::unique_ptr<PluginPickerComponent> activePicker;
39
+
40
+ // Shown instead of plugin editor when plugin is unsafe (audio-only mode)
41
+ juce::Label audioOnlyLabel;
42
 
43
+ bool showingError = false;
44
+ bool audioOnlyMode = false;
45
 
46
+ static constexpr int kTopBarHeight = 32;
47
+ static constexpr int kAudioOnlyHeight = 80;
48
+ static constexpr int kDefaultWidth = PluginPickerComponent::kWidth;
49
+ static constexpr int kDefaultHeight = kTopBarHeight;
50
 
51
  JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PluginBridgeEditor)
52
  };
Source/Plugin/PluginBridgeEditor.mm CHANGED
@@ -1,32 +1,31 @@
1
  #include "PluginBridgeEditor.h"
2
-
3
- #if JUCE_MAC
4
- #import <Cocoa/Cocoa.h>
5
- #import <QuartzCore/QuartzCore.h>
6
- #include "PrivateCA.h"
7
- #endif
8
-
9
- #include "json.hpp"
10
- using json = nlohmann::json;
11
-
12
- static const juce::Colour kBgDark (0xff0d0d1a);
13
- static const juce::Colour kBgPanel (0xff1a1a2e);
14
- static const juce::Colour kAccent (0xff6c63ff);
15
- static const juce::Colour kTextPrimary (0xffe8e8f0);
16
- static const juce::Colour kTextSecondary(0xff8888aa);
17
- static const juce::Colour kGreen (0xff4cef50);
18
- static const juce::Colour kRed (0xffef5350);
19
 
20
  PluginBridgeEditor::PluginBridgeEditor(PluginBridgeProcessor& p)
21
  : AudioProcessorEditor(&p), processorRef(p)
22
  {
 
 
 
 
23
  setSize(kDefaultWidth, kDefaultHeight);
24
  setResizable(true, true);
25
 
 
26
  titleLabel.setText("PluginBridge", juce::dontSendNotification);
27
- titleLabel.setFont(juce::FontOptions(13.0f));
28
  titleLabel.setJustificationType(juce::Justification::centredLeft);
29
- titleLabel.setColour(juce::Label::textColourId, kTextPrimary);
30
  addAndMakeVisible(titleLabel);
31
 
32
  mcpStatusLabel.setFont(juce::FontOptions(10.0f));
@@ -38,19 +37,47 @@ PluginBridgeEditor::PluginBridgeEditor(PluginBridgeProcessor& p)
38
  pluginStatusLabel.setColour(juce::Label::textColourId, kTextSecondary);
39
  addAndMakeVisible(pluginStatusLabel);
40
 
41
- selectButton.onClick = [this]() { showPluginMenu(); };
42
- selectButton.setColour(juce::TextButton::buttonColourId, kAccent);
 
 
 
 
 
 
 
43
  selectButton.setColour(juce::TextButton::textColourOffId, kTextPrimary);
 
44
  addAndMakeVisible(selectButton);
45
 
46
- // Callbacks
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  processorRef.onPluginLoaded = [this]()
48
  {
49
  juce::MessageManager::callAsync([this]()
50
  {
 
 
51
  showingError = false;
52
- // Request GUI display
53
- processorRef.showPluginGui();
 
 
 
54
  });
55
  };
56
 
@@ -58,101 +85,112 @@ PluginBridgeEditor::PluginBridgeEditor(PluginBridgeProcessor& p)
58
  {
59
  juce::MessageManager::callAsync([this]()
60
  {
61
- disconnectRemoteLayer();
 
 
62
  setSize(kDefaultWidth, kDefaultHeight);
63
  });
64
  };
65
 
66
- processorRef.onGuiReady = [this]()
67
- {
68
- juce::MessageManager::callAsync([this]() { connectRemoteLayer(); });
69
- };
70
-
71
- processorRef.onGuiResized = [this](int w, int h)
72
- {
73
- juce::MessageManager::callAsync([this, w, h]()
74
- {
75
- setSize(std::max(w, kDefaultWidth), h + kTopBarHeight);
76
- });
77
- };
78
-
79
  processorRef.onLoadFailed = [this](const juce::String& error)
80
  {
81
  juce::MessageManager::callAsync([this, error]()
82
  {
 
83
  pluginStatusLabel.setText(error, juce::dontSendNotification);
84
- pluginStatusLabel.setColour(juce::Label::textColourId, kRed);
85
- showingError = true;
 
 
86
  });
87
  };
88
 
89
- // If already loaded (restoring state)
90
- if (processorRef.isPluginLoaded() && processorRef.getRemoteLayerPortName().isNotEmpty())
91
- connectRemoteLayer();
 
 
 
 
 
 
 
 
 
 
92
 
93
  startTimerHz(4);
94
  }
95
 
96
  PluginBridgeEditor::~PluginBridgeEditor()
97
  {
 
98
  stopTimer();
99
- processorRef.onPluginLoaded = nullptr;
 
100
  processorRef.onPluginUnloaded = nullptr;
101
- processorRef.onGuiReady = nullptr;
102
- processorRef.onGuiResized = nullptr;
103
- processorRef.onLoadFailed = nullptr;
104
- disconnectRemoteLayer();
 
105
  }
106
 
 
 
107
  void PluginBridgeEditor::paint(juce::Graphics& g)
108
  {
109
  g.fillAll(kBgDark);
110
 
111
- auto topBar = getLocalBounds().removeFromTop(kTopBarHeight);
112
- g.setColour(kBgPanel);
113
- g.fillRect(topBar);
114
 
115
- g.setColour(kAccent.withAlpha(0.3f));
116
- g.drawHorizontalLine(kTopBarHeight - 1, 0.0f, (float)getWidth());
 
117
 
118
- // If no remote layer connected, show message in the body area
119
- if (!layerConnected && !processorRef.isPluginLoaded())
 
120
  {
121
- g.setColour(kTextSecondary);
122
  g.setFont(juce::FontOptions(12.0f));
123
- auto body = getLocalBounds().withTrimmedTop(kTopBarHeight);
124
- if (body.getHeight() > 20)
125
- g.drawText("Select a plugin to load", body, juce::Justification::centred);
126
  }
127
  }
128
 
129
  void PluginBridgeEditor::resized()
130
  {
131
- auto area = getLocalBounds();
132
- auto topBar = area.removeFromTop(kTopBarHeight).reduced(4, 2);
133
 
134
  titleLabel.setBounds(topBar.removeFromLeft(80));
135
- selectButton.setBounds(topBar.removeFromLeft(100).reduced(0, 2));
136
- topBar.removeFromLeft(4);
137
- pluginStatusLabel.setBounds(topBar.removeFromLeft(topBar.getWidth() - 60));
138
  mcpStatusLabel.setBounds(topBar);
139
 
140
- #if JUCE_MAC
141
- // Resize the CALayerHost to fill the area below the top bar
142
- if (layerConnected && remoteLayerClient)
143
- {
144
- CALayerHost* hostLayer = (__bridge CALayerHost*)remoteLayerClient;
145
- auto body = getLocalBounds().withTrimmedTop(kTopBarHeight);
146
- hostLayer.frame = CGRectMake(0, 0, body.getWidth(), body.getHeight());
147
- }
148
- #endif
149
  }
150
 
 
 
151
  void PluginBridgeEditor::timerCallback()
152
  {
 
153
  if (processorRef.isMcpServerRunning())
154
  {
155
- mcpStatusLabel.setText("MCP:" + juce::String(processorRef.getMcpServerPort()), juce::dontSendNotification);
 
156
  mcpStatusLabel.setColour(juce::Label::textColourId, kGreen);
157
  }
158
  else
@@ -161,212 +199,199 @@ void PluginBridgeEditor::timerCallback()
161
  mcpStatusLabel.setColour(juce::Label::textColourId, kRed);
162
  }
163
 
 
164
  if (processorRef.isPluginLoaded())
165
  {
166
  selectButton.setButtonText(processorRef.getLoadedPluginName());
167
  if (!showingError)
168
  {
169
- pluginStatusLabel.setText(juce::String(processorRef.getLoadedPluginParamCount()) + " params", juce::dontSendNotification);
170
- pluginStatusLabel.setColour(juce::Label::textColourId, kGreen);
 
 
 
 
171
  }
172
  }
173
  else if (!showingError)
174
  {
175
- selectButton.setButtonText("Select Plugin");
176
  pluginStatusLabel.setText("", juce::dontSendNotification);
177
  }
178
 
179
- // Check for crash
180
  if (processorRef.getConnectionState() == HelperConnection::State::Crashed)
181
  {
182
- disconnectRemoteLayer();
183
- pluginStatusLabel.setText("Plugin crashed — select again to retry", juce::dontSendNotification);
 
 
184
  pluginStatusLabel.setColour(juce::Label::textColourId, kRed);
185
  showingError = true;
186
  }
187
  }
188
 
189
- // --- Remote Layer Connection ---
190
 
191
- void PluginBridgeEditor::connectRemoteLayer()
192
  {
193
- #if JUCE_MAC
194
- disconnectRemoteLayer();
195
-
196
- // portName is now the CAContext contextId sent as a decimal string by the Helper
197
- auto portName = processorRef.getRemoteLayerPortName();
198
- if (portName.isEmpty()) return;
199
 
200
- uint32_t contextId = (uint32_t)portName.getLargeIntValue();
201
- if (contextId == 0)
202
  {
203
- DBG("PluginBridgeEditor: Invalid contextId in portName: " + portName);
 
 
204
  return;
205
  }
206
 
207
- int guiW = processorRef.getRemoteGuiWidth();
208
- int guiH = processorRef.getRemoteGuiHeight();
209
- if (guiW <= 0 || guiH <= 0) return;
210
-
211
- // CALayerHost embeds the Helper's CAContext layer into our view hierarchy.
212
- // No Mach port lookup needed — contextId is the only thing required.
213
- // Explicitly retain — no ARC in this target, and layer is a factory method.
214
- CALayerHost* hostLayer = [[CALayerHost layer] retain];
215
- hostLayer.contextId = contextId;
216
- hostLayer.frame = CGRectMake(0, 0, guiW, guiH);
217
- remoteLayerClient = (__bridge void*)hostLayer;
218
 
219
- // Make JUCE's NSView layer-backed and add the host layer as a sublayer
220
- if (auto* peer = getPeer())
221
  {
222
- NSView* nsView = (NSView*)peer->getNativeHandle();
223
- [nsView setWantsLayer:YES];
224
-
225
- if (nsView.layer)
226
- [nsView.layer addSublayer:hostLayer];
 
 
227
  }
228
 
229
- layerConnected = true;
 
230
 
231
- // Resize editor to fit plugin GUI + top bar
232
- setSize(std::max(guiW, kDefaultWidth), guiH + kTopBarHeight);
 
 
 
 
 
 
 
233
 
234
- DBG("PluginBridgeEditor: CALayerHost connected (contextId=" + juce::String(contextId) +
235
- " " + juce::String(guiW) + "x" + juce::String(guiH) + ")");
236
- #endif
237
- }
238
 
239
- void PluginBridgeEditor::disconnectRemoteLayer()
240
- {
241
- #if JUCE_MAC
242
- if (remoteLayerClient)
243
  {
244
- CALayerHost* hostLayer = (__bridge CALayerHost*)remoteLayerClient;
245
- [hostLayer removeFromSuperlayer];
246
- [hostLayer release];
247
- remoteLayerClient = nullptr;
248
- }
249
- layerConnected = false;
250
- #endif
251
- }
252
 
253
- // --- Mouse Event Forwarding ---
 
 
 
 
 
 
 
254
 
255
- void PluginBridgeEditor::sendMouseEvent(const juce::String& type, float x, float y, int button)
256
- {
257
- // Adjust Y for top bar offset
258
- float adjustedY = y - kTopBarHeight;
259
- if (adjustedY < 0) return; // Click is in top bar, not plugin area
260
 
261
- json cmd = {{"cmd", "mouse"}, {"type", type.toStdString()},
262
- {"x", x}, {"y", adjustedY}, {"button", button}};
263
- processorRef.getHelperConnection().sendCommand(cmd.dump());
264
- }
265
 
266
- void PluginBridgeEditor::mouseDown(const juce::MouseEvent& e)
267
- {
268
- if (e.y >= kTopBarHeight && layerConnected)
269
- sendMouseEvent("down", (float)e.x, (float)e.y, e.mods.isLeftButtonDown() ? 0 : 1);
270
- else
271
- juce::AudioProcessorEditor::mouseDown(e);
272
- }
273
 
274
- void PluginBridgeEditor::mouseUp(const juce::MouseEvent& e)
275
- {
276
- if (e.y >= kTopBarHeight && layerConnected)
277
- sendMouseEvent("up", (float)e.x, (float)e.y, 0);
278
- else
279
- juce::AudioProcessorEditor::mouseUp(e);
280
  }
281
 
282
- void PluginBridgeEditor::mouseMove(const juce::MouseEvent& e)
283
  {
284
- if (e.y >= kTopBarHeight && layerConnected)
285
- sendMouseEvent("move", (float)e.x, (float)e.y);
286
- else
287
- juce::AudioProcessorEditor::mouseMove(e);
288
- }
289
 
290
- void PluginBridgeEditor::mouseDrag(const juce::MouseEvent& e)
291
- {
292
- if (e.y >= kTopBarHeight && layerConnected)
293
- sendMouseEvent("drag", (float)e.x, (float)e.y, 0);
294
- else
295
- juce::AudioProcessorEditor::mouseDrag(e);
296
- }
297
 
298
- void PluginBridgeEditor::mouseWheelMove(const juce::MouseEvent& e, const juce::MouseWheelDetails& w)
299
- {
300
- if (e.y >= kTopBarHeight && layerConnected)
301
  {
302
- json cmd = {{"cmd", "mouse"}, {"type", "scroll"},
303
- {"x", (float)e.x}, {"y", (float)(e.y - kTopBarHeight)},
304
- {"dx", w.deltaX}, {"dy", w.deltaY}};
305
- processorRef.getHelperConnection().sendCommand(cmd.dump());
 
 
 
 
306
  }
307
  else
308
  {
309
- juce::AudioProcessorEditor::mouseWheelMove(e, w);
310
  }
 
 
311
  }
312
 
313
- // --- Plugin Menu ---
314
 
315
- void PluginBridgeEditor::showPluginMenu()
316
  {
317
- auto files = processorRef.getInstalledPluginFiles();
318
-
319
- juce::PopupMenu menu;
320
 
321
- if (files.isEmpty())
 
322
  {
323
- menu.addItem(-1, "(No plugins found)", false);
324
- menu.showMenuAsync(juce::PopupMenu::Options().withTargetComponent(&selectButton), [](int) {});
325
  return;
326
  }
327
 
328
- juce::PopupMenu vst3Menu;
329
- juce::PopupMenu auMenu;
330
- int itemId = 1;
331
-
332
- for (int i = 0; i < files.size(); ++i)
333
  {
334
- const auto& file = files[i];
335
- auto name = file.getFileNameWithoutExtension();
336
- auto path = file.getFullPathName();
337
- bool blocked = processorRef.isBlocked(path);
338
 
339
- if (file.getFileExtension() == ".vst3")
340
- vst3Menu.addItem(itemId, blocked ? name + " (not compatible)" : name, !blocked);
341
- else
342
- auMenu.addItem(itemId, blocked ? name + " (not compatible)" : name, !blocked);
 
 
343
 
344
- itemId++;
 
 
 
 
 
 
 
345
  }
 
346
 
347
- if (vst3Menu.getNumItems() > 0)
348
- menu.addSubMenu("VST3", vst3Menu);
349
- if (auMenu.getNumItems() > 0)
350
- menu.addSubMenu("Audio Units", auMenu);
351
 
352
- menu.addSeparator();
353
- menu.addItem(9999, "Clear blocklist");
 
 
 
 
 
 
 
 
 
 
 
354
 
355
- menu.showMenuAsync(juce::PopupMenu::Options().withTargetComponent(&selectButton),
356
- [this, files](int result)
357
- {
358
- if (result == 9999)
359
- {
360
- processorRef.getBlocklist().clear();
361
- return;
362
- }
363
-
364
- if (result > 0 && result <= files.size())
365
- {
366
- showingError = false;
367
- pluginStatusLabel.setText("Loading...", juce::dontSendNotification);
368
- pluginStatusLabel.setColour(juce::Label::textColourId, kTextSecondary);
369
- processorRef.loadPlugin(files[result - 1].getFullPathName());
370
- }
371
- });
372
  }
 
1
  #include "PluginBridgeEditor.h"
2
+ #include "PBLog.h"
3
+
4
+ static const juce::Colour kBgDark (0xff1e1e1e);
5
+ static const juce::Colour kBgPanel (0xff252525);
6
+ static const juce::Colour kBgTopBar (0xff1a1a1a);
7
+ static const juce::Colour kAccent (0xff5b9bd5);
8
+ static const juce::Colour kTextPrimary (0xffe0e0e0);
9
+ static const juce::Colour kTextSecondary(0xff888888);
10
+ static const juce::Colour kGreen (0xff5ea65e);
11
+ static const juce::Colour kRed (0xff994444);
12
+ static const juce::Colour kOrange (0xffb07a20);
 
 
 
 
 
 
13
 
14
  PluginBridgeEditor::PluginBridgeEditor(PluginBridgeProcessor& p)
15
  : AudioProcessorEditor(&p), processorRef(p)
16
  {
17
+ PBLog::startSession("Editor");
18
+ PBLOG("Editor", "Constructor — editor opened"
19
+ + (p.isPluginLoaded() ? " (plugin already loaded: " + p.getLoadedPluginName() + ")" : ""));
20
+
21
  setSize(kDefaultWidth, kDefaultHeight);
22
  setResizable(true, true);
23
 
24
+ // ── Top bar ───────────────────────────────────────────────────────────────
25
  titleLabel.setText("PluginBridge", juce::dontSendNotification);
26
+ titleLabel.setFont(juce::FontOptions(12.0f));
27
  titleLabel.setJustificationType(juce::Justification::centredLeft);
28
+ titleLabel.setColour(juce::Label::textColourId, juce::Colour(0xff666666));
29
  addAndMakeVisible(titleLabel);
30
 
31
  mcpStatusLabel.setFont(juce::FontOptions(10.0f));
 
37
  pluginStatusLabel.setColour(juce::Label::textColourId, kTextSecondary);
38
  addAndMakeVisible(pluginStatusLabel);
39
 
40
+ selectButton.onClick = [this]()
41
+ {
42
+ // Toggle picker: if already open, close it
43
+ if (activePicker)
44
+ hidePicker();
45
+ else
46
+ showPluginMenu();
47
+ };
48
+ selectButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2d2d2d));
49
  selectButton.setColour(juce::TextButton::textColourOffId, kTextPrimary);
50
+ selectButton.setColour(juce::ComboBox::outlineColourId, juce::Colour(0xff404040));
51
  addAndMakeVisible(selectButton);
52
 
53
+ // ── Audio-only label ──────────────────────────────────────────────────────
54
+ audioOnlyLabel.setFont(juce::FontOptions(11.0f));
55
+ audioOnlyLabel.setJustificationType(juce::Justification::centred);
56
+ audioOnlyLabel.setColour(juce::Label::textColourId, kOrange);
57
+ audioOnlyLabel.setColour(juce::Label::backgroundColourId, kBgDark);
58
+ audioOnlyLabel.setVisible(false);
59
+ addChildComponent(audioOnlyLabel);
60
+
61
+ // ── Processor callbacks ───────────────────────────────────────────────────
62
+ processorRef.onPluginWillLoad = [this]()
63
+ {
64
+ PBLOG("Editor", "onPluginWillLoad — detaching old editor NOW (before plugin destroyed)");
65
+ detachPluginEditor();
66
+ hideAudioOnlyUI();
67
+ };
68
+
69
  processorRef.onPluginLoaded = [this]()
70
  {
71
  juce::MessageManager::callAsync([this]()
72
  {
73
+ PBLOG("Editor", "onPluginLoaded — "
74
+ + juce::String(processorRef.getInProcessPlugin() ? "attaching GUI editor" : "showing audio-only UI"));
75
  showingError = false;
76
+ hidePicker(); // close picker before showing plugin
77
+ if (processorRef.getInProcessPlugin())
78
+ attachPluginEditor();
79
+ else
80
+ showAudioOnlyUI();
81
  });
82
  };
83
 
 
85
  {
86
  juce::MessageManager::callAsync([this]()
87
  {
88
+ PBLOG("Editor", "onPluginUnloaded — detaching and resizing");
89
+ detachPluginEditor();
90
+ hideAudioOnlyUI();
91
  setSize(kDefaultWidth, kDefaultHeight);
92
  });
93
  };
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  processorRef.onLoadFailed = [this](const juce::String& error)
96
  {
97
  juce::MessageManager::callAsync([this, error]()
98
  {
99
+ PBLOG("Editor", "onLoadFailed: " + error);
100
  pluginStatusLabel.setText(error, juce::dontSendNotification);
101
+ bool isScanning = error.startsWith("Scanning");
102
+ pluginStatusLabel.setColour(juce::Label::textColourId,
103
+ isScanning ? kOrange : kRed);
104
+ showingError = !isScanning;
105
  });
106
  };
107
 
108
+ processorRef.onScanProgress = [this](const juce::String&, bool)
109
+ {
110
+ juce::MessageManager::callAsync([this]() { repaint(); });
111
+ };
112
+
113
+ // Restore GUI if plugin already loaded (editor re-opened)
114
+ if (processorRef.isPluginLoaded())
115
+ {
116
+ if (processorRef.getInProcessPlugin())
117
+ attachPluginEditor();
118
+ else
119
+ showAudioOnlyUI();
120
+ }
121
 
122
  startTimerHz(4);
123
  }
124
 
125
  PluginBridgeEditor::~PluginBridgeEditor()
126
  {
127
+ PBLOG("Editor", "Destructor — detaching and clearing callbacks");
128
  stopTimer();
129
+ processorRef.onPluginWillLoad = nullptr;
130
+ processorRef.onPluginLoaded = nullptr;
131
  processorRef.onPluginUnloaded = nullptr;
132
+ processorRef.onLoadFailed = nullptr;
133
+ processorRef.onScanProgress = nullptr;
134
+ activePicker.reset();
135
+ detachPluginEditor();
136
+ PBLOG("Editor", "Destructor — done");
137
  }
138
 
139
+ // ── Paint / Resize ────────────────────────────────────────────────────────────
140
+
141
  void PluginBridgeEditor::paint(juce::Graphics& g)
142
  {
143
  g.fillAll(kBgDark);
144
 
145
+ // Top bar
146
+ g.setColour(kBgTopBar);
147
+ g.fillRect(getLocalBounds().removeFromTop(kTopBarHeight));
148
 
149
+ // Separator line under top bar
150
+ g.setColour(juce::Colour(0xff333333));
151
+ g.fillRect(0, kTopBarHeight - 1, getWidth(), 1);
152
 
153
+ // Empty state hint (only when no picker, no plugin, no error)
154
+ if (!activePicker && !pluginEditor && !audioOnlyMode
155
+ && !processorRef.isPluginLoaded() && !showingError)
156
  {
157
+ g.setColour(juce::Colour(0xff444444));
158
  g.setFont(juce::FontOptions(12.0f));
159
+ g.drawText("Click Select Plugin to browse your library",
160
+ getLocalBounds().withTrimmedTop(kTopBarHeight),
161
+ juce::Justification::centred);
162
  }
163
  }
164
 
165
  void PluginBridgeEditor::resized()
166
  {
167
+ auto area = getLocalBounds();
168
+ auto topBar = area.removeFromTop(kTopBarHeight).reduced(6, 4);
169
 
170
  titleLabel.setBounds(topBar.removeFromLeft(80));
171
+ selectButton.setBounds(topBar.removeFromLeft(110).reduced(0, 2));
172
+ topBar.removeFromLeft(6);
173
+ pluginStatusLabel.setBounds(topBar.removeFromLeft(topBar.getWidth() - 64));
174
  mcpStatusLabel.setBounds(topBar);
175
 
176
+ // Content area: picker > plugin editor > audio-only (priority order)
177
+ if (activePicker)
178
+ activePicker->setBounds(area);
179
+ else if (pluginEditor)
180
+ pluginEditor->setBounds(area);
181
+ else if (audioOnlyMode)
182
+ audioOnlyLabel.setBounds(area);
 
 
183
  }
184
 
185
+ // ── Timer ─────────────────────────────────────────────────────────────────────
186
+
187
  void PluginBridgeEditor::timerCallback()
188
  {
189
+ // MCP status
190
  if (processorRef.isMcpServerRunning())
191
  {
192
+ mcpStatusLabel.setText("MCP:" + juce::String(processorRef.getMcpServerPort()),
193
+ juce::dontSendNotification);
194
  mcpStatusLabel.setColour(juce::Label::textColourId, kGreen);
195
  }
196
  else
 
199
  mcpStatusLabel.setColour(juce::Label::textColourId, kRed);
200
  }
201
 
202
+ // Plugin status
203
  if (processorRef.isPluginLoaded())
204
  {
205
  selectButton.setButtonText(processorRef.getLoadedPluginName());
206
  if (!showingError)
207
  {
208
+ auto suffix = audioOnlyMode ? " [audio only]" : "";
209
+ pluginStatusLabel.setText(
210
+ juce::String(processorRef.getLoadedPluginParamCount()) + " params" + suffix,
211
+ juce::dontSendNotification);
212
+ pluginStatusLabel.setColour(juce::Label::textColourId,
213
+ audioOnlyMode ? kOrange : kGreen);
214
  }
215
  }
216
  else if (!showingError)
217
  {
218
+ selectButton.setButtonText(activePicker ? "Close Browser" : "Select Plugin");
219
  pluginStatusLabel.setText("", juce::dontSendNotification);
220
  }
221
 
222
+ // Helper crash
223
  if (processorRef.getConnectionState() == HelperConnection::State::Crashed)
224
  {
225
+ detachPluginEditor();
226
+ hideAudioOnlyUI();
227
+ pluginStatusLabel.setText("Plugin crashed — select again to retry",
228
+ juce::dontSendNotification);
229
  pluginStatusLabel.setColour(juce::Label::textColourId, kRed);
230
  showingError = true;
231
  }
232
  }
233
 
234
+ // ── Plugin Picker (inline browser) ───────────────────────────────────────────
235
 
236
+ void PluginBridgeEditor::showPluginMenu()
237
  {
238
+ auto files = processorRef.getInstalledPluginFiles();
239
+ auto& safetyDB = processorRef.getSafetyDB();
 
 
 
 
240
 
241
+ if (files.isEmpty())
 
242
  {
243
+ juce::PopupMenu m;
244
+ m.addItem(-1, "(No plugins found)", false);
245
+ m.showMenuAsync(juce::PopupMenu::Options().withTargetComponent(&selectButton), [](int){});
246
  return;
247
  }
248
 
249
+ // Build entry list
250
+ std::vector<PluginPickerComponent::Entry> entries;
251
+ entries.reserve((size_t)files.size());
 
 
 
 
 
 
 
 
252
 
253
+ for (auto& f : files)
 
254
  {
255
+ PluginPickerComponent::Entry e;
256
+ e.file = f;
257
+ e.name = f.getFileNameWithoutExtension();
258
+ e.manufacturer = processorRef.getPluginManufacturer(f);
259
+ e.blocked = processorRef.isBlocked(f.getFullPathName());
260
+ e.safety = safetyDB.getSafety(f.getFullPathName());
261
+ entries.push_back(std::move(e));
262
  }
263
 
264
+ // Create picker and wire callbacks
265
+ activePicker = std::make_unique<PluginPickerComponent>(std::move(entries));
266
 
267
+ activePicker->onPluginSelected = [this](const juce::File& file)
268
+ {
269
+ PBLOG("Editor", "Picker: selected \"" + file.getFileNameWithoutExtension() + "\"");
270
+ showingError = false;
271
+ pluginStatusLabel.setText("Loading...", juce::dontSendNotification);
272
+ pluginStatusLabel.setColour(juce::Label::textColourId, kTextSecondary);
273
+ processorRef.loadPlugin(file.getFullPathName());
274
+ // picker will be removed by onPluginLoaded callback
275
+ };
276
 
277
+ activePicker->onClose = [this]() { hidePicker(); };
 
 
 
278
 
279
+ activePicker->onClearBlocklist = [this]()
 
 
 
280
  {
281
+ PBLOG("Editor", "Picker: clearing blocklist");
282
+ processorRef.getBlocklist().clear();
283
+ };
 
 
 
 
 
284
 
285
+ activePicker->onClearSafetyCache = [this]()
286
+ {
287
+ PBLOG("Editor", "Picker: clearing safety cache — re-scanning all plugins");
288
+ auto& db = processorRef.getSafetyDB();
289
+ for (auto& f : processorRef.getInstalledPluginFiles())
290
+ db.setSafety(f.getFullPathName(), PluginSafetyDB::Safety::Unknown);
291
+ processorRef.startBackgroundScan();
292
+ };
293
 
294
+ // Hide plugin editor / audio-only during browse (they'll come back on load)
295
+ if (pluginEditor) pluginEditor->setVisible(false);
296
+ if (audioOnlyMode) audioOnlyLabel.setVisible(false);
 
 
297
 
298
+ addAndMakeVisible(activePicker.get());
 
 
 
299
 
300
+ // Resize editor to show the picker
301
+ int newW = std::max(getWidth(), PluginPickerComponent::kWidth);
302
+ int newH = kTopBarHeight + PluginPickerComponent::kHeight;
303
+ setSize(newW, newH);
 
 
 
304
 
305
+ PBLOG("Editor", "showPluginMenu: picker opened");
 
 
 
 
 
306
  }
307
 
308
+ void PluginBridgeEditor::hidePicker()
309
  {
310
+ if (!activePicker) return;
 
 
 
 
311
 
312
+ PBLOG("Editor", "hidePicker: closing browser");
313
+ removeChildComponent(activePicker.get());
314
+ activePicker.reset();
 
 
 
 
315
 
316
+ // Restore whatever was visible before
317
+ if (pluginEditor)
 
318
  {
319
+ pluginEditor->setVisible(true);
320
+ setSize(std::max(pluginEditor->getWidth(), kDefaultWidth),
321
+ pluginEditor->getHeight() + kTopBarHeight);
322
+ }
323
+ else if (audioOnlyMode)
324
+ {
325
+ audioOnlyLabel.setVisible(true);
326
+ setSize(kDefaultWidth, kTopBarHeight + kAudioOnlyHeight);
327
  }
328
  else
329
  {
330
+ setSize(kDefaultWidth, kDefaultHeight);
331
  }
332
+
333
+ repaint();
334
  }
335
 
336
+ // ── In-process plugin editor ──────────────────────────────────────────────────
337
 
338
+ void PluginBridgeEditor::attachPluginEditor()
339
  {
340
+ PBLOG("Editor", "attachPluginEditor: start");
341
+ detachPluginEditor();
342
+ hideAudioOnlyUI();
343
 
344
+ auto* plugin = processorRef.getInProcessPlugin();
345
+ if (!plugin || !plugin->hasEditor())
346
  {
347
+ PBLOG("Editor", "attachPluginEditor: no plugin or no editor — abort");
 
348
  return;
349
  }
350
 
351
+ PBLOG("Editor", "attachPluginEditor: calling createEditor() on \"" + plugin->getName() + "\"");
352
+ pluginEditor.reset(plugin->createEditor());
353
+ if (!pluginEditor)
 
 
354
  {
355
+ PBLOG("Editor", "attachPluginEditor: createEditor() returned null");
356
+ return;
357
+ }
 
358
 
359
+ addAndMakeVisible(pluginEditor.get());
360
+ int w = std::max(pluginEditor->getWidth(), kDefaultWidth);
361
+ int h = pluginEditor->getHeight() + kTopBarHeight;
362
+ PBLOG("Editor", "attachPluginEditor: SUCCESS size " + juce::String(w) + "x" + juce::String(h));
363
+ setSize(w, h);
364
+ }
365
 
366
+ void PluginBridgeEditor::detachPluginEditor()
367
+ {
368
+ if (pluginEditor)
369
+ {
370
+ PBLOG("Editor", "detachPluginEditor: removing editor component");
371
+ removeChildComponent(pluginEditor.get());
372
+ pluginEditor.reset();
373
+ PBLOG("Editor", "detachPluginEditor: done");
374
  }
375
+ }
376
 
377
+ // ── Audio-only fallback UI ────────────────────────────────────────────────────
 
 
 
378
 
379
+ void PluginBridgeEditor::showAudioOnlyUI()
380
+ {
381
+ detachPluginEditor();
382
+ audioOnlyMode = true;
383
+
384
+ auto name = processorRef.getLoadedPluginName();
385
+ audioOnlyLabel.setText(
386
+ name + "\n\nGUI unavailable — plugin failed safety scan.\n"
387
+ "Audio processing active. Use MCP or automation for parameters.",
388
+ juce::dontSendNotification);
389
+ audioOnlyLabel.setVisible(true);
390
+ setSize(kDefaultWidth, kTopBarHeight + kAudioOnlyHeight);
391
+ }
392
 
393
+ void PluginBridgeEditor::hideAudioOnlyUI()
394
+ {
395
+ audioOnlyMode = false;
396
+ audioOnlyLabel.setVisible(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  }
Source/Plugin/PluginBridgeProcessor.cpp CHANGED
@@ -1,6 +1,16 @@
1
  #include "PluginBridgeProcessor.h"
2
  #include "PluginBridgeEditor.h"
3
  #include "IPCProtocol.h"
 
 
 
 
 
 
 
 
 
 
4
 
5
  #include "json.hpp"
6
  using json = nlohmann::json;
@@ -11,44 +21,62 @@ PluginBridgeProcessor::PluginBridgeProcessor()
11
  .withOutput("Output", juce::AudioChannelSet::stereo(), true)),
12
  mcpServer(std::make_unique<McpServer>(PluginBridgeConstants::kDefaultMcpPort, helper))
13
  {
 
 
 
 
 
 
14
  helper.onHelperCrashed = [this]()
15
  {
 
 
 
 
 
 
 
 
 
 
16
  pluginLoaded = false;
17
  pluginName.clear();
18
  pluginParamCount = 0;
19
- remoteLayerPortName.clear();
20
- remoteGuiWidth = 0;
21
- remoteGuiHeight = 0;
22
  DBG("PluginBridgeProcessor: Helper crashed");
23
  };
24
 
25
- helper.onGuiReady = [this](const juce::String& portName, int width, int height)
26
- {
27
- remoteLayerPortName = portName;
28
- remoteGuiWidth = width;
29
- remoteGuiHeight = height;
30
- if (onGuiReady)
31
- juce::MessageManager::callAsync([this]() { if (onGuiReady) onGuiReady(); });
32
- };
33
-
34
- helper.onGuiResized = [this](int width, int height)
35
  {
36
- remoteGuiWidth = width;
37
- remoteGuiHeight = height;
38
- if (onGuiResized)
39
- juce::MessageManager::callAsync([this, width, height]() { if (onGuiResized) onGuiResized(width, height); });
 
 
 
 
 
 
 
 
40
  };
41
 
42
  // Wire audio analysis to MCP get_analysis tool
43
  mcpServer->getAnalysisCallback = [this]() { return analyser.getCompactAnalysis(); };
44
 
45
  mcpServer->start();
 
46
  }
47
 
48
  PluginBridgeProcessor::~PluginBridgeProcessor()
49
  {
 
 
50
  mcpServer->stop();
 
51
  helper.killHelper();
 
52
  }
53
 
54
  void PluginBridgeProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
@@ -56,7 +84,6 @@ void PluginBridgeProcessor::prepareToPlay(double sampleRate, int samplesPerBlock
56
  currentSampleRate = sampleRate;
57
  currentBlockSize = samplesPerBlock;
58
 
59
- // Prepare audio analyser
60
  analyser.prepareToPlay(sampleRate, samplesPerBlock);
61
 
62
  if (helper.isHelperRunning())
@@ -83,19 +110,16 @@ void PluginBridgeProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce:
83
  {
84
  juce::ScopedNoDenormals noDenormals;
85
 
86
- auto totalNumInputChannels = getTotalNumInputChannels();
87
- auto totalNumOutputChannels = getTotalNumOutputChannels();
88
- for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
89
  buffer.clear(i, 0, buffer.getNumSamples());
90
 
91
- if (!pluginLoaded || !helper.isHelperRunning())
92
- return;
93
 
94
  auto& shm = helper.getSharedAudio();
95
  if (!shm.isValid()) return;
96
 
97
  int numChannels = buffer.getNumChannels();
98
- int numSamples = buffer.getNumSamples();
99
 
100
  const float* inputPtrs[2] = {
101
  buffer.getReadPointer(0),
@@ -118,8 +142,6 @@ void PluginBridgeProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce:
118
  };
119
  shm.readOutput(outputPtrs, numChannels, numSamples);
120
  header->state.store(kStateIdle);
121
-
122
- // Analyse the output audio (post-plugin processing)
123
  analyser.processBlock(buffer);
124
  }
125
  }
@@ -129,75 +151,228 @@ juce::AudioProcessorEditor* PluginBridgeProcessor::createEditor()
129
  return new PluginBridgeEditor(*this);
130
  }
131
 
132
- // --- Plugin Loading ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
  void PluginBridgeProcessor::loadPlugin(const juce::String& path)
135
  {
 
 
 
136
  if (blocklist.isBlocked(path))
137
  {
138
- if (onLoadFailed)
139
- onLoadFailed("Not compatible (crashed previously)");
140
  return;
141
  }
142
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  if (!helper.isHelperRunning())
144
  {
 
145
  if (!helper.spawnHelper())
146
  {
147
- DBG("PluginBridgeProcessor: Failed to spawn helper");
148
- if (onLoadFailed)
149
- onLoadFailed("Failed to start helper process");
150
  return;
151
  }
 
152
 
153
- json configCmd = {{"cmd", "configure"},
154
- {"sampleRate", currentSampleRate},
155
- {"blockSize", currentBlockSize},
156
- {"channels", 2}};
157
  helper.sendCommand(configCmd.dump());
158
  }
159
 
160
- json loadCmd = {{"cmd", "load"},
161
- {"path", path.toStdString()},
162
- {"sampleRate", currentSampleRate},
163
- {"blockSize", currentBlockSize}};
164
-
165
  auto response = helper.sendCommand(loadCmd.dump());
 
166
 
167
  try
168
  {
169
  auto j = json::parse(response);
170
  if (j.value("ok", false))
171
  {
172
- pluginLoaded = true;
173
- pluginName = juce::String(j["name"].get<std::string>());
174
  pluginParamCount = j.value("params", 0);
175
- pluginPath = path;
176
- DBG("PluginBridgeProcessor: Loaded " + pluginName);
 
177
 
178
- if (onPluginLoaded)
179
- juce::MessageManager::callAsync([this]() { if (onPluginLoaded) onPluginLoaded(); });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  }
181
  else
182
  {
 
 
183
  pluginLoaded = false;
184
- auto error = juce::String(j.value("error", "unknown"));
185
  blocklist.addBlocked(path);
186
  if (onLoadFailed)
187
- onLoadFailed("Not compatible: " + error);
188
  }
189
  }
190
  catch (...)
191
  {
 
192
  pluginLoaded = false;
 
193
  blocklist.addBlocked(path);
194
- if (onLoadFailed)
195
- onLoadFailed("Helper communication error");
196
  }
197
  }
198
 
199
  void PluginBridgeProcessor::unloadPlugin()
200
  {
 
 
201
  if (helper.isHelperRunning())
202
  helper.sendCommand("{\"cmd\":\"unload\"}");
203
 
@@ -205,49 +380,271 @@ void PluginBridgeProcessor::unloadPlugin()
205
  pluginName.clear();
206
  pluginPath.clear();
207
  pluginParamCount = 0;
208
- remoteLayerPortName.clear();
209
- remoteGuiWidth = 0;
210
- remoteGuiHeight = 0;
211
 
212
  if (onPluginUnloaded)
213
  juce::MessageManager::callAsync([this]() { if (onPluginUnloaded) onPluginUnloaded(); });
214
  }
215
 
216
- void PluginBridgeProcessor::showPluginGui()
 
 
 
 
 
217
  {
218
- if (helper.isHelperRunning())
219
- helper.sendCommand("{\"cmd\":\"show_gui\"}");
 
 
 
 
 
 
 
 
220
  }
221
 
222
- void PluginBridgeProcessor::hidePluginGui()
 
 
 
223
  {
224
- if (helper.isHelperRunning())
225
- helper.sendCommand("{\"cmd\":\"hide_gui\"}");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  }
227
 
228
- // --- Plugin List ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
  juce::Array<juce::File> PluginBridgeProcessor::getInstalledPluginFiles() const
231
  {
232
  juce::Array<juce::File> results;
233
-
234
- juce::Array<juce::File> dirs = {
235
- juce::File("/Library/Audio/Plug-Ins/VST3"),
236
- juce::File::getSpecialLocation(juce::File::userHomeDirectory)
237
- .getChildFile("Library/Audio/Plug-Ins/VST3"),
238
- juce::File("/Library/Audio/Plug-Ins/Components"),
239
- juce::File::getSpecialLocation(juce::File::userHomeDirectory)
240
- .getChildFile("Library/Audio/Plug-Ins/Components")
 
 
 
 
 
241
  };
242
 
243
- for (auto& dir : dirs)
244
  {
245
- if (dir.isDirectory())
 
 
246
  {
247
- auto vst3s = dir.findChildFiles(juce::File::findDirectories, false, "*.vst3");
248
- auto aus = dir.findChildFiles(juce::File::findDirectories, false, "*.component");
249
- results.addArray(vst3s);
250
- results.addArray(aus);
251
  }
252
  }
253
 
@@ -260,7 +657,154 @@ bool PluginBridgeProcessor::isBlocked(const juce::String& path) const
260
  return blocklist.isBlocked(path);
261
  }
262
 
263
- // --- State ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
  void PluginBridgeProcessor::getStateInformation(juce::MemoryBlock& destData)
266
  {
@@ -273,7 +817,8 @@ void PluginBridgeProcessor::getStateInformation(juce::MemoryBlock& destData)
273
  try {
274
  auto j = json::parse(response);
275
  if (j.value("ok", false))
276
- state.setProperty("pluginState", juce::String(j["state"].get<std::string>()), nullptr);
 
277
  } catch (...) {}
278
  }
279
 
@@ -284,34 +829,25 @@ void PluginBridgeProcessor::getStateInformation(juce::MemoryBlock& destData)
284
  void PluginBridgeProcessor::setStateInformation(const void* data, int sizeInBytes)
285
  {
286
  auto state = juce::ValueTree::readFromData(data, (size_t)sizeInBytes);
287
- if (state.isValid())
288
- {
289
- auto path = state.getProperty("pluginPath").toString();
290
- if (path.isNotEmpty())
291
- {
292
- loadPlugin(path);
293
 
294
- auto pluginState = state.getProperty("pluginState").toString();
295
- if (pluginState.isNotEmpty() && helper.isHelperRunning())
296
- {
297
- json cmd = {{"cmd", "set_state"}, {"state", pluginState.toStdString()}};
298
- helper.sendCommand(cmd.dump());
299
- }
300
- }
301
- }
302
- }
303
 
304
- // --- MCP ---
305
 
306
- bool PluginBridgeProcessor::isMcpServerRunning() const
307
- {
308
- return mcpServer && mcpServer->isRunning();
 
 
 
309
  }
310
 
311
- int PluginBridgeProcessor::getMcpServerPort() const
312
- {
313
- return mcpServer ? mcpServer->getPort() : 0;
314
- }
315
 
316
  juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
317
  {
 
1
  #include "PluginBridgeProcessor.h"
2
  #include "PluginBridgeEditor.h"
3
  #include "IPCProtocol.h"
4
+ #include "PBLog.h"
5
+
6
+ #include <sys/wait.h>
7
+ #include <spawn.h>
8
+ #include <signal.h>
9
+ #include <unistd.h>
10
+ #include <fcntl.h>
11
+ #include <chrono>
12
+
13
+ extern char** environ;
14
 
15
  #include "json.hpp"
16
  using json = nlohmann::json;
 
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");
26
+
27
+ inProcessFormatManager.addFormat(new juce::VST3PluginFormat());
28
+ inProcessFormatManager.addFormat(new juce::AudioUnitPluginFormat());
29
+
30
  helper.onHelperCrashed = [this]()
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)");
39
+ safetyDB.setSafety(pluginPath, PluginSafetyDB::Safety::Unsafe);
40
+ }
41
+
42
  pluginLoaded = false;
43
  pluginName.clear();
44
  pluginParamCount = 0;
45
+ unloadInProcessPlugin();
 
 
46
  DBG("PluginBridgeProcessor: Helper crashed");
47
  };
48
 
49
+ helper.onParamChanged = [this](int idx, float val)
 
 
 
 
 
 
 
 
 
50
  {
51
+ if (!inProcessPlugin) return;
52
+ auto& params = inProcessPlugin->getParameters();
53
+ if (idx < 0 || idx >= params.size()) return;
54
+ suppressParamSync.store(true);
55
+ juce::MessageManager::callAsync([this, idx, val]()
56
+ {
57
+ if (!inProcessPlugin) return;
58
+ auto& p = inProcessPlugin->getParameters();
59
+ if (idx < p.size())
60
+ p[idx]->setValueNotifyingHost(juce::jlimit(0.0f, 1.0f, val));
61
+ suppressParamSync.store(false);
62
+ });
63
  };
64
 
65
  // Wire audio analysis to MCP get_analysis tool
66
  mcpServer->getAnalysisCallback = [this]() { return analyser.getCompactAnalysis(); };
67
 
68
  mcpServer->start();
69
+ startBackgroundScan();
70
  }
71
 
72
  PluginBridgeProcessor::~PluginBridgeProcessor()
73
  {
74
+ PBLOG("Processor", "Destructor — tearing down");
75
+ stopBackgroundScan();
76
  mcpServer->stop();
77
+ unloadInProcessPlugin();
78
  helper.killHelper();
79
+ PBLOG("Processor", "Destructor — done");
80
  }
81
 
82
  void PluginBridgeProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
 
84
  currentSampleRate = sampleRate;
85
  currentBlockSize = samplesPerBlock;
86
 
 
87
  analyser.prepareToPlay(sampleRate, samplesPerBlock);
88
 
89
  if (helper.isHelperRunning())
 
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 || !helper.isHelperRunning()) return;
 
117
 
118
  auto& shm = helper.getSharedAudio();
119
  if (!shm.isValid()) return;
120
 
121
  int numChannels = buffer.getNumChannels();
122
+ int numSamples = buffer.getNumSamples();
123
 
124
  const float* inputPtrs[2] = {
125
  buffer.getReadPointer(0),
 
142
  };
143
  shm.readOutput(outputPtrs, numChannels, numSamples);
144
  header->state.store(kStateIdle);
 
 
145
  analyser.processBlock(buffer);
146
  }
147
  }
 
151
  return new PluginBridgeEditor(*this);
152
  }
153
 
154
+ // ── In-process plugin (GUI instance) ─────────────────────────────────────────
155
+
156
+ void PluginBridgeProcessor::loadInProcessPlugin(const juce::String& path)
157
+ {
158
+ PBLOG("Processor", "loadInProcessPlugin: " + juce::File(path).getFileName());
159
+ unloadInProcessPlugin();
160
+
161
+ juce::File pluginFile(path);
162
+ if (!pluginFile.exists())
163
+ {
164
+ PBLOG("Processor", "loadInProcessPlugin: file not found — abort");
165
+ return;
166
+ }
167
+
168
+ juce::OwnedArray<juce::PluginDescription> typesFound;
169
+ for (int i = 0; i < inProcessFormatManager.getNumFormats(); ++i)
170
+ {
171
+ auto* format = inProcessFormatManager.getFormat(i);
172
+ bool isVST3 = pluginFile.getFileExtension() == ".vst3";
173
+ bool isAU = pluginFile.getFileExtension() == ".component";
174
+ if (isVST3 && format->getName() != "VST3") continue;
175
+ if (isAU && format->getName() != "AudioUnit") continue;
176
+ format->findAllTypesForFile(typesFound, pluginFile.getFullPathName());
177
+ if (typesFound.size() > 0) break;
178
+ }
179
+
180
+ if (typesFound.isEmpty())
181
+ {
182
+ PBLOG("Processor", "loadInProcessPlugin: no types found");
183
+ return;
184
+ }
185
+
186
+ PBLOG("Processor", "loadInProcessPlugin: creating instance \"" + typesFound[0]->name + "\"");
187
+ juce::String err;
188
+ inProcessPlugin = inProcessFormatManager.createPluginInstance(
189
+ *typesFound[0], currentSampleRate, currentBlockSize, err);
190
+
191
+ if (!inProcessPlugin)
192
+ {
193
+ PBLOG("Processor", "loadInProcessPlugin: createPluginInstance FAILED — " + err);
194
+ DBG("PluginBridgeProcessor: in-process load failed: " + err);
195
+ return;
196
+ }
197
+
198
+ // Bus layout — needed so the plugin knows its I/O configuration for GUI display.
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());
206
+ layout.outputBuses.add(juce::AudioChannelSet::stereo());
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);
213
+ if (j.value("ok", false))
214
+ {
215
+ auto stateB64 = juce::String(j["state"].get<std::string>());
216
+ if (stateB64.isNotEmpty())
217
+ {
218
+ juce::MemoryBlock stateData;
219
+ if (stateData.fromBase64Encoding(stateB64))
220
+ inProcessPlugin->setStateInformation(stateData.getData(), (int)stateData.getSize());
221
+ }
222
+ }
223
+ } catch (...) {}
224
+
225
+ inProcessPlugin->addListener(this);
226
+ PBLOG("Processor", "loadInProcessPlugin: SUCCESS — \"" + inProcessPlugin->getName() + "\"");
227
+ DBG("PluginBridgeProcessor: in-process plugin loaded for GUI: " + inProcessPlugin->getName());
228
+ }
229
+
230
+ void PluginBridgeProcessor::unloadInProcessPlugin()
231
+ {
232
+ if (inProcessPlugin)
233
+ {
234
+ PBLOG("Processor", "unloadInProcessPlugin: removing \"" + inProcessPlugin->getName() + "\"");
235
+ inProcessPlugin->removeListener(this);
236
+ inProcessPlugin->releaseResources();
237
+ inProcessPlugin.reset();
238
+ PBLOG("Processor", "unloadInProcessPlugin: done");
239
+ }
240
+ else
241
+ {
242
+ PBLOG("Processor", "unloadInProcessPlugin: nothing to unload");
243
+ }
244
+ }
245
+
246
+ void PluginBridgeProcessor::audioProcessorParameterChanged(juce::AudioProcessor*, int paramIdx, float value)
247
+ {
248
+ if (suppressParamSync.load()) return;
249
+ if (!helper.isHelperRunning()) return;
250
+
251
+ auto cmdStr = json({{"cmd","set_params"},{"values",{{std::to_string(paramIdx), value}}}}).dump();
252
+ std::thread([this, cmdStr]() { helper.sendCommand(cmdStr); }).detach();
253
+ }
254
+
255
+ // ── Plugin Loading ────────────────────────────────────────────────────────────
256
 
257
  void PluginBridgeProcessor::loadPlugin(const juce::String& path)
258
  {
259
+ PBLOG("Processor", "loadPlugin: " + juce::File(path).getFileName()
260
+ + " (currently loaded: " + (pluginLoaded ? pluginName : "none") + ")");
261
+
262
  if (blocklist.isBlocked(path))
263
  {
264
+ PBLOG("Processor", "loadPlugin: BLOCKED — skipping");
265
+ if (onLoadFailed) onLoadFailed("Not compatible (crashed previously)");
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"
273
+ : safety == PluginSafetyDB::Safety::Unsafe ? "UNSAFE"
274
+ : "UNKNOWN"));
275
+
276
+ if (safety == PluginSafetyDB::Safety::Unknown)
277
+ {
278
+ if (onLoadFailed) onLoadFailed("Scanning plugin...");
279
+ std::thread([this, path]()
280
+ {
281
+ bool safe = runPluginScan(path);
282
+ safetyDB.setSafety(path, safe ? PluginSafetyDB::Safety::Safe
283
+ : PluginSafetyDB::Safety::Unsafe);
284
+ juce::MessageManager::callAsync([this, path]() { loadPlugin(path); });
285
+ }).detach();
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");
301
  if (!helper.spawnHelper())
302
  {
303
+ PBLOG("Processor", "loadPlugin: spawnHelper FAILED");
304
+ if (onLoadFailed) onLoadFailed("Failed to start helper process");
 
305
  return;
306
  }
307
+ PBLOG("Processor", "loadPlugin: Helper spawned OK");
308
 
309
+ json configCmd = {{"cmd","configure"},{"sampleRate",currentSampleRate},
310
+ {"blockSize",currentBlockSize},{"channels",2}};
 
 
311
  helper.sendCommand(configCmd.dump());
312
  }
313
 
314
+ PBLOG("Processor", "loadPlugin: sending load cmd to Helper...");
315
+ json loadCmd = {{"cmd","load"},{"path",path.toStdString()},
316
+ {"sampleRate",currentSampleRate},{"blockSize",currentBlockSize}};
 
 
317
  auto response = helper.sendCommand(loadCmd.dump());
318
+ PBLOG("Processor", "loadPlugin: Helper response: " + juce::String(response));
319
 
320
  try
321
  {
322
  auto j = json::parse(response);
323
  if (j.value("ok", false))
324
  {
325
+ pluginLoaded = true;
326
+ pluginName = juce::String(j["name"].get<std::string>());
327
  pluginParamCount = j.value("params", 0);
328
+ pluginPath = path;
329
+ PBLOG("Processor", "loadPlugin: Helper loaded \"" + pluginName
330
+ + "\" (" + juce::String(pluginParamCount) + " params)");
331
 
332
+ if (safety == PluginSafetyDB::Safety::Safe)
333
+ {
334
+ juce::MessageManager::callAsync([this, path]()
335
+ {
336
+ PBLOG("Processor", "loadPlugin: [msg thread] calling loadInProcessPlugin");
337
+ loadInProcessPlugin(path);
338
+ PBLOG("Processor", "loadPlugin: [msg thread] firing onPluginLoaded");
339
+ if (onPluginLoaded) onPluginLoaded();
340
+ });
341
+ }
342
+ else
343
+ {
344
+ PBLOG("Processor", "loadPlugin: UNSAFE — skipping in-process load, audio-only mode");
345
+ juce::MessageManager::callAsync([this]()
346
+ {
347
+ if (onPluginLoaded) onPluginLoaded();
348
+ });
349
+ }
350
  }
351
  else
352
  {
353
+ PBLOG("Processor", "loadPlugin: ❌ Helper refused load: "
354
+ + juce::String(j.value("error", "unknown")));
355
  pluginLoaded = false;
356
+ pluginPath.clear(); // prevent onHelperCrashed stamping the wrong plugin
357
  blocklist.addBlocked(path);
358
  if (onLoadFailed)
359
+ onLoadFailed("Not compatible: " + juce::String(j.value("error", "unknown")));
360
  }
361
  }
362
  catch (...)
363
  {
364
+ PBLOG("Processor", "loadPlugin: ❌ exception parsing Helper response");
365
  pluginLoaded = false;
366
+ pluginPath.clear(); // prevent onHelperCrashed stamping the wrong plugin
367
  blocklist.addBlocked(path);
368
+ if (onLoadFailed) onLoadFailed("Helper communication error");
 
369
  }
370
  }
371
 
372
  void PluginBridgeProcessor::unloadPlugin()
373
  {
374
+ PBLOG("Processor", "unloadPlugin: \"" + pluginName + "\"");
375
+
376
  if (helper.isHelperRunning())
377
  helper.sendCommand("{\"cmd\":\"unload\"}");
378
 
 
380
  pluginName.clear();
381
  pluginPath.clear();
382
  pluginParamCount = 0;
383
+
384
+ unloadInProcessPlugin();
385
+ PBLOG("Processor", "unloadPlugin: done");
386
 
387
  if (onPluginUnloaded)
388
  juce::MessageManager::callAsync([this]() { if (onPluginUnloaded) onPluginUnloaded(); });
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
  {
398
+ switch (sig)
399
+ {
400
+ case SIGSEGV: return "SIGSEGV (segmentation fault)";
401
+ case SIGABRT: return "SIGABRT (abort)";
402
+ case SIGBUS: return "SIGBUS (bus error)";
403
+ case SIGILL: return "SIGILL (illegal instruction)";
404
+ case SIGFPE: return "SIGFPE (floating point exception)";
405
+ case SIGKILL: return "SIGKILL (killed — timeout)";
406
+ default: return "signal " + juce::String(sig);
407
+ }
408
  }
409
 
410
+ static void writeScanLog(const juce::String& pluginPath,
411
+ bool safe,
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");
421
+ auto name = juce::File(pluginPath).getFileName();
422
+
423
+ juce::String entry;
424
+ entry << "────────────────────────────────────────\n"
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
+ .getParentDirectory()
450
+ .getChildFile("Resources")
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
+ }
464
+
465
+ if (!helperApp.exists())
466
+ {
467
+ writeScanLog(path, false, "Helper binary not found", {});
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
+ (char*)helperPathStr.c_str(),
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]); // Parent never writes
501
+
502
+ if (spawnResult != 0)
503
+ {
504
+ close(pipeFd[0]);
505
+ writeScanLog(path, false, "posix_spawn failed: " + juce::String(strerror(spawnResult)), {});
506
+ return false;
507
+ }
508
+
509
+ // Poll with 10s timeout
510
+ bool timedOut = true;
511
+ int finalStatus = 0;
512
+
513
+ auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
514
+ while (std::chrono::steady_clock::now() < deadline)
515
+ {
516
+ pid_t result = waitpid(pid, &finalStatus, WNOHANG);
517
+ if (result == pid) { timedOut = false; break; }
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
+ char buf[8192];
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 (timedOut)
545
+ {
546
+ reason = "Timed out after 10s (hung on load or init)";
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
+ {
572
+ if (scanRunning.load()) return;
573
+ scanRunning.store(true);
574
+
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 ───────────────────────────────────────────────────────────────
620
 
621
  juce::Array<juce::File> PluginBridgeProcessor::getInstalledPluginFiles() const
622
  {
623
  juce::Array<juce::File> results;
624
+ std::set<std::string> seen; // deduplicate by stem name — VST3 wins over AU
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"), "*.vst3" },
632
+ { juce::File::getSpecialLocation(juce::File::userHomeDirectory)
633
+ .getChildFile("Library/Audio/Plug-Ins/VST3"), "*.vst3" },
634
+ { juce::File("/Library/Audio/Plug-Ins/Components"), "*.component" },
635
+ { juce::File::getSpecialLocation(juce::File::userHomeDirectory)
636
+ .getChildFile("Library/Audio/Plug-Ins/Components"), "*.component" }
637
  };
638
 
639
+ for (auto& spec : specs)
640
  {
641
+ if (!spec.dir.isDirectory()) continue;
642
+ auto files = spec.dir.findChildFiles(juce::File::findDirectories, false, spec.pattern);
643
+ for (auto& f : files)
644
  {
645
+ auto stem = f.getFileNameWithoutExtension().toStdString();
646
+ if (seen.insert(stem).second) // first time we see this name → keep it
647
+ results.add(f);
 
648
  }
649
  }
650
 
 
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
+ // Skip "(C)", "(c)"
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
+ std::lock_guard<std::mutex> lock(manufacturerCacheMutex);
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
+ auto j = json::parse(moduleInfo.loadFileAsString().toStdString());
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
+ mfr = first;
796
+ else
797
+ mfr = name;
798
+ }
799
+
800
+ if (mfr.isEmpty()) mfr = "Other";
801
+
802
+ std::lock_guard<std::mutex> lock(manufacturerCacheMutex);
803
+ manufacturerCache[pathKey] = mfr.toStdString();
804
+ return mfr;
805
+ }
806
+
807
+ // ── State ─────────────────────────────────────────────────────────────────────
808
 
809
  void PluginBridgeProcessor::getStateInformation(juce::MemoryBlock& destData)
810
  {
 
817
  try {
818
  auto j = json::parse(response);
819
  if (j.value("ok", false))
820
+ state.setProperty("pluginState",
821
+ juce::String(j["state"].get<std::string>()), nullptr);
822
  } catch (...) {}
823
  }
824
 
 
829
  void PluginBridgeProcessor::setStateInformation(const void* data, int sizeInBytes)
830
  {
831
  auto state = juce::ValueTree::readFromData(data, (size_t)sizeInBytes);
832
+ if (!state.isValid()) return;
 
 
 
 
 
833
 
834
+ auto path = state.getProperty("pluginPath").toString();
835
+ if (path.isEmpty()) return;
 
 
 
 
 
 
 
836
 
837
+ loadPlugin(path);
838
 
839
+ auto pluginState = state.getProperty("pluginState").toString();
840
+ if (pluginState.isNotEmpty() && helper.isHelperRunning())
841
+ {
842
+ json cmd = {{"cmd","set_state"},{"state",pluginState.toStdString()}};
843
+ helper.sendCommand(cmd.dump());
844
+ }
845
  }
846
 
847
+ // ── MCP ───────────────────────────────────────────────────────────────────────
848
+
849
+ bool PluginBridgeProcessor::isMcpServerRunning() const { return mcpServer && mcpServer->isRunning(); }
850
+ int PluginBridgeProcessor::getMcpServerPort() const { return mcpServer ? mcpServer->getPort() : 0; }
851
 
852
  juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
853
  {
Source/Plugin/PluginBridgeProcessor.h CHANGED
@@ -5,10 +5,15 @@
5
  #include "HelperConnection.h"
6
  #include "McpServer.h"
7
  #include "Blocklist.h"
 
8
  #include "AudioAnalyser.h"
9
  #include "Constants.h"
 
 
 
10
 
11
- class PluginBridgeProcessor : public juce::AudioProcessor
 
12
  {
13
  public:
14
  PluginBridgeProcessor();
@@ -40,11 +45,9 @@ public:
40
  int getMcpServerPort() const;
41
  HelperConnection& getHelperConnection() { return helper; }
42
 
43
- // Plugin lifecycle (via Helper)
44
  void loadPlugin(const juce::String& pluginPath);
45
  void unloadPlugin();
46
- void showPluginGui();
47
- void hidePluginGui();
48
 
49
  bool isPluginLoaded() const { return pluginLoaded; }
50
  juce::String getLoadedPluginName() const { return pluginName; }
@@ -52,46 +55,73 @@ public:
52
  HelperConnection::State getConnectionState() const { return helper.getState(); }
53
  juce::String getLastPluginPath() const { return pluginPath; }
54
 
 
 
 
55
  // Plugin list
56
  juce::Array<juce::File> getInstalledPluginFiles() const;
57
 
 
 
 
 
58
  // Blocklist
59
  bool isBlocked(const juce::String& path) const;
60
  Blocklist& getBlocklist() { return blocklist; }
61
 
 
 
 
 
 
62
  // Audio analysis (for MCP get_analysis)
63
  std::string getAnalysis() const { return analyser.getCompactAnalysis(); }
64
 
65
- // Remote GUI info (from Helper via IPC)
66
- juce::String getRemoteLayerPortName() const { return remoteLayerPortName; }
67
- int getRemoteGuiWidth() const { return remoteGuiWidth; }
68
- int getRemoteGuiHeight() const { return remoteGuiHeight; }
69
-
70
  // Callbacks for editor
 
71
  std::function<void()> onPluginLoaded;
72
  std::function<void()> onPluginUnloaded;
73
- std::function<void()> onGuiReady;
74
- std::function<void(int, int)> onGuiResized;
75
  std::function<void(const juce::String&)> onLoadFailed;
 
76
 
77
  private:
 
 
 
 
 
 
 
 
 
 
 
78
  HelperConnection helper;
79
  std::unique_ptr<McpServer> mcpServer;
80
- Blocklist blocklist;
 
81
  AudioAnalyser analyser;
82
 
 
 
 
 
 
83
  bool pluginLoaded = false;
84
  juce::String pluginName;
85
  juce::String pluginPath;
86
  int pluginParamCount = 0;
87
 
88
- // Remote GUI state
89
- juce::String remoteLayerPortName;
90
- int remoteGuiWidth = 0;
91
- int remoteGuiHeight = 0;
92
-
93
  double currentSampleRate = 44100.0;
94
  int currentBlockSize = 512;
95
 
 
 
 
 
 
 
 
 
96
  JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PluginBridgeProcessor)
97
  };
 
5
  #include "HelperConnection.h"
6
  #include "McpServer.h"
7
  #include "Blocklist.h"
8
+ #include "PluginSafetyDB.h"
9
  #include "AudioAnalyser.h"
10
  #include "Constants.h"
11
+ #include <map>
12
+ #include <mutex>
13
+ #include <set>
14
 
15
+ class PluginBridgeProcessor : public juce::AudioProcessor,
16
+ private juce::AudioProcessorListener
17
  {
18
  public:
19
  PluginBridgeProcessor();
 
45
  int getMcpServerPort() const;
46
  HelperConnection& getHelperConnection() { return helper; }
47
 
48
+ // Plugin lifecycle
49
  void loadPlugin(const juce::String& pluginPath);
50
  void unloadPlugin();
 
 
51
 
52
  bool isPluginLoaded() const { return pluginLoaded; }
53
  juce::String getLoadedPluginName() const { return pluginName; }
 
55
  HelperConnection::State getConnectionState() const { return helper.getState(); }
56
  juce::String getLastPluginPath() const { return pluginPath; }
57
 
58
+ // In-process plugin instance (GUI only — null when plugin is unsafe)
59
+ juce::AudioPluginInstance* getInProcessPlugin() const { return inProcessPlugin.get(); }
60
+
61
  // Plugin list
62
  juce::Array<juce::File> getInstalledPluginFiles() const;
63
 
64
+ // Manufacturer name for a plugin file (reads moduleinfo.json for VST3, heuristic for AU)
65
+ // Fast — no plugin loading. Results cached in memory.
66
+ juce::String getPluginManufacturer(const juce::File& pluginFile) const;
67
+
68
  // Blocklist
69
  bool isBlocked(const juce::String& path) const;
70
  Blocklist& getBlocklist() { return blocklist; }
71
 
72
+ // Safety DB
73
+ PluginSafetyDB& getSafetyDB() { return safetyDB; }
74
+ void startBackgroundScan();
75
+ void stopBackgroundScan();
76
+
77
  // Audio analysis (for MCP get_analysis)
78
  std::string getAnalysis() const { return analyser.getCompactAnalysis(); }
79
 
 
 
 
 
 
80
  // Callbacks for editor
81
+ std::function<void()> onPluginWillLoad; // fired BEFORE old plugin/editor torn down
82
  std::function<void()> onPluginLoaded;
83
  std::function<void()> onPluginUnloaded;
 
 
84
  std::function<void(const juce::String&)> onLoadFailed;
85
+ std::function<void(const juce::String&, bool)> onScanProgress; // path, safe
86
 
87
  private:
88
+ void loadInProcessPlugin(const juce::String& path);
89
+ void unloadInProcessPlugin();
90
+
91
+ // Spawns Helper with --scan <path>, waits up to 10s, returns true if safe.
92
+ // Safe to call from any thread. Never crashes Ableton on plugin crash.
93
+ bool runPluginScan(const juce::String& path);
94
+
95
+ // AudioProcessorListener — forwards in-process param changes to Helper
96
+ void audioProcessorParameterChanged(juce::AudioProcessor*, int paramIdx, float value) override;
97
+ void audioProcessorChanged(juce::AudioProcessor*, const ChangeDetails&) override {}
98
+
99
  HelperConnection helper;
100
  std::unique_ptr<McpServer> mcpServer;
101
+ Blocklist blocklist;
102
+ PluginSafetyDB safetyDB;
103
  AudioAnalyser analyser;
104
 
105
+ // In-process plugin (GUI instance — nullptr for unsafe plugins)
106
+ juce::AudioPluginFormatManager inProcessFormatManager;
107
+ std::unique_ptr<juce::AudioPluginInstance> inProcessPlugin;
108
+ std::atomic<bool> suppressParamSync{false};
109
+
110
  bool pluginLoaded = false;
111
  juce::String pluginName;
112
  juce::String pluginPath;
113
  int pluginParamCount = 0;
114
 
 
 
 
 
 
115
  double currentSampleRate = 44100.0;
116
  int currentBlockSize = 512;
117
 
118
+ // Background scanner
119
+ std::thread scanThread;
120
+ std::atomic<bool> scanRunning{false};
121
+
122
+ // Manufacturer name cache (path → manufacturer)
123
+ mutable std::map<std::string, std::string> manufacturerCache;
124
+ mutable std::mutex manufacturerCacheMutex;
125
+
126
  JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PluginBridgeProcessor)
127
  };