# 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` (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 searchParams(const String& keyword) const; std::vector> getParams(const std::vector& indices) const; bool setParams(const std::map& values); // batch set StringArray getLoadedPluginNames() const; private: AudioPluginFormatManager formatManager; std::unique_ptr 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& ids); nlohmann::json toolSetParams(const std::string& plugin, const std::map& 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& 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& 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 oversampler{2, 2, juce::dsp::Oversampling::filterHalfBandPolyphaseIIR}; std::atomic truePeak{0.0f}; // FFT (JUCE built-in) juce::dsp::FFT fft{10}; // 1024-point juce::dsp::WindowingFunction window{1024, juce::dsp::WindowingFunction::hann}; // Ring buffer (host blocks → analysis frames) std::array ringBuffer{}; int writePos = 0; // Band energy (7 bands) struct BandEnergy { std::atomic sub_bass; // 20-60 Hz std::atomic bass; // 60-250 Hz std::atomic low_mid; // 250-500 Hz std::atomic mid; // 500-2k Hz std::atomic high_mid; // 2k-4k Hz std::atomic highs; // 4k-8k Hz std::atomic brilliance; // 8k-20k Hz } bands; // Stereo std::atomic stereoWidth{0.0f}; std::atomic correlation{0.0f}; // Spectral centroid std::atomic centroid{0.0f}; // Silence gate std::atomic 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` 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 ✅ |