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 β
|
|