supercollider-mcp
by AJBogo9
README.md
# supercollider-mcp
A Claude Code plugin that connects Claude to SuperCollider, enabling AI-driven
music composition with both real-time playback and non-realtime (NRT) audio
rendering at 50-150x realtime speed.
It bundles three things: the MCP server that drives SuperCollider, a `music-craft`
skill that auto-loads composition and mixing guidance, and a guided `/setup` flow.
## Why SuperCollider?
[SuperCollider](https://github.com/supercollider/supercollider) is a platform for audio synthesis and algorithmic composition. Its synthesis server (`scsynth`) runs independently of any frontend and accepts OSC messages, making it straightforward to drive programmatically.
Two features make it well-suited for AI-assisted composition:
- **Real-time synthesis**: send code, hear results immediately, iterate
- **Non-realtime (NRT) rendering**: render an audio score to a file at 50-150x realtime speed (a 5-minute piece renders in 2-4 seconds, no waiting)
Because the model can't hear, the server also exposes an analysis loop (`sc_analyze`, `sc_compare`) that turns a render into numbers, closing the quality side of `render -> analyze -> adjust`.
## Requirements
- Linux (tested on Ubuntu 24.04)
- `supercollider-server` package (`scsynth` 3.13+)
- PipeWire with JACK compatibility (`pw-jack`)
- Python 3.11+
`/setup` checks for these and guides you through anything missing. The `pw-scsynth` wrapper that routes scsynth through PipeWire is **bundled** (`bin/pw-scsynth`) and put on PATH automatically, so you do not create it by hand.
## Install (Claude Code plugin)
1. Install this repo as a plugin in Claude Code (via the `/plugin` menu or a local marketplace pointing at it).
2. Run **`/setup`**. It detects `scsynth`, `pw-jack`, and Python; guides any missing installs (it never runs `sudo` silently); builds the Python venv at `~/.local/share/sc-mcp/venv`; and verifies with a real render.
3. Reload the Claude Code session so the `supercollider` MCP server connects.
`.mcp.json` and the venv location are handled for you and should not be hand-edited: the committed `.mcp.json` launches `${CLAUDE_PLUGIN_ROOT}/bin/sc-mcp-server`, which locates the venv and runs the server from the plugin root.
The `music-craft` skill loads automatically whenever you compose, arrange, mix, or render, so quality guidance applies without being asked for.
> **Standalone (no plugin):** you can also run the server directly. Create a venv with the dependencies (`pip install .`) and either point your MCP client's `command` at `<venv>/bin/python -m sc_mcp.server`, or run the bundled launcher with `SC_MCP_VENV=<venv> ./bin/sc-mcp-server`. scsynth must be able to reach `pw-scsynth` on PATH (the launcher adds `bin/` for you).
## Tools
| Tool | Description |
|---|---|
| `sc_boot` | Boot scsynth (auto-called by sc_play) |
| `sc_ping` | Check server status |
| `sc_quit` | Shut down scsynth |
| `sc_play(code)` | Run Python/supriya code on the live server |
| `sc_stop` | Stop all sounds, signal song threads to exit |
| `sc_log` | Read scsynth output and exec errors |
| `sc_render(code, duration, output_path, seed, genre)` | NRT render to file (24-bit AIFF, even with a `.wav` name); returns a one-line analysis summary. `seed` = reproducible render; `genre` = analysis profile |
| `sc_analyze(path, time_series, genre)` | Analyze a render: loudness, dynamics, spectrum, stereo, key (`genre` picks the target profile) |
| `sc_prepare_reference(path, output_path, start, duration)` | Decode/trim/resample/normalize a source clip into a render-comparable reference WAV |
| `sc_compare(reference, candidate)` | Perceptual similarity [0-1] of a render vs a reference (mel/timbre, chroma/harmony, RMS-envelope) |
| `save_song` / `load_song` / `list_songs` | Song library |
| `save_pattern` / `load_pattern` / `list_patterns` | Pattern snippets |
Library files (songs, patterns, renders, references) default to `~/.local/share/sc-mcp/`. Override any location with the `SC_SONGS_DIR`, `SC_PATTERNS_DIR`, `SC_RENDERS_DIR`, or `SC_REFERENCES_DIR` environment variables.
## SynthDef library
Eight built-in voice SynthDefs, available in every `sc_play` and `sc_render` call. All of them self-terminate, and none take a `gate`:
- `ambient_pad` -- warm slow-attack pad, good for chords
- `bass_drone` -- dark low-pass sawtooth sub-bass
- `pluck_tone` -- Karplus-Strong plucked string
- `noise_wind` -- band-pass filtered noise (wind, breath)
- `choir_wash` -- formant-filtered pink noise (choir approximation)
- `sample_one_shot` -- one-shot buffer player for audio files (live playback only)
- `kick_drum` -- sine kick with an exponential pitch drop from `punch` to `tone`
- `hihat` -- high-passed white-noise hat (`open_hat=1` for an open tail)
Each also takes an `out` bus (default `MIX_BUS`). Two FX defs back the mastering chain: `master_bus` (auto-added; limiter to hardware) and `reverb_send` (global reverb return); use the `master()` / `reverb()` context helpers rather than adding them by hand.
**33 SynthDefs total** (a `sc_mcp/stdlib/` package, one module per family). Beyond the originals: bass (`sub_bass`, `reese_bass`, `tuned_808`), percussion (`snare_drum`, `clap`, `tom`, `open_hihat`, `crash`, `ride`, and the cinematic `timpani`/`taiko`), strings/brass (`string_ensemble`, `brass_braam`), leads (`fm_lead`, `fm_bell`, `saw_lead`, `acid_lead`, `stab`), and transition FX (`riser`, `downlifter`, `noise_impact`, `boom`). A `sequence()`/`euclid()`/`beats()` clock and a tempo-synced `sidechain()` pump round out the rhythm tools. This makes **hybrid cinematic / trailer** music buildable end-to-end (analyze with `genre="cinematic"`).
See the `music-craft` skill for how to shape each one and a full cinematic recipe.
### Sampling
`load_sample(path)` now works in **both live and NRT**, so the fast render→analyze loop can use real recorded audio. Helpers: `sample(path, at, note=…)` (load + pitch + play), `multisample(mapping, note)` (keyboard-mapped instrument with round-robin + velocity layers), `chop(path, slices, order=…)` (break chopping), and `slicer` (play a buffer region). A tiny **synthesized CC0 starter kit** is bundled under `samples/starter/` (drum one-shots + an `fm_bell` mallet multisample) and resolvable with `sample_path("starter/drums/kick.wav")`; regenerate it with `scripts/build_starter_kit.py`. For realistic orchestra/piano, fetch real CC0 libraries with `scripts/fetch_samples.sh` and canonicalize clips with `prepare_sample()`.
## NRT example
```python
sc_render("""
# Ambient chord progression. Each pad self-terminates after attack+sustain+release.
chords = [[293, 369, 440], [247, 311, 392], [261, 329, 415]]
for i, freqs in enumerate(chords):
for freq in freqs:
with score.at(i * 6.0):
score.add_synth(ambient_pad, freq=freq, amp=0.25,
attack=2.0, sustain=3.0, release=2.0)
with score.at(duration):
score.do_nothing()
""", duration=20.0, output_path="/tmp/ambient.wav")
```
Renders 20 seconds of audio in under 0.1 seconds.
> **File format:** `sc_render` always writes **24-bit AIFF**, even when `output_path` ends in `.wav`. The plugin's own tools (`sc_analyze`, `sc_compare`) and libsndfile/soundfile read it transparently by content, but a strict WAV-only player will reject it and decoding it as 16-bit PCM turns the audio into noise. Decode as AIFF, or name outputs `.aiff` to avoid surprise.
## Real-time example
```python
sc_play("""
import threading, random, time
BEAT = 60.0 / 90.0
def bass_loop():
while not stop.is_set():
with server.at():
server.add_synth(bass_drone, freq=55, amp=0.4, attack=1.0, sustain=2.0, release=1.0)
stop.wait(4 * BEAT)
def melody_loop():
scale = [440, 494, 554, 587, 659]
while not stop.is_set():
with server.at():
server.add_synth(pluck_tone, freq=random.choice(scale), amp=0.3, decay=2.0)
stop.wait(random.uniform(0.5, 2.0))
for fn in [bass_loop, melody_loop]:
threading.Thread(target=fn, daemon=True).start()
""")
# Stop with: sc_stop()
```
## Audio analysis
The model can't hear the audio, so `sc_analyze` turns a render into measurable proxies it can reason about, closing the quality side of the loop (`render -> analyze -> adjust`). It reads the rendered file directly (no real-time playback or recording) and reports:
- **Loudness**: integrated LUFS, loudness range, true peak (dBTP), clipping
- **Dynamics**: dynamic range (peak - RMS)
- **Spectral balance**: 6-band energy, spectral centroid, mud/harshness flags
- **Stereo**: phase correlation, width, and a mono-bass check
- **Key**: detected key/scale, to compare against what was intended
```python
sc_render("...", duration=180.0, output_path="/tmp/track.wav")
# -> Rendered 180.0s ... Analysis: -11.9 LUFS, peak -3.0 dBTP, DR 11.3 dB, ...
sc_analyze("/tmp/track.wav")
# -> full report with per-band levels, stereo metrics, detected key, and notes
```
`sc_render` folds a one-line summary into its output automatically; call `sc_analyze` for the full report.
To match a target track, prepare it once with `sc_prepare_reference`, then call `sc_compare(reference, candidate)` after each render to score how close you are, extending the loop to `render -> analyze/compare -> adjust`.
Pass `genre=` (e.g. `sc_analyze(path, genre="cinematic")`) to judge against a genre profile instead of the ambient defaults.
## Mastering, reverb & reproducibility
Every voice routes through a private mix bus into an auto-added **master** (`In -> HPF -> tilt EQ -> brickwall limiter at -1 dBTP -> hardware`), so every render is peak-safe with no extra work and existing songs are unchanged apart from the safety limiter. For shared space, call `reverb(room=0.85)` once and route voices with `out=REVERB_BUS`; the reverb return is limited too. Pass `seed=` to `sc_render` for reproducible takes (pre-generated random values render identically; UGen noise needs a `RandSeed.ir` UGen).
## Security note
`sc_play` and `sc_render` `exec()` the code you pass them **by design** --- this is an audio scripting tool, and arbitrary Python (and thus filesystem/network access) runs with the server's privileges. Only send code you trust, and run the server where you would run any other local dev tool. Uncaught exceptions inside a live song-loop thread are captured into `sc_log` rather than crashing silently.