collin / strudel-claude
Strudel + Claude API = Good Vibes
git clone git@anvil.richardscollin.com:collin/strudel-claude.gitCopied!· commits
Strudel × Claude
Live-code music by chatting. A Strudel IDE on the left, a Claude chat panel on the right. Describe the music you want and Claude writes a runnable Strudel pattern into the editor and plays it.
┌────────────────────────────┬──────────────────────┐
│ 🌀 Strudel IDE │ ✦ Claude chat │
│ (CodeMirror editor) │ "dark techno with │
│ ▶ Play / ■ Stop (toggle) │ a rolling bass" │
│ stack( s("bd*4") ... ) │ → writes + plays it │
└────────────────────────────┴──────────────────────┘
Architecture
- Frontend (Vite): CodeMirror editor +
@strudel/webaudio engine + chat UI. Sounds come from Strudel's standard prebake: 71 drum machines, the GM soundfont set, VCSL instruments, and a handful of character samples. - Backend (Express): holds your Anthropic key, calls the Claude Messages API
(
claude-opus-4-8) with the system prompt fromPROMPT.mdand structured output ({ message, code }). The browser never sees the key.
browser ──/api/generate──▶ Express ──▶ Claude API
◀── { message, code } ──
Setup
Credentials — any of these works:
ant auth login(recommended), orcp .env.example .envand setANTHROPIC_API_KEY, or- nothing at all: click 🔑 Add API key at the bottom of the sidebar and paste
your own. It is kept in that browser's localStorage and sent to the backend in
an
x-anthropic-keyheader, which uses it for that request only — never logged, never stored server-side. A personal key takes precedence over the server's, so several people can share one instance with their own billing.
npm install
npm run dev # starts Vite (5173) + backend (8787) together
Open http://localhost:5173, press ▶ Play for the starter pattern, or ask Claude on the right (Cmd/Ctrl+Enter to send).
Driving it from a terminal
The app broadcasts what it is playing — the code, the timing of every event in
it, and the chat transcript — over a WebSocket at /ws. A terminal client
draws all three and can send prompts back:
npm run tui # from a checkout
curl -s http://localhost:8787/tui.py | python3 - localhost:8787 # from anywhere
tui/strudel-tui.py is one file, standard library only — no pip install, and
the server hands it out at /tui.py as text/plain, so you can read it in a
browser or curl -sO it before running it. It takes the app's address
(localhost:8787, https://you.ngrok.app, ws://…/ws — all understood) and
otherwise defaults to localhost:8787.
● playing cycle 2.000 0.500 cps 120 bpm
────────────────────────────────────────────────────────────────────────────
1 stack( │› you
2 s("bd*4").gain(0.9), │ give me a dark rolling techno
3 s("~ cp").room(0.3), │
4 note("<c2 g1>").s("sawtooth") │✦ claude
5 ) │ Rolling kick on every beat,
↑ the characters that made each event │ offbeat hats, a low saw bass.
light up as it sounds and fade out │
across its length │✦ claude is thinking…
╷·····┃···············╷······················╷············· ← one cycle, ┃ = now
› make it half time and add a clap on 3
Panes sit side by side when the terminal is at least 100 columns wide and stack when it isn't. Keys:
q | quit |
i or Enter | write a prompt (Enter sends, Esc cancels, ^U/^W edit) |
j / k | scroll the code |
J / K | scroll the chat |
f | follow the sounding code (on by default) |
A prompt typed here goes through the same path as the composer in the page, so the turn appears in both, reverts in both, and is saved to the session like any other. If a turn is already in flight the new prompt is refused rather than queued — two user messages in a row is an API error — and the terminal says so.
--light for a light-background terminal. It needs 24-bit colour, which every
current terminal has.
How the timing gets there
Each message carries a window of upcoming events plus the playhead position
and tempo, twice a second — not a frame per redraw. The viewer runs its own
clock and animates between messages, so it stays smooth over a slow link and
survives the browser tab throttling its timers to 1 Hz in the background. That
last part matters: Strudel's own highlighting is requestAnimationFrame-driven
and stops dead in a hidden tab, which is exactly when you are looking at the
terminal instead — so src/broadcast.js polls the scheduler rather than
hanging off the draw loop.
The hub takes one producer (the browser tab that owns the audio) and any number of viewers. A viewer may send exactly one kind of message — a prompt — and the hub drops everything else it sends, so a terminal cannot rewrite the editor, work the transport, or push anything onto another viewer's screen.
Open the app in two tabs and the first one keeps the slot; the second is told
to stand by and stays silent until the first goes away, at which point it is
promoted and re-announces itself. The terminal shows (+1 idle tab) so it is
clear why it isn't following the tab you happen to be looking at. The slot only
changes hands when it is free — an earlier version gave it to whoever connected
last, and since a displaced tab reconnects half a second later, two open tabs
took turns evicting each other and the terminal flipped between them twice a
second, forever.
It is unauthenticated, though. If you expose the port, anyone who can reach it can read your session and spend your API credit on prompts, which is a bigger deal than it was when viewers could only watch. Keep it on localhost, or put something in front of it, unless you mean to share the console.
How it works
src/strudel.js— wrapsinitStrudel()/evaluate()/hush().src/main.js— editor, transport, and chat; drops Claude's code into the editor and auto-plays it.src/sessions.js— localStorage-backed sessions (code + chat per session).- Revert: every user message stores the code as it was before that turn, so hovering Claude's reply and clicking ↩ undoes that turn — rewinding both the chat and the pattern. The status bar then offers a one-level Undo.
src/apikey.js— the bring-your-own-key control;GET /api/configtells it whether the server has a key of its own.server/index.js— the Claude call, history trimming and prompt caching.server/broadcast.js— the/wstiming hub;src/broadcast.jsfeeds it andtui/strudel-tui.pyconsumes it.PROMPT.md— the entire system prompt, read from disk at boot. Edit it to teach Claude new sounds or change house style; no code change needed, and the prompt diffs like any other file. Restart the API to pick up edits.
Keeping the prompt honest
PROMPT.md tells Claude not to invent sound names, because an unknown name in
Strudel plays silence rather than raising — a wrong name is a part the user
never hears. So section 3 of the prompt has to match what src/strudel.js
actually prebakes. If you add or remove a sample pack, update that section.
Ideas to extend
- Stream Claude's reply token-by-token into the chat bubble.
- Add a "remix current selection" action.
- Swap the plain editor for
@strudel/codemirrorto get Strudel-native highlighting and event flashing. - Persist sessions / share patterns by URL.