Update CLAUDE.md — reflects out-of-process architecture, current status, lessons learned
Browse files
CLAUDE.md
CHANGED
|
@@ -2,118 +2,125 @@
|
|
| 2 |
|
| 3 |
## What This Is
|
| 4 |
A JUCE-based VST3/AU plugin that:
|
| 5 |
-
1. Hosts any third-party VST3/AU plugin
|
| 6 |
2. Exposes ALL hosted plugin parameters via a local MCP server (port 16620)
|
| 7 |
-
3.
|
| 8 |
4. Any AI that speaks MCP (Claude Code, Codex CLI, Gemini CLI) can control any plugin
|
| 9 |
|
| 10 |
## Current Status
|
| 11 |
-
- **Sprint 1:** ✅ Done — plugin shell + MCP server
|
| 12 |
-
- **Sprint 2:** ✅ Done —
|
| 13 |
-
- **Sprint 3:** Next — full MCP client integration test
|
| 14 |
-
- **Sprint 4:** Pending — real-time audio analysis
|
| 15 |
|
| 16 |
-
##
|
| 17 |
-
- **Platform:** macOS only (for now)
|
| 18 |
-
- **License:** GPL (JUCE GPL mode — no commercial license needed, doesn't hamper project)
|
| 19 |
-
- **VST3 install path:** `~/Library/Audio/Plug-Ins/VST3/`
|
| 20 |
-
- **AU install path:** `~/Library/Audio/Plug-Ins/Components/`
|
| 21 |
-
- **Test plugins:** FabFilter Pro-Q 4 (EQ, 737 params), LA-2A compressor (fewer params)
|
| 22 |
-
- **Xcode:** CLI tools 2410+ (no full Xcode.app needed)
|
| 23 |
-
- **CMake:** 3.28 via pip (system CMake 4.x incompatible with JUCE 8)
|
| 24 |
-
- **DAW:** Ableton Live (primary), Logic Pro (secondary AU test)
|
| 25 |
-
- **Build generator:** Unix Makefiles or Ninja (NOT Xcode generator — no Xcode.app)
|
| 26 |
|
| 27 |
-
## Architecture (Single Plugin Host, Not a Graph)
|
| 28 |
```
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
├──
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
```
|
| 37 |
|
|
|
|
|
|
|
| 38 |
## Build System
|
| 39 |
- **JUCE 8** via CMake (git submodule at `/JUCE`)
|
| 40 |
-
- **
|
| 41 |
-
-
|
| 42 |
-
-
|
| 43 |
-
- **
|
|
|
|
| 44 |
- **Build:** `cd build && cmake .. -DCMAKE_C_COMPILER=/usr/bin/cc -DCMAKE_CXX_COMPILER=/usr/bin/c++ && cmake --build . --config Release`
|
| 45 |
-
- **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
## MCP Protocol
|
| 48 |
-
- Transport: Streamable HTTP (NOT stdio — we're inside a DAW process)
|
| 49 |
- Endpoint: `POST http://127.0.0.1:16620/mcp`
|
| 50 |
-
- Health
|
| 51 |
- Wire format: JSON-RPC 2.0
|
| 52 |
-
- Handshake: `initialize` → `notifications/initialized` → then tools/list, tools/call
|
| 53 |
- 5 tools: `list_plugins`, `search_param`, `get_params`, `set_params`, `get_analysis`
|
| 54 |
|
| 55 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
- `formatManager.addFormat(new juce::VST3PluginFormat())` — explicit registration
|
| 57 |
- `formatManager.addFormat(new juce::AudioUnitPluginFormat())` — explicit registration
|
| 58 |
- `instance->enableAllBuses()` — MUST call before using
|
| 59 |
- `plugin->getParameters()` → `Array<AudioProcessorParameter*>`
|
| 60 |
-
-
|
| 61 |
- `param->beginChangeGesture()` / `setValueNotifyingHost(val)` / `endChangeGesture()`
|
| 62 |
-
- `
|
| 63 |
-
- `juce::PluginDirectoryScanner` for system plugin scan
|
| 64 |
-
- `juce::KnownPluginList` for storing scan results
|
| 65 |
|
| 66 |
## Key JUCE 8 APIs (BROKEN — do NOT use)
|
| 67 |
-
- ❌ `addDefaultFormats()` — deleted
|
| 68 |
-
- ❌ `
|
| 69 |
- ❌ `getParameter(int)` / `setParameter(int, float)` — deprecated, asserts
|
| 70 |
|
| 71 |
-
##
|
| 72 |
-
-
|
| 73 |
-
-
|
| 74 |
-
-
|
| 75 |
-
-
|
| 76 |
-
-
|
| 77 |
-
|
| 78 |
-
## Design Laws (Non-Negotiable)
|
| 79 |
-
- Never return all parameters — search first, fetch by ID
|
| 80 |
-
- Delta only after param changes
|
| 81 |
-
- Single-line compact responses (numbers only)
|
| 82 |
-
- Batch operations (set 10 params in one call)
|
| 83 |
-
- Silence detection (no analysis if RMS < -60dB)
|
| 84 |
-
- Target: <2,000 tokens per full mixing session
|
| 85 |
|
| 86 |
-
##
|
| 87 |
-
- HTTP handlers run on httplib's thread pool — NOT audio thread
|
| 88 |
-
- Plugin scan runs in background thread — NOT message thread (crashes DAW otherwise)
|
| 89 |
-
- Use `CriticalSection` / `ScopedLock` for plugin hot-swap in processBlock
|
| 90 |
-
- Cap httplib thread pool to 2 threads
|
| 91 |
-
- `httplib::Server` lifecycle: constructor starts thread, destructor calls stop()+join()
|
| 92 |
-
|
| 93 |
-
## What NOT To Do
|
| 94 |
-
- Don't use `AudioProcessorGraph` — we host ONE plugin, not a graph
|
| 95 |
-
- Don't scan plugins on editor open — scan only on button click, in background thread
|
| 96 |
-
- Don't use `addDefaultFormats()` — deleted in JUCE 8
|
| 97 |
-
- Don't use stdio MCP transport — plugin is inside DAW, not a subprocess
|
| 98 |
-
- Don't target Windows yet — macOS only for now
|
| 99 |
-
|
| 100 |
-
## Repo Structure
|
| 101 |
```
|
| 102 |
pluginbridge/
|
| 103 |
-
├──
|
| 104 |
-
├──
|
| 105 |
-
├── SYNC.md ← coordination between HF Claude and Claude Code
|
| 106 |
-
├── CMakeLists.txt ← JUCE 8 CMake (LANGUAGES C CXX)
|
| 107 |
-
├── .gitignore
|
| 108 |
-
├── JUCE/ ← git submodule
|
| 109 |
├── libs/
|
| 110 |
-
│ ├──
|
| 111 |
-
│
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
## What This Is
|
| 4 |
A JUCE-based VST3/AU plugin that:
|
| 5 |
+
1. Hosts any third-party VST3/AU plugin in a **separate crash-safe process**
|
| 6 |
2. Exposes ALL hosted plugin parameters via a local MCP server (port 16620)
|
| 7 |
+
3. Audio routes through shared memory (zero added latency)
|
| 8 |
4. Any AI that speaks MCP (Claude Code, Codex CLI, Gemini CLI) can control any plugin
|
| 9 |
|
| 10 |
## Current Status
|
| 11 |
+
- **Sprint 1:** ✅ Done — plugin shell + MCP server
|
| 12 |
+
- **Sprint 2:** ✅ Done — out-of-process plugin hosting, crash-safe, GUI working
|
| 13 |
+
- **Sprint 3:** Next — full MCP client integration test (search/get/set params via curl)
|
| 14 |
+
- **Sprint 4:** Pending — real-time audio analysis (LUFS, FFT)
|
| 15 |
|
| 16 |
+
## Architecture: OUT-OF-PROCESS (Crash-Safe)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
|
|
|
| 18 |
```
|
| 19 |
+
┌─ Ableton Process ─────────────────┐ ┌─ PluginBridgeHelper Process ─────┐
|
| 20 |
+
│ │ │ │
|
| 21 |
+
│ PluginBridge.vst3 (thin shell) │ │ Standalone executable │
|
| 22 |
+
│ ├── McpServer (port 16620) │ │ ├── Loads third-party plugin │
|
| 23 |
+
│ ├── HelperConnection │ │ ├── processBlock via shared mem │
|
| 24 |
+
│ │ ├── Shared memory (audio) │◄───►│ ├── Shows plugin GUI (floating) │
|
| 25 |
+
│ │ ├── Semaphores (sync) │ │ └── Handles param get/set │
|
| 26 |
+
│ │ └── Unix socket (IPC/cmds) │◄───►│ │
|
| 27 |
+
│ └── processBlock(): │ │ If crash → only helper dies │
|
| 28 |
+
│ write shm → signal → wait │ │ Ableton stays alive │
|
| 29 |
+
│ read output from shm │ │ │
|
| 30 |
+
└────────────────────────────────────┘ └───────────────────────────────────┘
|
| 31 |
```
|
| 32 |
|
| 33 |
+
**Why out-of-process:** Some plugins (Ozone 12, Kickstart 2) call abort() or show NSAlert during loading, which crashes the entire host process. By running in a separate process, crashes are isolated — Ableton never dies.
|
| 34 |
+
|
| 35 |
## Build System
|
| 36 |
- **JUCE 8** via CMake (git submodule at `/JUCE`)
|
| 37 |
+
- **Two build targets:**
|
| 38 |
+
- `PluginBridge` — VST3/AU plugin (loaded in DAW)
|
| 39 |
+
- `PluginBridgeHelper` — standalone executable (bundled inside .vst3/Contents/Resources/)
|
| 40 |
+
- **cpp-httplib** — `libs/httplib.h` (MIT, header-only)
|
| 41 |
+
- **nlohmann/json** — `libs/json.hpp` (MIT, header-only)
|
| 42 |
- **Build:** `cd build && cmake .. -DCMAKE_C_COMPILER=/usr/bin/cc -DCMAKE_CXX_COMPILER=/usr/bin/c++ && cmake --build . --config Release`
|
| 43 |
+
- **CMake 3.28** required (via pip — system CMake 4.x incompatible with JUCE 8)
|
| 44 |
+
|
| 45 |
+
## Project Decisions (Confirmed)
|
| 46 |
+
- **Platform:** macOS only
|
| 47 |
+
- **License:** GPL (JUCE GPL mode)
|
| 48 |
+
- **VST3 install:** `~/Library/Audio/Plug-Ins/VST3/`
|
| 49 |
+
- **Test plugins:** Pro-Q 4, LA-2A, Kickstart 2 (crash test)
|
| 50 |
+
- **Xcode CLI tools:** 2410+
|
| 51 |
+
- **DAW:** Ableton Live 12 (primary)
|
| 52 |
+
|
| 53 |
+
## IPC Between Plugin ↔ Helper
|
| 54 |
+
- **Audio:** POSIX shared memory (`shm_open` + `mmap`) — zero-copy
|
| 55 |
+
- **Sync:** Named semaphores (`sem_open`) — ~microsecond wakeup
|
| 56 |
+
- **Commands:** Unix domain socket, newline-delimited JSON
|
| 57 |
+
- **Protocol:** See `Source/Shared/IPCProtocol.h` for all message formats
|
| 58 |
|
| 59 |
## MCP Protocol
|
|
|
|
| 60 |
- Endpoint: `POST http://127.0.0.1:16620/mcp`
|
| 61 |
+
- Health: `GET http://127.0.0.1:16620/health`
|
| 62 |
- Wire format: JSON-RPC 2.0
|
|
|
|
| 63 |
- 5 tools: `list_plugins`, `search_param`, `get_params`, `set_params`, `get_analysis`
|
| 64 |
|
| 65 |
+
## Plugin GUI Behavior
|
| 66 |
+
- GUI runs in Helper process as a **floating window** (always-on-top)
|
| 67 |
+
- Uses `NSApplicationActivationPolicyAccessory` — no Dock icon, no focus steal
|
| 68 |
+
- `orderFrontRegardless` — shows without switching away from Ableton
|
| 69 |
+
- Toggle via "Open GUI" / "Close GUI" button in PluginBridge's panel
|
| 70 |
+
- If plugin crashes → GUI disappears, PluginBridge shows "Plugin crashed - click Reload"
|
| 71 |
+
|
| 72 |
+
## Key JUCE 8 APIs (Verified Working)
|
| 73 |
- `formatManager.addFormat(new juce::VST3PluginFormat())` — explicit registration
|
| 74 |
- `formatManager.addFormat(new juce::AudioUnitPluginFormat())` — explicit registration
|
| 75 |
- `instance->enableAllBuses()` — MUST call before using
|
| 76 |
- `plugin->getParameters()` → `Array<AudioProcessorParameter*>`
|
| 77 |
+
- `dynamic_cast<juce::AudioPluginInstance::HostedParameter*>(param)` for stable ID
|
| 78 |
- `param->beginChangeGesture()` / `setValueNotifyingHost(val)` / `endChangeGesture()`
|
| 79 |
+
- `juce_add_gui_app()` for Helper target in CMake
|
|
|
|
|
|
|
| 80 |
|
| 81 |
## Key JUCE 8 APIs (BROKEN — do NOT use)
|
| 82 |
+
- ❌ `addDefaultFormats()` — deleted
|
| 83 |
+
- ❌ `juce::Thread::setCurrentThreadPriority()` — removed, use native pthread
|
| 84 |
- ❌ `getParameter(int)` / `setParameter(int, float)` — deprecated, asserts
|
| 85 |
|
| 86 |
+
## macOS-Specific Gotchas (Learned the Hard Way)
|
| 87 |
+
- ❌ `MSG_NOSIGNAL` — doesn't exist on macOS. Use `signal(SIGPIPE, SIG_IGN)` + `SO_NOSIGPIPE`
|
| 88 |
+
- ❌ Plugin scanning in background thread — some plugins show NSAlert → crash. Solution: don't scan at all, just list .vst3/.component files from disk
|
| 89 |
+
- ❌ `NSApplicationActivationPolicyRegular` — steals focus from DAW. Use `Accessory` (policy 1)
|
| 90 |
+
- ❌ `activateIgnoringOtherApps:YES` — switches away from Ableton. Use `orderFrontRegardless` instead
|
| 91 |
+
- ✅ `posix_spawn` — works for launching helper from within a plugin process
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
+
## File Structure
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
```
|
| 95 |
pluginbridge/
|
| 96 |
+
├── CMakeLists.txt (builds PluginBridge + PluginBridgeHelper)
|
| 97 |
+
├── JUCE/ (git submodule)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
├── libs/
|
| 99 |
+
│ ├── httplib.h
|
| 100 |
+
│ └── json.hpp
|
| 101 |
+
├── Source/
|
| 102 |
+
│ ├── Plugin/ ← VST3/AU (runs inside Ableton)
|
| 103 |
+
│ │ ├── PluginBridgeProcessor.h/.cpp
|
| 104 |
+
│ │ ├── PluginBridgeEditor.h/.cpp
|
| 105 |
+
│ │ ├── HelperConnection.h/.cpp
|
| 106 |
+
│ │ └── McpServer.h/.cpp
|
| 107 |
+
│ ├── Helper/ ← Standalone exe (separate process)
|
| 108 |
+
│ │ ├── main.cpp
|
| 109 |
+
│ │ ├── HelperPluginHost.h/.cpp
|
| 110 |
+
│ │ └── HelperIPC.h/.cpp
|
| 111 |
+
│ └── Shared/ ← Used by both targets
|
| 112 |
+
│ ├── SharedAudioBuffer.h
|
| 113 |
+
│ ├── IPCProtocol.h
|
| 114 |
+
│ └── Constants.h
|
| 115 |
+
├── CLAUDE.md
|
| 116 |
+
├── ROADMAP.md
|
| 117 |
+
└── SYNC.md
|
| 118 |
```
|
| 119 |
+
|
| 120 |
+
## What NOT To Do
|
| 121 |
+
- Don't load plugins in-process — crashes DAW with misbehaving plugins
|
| 122 |
+
- Don't use `PluginDirectoryScanner` — triggers plugin code execution → crash
|
| 123 |
+
- Don't use `addDefaultFormats()` — deleted in JUCE 8
|
| 124 |
+
- Don't steal focus when showing GUI — use Accessory policy
|
| 125 |
+
- Don't use `MSG_NOSIGNAL` — macOS doesn't have it
|
| 126 |
+
- Don't use `juce::Thread::setCurrentThreadPriority()` — removed in JUCE 8
|