Add refined roadmap after deep research of all referenced repos
Browse files- ROADMAP.md +469 -0
ROADMAP.md
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PluginBridge β Refined Roadmap (Post-Research)
|
| 2 |
+
> Updated after deep-diving all referenced GitHub repos + MCP spec
|
| 3 |
+
> Status: Research complete. Ready to build.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## Research Verdicts
|
| 8 |
+
|
| 9 |
+
| Repo | Verdict | Use As |
|
| 10 |
+
|---|---|---|
|
| 11 |
+
| `getdunne/juce-plugin-wrapper` | β Don't fork β dead (2021), JUCE 6, GPL-3, hardcoded plugin, no param exposure | **Reference only** β copy bus-sync pattern |
|
| 12 |
+
| JUCE AudioPluginHost | β
Gold standard | **Primary reference** β all hosting patterns extracted |
|
| 13 |
+
| `cpp-httplib` | β
Perfect fit | **Use directly** β header-only, background thread, MIT |
|
| 14 |
+
| `klangfreund/LUFSMeter` | β
Drop-in | **Embed directly** β MIT, 4 files, clean API |
|
| 15 |
+
| `adamstark/Sound-Analyser` / Gist | β οΈ GPL β can't use code directly | **Reference only** β reimplement spectral math with JUCE FFT |
|
| 16 |
+
| `josmithiii/mcp-servers-jos` | β
Shows MCP structure | **Reference** β but uses stdio/TypeScript, we need HTTP/C++ |
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## Critical Architecture Decisions (Changed From Original Plan)
|
| 21 |
+
|
| 22 |
+
### 1. Don't Fork juce-plugin-wrapper β Build Fresh with CMake
|
| 23 |
+
|
| 24 |
+
**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.
|
| 25 |
+
|
| 26 |
+
**What to copy from it:** The `getBusesPropertiesFromProcessor()` + `prepareToPlay()` bus-sync pattern (~30 lines). That's it.
|
| 27 |
+
|
| 28 |
+
### 2. Plugin Architecture: NOT a Graph β Single Hosted Instance
|
| 29 |
+
|
| 30 |
+
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:
|
| 31 |
+
|
| 32 |
+
```
|
| 33 |
+
PluginBridgeProcessor (our AudioProcessor β the outer shell)
|
| 34 |
+
βββ hostedPlugin: AudioPluginInstance* (the inner VST3/AU)
|
| 35 |
+
βββ httpServer: httplib::Server (background thread, port 16620)
|
| 36 |
+
βββ analyser: AudioAnalyser (LUFS + FFT in processBlock)
|
| 37 |
+
βββ processBlock():
|
| 38 |
+
buffer β analyser.process(buffer)
|
| 39 |
+
β hostedPlugin->processBlock(buffer, midi)
|
| 40 |
+
β output
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
No graph. No AudioProcessorPlayer. No AudioDeviceManager. The DAW drives our `processBlock` β we just intercept + forward.
|
| 44 |
+
|
| 45 |
+
### 3. MCP Transport: Streamable HTTP (Not stdio)
|
| 46 |
+
|
| 47 |
+
**Why:** Plugin runs inside a DAW process β can't spawn as a subprocess. Must expose HTTP endpoint.
|
| 48 |
+
|
| 49 |
+
**Protocol:** JSON-RPC 2.0 over HTTP POST to `http://127.0.0.1:16620/mcp`
|
| 50 |
+
|
| 51 |
+
**Required handshake:** `initialize` β `notifications/initialized` before any tool calls.
|
| 52 |
+
|
| 53 |
+
### 4. Plugin Discovery: File Path Based (Not System Scan)
|
| 54 |
+
|
| 55 |
+
Don't scan the entire system on load. Instead:
|
| 56 |
+
- User loads PluginBridge on a track
|
| 57 |
+
- PluginBridge GUI has a "Load Plugin" button β native file picker
|
| 58 |
+
- User selects a `.vst3` or `.component` file
|
| 59 |
+
- We call `formatManager.createPluginInstanceAsync(desc, sr, bs, callback)`
|
| 60 |
+
- Plugin state (which inner plugin to load) persists via `getStateInformation()`
|
| 61 |
+
|
| 62 |
+
This avoids the AudioPluginHost's `KnownPluginList` dependency entirely.
|
| 63 |
+
|
| 64 |
+
### 5. Spectral Analysis: Use JUCE DSP FFT (Not Gist/KissFFT)
|
| 65 |
+
|
| 66 |
+
**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.
|
| 67 |
+
|
| 68 |
+
### 6. True Peak: Add via 4Γ Oversampling
|
| 69 |
+
|
| 70 |
+
LUFSMeter doesn't include True Peak. Add it using `juce::dsp::Oversampling<float>` (4Γ oversample β peak detect β report in dBTP).
|
| 71 |
+
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
## Revised Phase 1 β Core Plugin + MCP Server
|
| 75 |
+
|
| 76 |
+
### File Structure
|
| 77 |
+
```
|
| 78 |
+
PluginBridge/
|
| 79 |
+
βββ CMakeLists.txt (JUCE 8 CMake)
|
| 80 |
+
βββ libs/
|
| 81 |
+
β βββ httplib.h (cpp-httplib, single header)
|
| 82 |
+
β βββ json.hpp (nlohmann/json, single header)
|
| 83 |
+
βββ Source/
|
| 84 |
+
β βββ PluginBridgeProcessor.h/.cpp (main AudioProcessor)
|
| 85 |
+
β βββ PluginBridgeEditor.h/.cpp (GUI β load button, param list)
|
| 86 |
+
β βββ HostedPluginManager.h/.cpp (load/unload inner plugin, param enumeration)
|
| 87 |
+
β βββ McpServer.h/.cpp (HTTP server + JSON-RPC + tool dispatch)
|
| 88 |
+
β βββ AudioAnalyser.h/.cpp (Phase 2 β LUFS + FFT)
|
| 89 |
+
βββ Resources/
|
| 90 |
+
βββ (icons, etc.)
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
### Key Classes
|
| 94 |
+
|
| 95 |
+
#### `HostedPluginManager`
|
| 96 |
+
```cpp
|
| 97 |
+
class HostedPluginManager {
|
| 98 |
+
public:
|
| 99 |
+
void loadPlugin(const File& pluginFile, double sampleRate, int blockSize);
|
| 100 |
+
void unloadPlugin();
|
| 101 |
+
|
| 102 |
+
AudioPluginInstance* getPlugin() const;
|
| 103 |
+
bool isLoaded() const;
|
| 104 |
+
|
| 105 |
+
// Parameter surface for MCP
|
| 106 |
+
struct ParamInfo {
|
| 107 |
+
int index;
|
| 108 |
+
String id; // stable HostedAudioProcessorParameter ID
|
| 109 |
+
String name;
|
| 110 |
+
float value; // 0.0β1.0 normalized
|
| 111 |
+
String displayText; // formatted value string
|
| 112 |
+
bool automatable;
|
| 113 |
+
};
|
| 114 |
+
|
| 115 |
+
std::vector<ParamInfo> searchParams(const String& keyword) const;
|
| 116 |
+
std::vector<std::pair<int, float>> getParams(const std::vector<int>& indices) const;
|
| 117 |
+
bool setParams(const std::map<int, float>& values); // batch set
|
| 118 |
+
StringArray getLoadedPluginNames() const;
|
| 119 |
+
|
| 120 |
+
private:
|
| 121 |
+
AudioPluginFormatManager formatManager;
|
| 122 |
+
std::unique_ptr<AudioPluginInstance> hostedPlugin;
|
| 123 |
+
CriticalSection pluginLock; // protects hot-swap
|
| 124 |
+
};
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
#### `McpServer`
|
| 128 |
+
```cpp
|
| 129 |
+
class McpServer {
|
| 130 |
+
public:
|
| 131 |
+
McpServer(HostedPluginManager& mgr, AudioAnalyser& analyser);
|
| 132 |
+
~McpServer();
|
| 133 |
+
|
| 134 |
+
void start(int port = 16620);
|
| 135 |
+
void stop();
|
| 136 |
+
bool isRunning() const;
|
| 137 |
+
|
| 138 |
+
private:
|
| 139 |
+
void handleInitialize(const nlohmann::json& req, nlohmann::json& res);
|
| 140 |
+
void handleToolsList(const nlohmann::json& req, nlohmann::json& res);
|
| 141 |
+
void handleToolsCall(const nlohmann::json& req, nlohmann::json& res);
|
| 142 |
+
|
| 143 |
+
// Tool implementations
|
| 144 |
+
nlohmann::json toolListPlugins();
|
| 145 |
+
nlohmann::json toolSearchParam(const std::string& plugin, const std::string& keyword);
|
| 146 |
+
nlohmann::json toolGetParams(const std::string& plugin, const std::vector<int>& ids);
|
| 147 |
+
nlohmann::json toolSetParams(const std::string& plugin, const std::map<int, float>& values);
|
| 148 |
+
nlohmann::json toolGetAnalysis();
|
| 149 |
+
|
| 150 |
+
httplib::Server server;
|
| 151 |
+
std::thread serverThread;
|
| 152 |
+
HostedPluginManager& pluginManager;
|
| 153 |
+
AudioAnalyser& analyser;
|
| 154 |
+
String sessionId;
|
| 155 |
+
bool initialized = false;
|
| 156 |
+
};
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
#### `PluginBridgeProcessor::processBlock`
|
| 160 |
+
```cpp
|
| 161 |
+
void PluginBridgeProcessor::processBlock(AudioBuffer<float>& buffer, MidiBuffer& midi)
|
| 162 |
+
{
|
| 163 |
+
// 1. Pre-analysis (before processing β captures input signal)
|
| 164 |
+
// analyser.captureInput(buffer); // Phase 2
|
| 165 |
+
|
| 166 |
+
// 2. Forward to hosted plugin
|
| 167 |
+
if (auto* plugin = hostManager.getPlugin())
|
| 168 |
+
{
|
| 169 |
+
ScopedLock sl(hostManager.getPluginLock());
|
| 170 |
+
plugin->processBlock(buffer, midi);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// 3. Post-analysis (after processing β captures output signal)
|
| 174 |
+
// analyser.captureOutput(buffer); // Phase 2
|
| 175 |
+
}
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
### MCP Protocol Implementation
|
| 179 |
+
|
| 180 |
+
Single endpoint: `POST http://127.0.0.1:16620/mcp`
|
| 181 |
+
|
| 182 |
+
```cpp
|
| 183 |
+
// In McpServer::start()
|
| 184 |
+
server.Post("/mcp", [this](const httplib::Request& req, httplib::Response& res) {
|
| 185 |
+
auto body = nlohmann::json::parse(req.body);
|
| 186 |
+
nlohmann::json response;
|
| 187 |
+
|
| 188 |
+
std::string method = body["method"];
|
| 189 |
+
|
| 190 |
+
if (method == "initialize")
|
| 191 |
+
handleInitialize(body, response);
|
| 192 |
+
else if (method == "notifications/initialized")
|
| 193 |
+
return; // notification β no response
|
| 194 |
+
else if (method == "tools/list")
|
| 195 |
+
handleToolsList(body, response);
|
| 196 |
+
else if (method == "tools/call")
|
| 197 |
+
handleToolsCall(body, response);
|
| 198 |
+
else {
|
| 199 |
+
response = {
|
| 200 |
+
{"jsonrpc", "2.0"},
|
| 201 |
+
{"id", body["id"]},
|
| 202 |
+
{"error", {{"code", -32601}, {"message", "Method not found"}}}
|
| 203 |
+
};
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
res.set_content(response.dump(), "application/json");
|
| 207 |
+
});
|
| 208 |
+
```
|
| 209 |
+
|
| 210 |
+
### Tool Definitions (returned by `tools/list`)
|
| 211 |
+
```json
|
| 212 |
+
{
|
| 213 |
+
"tools": [
|
| 214 |
+
{
|
| 215 |
+
"name": "list_plugins",
|
| 216 |
+
"description": "List all loaded plugin instances",
|
| 217 |
+
"inputSchema": { "type": "object", "properties": {} }
|
| 218 |
+
},
|
| 219 |
+
{
|
| 220 |
+
"name": "search_param",
|
| 221 |
+
"description": "Search plugin parameters by keyword. Returns matching param IDs and current values.",
|
| 222 |
+
"inputSchema": {
|
| 223 |
+
"type": "object",
|
| 224 |
+
"properties": {
|
| 225 |
+
"plugin": { "type": "string", "description": "Plugin name from list_plugins" },
|
| 226 |
+
"keyword": { "type": "string", "description": "Search term (e.g. 'band 1', 'cutoff', 'gain')" }
|
| 227 |
+
},
|
| 228 |
+
"required": ["plugin", "keyword"]
|
| 229 |
+
}
|
| 230 |
+
},
|
| 231 |
+
{
|
| 232 |
+
"name": "get_params",
|
| 233 |
+
"description": "Get current values for specific parameter IDs",
|
| 234 |
+
"inputSchema": {
|
| 235 |
+
"type": "object",
|
| 236 |
+
"properties": {
|
| 237 |
+
"plugin": { "type": "string" },
|
| 238 |
+
"ids": { "type": "array", "items": { "type": "integer" } }
|
| 239 |
+
},
|
| 240 |
+
"required": ["plugin", "ids"]
|
| 241 |
+
}
|
| 242 |
+
},
|
| 243 |
+
{
|
| 244 |
+
"name": "set_params",
|
| 245 |
+
"description": "Set parameter values by ID. Batch operation.",
|
| 246 |
+
"inputSchema": {
|
| 247 |
+
"type": "object",
|
| 248 |
+
"properties": {
|
| 249 |
+
"plugin": { "type": "string" },
|
| 250 |
+
"values": { "type": "object", "description": "Map of param_id (int) β value (0.0β1.0)" }
|
| 251 |
+
},
|
| 252 |
+
"required": ["plugin", "values"]
|
| 253 |
+
}
|
| 254 |
+
},
|
| 255 |
+
{
|
| 256 |
+
"name": "get_analysis",
|
| 257 |
+
"description": "Get real-time audio analysis. Returns LUFS, frequency balance, stereo width.",
|
| 258 |
+
"inputSchema": { "type": "object", "properties": {} }
|
| 259 |
+
}
|
| 260 |
+
]
|
| 261 |
+
}
|
| 262 |
+
```
|
| 263 |
+
|
| 264 |
+
---
|
| 265 |
+
|
| 266 |
+
## Revised Phase 2 β Audio Analysis
|
| 267 |
+
|
| 268 |
+
### Architecture
|
| 269 |
+
```cpp
|
| 270 |
+
class AudioAnalyser {
|
| 271 |
+
public:
|
| 272 |
+
void prepareToPlay(double sampleRate, int blockSize);
|
| 273 |
+
void processBlock(const AudioBuffer<float>& buffer); // called from audio thread
|
| 274 |
+
|
| 275 |
+
// Thread-safe queries (called from HTTP thread)
|
| 276 |
+
String getCompactAnalysis() const; // β "-14.2 LUFS | bass:+3dB | stereo:0.8"
|
| 277 |
+
|
| 278 |
+
private:
|
| 279 |
+
// LUFS (from LUFSMeter β MIT, embed directly)
|
| 280 |
+
Ebu128LoudnessMeter lufsMeter;
|
| 281 |
+
|
| 282 |
+
// True Peak (JUCE oversampling)
|
| 283 |
+
juce::dsp::Oversampling<float> oversampler{2, 2, juce::dsp::Oversampling<float>::filterHalfBandPolyphaseIIR};
|
| 284 |
+
std::atomic<float> truePeak{0.0f};
|
| 285 |
+
|
| 286 |
+
// FFT (JUCE built-in)
|
| 287 |
+
juce::dsp::FFT fft{10}; // 1024-point
|
| 288 |
+
juce::dsp::WindowingFunction<float> window{1024, juce::dsp::WindowingFunction<float>::hann};
|
| 289 |
+
|
| 290 |
+
// Ring buffer (host blocks β analysis frames)
|
| 291 |
+
std::array<float, 2048> ringBuffer{};
|
| 292 |
+
int writePos = 0;
|
| 293 |
+
|
| 294 |
+
// Band energy (7 bands)
|
| 295 |
+
struct BandEnergy {
|
| 296 |
+
std::atomic<float> sub_bass; // 20-60 Hz
|
| 297 |
+
std::atomic<float> bass; // 60-250 Hz
|
| 298 |
+
std::atomic<float> low_mid; // 250-500 Hz
|
| 299 |
+
std::atomic<float> mid; // 500-2k Hz
|
| 300 |
+
std::atomic<float> high_mid; // 2k-4k Hz
|
| 301 |
+
std::atomic<float> highs; // 4k-8k Hz
|
| 302 |
+
std::atomic<float> brilliance; // 8k-20k Hz
|
| 303 |
+
} bands;
|
| 304 |
+
|
| 305 |
+
// Stereo
|
| 306 |
+
std::atomic<float> stereoWidth{0.0f};
|
| 307 |
+
std::atomic<float> correlation{0.0f};
|
| 308 |
+
|
| 309 |
+
// Spectral centroid
|
| 310 |
+
std::atomic<float> centroid{0.0f};
|
| 311 |
+
|
| 312 |
+
// Silence gate
|
| 313 |
+
std::atomic<bool> isSilent{true};
|
| 314 |
+
static constexpr float silenceThresholdDb = -60.0f;
|
| 315 |
+
};
|
| 316 |
+
```
|
| 317 |
+
|
| 318 |
+
### `getCompactAnalysis()` output format
|
| 319 |
+
```
|
| 320 |
+
"-14.2 LUFS | TP:-1.1 | bass:+3dB | mids:ok | highs:-2dB | stereo:0.8 | bright"
|
| 321 |
+
```
|
| 322 |
+
|
| 323 |
+
Rules:
|
| 324 |
+
- Only report bands that deviate >Β±2dB from flat reference
|
| 325 |
+
- "ok" for bands within Β±2dB
|
| 326 |
+
- Stereo width as 0.0 (mono) to 1.0 (wide)
|
| 327 |
+
- "bright"/"dark"/"balanced" from spectral centroid
|
| 328 |
+
- Returns `"silent"` if RMS < -60dB (silence gate)
|
| 329 |
+
- Max ~60 tokens per call
|
| 330 |
+
|
| 331 |
+
---
|
| 332 |
+
|
| 333 |
+
## Implementation Order (What to Build First)
|
| 334 |
+
|
| 335 |
+
### Sprint 1 (Week 1): Minimal Viable Plugin
|
| 336 |
+
1. Create JUCE 8 CMake project (VST3 + AU targets)
|
| 337 |
+
2. `PluginBridgeProcessor` β empty shell that passes audio through
|
| 338 |
+
3. Build & load in Ableton β verify audio passthrough works
|
| 339 |
+
4. Add `httplib.h` + background server thread
|
| 340 |
+
5. Verify `curl http://localhost:16620/mcp` returns a response from inside Ableton
|
| 341 |
+
|
| 342 |
+
**Done when:** Plugin loads in Ableton, passes audio, responds to HTTP.
|
| 343 |
+
|
| 344 |
+
### Sprint 2 (Week 2): Plugin Hosting
|
| 345 |
+
6. `HostedPluginManager` β load a VST3 by file path
|
| 346 |
+
7. GUI file picker β load Pro-Q 4
|
| 347 |
+
8. Audio routing through hosted plugin (copy bus-sync from juce-plugin-wrapper)
|
| 348 |
+
9. Parameter enumeration via `getParameters()` + `HostedAudioProcessorParameter`
|
| 349 |
+
10. Verify all Pro-Q 4 params visible in debug log
|
| 350 |
+
|
| 351 |
+
**Done when:** Pro-Q 4 loads inside PluginBridge, processes audio, params enumerated.
|
| 352 |
+
|
| 353 |
+
### Sprint 3 (Week 3): MCP Tools
|
| 354 |
+
11. Implement JSON-RPC dispatch (initialize β tools/list β tools/call)
|
| 355 |
+
12. `list_plugins` tool
|
| 356 |
+
13. `search_param` tool (fuzzy keyword match on param names)
|
| 357 |
+
14. `get_params` tool
|
| 358 |
+
15. `set_params` tool (with `beginChangeGesture`/`endChangeGesture`)
|
| 359 |
+
16. Add to `.mcp.json`, test from Claude Code
|
| 360 |
+
17. Token test: set a Pro-Q 4 band from Claude
|
| 361 |
+
|
| 362 |
+
**Done when:** Claude sets Pro-Q 4 Band 1 Gain via MCP. Full loop < 500 tokens.
|
| 363 |
+
|
| 364 |
+
### Sprint 4 (Week 4): Audio Analysis
|
| 365 |
+
18. Embed `Ebu128LoudnessMeter` (4 files, MIT)
|
| 366 |
+
19. Add JUCE FFT + Hann window + ring buffer accumulator
|
| 367 |
+
20. Implement 7-band energy calculation
|
| 368 |
+
21. Add M/S stereo width
|
| 369 |
+
22. Add True Peak via 4Γ oversampling
|
| 370 |
+
23. Implement `get_analysis` tool
|
| 371 |
+
24. Silence gate
|
| 372 |
+
25. Token test: full analysis < 60 tokens
|
| 373 |
+
|
| 374 |
+
**Done when:** `get_analysis()` returns meaningful data on a playing track.
|
| 375 |
+
|
| 376 |
+
---
|
| 377 |
+
|
| 378 |
+
## Build Requirements
|
| 379 |
+
|
| 380 |
+
| Dependency | Source | License | Size |
|
| 381 |
+
|---|---|---|---|
|
| 382 |
+
| JUCE 8 | git submodule | dual GPL/commercial | ~200MB |
|
| 383 |
+
| cpp-httplib | `httplib.h` drop-in | MIT | 1 file, 800KB |
|
| 384 |
+
| nlohmann/json | `json.hpp` drop-in | MIT | 1 file, 850KB |
|
| 385 |
+
| LUFSMeter core | 4 files copied | MIT | ~20KB |
|
| 386 |
+
| CMake 3.22+ | system | β | β |
|
| 387 |
+
|
| 388 |
+
### CMakeLists.txt skeleton
|
| 389 |
+
```cmake
|
| 390 |
+
cmake_minimum_required(VERSION 3.22)
|
| 391 |
+
project(PluginBridge VERSION 0.1.0)
|
| 392 |
+
|
| 393 |
+
add_subdirectory(JUCE)
|
| 394 |
+
|
| 395 |
+
juce_add_plugin(PluginBridge
|
| 396 |
+
COMPANY_NAME "PluginBridge"
|
| 397 |
+
PLUGIN_MANUFACTURER_CODE Plbr
|
| 398 |
+
PLUGIN_CODE Plbr
|
| 399 |
+
FORMATS VST3 AU
|
| 400 |
+
PRODUCT_NAME "PluginBridge"
|
| 401 |
+
IS_SYNTH FALSE
|
| 402 |
+
NEEDS_MIDI_INPUT TRUE
|
| 403 |
+
NEEDS_MIDI_OUTPUT TRUE
|
| 404 |
+
IS_MIDI_EFFECT FALSE
|
| 405 |
+
EDITOR_WANTS_KEYBOARD_FOCUS FALSE
|
| 406 |
+
COPY_PLUGIN_AFTER_BUILD TRUE
|
| 407 |
+
)
|
| 408 |
+
|
| 409 |
+
target_sources(PluginBridge PRIVATE
|
| 410 |
+
Source/PluginBridgeProcessor.cpp
|
| 411 |
+
Source/PluginBridgeEditor.cpp
|
| 412 |
+
Source/HostedPluginManager.cpp
|
| 413 |
+
Source/McpServer.cpp
|
| 414 |
+
Source/AudioAnalyser.cpp
|
| 415 |
+
Source/LufsMeter/Ebu128LoudnessMeter.cpp
|
| 416 |
+
Source/LufsMeter/SecondOrderIIRFilter.cpp
|
| 417 |
+
)
|
| 418 |
+
|
| 419 |
+
target_include_directories(PluginBridge PRIVATE
|
| 420 |
+
libs/ # httplib.h, json.hpp
|
| 421 |
+
Source/LufsMeter/
|
| 422 |
+
)
|
| 423 |
+
|
| 424 |
+
target_compile_definitions(PluginBridge PUBLIC
|
| 425 |
+
JUCE_PLUGINHOST_VST3=1
|
| 426 |
+
JUCE_PLUGINHOST_AU=1
|
| 427 |
+
JUCE_WEB_BROWSER=0
|
| 428 |
+
JUCE_USE_CURL=0
|
| 429 |
+
DONT_SET_USING_JUCE_NAMESPACE=1
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
target_link_libraries(PluginBridge PRIVATE
|
| 433 |
+
juce::juce_audio_utils
|
| 434 |
+
juce::juce_audio_processors
|
| 435 |
+
juce::juce_dsp
|
| 436 |
+
juce::juce_gui_basics
|
| 437 |
+
juce::juce_recommended_config_flags
|
| 438 |
+
juce::juce_recommended_lto_flags
|
| 439 |
+
juce::juce_recommended_warning_flags
|
| 440 |
+
)
|
| 441 |
+
```
|
| 442 |
+
|
| 443 |
+
---
|
| 444 |
+
|
| 445 |
+
## Risk Register
|
| 446 |
+
|
| 447 |
+
| Risk | Likelihood | Impact | Mitigation |
|
| 448 |
+
|---|---|---|---|
|
| 449 |
+
| VST3 hosting crashes on some plugins | Medium | High | Crash isolation: catch exceptions in processBlock, unload plugin on repeated crashes |
|
| 450 |
+
| Port 16620 conflict | Low | Medium | Fallback: try 16621-16625, report active port in plugin GUI |
|
| 451 |
+
| `createPluginInstanceAsync` fails for some AU | Medium | Medium | Fall back to sync `createPluginInstance()` with timeout |
|
| 452 |
+
| DAW sandbox blocks localhost HTTP | Low | High | Test in Ableton/Logic/Reaper early β if blocked, fall back to Unix domain socket |
|
| 453 |
+
| Parameter IDs not stable across plugin versions | Medium | Low | Always use `search_param` by name first, not cached IDs |
|
| 454 |
+
| Multiple PluginBridge instances (multiple tracks) | Certain | Medium | Each instance gets its own port (16620 + instance_index). `list_plugins` shows all. |
|
| 455 |
+
| 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 |
|
| 456 |
+
|
| 457 |
+
---
|
| 458 |
+
|
| 459 |
+
## What Changed From Original Roadmap
|
| 460 |
+
|
| 461 |
+
| Original Plan | Revised | Why |
|
| 462 |
+
|---|---|---|
|
| 463 |
+
| Fork juce-plugin-wrapper | Build fresh (CMake, JUCE 8) | Dead repo, GPL, JUCE 6, missing everything |
|
| 464 |
+
| Borrow AudioPluginHost settings file | Runtime file picker | Simpler, no dependency on external app |
|
| 465 |
+
| Use Gist/Sound-Analyser for FFT | Use JUCE `dsp::FFT` | GPL license conflict |
|
| 466 |
+
| Single port for all instances | Port-per-instance | Multiple tracks need independent access |
|
| 467 |
+
| cpp-httplib default thread pool | Cap at 2-4 threads | Reduce OS thread pressure in plugin host |
|
| 468 |
+
| Phase 3 "MCP + JUCE already exists" | Implement JSON-RPC from scratch in C++ | josmithiii repo is TypeScript/stdio β not usable |
|
| 469 |
+
| 5 tools | 5 tools (unchanged) | API design validated β
|
|