pluginbridge / ROADMAP.md
RAM2118's picture
Add refined roadmap after deep research of all referenced repos
985bfc7 verified
|
Raw
History Blame Contribute Delete
17 kB

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

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

  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

  1. HostedPluginManager β€” load a VST3 by file path
  2. GUI file picker β†’ load Pro-Q 4
  3. Audio routing through hosted plugin (copy bus-sync from juce-plugin-wrapper)
  4. Parameter enumeration via getParameters() + HostedAudioProcessorParameter
  5. 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

  1. Implement JSON-RPC dispatch (initialize β†’ tools/list β†’ tools/call)
  2. list_plugins tool
  3. search_param tool (fuzzy keyword match on param names)
  4. get_params tool
  5. set_params tool (with beginChangeGesture/endChangeGesture)
  6. Add to .mcp.json, test from Claude Code
  7. 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

  1. Embed Ebu128LoudnessMeter (4 files, MIT)
  2. Add JUCE FFT + Hann window + ring buffer accumulator
  3. Implement 7-band energy calculation
  4. Add M/S stereo width
  5. Add True Peak via 4Γ— oversampling
  6. Implement get_analysis tool
  7. Silence gate
  8. 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 βœ