File size: 16,958 Bytes
985bfc7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
# PluginBridge β€” Refined Roadmap (Post-Research)
> Updated after deep-diving all referenced GitHub repos + MCP spec
> Status: Research complete. Ready to build.

---

## Research Verdicts

| Repo | Verdict | Use As |
|---|---|---|
| `getdunne/juce-plugin-wrapper` | ❌ Don't fork β€” dead (2021), JUCE 6, GPL-3, hardcoded plugin, no param exposure | **Reference only** β€” copy bus-sync pattern |
| JUCE AudioPluginHost | βœ… Gold standard | **Primary reference** β€” all hosting patterns extracted |
| `cpp-httplib` | βœ… Perfect fit | **Use directly** β€” header-only, background thread, MIT |
| `klangfreund/LUFSMeter` | βœ… Drop-in | **Embed directly** β€” MIT, 4 files, clean API |
| `adamstark/Sound-Analyser` / Gist | ⚠️ GPL β€” can't use code directly | **Reference only** β€” reimplement spectral math with JUCE FFT |
| `josmithiii/mcp-servers-jos` | βœ… Shows MCP structure | **Reference** β€” but uses stdio/TypeScript, we need HTTP/C++ |

---

## Critical Architecture Decisions (Changed From Original Plan)

### 1. Don't Fork juce-plugin-wrapper β†’ Build Fresh with CMake

**Why:** The wrapper is JUCE 6, Projucer-only, GPL-3, and missing everything we need (parameter enumeration, runtime plugin selection, FX mode). Starting fresh with JUCE 8 + CMake is faster than upgrading a 4-year-old skeleton.

**What to copy from it:** The `getBusesPropertiesFromProcessor()` + `prepareToPlay()` bus-sync pattern (~30 lines). That's it.

### 2. Plugin Architecture: NOT a Graph β€” Single Hosted Instance

The original plan says "host any plugin." The AudioPluginHost uses `AudioProcessorGraph` for routing multiple plugins. **PluginBridge doesn't need this.** We host ONE plugin at a time (the one on the track). Architecture:

```
PluginBridgeProcessor (our AudioProcessor β€” the outer shell)
  β”œβ”€β”€ hostedPlugin: AudioPluginInstance*  (the inner VST3/AU)
  β”œβ”€β”€ httpServer: httplib::Server         (background thread, port 16620)
  β”œβ”€β”€ analyser: AudioAnalyser             (LUFS + FFT in processBlock)
  └── processBlock():
        buffer β†’ analyser.process(buffer)
               β†’ hostedPlugin->processBlock(buffer, midi)
               β†’ output
```

No graph. No AudioProcessorPlayer. No AudioDeviceManager. The DAW drives our `processBlock` β€” we just intercept + forward.

### 3. MCP Transport: Streamable HTTP (Not stdio)

**Why:** Plugin runs inside a DAW process β€” can't spawn as a subprocess. Must expose HTTP endpoint.

**Protocol:** JSON-RPC 2.0 over HTTP POST to `http://127.0.0.1:16620/mcp`

**Required handshake:** `initialize` β†’ `notifications/initialized` before any tool calls.

### 4. Plugin Discovery: File Path Based (Not System Scan)

Don't scan the entire system on load. Instead:
- User loads PluginBridge on a track
- PluginBridge GUI has a "Load Plugin" button β†’ native file picker
- User selects a `.vst3` or `.component` file
- We call `formatManager.createPluginInstanceAsync(desc, sr, bs, callback)`
- Plugin state (which inner plugin to load) persists via `getStateInformation()`

This avoids the AudioPluginHost's `KnownPluginList` dependency entirely.

### 5. Spectral Analysis: Use JUCE DSP FFT (Not Gist/KissFFT)

**Why:** Gist is GPL. JUCE has `juce::dsp::FFT` built-in (optimized, uses vDSP on macOS). Spectral features (centroid, crest, band energy) are each 5-10 lines of math.

### 6. True Peak: Add via 4Γ— Oversampling

LUFSMeter doesn't include True Peak. Add it using `juce::dsp::Oversampling<float>` (4Γ— oversample β†’ peak detect β†’ report in dBTP).

---

## Revised Phase 1 β€” Core Plugin + MCP Server

### File Structure
```
PluginBridge/
β”œβ”€β”€ CMakeLists.txt                    (JUCE 8 CMake)
β”œβ”€β”€ libs/
β”‚   β”œβ”€β”€ httplib.h                     (cpp-httplib, single header)
β”‚   └── json.hpp                      (nlohmann/json, single header)
β”œβ”€β”€ Source/
β”‚   β”œβ”€β”€ PluginBridgeProcessor.h/.cpp  (main AudioProcessor)
β”‚   β”œβ”€β”€ PluginBridgeEditor.h/.cpp     (GUI β€” load button, param list)
β”‚   β”œβ”€β”€ HostedPluginManager.h/.cpp    (load/unload inner plugin, param enumeration)
β”‚   β”œβ”€β”€ McpServer.h/.cpp              (HTTP server + JSON-RPC + tool dispatch)
β”‚   └── AudioAnalyser.h/.cpp          (Phase 2 β€” LUFS + FFT)
└── Resources/
    └── (icons, etc.)
```

### Key Classes

#### `HostedPluginManager`
```cpp
class HostedPluginManager {
public:
    void loadPlugin(const File& pluginFile, double sampleRate, int blockSize);
    void unloadPlugin();
    
    AudioPluginInstance* getPlugin() const;
    bool isLoaded() const;
    
    // Parameter surface for MCP
    struct ParamInfo {
        int index;
        String id;          // stable HostedAudioProcessorParameter ID
        String name;
        float value;        // 0.0–1.0 normalized
        String displayText; // formatted value string
        bool automatable;
    };
    
    std::vector<ParamInfo> searchParams(const String& keyword) const;
    std::vector<std::pair<int, float>> getParams(const std::vector<int>& indices) const;
    bool setParams(const std::map<int, float>& values); // batch set
    StringArray getLoadedPluginNames() const;

private:
    AudioPluginFormatManager formatManager;
    std::unique_ptr<AudioPluginInstance> hostedPlugin;
    CriticalSection pluginLock; // protects hot-swap
};
```

#### `McpServer`
```cpp
class McpServer {
public:
    McpServer(HostedPluginManager& mgr, AudioAnalyser& analyser);
    ~McpServer();
    
    void start(int port = 16620);
    void stop();
    bool isRunning() const;

private:
    void handleInitialize(const nlohmann::json& req, nlohmann::json& res);
    void handleToolsList(const nlohmann::json& req, nlohmann::json& res);
    void handleToolsCall(const nlohmann::json& req, nlohmann::json& res);
    
    // Tool implementations
    nlohmann::json toolListPlugins();
    nlohmann::json toolSearchParam(const std::string& plugin, const std::string& keyword);
    nlohmann::json toolGetParams(const std::string& plugin, const std::vector<int>& ids);
    nlohmann::json toolSetParams(const std::string& plugin, const std::map<int, float>& values);
    nlohmann::json toolGetAnalysis();
    
    httplib::Server server;
    std::thread serverThread;
    HostedPluginManager& pluginManager;
    AudioAnalyser& analyser;
    String sessionId;
    bool initialized = false;
};
```

#### `PluginBridgeProcessor::processBlock`
```cpp
void PluginBridgeProcessor::processBlock(AudioBuffer<float>& buffer, MidiBuffer& midi)
{
    // 1. Pre-analysis (before processing β€” captures input signal)
    // analyser.captureInput(buffer);  // Phase 2
    
    // 2. Forward to hosted plugin
    if (auto* plugin = hostManager.getPlugin())
    {
        ScopedLock sl(hostManager.getPluginLock());
        plugin->processBlock(buffer, midi);
    }
    
    // 3. Post-analysis (after processing β€” captures output signal)
    // analyser.captureOutput(buffer);  // Phase 2
}
```

### MCP Protocol Implementation

Single endpoint: `POST http://127.0.0.1:16620/mcp`

```cpp
// In McpServer::start()
server.Post("/mcp", [this](const httplib::Request& req, httplib::Response& res) {
    auto body = nlohmann::json::parse(req.body);
    nlohmann::json response;
    
    std::string method = body["method"];
    
    if (method == "initialize")
        handleInitialize(body, response);
    else if (method == "notifications/initialized")
        return; // notification β€” no response
    else if (method == "tools/list")
        handleToolsList(body, response);
    else if (method == "tools/call")
        handleToolsCall(body, response);
    else {
        response = {
            {"jsonrpc", "2.0"},
            {"id", body["id"]},
            {"error", {{"code", -32601}, {"message", "Method not found"}}}
        };
    }
    
    res.set_content(response.dump(), "application/json");
});
```

### Tool Definitions (returned by `tools/list`)
```json
{
  "tools": [
    {
      "name": "list_plugins",
      "description": "List all loaded plugin instances",
      "inputSchema": { "type": "object", "properties": {} }
    },
    {
      "name": "search_param",
      "description": "Search plugin parameters by keyword. Returns matching param IDs and current values.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "plugin": { "type": "string", "description": "Plugin name from list_plugins" },
          "keyword": { "type": "string", "description": "Search term (e.g. 'band 1', 'cutoff', 'gain')" }
        },
        "required": ["plugin", "keyword"]
      }
    },
    {
      "name": "get_params",
      "description": "Get current values for specific parameter IDs",
      "inputSchema": {
        "type": "object",
        "properties": {
          "plugin": { "type": "string" },
          "ids": { "type": "array", "items": { "type": "integer" } }
        },
        "required": ["plugin", "ids"]
      }
    },
    {
      "name": "set_params",
      "description": "Set parameter values by ID. Batch operation.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "plugin": { "type": "string" },
          "values": { "type": "object", "description": "Map of param_id (int) β†’ value (0.0–1.0)" }
        },
        "required": ["plugin", "values"]
      }
    },
    {
      "name": "get_analysis",
      "description": "Get real-time audio analysis. Returns LUFS, frequency balance, stereo width.",
      "inputSchema": { "type": "object", "properties": {} }
    }
  ]
}
```

---

## Revised Phase 2 β€” Audio Analysis

### Architecture
```cpp
class AudioAnalyser {
public:
    void prepareToPlay(double sampleRate, int blockSize);
    void processBlock(const AudioBuffer<float>& buffer); // called from audio thread
    
    // Thread-safe queries (called from HTTP thread)
    String getCompactAnalysis() const; // β†’ "-14.2 LUFS | bass:+3dB | stereo:0.8"
    
private:
    // LUFS (from LUFSMeter β€” MIT, embed directly)
    Ebu128LoudnessMeter lufsMeter;
    
    // True Peak (JUCE oversampling)
    juce::dsp::Oversampling<float> oversampler{2, 2, juce::dsp::Oversampling<float>::filterHalfBandPolyphaseIIR};
    std::atomic<float> truePeak{0.0f};
    
    // FFT (JUCE built-in)
    juce::dsp::FFT fft{10}; // 1024-point
    juce::dsp::WindowingFunction<float> window{1024, juce::dsp::WindowingFunction<float>::hann};
    
    // Ring buffer (host blocks β†’ analysis frames)
    std::array<float, 2048> ringBuffer{};
    int writePos = 0;
    
    // Band energy (7 bands)
    struct BandEnergy {
        std::atomic<float> sub_bass;  // 20-60 Hz
        std::atomic<float> bass;      // 60-250 Hz
        std::atomic<float> low_mid;   // 250-500 Hz
        std::atomic<float> mid;       // 500-2k Hz
        std::atomic<float> high_mid;  // 2k-4k Hz
        std::atomic<float> highs;     // 4k-8k Hz
        std::atomic<float> brilliance; // 8k-20k Hz
    } bands;
    
    // Stereo
    std::atomic<float> stereoWidth{0.0f};
    std::atomic<float> correlation{0.0f};
    
    // Spectral centroid
    std::atomic<float> centroid{0.0f};
    
    // Silence gate
    std::atomic<bool> isSilent{true};
    static constexpr float silenceThresholdDb = -60.0f;
};
```

### `getCompactAnalysis()` output format
```
"-14.2 LUFS | TP:-1.1 | bass:+3dB | mids:ok | highs:-2dB | stereo:0.8 | bright"
```

Rules:
- Only report bands that deviate >Β±2dB from flat reference
- "ok" for bands within Β±2dB
- Stereo width as 0.0 (mono) to 1.0 (wide)
- "bright"/"dark"/"balanced" from spectral centroid
- Returns `"silent"` if RMS < -60dB (silence gate)
- Max ~60 tokens per call

---

## Implementation Order (What to Build First)

### Sprint 1 (Week 1): Minimal Viable Plugin
1. Create JUCE 8 CMake project (VST3 + AU targets)
2. `PluginBridgeProcessor` β€” empty shell that passes audio through
3. Build & load in Ableton β€” verify audio passthrough works
4. Add `httplib.h` + background server thread
5. Verify `curl http://localhost:16620/mcp` returns a response from inside Ableton

**Done when:** Plugin loads in Ableton, passes audio, responds to HTTP.

### Sprint 2 (Week 2): Plugin Hosting
6. `HostedPluginManager` β€” load a VST3 by file path
7. GUI file picker β†’ load Pro-Q 4
8. Audio routing through hosted plugin (copy bus-sync from juce-plugin-wrapper)
9. Parameter enumeration via `getParameters()` + `HostedAudioProcessorParameter`
10. Verify all Pro-Q 4 params visible in debug log

**Done when:** Pro-Q 4 loads inside PluginBridge, processes audio, params enumerated.

### Sprint 3 (Week 3): MCP Tools
11. Implement JSON-RPC dispatch (initialize β†’ tools/list β†’ tools/call)
12. `list_plugins` tool
13. `search_param` tool (fuzzy keyword match on param names)
14. `get_params` tool
15. `set_params` tool (with `beginChangeGesture`/`endChangeGesture`)
16. Add to `.mcp.json`, test from Claude Code
17. Token test: set a Pro-Q 4 band from Claude

**Done when:** Claude sets Pro-Q 4 Band 1 Gain via MCP. Full loop < 500 tokens.

### Sprint 4 (Week 4): Audio Analysis
18. Embed `Ebu128LoudnessMeter` (4 files, MIT)
19. Add JUCE FFT + Hann window + ring buffer accumulator
20. Implement 7-band energy calculation
21. Add M/S stereo width
22. Add True Peak via 4Γ— oversampling
23. Implement `get_analysis` tool
24. Silence gate
25. Token test: full analysis < 60 tokens

**Done when:** `get_analysis()` returns meaningful data on a playing track.

---

## Build Requirements

| Dependency | Source | License | Size |
|---|---|---|---|
| JUCE 8 | git submodule | dual GPL/commercial | ~200MB |
| cpp-httplib | `httplib.h` drop-in | MIT | 1 file, 800KB |
| nlohmann/json | `json.hpp` drop-in | MIT | 1 file, 850KB |
| LUFSMeter core | 4 files copied | MIT | ~20KB |
| CMake 3.22+ | system | β€” | β€” |

### CMakeLists.txt skeleton
```cmake
cmake_minimum_required(VERSION 3.22)
project(PluginBridge VERSION 0.1.0)

add_subdirectory(JUCE)

juce_add_plugin(PluginBridge
    COMPANY_NAME "PluginBridge"
    PLUGIN_MANUFACTURER_CODE Plbr
    PLUGIN_CODE Plbr
    FORMATS VST3 AU
    PRODUCT_NAME "PluginBridge"
    IS_SYNTH FALSE
    NEEDS_MIDI_INPUT TRUE
    NEEDS_MIDI_OUTPUT TRUE
    IS_MIDI_EFFECT FALSE
    EDITOR_WANTS_KEYBOARD_FOCUS FALSE
    COPY_PLUGIN_AFTER_BUILD TRUE
)

target_sources(PluginBridge PRIVATE
    Source/PluginBridgeProcessor.cpp
    Source/PluginBridgeEditor.cpp
    Source/HostedPluginManager.cpp
    Source/McpServer.cpp
    Source/AudioAnalyser.cpp
    Source/LufsMeter/Ebu128LoudnessMeter.cpp
    Source/LufsMeter/SecondOrderIIRFilter.cpp
)

target_include_directories(PluginBridge PRIVATE
    libs/          # httplib.h, json.hpp
    Source/LufsMeter/
)

target_compile_definitions(PluginBridge PUBLIC
    JUCE_PLUGINHOST_VST3=1
    JUCE_PLUGINHOST_AU=1
    JUCE_WEB_BROWSER=0
    JUCE_USE_CURL=0
    DONT_SET_USING_JUCE_NAMESPACE=1
)

target_link_libraries(PluginBridge PRIVATE
    juce::juce_audio_utils
    juce::juce_audio_processors
    juce::juce_dsp
    juce::juce_gui_basics
    juce::juce_recommended_config_flags
    juce::juce_recommended_lto_flags
    juce::juce_recommended_warning_flags
)
```

---

## Risk Register

| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| VST3 hosting crashes on some plugins | Medium | High | Crash isolation: catch exceptions in processBlock, unload plugin on repeated crashes |
| Port 16620 conflict | Low | Medium | Fallback: try 16621-16625, report active port in plugin GUI |
| `createPluginInstanceAsync` fails for some AU | Medium | Medium | Fall back to sync `createPluginInstance()` with timeout |
| DAW sandbox blocks localhost HTTP | Low | High | Test in Ableton/Logic/Reaper early β€” if blocked, fall back to Unix domain socket |
| Parameter IDs not stable across plugin versions | Medium | Low | Always use `search_param` by name first, not cached IDs |
| Multiple PluginBridge instances (multiple tracks) | Certain | Medium | Each instance gets its own port (16620 + instance_index). `list_plugins` shows all. |
| Thread safety: HTTP handler reads param while audio writes | Certain | High | Use `std::atomic<float>` for param cache, never call `getValue()` from HTTP thread directly |

---

## What Changed From Original Roadmap

| Original Plan | Revised | Why |
|---|---|---|
| Fork juce-plugin-wrapper | Build fresh (CMake, JUCE 8) | Dead repo, GPL, JUCE 6, missing everything |
| Borrow AudioPluginHost settings file | Runtime file picker | Simpler, no dependency on external app |
| Use Gist/Sound-Analyser for FFT | Use JUCE `dsp::FFT` | GPL license conflict |
| Single port for all instances | Port-per-instance | Multiple tracks need independent access |
| cpp-httplib default thread pool | Cap at 2-4 threads | Reduce OS thread pressure in plugin host |
| Phase 3 "MCP + JUCE already exists" | Implement JSON-RPC from scratch in C++ | josmithiii repo is TypeScript/stdio β€” not usable |
| 5 tools | 5 tools (unchanged) | API design validated βœ… |