Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

HTTP API

claudius studio, claudius serve and a claudius mcp that runs its own session answer HTTP on http://127.0.0.1:7878 (--port, CLAUDIUS_PORT). The studio, in its window and in the browser, uses nothing but this API, so anything it does you can do with curl. The types are plain serde structs in the claudius-api crate (crates/claudius-api/src/lib.rs), which also compiles to WebAssembly for the web studio.

Routes

routeanswer
GET /api/hello{"name":"claudius","version":"0.1.0","state":1,"ears":false}: is this claudius, which version, the state version, whether earshot’s models are loaded
GET /api/statethe state at once
GET /api/state?since=Nthe state as soon as its version is past N (long polling)
GET /api/state?settled=1the state once no job runs and the render is of the current song
POST /api/commandruns one command (JSON body) and answers with a reply
GET /api/scriptthe song as a script, text/plain (// no song yet: try song("house") before the first song)
GET /api/referencethe script language reference, text/markdown
GET /api/soundsevery sound name: drum voices, synth presets, installed instruments, and every pack
GET /api/audio/{version}.wavthe master of that render, 48 kHz 24-bit
GET /api/audio/{version}.mp3the same as MP3, encoded on the first request
GET /api/stems/{version}/{track}.wavone track of a render made with stems
/mcpthe MCP endpoint
GET / and anything elsethe web studio; when it is not built, a page that says how to build it

state, since, settled and timeout combine as described below; timeout is in seconds, 20 by default and 600 at most.

Versions and long polling

Two numbers tell a client what changed:

  • version goes up on every change to anything in the state: a log line, the progress of a job, a new render, an edit.
  • song_version is the version at which the song itself last changed. A render belongs to the song version it was made from, in render.version, so the render is current when render.version >= song_version.

A client keeps itself up to date with one request in flight:

curl -s http://127.0.0.1:7878/api/state                 # version 7
curl -s 'http://127.0.0.1:7878/api/state?since=7'       # returns when the version passes 7, or after 20 s

since=N answers as soon as the version is greater than N, and after timeout seconds otherwise, with whatever the state is then. The client takes the version of each answer as its next since. A since ahead of the server’s version, as after a restart, waits out the timeout once and then answers with the current version.

settled=1 waits for something else: that no job is running and that the render is of the current song. That is the moment the verdict describes the song as it is now. When the session renders only on request (--no-auto-render) and no render was asked for, it is settled at once. settled takes precedence over since.

curl -s -X POST http://127.0.0.1:7878/api/command -H 'Content-Type: application/json' \
  -d '{"cmd":"new_song","genre":"house","seed":9,"bpm":126}'
curl -s 'http://127.0.0.1:7878/api/state?settled=1&timeout=120'    # returned after 7.9 s, with the render and verdict

The MCP tools wait the same way after every edit.

The state

GET /api/state returns a State. Here it is after the request above, with long lists cut short:

{
  "version": 7, "song_version": 2, "busy": null, "progress": null, "ears_loading": false,
  "can_undo": true, "can_redo": false,
  "undo_label": "new song: Crimson Tide — house at 126 BPM in G dorian (seed 9)", "redo_label": null,
  "log": ["sample library: Apple Loops and your folders",
          "new song: Crimson Tide — house at 126 BPM in G dorian (seed 9)",
          "rendered in 7.7 s: -10.1 LUFS, verdict 98.0 — 98/100 (arc 0.93, mix 1.00, theory 1.00)"],
  "genres": [{ "name": "deep_house", "description": "Deep house: softer four-on-the-floor, ...", "bpm": [118.0, 124.0] }],
  "song": {
    "title": "Crimson Tide", "genre": "house", "seed": 9, "bpm": 126.0, "key": "G dorian", "swing": 0.5257842,
    "bars": 64, "seconds": 121.9047619047619,
    "progressions": [["A", ["i7", "ii7", "i7", "IV7"]], ["B", ["i7", "VIImaj7", "i7", "IV7"]]],
    "sections": [{ "name": "intro", "kind": "intro", "start_bar": 0, "bars": 8, "energy": 0.4,
                   "layers": ["kick", "hat", "perc", "top"], "prog": "A", "variant": "sparse",
                   "filter_in": true, "build": false, "gap": false, "impact": false, "fills": false }],
    "layers": ["kick", "clap", "hat", "ohat", "shaker", "perc", "bass", "chords", "pad", "lead", "arp"],
    "tracks": [{ "name": "kick", "kind": "drum", "role": "kick", "sound": "sampled bd (tr909:bd)",
                 "level_db": 0.0, "pan": 0.0, "reverb": 0.0, "delay": 0.0, "duck": 0.0,
                 "muted": false, "solo": false, "loudness": -13.2 }],
    "drums": [{ "lane": "kick", "pattern_bars": 4,
                "variants": [["main", [0.99353635, 0.0, 0.0, 0.0, 0.9903264, 0.0, 0.0, 0.0]]] }],
    "parts": [{ "name": "bass", "role": "bass", "style": "syncopated", "styles": ["offbeat", "syncopated", "offbeat"],
                "preset": "saw_bass", "presets": ["organ_bass", "pluck_bass", "saw_bass", "sub_bass", "808", "reese",
                "acid", "wobble", "upright_bass", "electric_bass", "synth_bass", "contrabass"],
                "register": [33, 52], "density": 0.42195085 }],
    "script": "// Crimson Tide — house (seed 9)\n// song(\"house\", 9);   // uncomment to start over from the generator\n...",
    "lineage": ["generated: house seed 9"]
  },
  "render": {
    "version": 2, "url": "/api/audio/2.mp3", "seconds": 123.58135416666667, "lufs": -10.091306995130768,
    "true_peak_db": -1.05, "bpm": 126.0,
    "sections": [["intro", 0.0, 15.238095238095237], ["groove", 15.238095238095237, 45.71428571428571]],
    "waveform": [0.8221747, 0.501973, 0.47014454, 0.8221747],
    "stems": []
  },
  "verdict": {
    "total": 97.96757459489606, "summary": "98/100 (arc 0.93, mix 1.00, theory 1.00)",
    "scores": [["arc", 0.9288651108213621], ["mix", 1.0], ["theory", 1.0]],
    "issues": [], "ears": null,
    "measures": [["arc_correlation", 0.86], ["band_air_db", -19.1], ["band_low_db", -3.2]]
  }
}

State

fieldtypemeaning
versionintegergoes up on every change
song_versionintegerthe version of the last change to the song
songSongView or nullthe open song, null before the first one
renderRenderView or nullthe latest finished render; it may be of an older song version while a new render runs
verdictVerdictView or nullthe verdict of that render
busystring or nullthe running job: "rendering", "listening", "mutating: judging 4 variations", "evolving: generation 2 of 3 done", "saving"
progressnumber or null0 to 1, when the job knows it
ears_loadingbooleanearshot’s models are still loading; until they are, verdicts come from the meters and music theory only
can_undo, can_redoboolean
undo_label, redo_labelstring or nullwhat undo would take back, what redo would bring back
logstringsthe last 200 lines of the session log, oldest first
genresGenreInfo listevery genre: name, description, bpm range

SongView

fieldmeaning
title, genre, seed, bpm, key, swingthe song’s settings; key reads like "A minor"
bars, secondsits length
progressions[name, [numerals]] pairs, "A" and "B" in generated songs
sectionsSectionView per section: name, kind, start_bar, bars, energy, layers, prog, variant and the flags filter_in, build, gap, impact, fills
layersevery name a section can switch on: drum lanes, parts, loops, pattern tracks
tracksTrackView per track: name, kind (drum, part, loop, pattern, fx), role, sound in words, the mix (level_db, pan, reverb, delay, duck, muted, solo) and loudness in the master (LUFS while playing) once rendered
drumsDrumView per lane: lane, pattern_bars, and variants as [name, velocities] with one velocity per sixteenth (0 for none)
partsPartView per generated part: name, role, style, the styles its genre offers, preset, the presets it may switch to, register (MIDI low, high), density
scriptthe song as a script; running it on the song changes nothing
lineagewhat was done to the song, oldest first

The last entry of tracks is always fx, the transition effects (crashes, risers, impacts), whose mix scripts set with mix("fx", ...).

RenderView

fieldmeaning
versionthe song version it was rendered from
url/api/audio/<version>.mp3; the same path ending in .wav gives the 24-bit master
seconds, lufs, true_peak_db, bpmthe master’s length and loudness
sections[name, start seconds, end seconds], for drawing the arrangement
waveformabout 1000 peak values from 0 to 1
stemstrack names with a stem at /api/stems/<version>/<name>.wav (empty unless rendered with stems)

VerdictView

fieldmeaning
total0 to 100
summaryone line: the total, the scores, the first three issues
scores[name, 0..1] for mix, arc, theory, and with the ears genre, production (and refs)
issues[severity 0..1, what, where to fix it], worst first
earsEarsView or null: genre_query, genre_z, best_alt, best_alt_z, production_z, worst_flaw, worst_flaw_z, sounds_like, p_real
measures[name, value] meters: lufs, true_peak_db, band_*_db, stereo_correlation, drop_minus_break_lu, …

The critic and the ears explains every score, measure and issue target.

Commands

POST /api/command takes one command as a JSON object tagged by "cmd", with Content-Type: application/json. The fields marked optional may be left out.

cmdfieldswhat it does
new_songgenre; optional seed, bpm, key, loops (false)composes a new song
loadpathopens a song folder, a blueprint.json, or a script file (run on the open song)
saveoptional pathwrites blueprint.json and song.rhai now, then the whole folder from a render (see Jobs); default folder ~/Music/claudius/<genre>-<title>-<seed>
set_tempobpmas tempo()
set_keykeyas key(): the progressions follow a change of mode
set_swingswingas swing()
set_progressionname, chords (strings)as prog()
set_sectionindex; optional name, bars, energy, layers, prog, variant, filter_in, build, gap, impact, fillschanges any of a section’s properties
add_sectionafter, kind, barsinserts a section after index after, with that section’s layers, energy and progression
remove_sectionindexremoves a section (a song keeps at least one)
move_sectionfrom, tomoves a section
toggle_layersection, layerswitches a track on or off in a section
set_drum_steplane, variant, step, velocitysets one sixteenth (0 removes the hit); a new variant starts as a copy of main
set_drum_patternlane, variant, stepsa whole variant in step notation, as drum()
set_partname; optional style, preset, register ([low, high]), density, reseed (false)as part(); reseed writes the part again with a new seed
set_mixtrack; optional level_db, pan, reverb, delay, duck, muted, soloas mix()
run_scriptcoderuns a script on the song
analyzepaththe analysis of an audio file, as text in the reply’s message
remakepath; optional genre, seed, bpma new song on the file’s skeleton; the genre defaults to the open song’s, and the tempo to the recording’s, halved or doubled into the genre’s range when it is outside
mutateoptional count (4, 1 to 32), keep_best (true)a job: tries variations and keeps the best if it beats the song
evolveoptional generations (3, 1 to 20), children (6, 2 to 32)a job: an evolutionary search from the song
install_packsoptional names (the missing default packs when left out)a job: downloads sample packs, as claudius setup does; not the opt-in packs, whose licence is unclear, and not on a shared claudius
renderoptional stems (false)a job: renders and judges the song
undo, redo
cancelstops a mutation or evolution after its current candidate or generation
{"cmd": "set_tempo", "bpm": 124}
{"cmd": "set_section", "index": 0, "energy": 0.5, "layers": ["kick", "hat"]}
{"cmd": "set_drum_step", "lane": "kick", "variant": "main", "step": 3, "velocity": 0.6}
{"cmd": "set_part", "name": "bass", "style": "rolling", "reseed": true}
{"cmd": "run_script", "code": "tempo(126); mix(\"lead\", #{ level: -2 });"}

set_tempo, set_key, set_swing, set_progression, set_drum_pattern, set_part and set_mix are turned into the matching script call and run as a script, so they accept exactly what the script functions accept and fail with the same messages. The other edits change the blueprint directly. Every edit that changes the song is one undo step with a label, and starts a render after 200 ms unless the session runs with --no-auto-render.

Jobs

render, mutate, evolve and save are jobs. A job runs on its own thread, one at a time, and the command answers as soon as it has started. Follow it in busy, progress and log, or wait with settled=1. A mutate or evolve while a job runs is refused, and the refusal leaves the version as it was; render and save wait for the job in progress instead:

{"ok": true, "message": "rendering after the job in progress", "version": 4}
{"ok": false, "message": "busy (rendering); try again when it is done, or cancel it", "version": 4}

A save writes blueprint.json and song.rhai at once. Asked for while the session is idle, it starts its own render; asked for during a job, such as the render that follows an edit, it waits, and the job slot passes straight to it when that job ends, so the session never looks idle in between and settled=1 waits for the save too:

{"ok": true, "message": "saving to /tmp/cl-save2 (blueprint.json and song.rhai now, the audio after the render)", "version": 14}
{"ok": true, "message": "saving to /tmp/cl-save2: blueprint.json and song.rhai now, the audio once the current job (rendering) is done", "version": 4}

Either way the save renders the song as it is when its turn comes, with stems, and rewrites the whole folder from that render, so the score in it is always the song its audio plays: song.wav, song.mp3, blueprint.json, song.rhai, project.json, report.json, verdict.json, credits.txt when sample packs play, and stems/ as 32-bit float. The log then says saved <folder>, or could not save <folder>: <reason>.

cancel does not stop a render that has started.

Replies and errors

Every command answers with a Reply:

fieldmeaning
okwhether the command did what it was asked
messagewhat happened, or why not
versionthe state version after the command
{"ok": true, "message": "new song: Crimson Tide — house at 124 BPM in G dorian (seed 9)", "version": 2}
{"ok": false, "message": "no progression 'Z' (there are A, B)", "version": 11}
{"ok": false, "message": "Runtime error: expected ']', found the end (line 1, position 12)", "version": 11}

A command claudius understood but refused (a bad edit, a script error, a busy session) answers 200 OK with "ok": false, and changes nothing. The HTTP status says when the request itself is wrong:

statuswhenbody
400 Bad Requestthe JSON is not a commanda Reply whose message starts with not a command: and names the unknown cmd or the missing field
401 Unauthorizeda token is required and missing or wrongthis claudius needs its token (Authorization: Bearer <token>, or ?token=<token>)
403 Forbiddenthe Host header is not a loopback name and the server is not --publicclaudius only answers on localhost (start it with --public and a --token to share it)
404 Not Foundno such render or stemno render of version 999, no stem 'kick' in render 13 (render with stems), audio is /api/audio/{version}.wav or .mp3
413 Payload Too Largea command body over 1 MiBFailed to buffer the request body: length limit exceeded
415 Unsupported Media Typea command without Content-Type: application/jsoncommands are JSON (Content-Type: application/json)

Audio

/api/audio/{version}.wav and .mp3 serve the render of song version version, with Range requests, so players can seek:

$ curl -sI -H 'Range: bytes=0-99' http://127.0.0.1:7878/api/audio/13.wav
HTTP/1.1 206 Partial Content
content-type: audio/wav
accept-ranges: bytes
content-range: bytes 0-99/72955106
content-length: 100

The MP3 is encoded by ffmpeg on the first request (a few seconds for a song) and kept. Without ffmpeg, the .mp3 address serves the WAV, which browsers play too. A session keeps its last ten renders on disk, so older versions return 404. Stems exist only for a render made with {"cmd": "render", "stems": true} or by save, and only the latest render’s stems are served, as 24-bit WAV; the copies a save writes into the song folder are 32-bit float.

Who may call it

The server is written for one person on one computer, and opens up only when asked. The security model has the reasoning; these are the rules as a client sees them.

Host names. Without --public, every request must name a loopback host in its Host header: localhost, 127.0.0.1 or [::1], with any port. Anything else gets 403, which stops a web page from reaching claudius through DNS rebinding. It is not access control: any program on the machine can send Host: localhost.

Tokens. With --token <secret> (or CLAUDIUS_TOKEN), and always with --public, which makes a token when it is given none, every request must carry the token, in one of three ways:

  • Authorization: Bearer <secret>;
  • ?token=<secret> in the URL, which also answers with Set-Cookie: claudius_token=<secret>; Path=/; HttpOnly; SameSite=Strict, so a browser opened at /?token=<secret> keeps working without it. When the request came through https (X-Forwarded-Proto: https, which tunnels send), the cookie is also Secure;
  • that cookie.

The comparison takes the same time wherever the strings differ. CORS preflight requests (OPTIONS) are answered before the token check, so a browser can ask what is allowed.

Commands are JSON. /api/command refuses any other content type, and the MCP endpoint does the same, so a page on another site cannot send a command with a plain form post or a no-cors fetch.

CORS. Cross-origin requests are allowed from pages served by http://localhost, http://127.0.0.1 and http://[::1] on any port, for GET, POST and DELETE, with the headers Content-Type, Authorization, mcp-session-id and mcp-protocol-version. Pages from anywhere else, including https://localhost, get no Access-Control-Allow-Origin, so the browser does not let them read the answer.

Security headers. Every response carries:

x-content-type-options: nosniff
referrer-policy: no-referrer
x-frame-options: DENY
content-security-policy: frame-ancestors 'none'

so a browser does not guess content types, leak the address in a Referer, or let another page frame the studio.