beats-radio
v0.2.1
Published
Agent-composed Strudel radio: a bun server + prebuilt browser player you run with `bunx beats-radio start`.
Readme
beats-radio
I spent some time teaching Claude to write music in Javascript and inadvertently recreated this meme.
Watch an example
TODO: add
(or visit my radio)
Why did you make this?
My thought process:
- I wonder if Claude can write music?
- Ok, it’s not an audio model, but surely there’s a music coding library? LLMs love writing code. Yes, I know that Suno exists...
- Strudel looks good, let’s try that!
- Ok, some other people have tried that too
- How can it write good songs if it can’t hear them?
- Hmm alright let’s teach it some rules of style
- Back to #4, I don’t actually know that much music theory
- Ok, now we’re getting somewhere
- Ah, there’s a
/radioskill now in Claude Code - That just opens YouTube, lame. Let the agents create!
- Let's make this into an agent-driven radio
- (Fable gets released) it should have Winamp style visualisations too, right?
- Here we are :sparkle:
It also turns out Claude loves writing Chromatics-inspired italodisco and Ethiopian jazz. Who knew?
How does this work?
Your coding agent composes full, arranged songs in Strudel (a live-coding music language); a browser player (the 'radio') streams the shared queue with the real Web Audio engine
The /beats skill writes a song based on a theme (yours or your agents), and you can work collaboratively with your agent to give feedback, tweak or rewrite each song
Get started
Requires bun ≥ 1.2.
To have an agent compose for the radio, install the beats skill to drive the beats-radio CLI. Two ways:
Any SKILL.md-aware agent (Claude Code, Codex, …):
bunx beats-radio install-skillThis copies skills/beats into the agent homes it finds (~/.claude/skills, ~/.codex/skills);
pass --target <dir> to install elsewhere.
Claude Code plugin:
/plugin marketplace add morganjbruce/beats-radio
/plugin install beats@funkmasterAnd you then can compose with:
/beats <theme>The skill will open http://localhost:3001: click ▶ Start radio once to unlock browser audio.
Usage
Themes —
/beats midnight city pop,/beats something like Boards of Canada, or just/beatsand let it choose. The agent plans, writes, validates, and iterates on a full arranged song (real structure and an arc, not a 4-bar loop), then posts it to the queue.The queue — songs stream to every open player tab over SSE and advance on the cycle boundary. Each song has a permalink and shows its generation provenance; you can delete songs from the player.
Evolving radio -
/loop 3m /beats <your theme>to generate songs on an ongoing basisTeam / remote radio — point your agent and the CLI at a shared deployment by putting
BEATS_SERVERandBEATS_TOKENin~/.beats/env:BEATS_SERVER=https://your-radio.example.com BEATS_TOKEN=your-secret-tokenThen
bunx beats-radio post <artifact> --remote(or ask your agent for the remote radio).
Development
Working in a checkout of this repo:
bun install
bun run devdev runs Vite (player on http://localhost:5173) + a watched server (:3001) concurrently.
The repo dogfoods its own
skill: .claude/skills/beats symlinks to skills/beats, so /beats here uses the same skill
that ships in the package.
Deploy your own public radio
The appliance runs anywhere bun does. The repo includes a Dockerfile and fly.toml for
fly.io:
fly launch --no-deploy # create the app (keep the bundled fly.toml)
fly volumes create beats_data --size 1 --region lhr # persistent state for history.db + songs
fly secrets set BEATS_TOKEN=<token> # required to post to the public radio
fly deployNotes:
- The volume mounts at
/app/.beats(the server's cwd-relative default), so history and posted songs survive redeploys. - Keep
auto_stop_machines = "off"withmin_machines_running = 1: the radio holds open SSE streams, so the machine must stay up for the queue and listeners to survive idle periods. - Once deployed, listeners just open the app URL; composers point
~/.beats/envat it (see Usage above).
Share it with your friends, send songs from each other's agents!
CLI reference
| Command | What it does |
| --- | --- |
| start [--port N] [--open] | Start the server + built-in player on :3001 (--open launches a browser) |
| stop | Stop a radio started with start (reads the pid file in the state dir) |
| validate <artifact.mjs> | Statically check a song artifact (regex + syntax) before it plays |
| post <artifact.mjs> [--remote] | Post a song to the running radio; --remote uses ~/.beats/env |
| sounds | Print the authoritative prebaked-sound inventory as JSON |
| install-skill [--target <dir>] | Copy the beats skill into ~/.agents/skills (the universal dir) plus ~/.claude/skills / ~/.codex/skills where present (or --target) |
| version | Print the version |
What would make this better?
- A real critique loop. Claude can’t process audio, so we could feed it through a model that can, like Gemini. But I wanted something that was only dependent on one agent harness, so we just have structural validation & a bunch of rules to follow... which mostly works, but sometimes you get slop
- Build a corpus of generated songs that meet human preference, and use those as examples for Claude of good/bad songwriting.
- It’s probably overfitted on verse-chorus-verse-chorus-bridge songs, because I got sick of little loops
Huge appreciation for:
- This excellent project, from which I cribbed a bunch of my initial prompting (particularly around harmony & music theory)
- The folks behind Strudel, tidecycle and the many, many giants that this is standing on :)
See also:
Thanks, I hate it
- It’s not a replacement for human creativity and genius, I built this to satisfy my curiousity!
- Best to think of these as rough prototypes, not real songs
- Fable/Opus love using every cliche in the book, but sometimes, they generate something that’s (at the very least) memorable, and sometimes genuinely touching
- You probably shouldn't read the code too closely, this is the repo where I let the agents run wild
