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
.vst3or.componentfile - 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
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
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
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
// 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)
{
"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
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
- Create JUCE 8 CMake project (VST3 + AU targets)
PluginBridgeProcessorβ empty shell that passes audio through- Build & load in Ableton β verify audio passthrough works
- Add
httplib.h+ background server thread - Verify
curl http://localhost:16620/mcpreturns a response from inside Ableton
Done when: Plugin loads in Ableton, passes audio, responds to HTTP.
Sprint 2 (Week 2): Plugin Hosting
HostedPluginManagerβ load a VST3 by file path- GUI file picker β load Pro-Q 4
- Audio routing through hosted plugin (copy bus-sync from juce-plugin-wrapper)
- Parameter enumeration via
getParameters()+HostedAudioProcessorParameter - 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
- Implement JSON-RPC dispatch (initialize β tools/list β tools/call)
list_pluginstoolsearch_paramtool (fuzzy keyword match on param names)get_paramstoolset_paramstool (withbeginChangeGesture/endChangeGesture)- Add to
.mcp.json, test from Claude Code - 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
- Embed
Ebu128LoudnessMeter(4 files, MIT) - Add JUCE FFT + Hann window + ring buffer accumulator
- Implement 7-band energy calculation
- Add M/S stereo width
- Add True Peak via 4Γ oversampling
- Implement
get_analysistool - Silence gate
- 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_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 β |