chirp · Rust · CPU only · one binary

Chirp

Chirp is a synthesizer and sequencer driven by text: a line of score, or a YAML document, is rendered to sound by a CPU engine written in Rust, from a two-note notification to a mixed and mastered song. It plays in a terminal and takes edits while it plays, drives MIDI gear on the same clock, serves agents over MCP, and runs in a browser tab, compiled to WebAssembly.

Open the app How it works

chirp @demo
Waveform: the built-in demo song The rendered audio of the built-in demo song, 8.1 seconds.

52 voices8.1 srendered in 569 ms-16.0 LUFSplan 38d4ca12Open in the app

The built-in demo: kit, ducked pad, glide bass, strummed guitar and a shared reverb, rendered by the engine for this page.

1Write a sound

Chirp's score notation reads left to right, one token per event. Its one rule is that settings latch: an octave, a length, a velocity or any key=value applies to every later token until it changes, so a line says only what moves. Brackets scope changes, key= resets one setting and reset resets them all.

expr.pm=1a latched expression: palm mute on from here
F1/16@118F in octave 1, a sixteenth, velocity 118 for this note
F@66octave and length latched; softer
F@74the next sixteenth
.a rest of the latched length
F2/8^phan eighth with a pinch harmonic, this note only

A note is a name (C#4, Bb3), a MIDI number (m60) or a frequency (440hz). A chord is a symbol, C:maj7, voiced by the patch (close, open, drop-2 or real guitar fretboard shapes), or explicit pitches, C4,E4,G4. Lengths are note values (/8, /4. dotted, /12 triplet, /2+8 tied) or times (/150ms, /2b). Every command-line argument is one track, and track options bind to the next score and latch to later ones.

chirp 'C6/16 E G C7/8'
Waveform: a rising blip The rendered audio of a rising blip, 1.8 seconds.
a rising blip Piano roll of a rising blip: each note as rendered, pitch against time in beats. C6 C7 1

4 voices1.8 srendered in 47 ms-16.0 LUFSplan ab2e9ccbOpen in the app

Four notes on the default patch. The octave latches from C6 and the sixteenth from /16, until C7/8 changes both.
chirp -p guitar-8string -t 90 'E:m/2 C:maj G:maj D:maj/1'
Waveform: strummed chords on an eight-string The rendered audio of strummed chords on an eight-string, 6.8 seconds.
strummed chords on an eight-string Piano roll of strummed chords on an eight-string: each note as rendered, pitch against time in beats. C2 C3 C4 1 2 3

29 voices6.8 srendered in 277 ms-17.0 LUFSplan a193a278Open in the app

Chord symbols on a guitar patch become fretboard voicings, strummed: the roll shows each string arriving a few milliseconds after the last.

Drum kits map symbols to pieces: k kick, s snare, h hat, oh open hat, played together with commas (k,c). A kit latches sixteenth steps, so k . h . s . h . is half a bar of a groove. Any track can follow another's level: --duck kick is a sidechain, measured before the kick's fader, so even a muted trigger track can pump a pad.

chirp -t 120 -n kick -p kit '[k . . . k . . .]x4' --reset -p pad --duck kick 'C4:m7/1 Ab3:maj7/1'
Waveform: a kick ducking a pad The rendered audio of a kick ducking a pad, 7.7 seconds.

16 voices7.7 srendered in 366 ms-16.0 LUFSplan fb848f88Open in the app

A pad ducked by a kick. The waveform shows the pad dipping on every beat.

The rest of the notation is for rhythm and variation. k(5,16)/16 spreads five hits evenly over sixteen steps, a Euclidean rhythm; tracks with different step lengths make polyrhythms. (C|Eb|Gb) picks one alternative each time it plays and F1/16?0.6 plays with probability 0.6; choices re-roll on every repeat and every loop pass, from the song's seed, so a loop varies without drifting. h/8^r=3 ratchets three hits into one note, s^flam adds a grace note, and %1 %b3 %5 are scale degrees in a latched key= and mode=.

chirp -t 104 -p kit '[k(5,16)/16]x4 ; [s(2,8,2)/8]x4 ; [h(7,12)/12]x4'
Waveform: three Euclidean rhythms The rendered audio of three Euclidean rhythms, 9.3 seconds.
three Euclidean rhythms Piano roll of three Euclidean rhythms: kit pieces by row over time. kick snare hat 1 2

56 voices9.3 srendered in 190 ms-17.3 LUFSplan 821af780Open in the app

Five kicks over sixteen sixteenths, two snares over eight eighths (rotated), seven hats over twelve triplet eighths: three cycles that line up only on the bar.

2The song model

A score line is sugar. The general form is a song: YAML or JSON with tracks, buses, a master, inline patches, embedded songs, a tempo map and MIDI clock. A track plays a native patch, drives a MIDI device or embeds another song, and every kind sits on one sample-exact timeline. Saved songs compose: @done is a track or a score event, with the same transpose, stretch, envelope, effects and sends as any other, and with overrides such as @done tempo=150.

tempo: 112
buses: {verb: {fx: ["reverb mix=1 size=0.85"]}}
tracks:
  - {name: drums, patch: kit, score: "[k . h . s . h . k k h . s . oh .]x2"}
  - {name: pad, patch: pad, score: "C4:m7/1 Ab3:maj7/1", fx: ["duck by=drums depth=-9dB"]}
  - {name: gtr, patch: guitar, score: "R/2 C:m/4 . Ab:maj/4", sends: {verb: -12dB}}
  - {song: "done tempo=180", at: 2bar, transpose: 12, envelope: "a=1s r=1s"}
  - {midi: {device: ableton, channel: 2, cc: {cutoff: {value: 0.2, mod: [{src: lfo.slow, amount: 0.6}]}}},
     lfo: {slow: "sine rate=1/1"}, score: "C3:m7/1"}

A song with tempo: inherit is a part: it plays in the tempo of whatever embeds it, following tempo maps, so beats, riffs and layers combine freely, and tempo: x3/2 plays at a ratio of the host's tempo for metric modulation. A song's harmony: is a progression, in Roman numerals or chord symbols, and notes written as chord tones ($1 $3 $5 $7) follow whichever chord is sounding at their beat. One bass line then plays over any progression.

tempo: 86
harmony: "key=F2 mode=minor i/1 VI/1 III/1 VII7/1"
tracks:
  - {name: beat, song: beat-dusty}
  - {name: bass, song: harm-bass}
  - {name: arp, song: harm-arp}
  - {name: swell, song: harm-swell}
Waveform: parts following a progression The rendered audio of parts following a progression, 12.6 seconds.
parts following a progression Piano roll of parts following a progression: each note as rendered, pitch against time in beats. C1 C2 C3 C4 C5 1 2 3 4 5 bass arp swell

161 voices12.6 srendered in 1312 ms-16.0 LUFSplan 83fa7a0cOpen in the app

Four library parts over i VI III VII7 in F minor. Only the progression is written here; the bass, arpeggio and swell each find their notes from it, which is why the roll follows the chords.

Timing is part of the model. A tempo map is a list of {at, bpm, ramp, meter} points, and meter: 7/8 changes the meter from that point, so bar positions, bar-quantised live edits and the live view follow odd-meter sections. humanize is how a player feels a part: timing and velocity jitter, a drift that moves the player ahead of and behind the beat over bars rather than note by note, varied note lengths, and accents that follow the meter. A song's feel is the band's shared push and pull on top. All of it is off by default, and all of it is seeded, so the same song renders identically.

3From text to samples

Every input takes the same path. The command line, a score string, a song file and an agent's MCP arguments all become one spec, the model people and agents write. Resolve makes it self-contained. Lower compiles it into a plan, in which every voice's start, release and end frame is already known. Then the engine and the MIDI scheduler play the same plan.

From text to samples Chirp's pipeline. Four kinds of input (command-line arguments, which latch; score notation; a YAML or JSON song; MCP arguments) all become a song spec. Resolve makes it canonical and self-contained: every referenced patch is merged from its extends chain and inlined, referenced songs are resolved recursively, and scores become explicit notes. Lower turns the canonical song into a plan: compiled voice programs with exact start, release and end frames, mixer nodes in processing order and MIDI events on the same frame timeline, with a content hash. The plan drives the CPU engine, which writes to Pulse, CoreAudio, files or a command, and the MIDI scheduler, which sends to ALSA or CoreMIDI; plans also export as Standard MIDI Files. argvlatching options scoreC:maj7/2 k . s . YAML / JSONa whole song MCP argsagents Song (spec)strict: unknown fieldsfail with a path canonicalself-containedpatches inlined Plansample-exactcontent hash CPU enginePulse · CoreAudio · wav · raw MIDI schedulerALSA seq · CoreMIDI .mid resolve lower round-trips: the canonical songreplays to the same plan hash
One path for every input. The plan's content hash is the identity of what you hear.

Strict at the edge

The spec rejects anything it doesn't know, with a path to it, which is what lets an agent repair its own mistakes:

chirp plan --set tracks.0.gian=-3dB 'C4'
chirp: unknown field `gian`, expected one of `name`, `patch`, `midi`, `input`, `double`, `song`, `score`, `notes`, `at`, `repeat`, `transpose`, `stretch`, `vel`, `seed`, `params`, `gain`, `pan`, `envelope`, `lfo`, `auto`, `fx`, `sends`, `out`, `mute` at tracks[0].gian
chirp -p pad 'C3:m9/1 Ab2:mj7/1'
chirp: track `pad`: score token 2 `Ab2:mj7/1`: unknown chord quality `mj7` (known: maj m dim aug sus2 sus4 5 6 m6 7 maj7 m7 …)

Unit strings (-6dB, 2k, 120ms, 1/8, 2b, 1bar) and key-value strings ("reverb mix=0.3") are accepted wherever they make sense, so a song stays short without being loose.

Canonical and replayable

Resolve merges every patch from its extends chain and inlines it, kit pieces included, keyed by the reference text; it resolves embedded songs recursively and expands scores into explicit notes. chirp plan prints the result, and it replays to the same plan hash, which this page relies on: every example's one-liner and the song its Open in the app link carries were checked to plan identically before their audio was rendered.

Known before it sounds

Lowering knows every note before the first sample, so voices are allocated with exact lifetimes and poly, mono, legato and choke policies are applied up front. A cue resolves and lowers in 0.1 to 0.3 ms, and audio starts streaming while the rest renders, so a cue reaches the sound server 3 to 15 ms after the process starts.

4The engine

The engine is offline-first and pull-based. It renders 256-frame chunks, and within a chunk it renders the mixer graph level by level: each node's dependency depth (routing, sidechain keys and follow sources first) groups nodes that cannot affect each other. Those render in parallel on native builds and one after another in WebAssembly, with bit-identical output. Parameters update every 32 frames; gains and amplitude envelopes ramp every sample.

Rendering by dependency depth How the engine renders one 256-frame chunk of a song with a kit, a bass, a pad ducked by the kit, a guitar sending to a reverb bus, and the master. Nodes are grouped by dependency depth: level 0 holds the tracks nothing else depends on first, including the kick that the pad's duck follows; level 1 the tracks that read a sidechain; level 2 the reverb bus; level 3 the master, its limiter and the output. Nodes in one level render in parallel on native builds and one after another in WebAssembly, with bit-identical output. Parameters update every 32 frames; gains and amplitude envelopes ramp every sample. kitsidechain key bass guitar padduck by=kit bus: verbFDN reverb masterfx → volume→ limiter −1 dBFS LEVEL 0 LEVEL 1 LEVEL 2 LEVEL 3 follow send −12 dB
Levels, not threads per track. A sidechain key renders before the track that follows it; buses after their sends; the master last.

5Physical models

A patch is a synth or a kit. A synth stacks layers, with phase modulation between them, filters, envelopes, LFOs and drive. Oscillators, noise and additive layers are there, but the patches that sound like instruments are physical models: a string, a bore or a vocal tract is simulated, and the player's gestures (pick position, bow pressure, breath, vowel) are its parameters. That is why a held note keeps living and a hard pick sounds different from a soft one.

Four physical models Signal flow of four of Chirp's physical instrument models. Plucked string: an excitation (a pick's noise burst or a finger's soft triangle) through a pick-position comb into a Karplus-Strong loop of a fractional delay, a two-tap loss filter, four dispersion allpasses for stiffness and a fret-buzz clip, read out through a pickup comb and resonance. Bowed string: two delay lines meeting at the bow, where a friction table sets stick and slip, a bridge loss filter, then three body resonances. Wind: an exciter (reed, air jet, lips or saxophone reed) coupled to a bore waveguide and a bell. Singing voice: a glottal pulse with breath noise and jitter through a cascade of five formant resonators gliding between vowel targets, with consonants at onset and coda. excitepick or finger pick combposition 1/n delayfractional lossT60 · damping stiff4 allpasses buzzfret clip pickupcomb + resonance bowpressure · speed nut sidedelay frictionstick–slip table bridge sidedelay bridge lossone-pole body3 resonances breathpressure · noise exciterreed · jet · lips borewaveguide bellbright glottiseffort · jitter breathaspiration F1 … F5Klatt cascade ringsinger's formant consonantsonset · coda plucked guitar, bass, harp Karplus–Strong loop bowed violin, cello, erhu waveguide wind clarinet, flute, brass, sax pressure wave voice sung syllables
Four models. Coloured blocks are where the player's gesture enters; the dashed paths are each model's feedback loop.

Plucked strings

The guitar is a DI signal from a Karplus–Strong string. The excitation is a pick's bright noise burst or, as soft rises, the smooth triangle of a finger, whose harmonics fall as 1/n². A comb at the pick position cancels harmonics: picking at 1/7 of the string removes the 7th harmonic, which is a quarter-tone flat of any scale note and is heard as a wrong pitch over a low riff. Four allpasses give wound strings their stretched partials, and their delay at the fundamental is subtracted from the loop, so a stiff string stays in tune. A fret-buzz clip, a choke that darkens the loop after release, pinch harmonics tiled at a harmonic node, and pickup combs with their electrical resonance (a humbucker near 2.5 kHz, a single coil at 4.5 to 7 kHz) complete it. Every stroke varies a little in pick position, brightness, a few cents and about a decibel, and hard picks start slightly sharp.

chirp -t 132 '@beat-djent ; @riff-chug7'
Waveform: a double-tracked riff over a kit The rendered audio of a double-tracked riff over a kit, 8.8 seconds.

303 voices8.8 srendered in 793 ms-16.0 LUFSplan 86655c0eOpen in the app

Two library parts at 132 BPM: a sixteenth double-kick groove and a seven-sixteenth polymetric chug, double-tracked through modelled amps and cabinets.

That riff is double-tracked: double: records a part twice, as two hard-panned tracks with their own seeds for timing and picking, and optionally a different rig on each side. They are genuinely two takes. A doubled riff measures an L/R correlation of about 0.2 between 400 Hz and 3.5 kHz with no widener at all.

Two takes of one riff The first two beats of the double-tracked riff in the chug example. Each onset of the left guitar is drawn above the matching onset of the right guitar; they differ by a few milliseconds in each direction, as two players would, because each side has its own seed for timing and picking. gtr-l gtr-r +3.8 ms +1.0 ms +4.7 ms +1.0 ms beat 1 beat 2
Two players. The same riff's first beats on the left and right guitars, as exported by the engine: every onset lands a few milliseconds apart.

Bowed strings

Bowed layers are a digital waveguide in the McIntyre–Schumacher–Woodhouse line: two delay lines meet at the bow, where a friction table decides whether the string sticks to the bow hair or slips. The built-ins are bowed inside the stable window, in Helmholtz motion, which a test pins: outside it the string slides into double slip, an octave up. The bridge's loss filter has its group delay taken off the loop, so the string is in tune within a cent, and three band-passes sized by body_size make the body. speak is how readily a stroke finds its tone, bite the extra pressure of a fresh stroke, and ^slur continues the bow instead of starting a new stroke.

chirp -p violin -t 84 'E5/4 F#5^slur G5/2^slur B4/4 D5^slur E5/2'
Waveform: slurred violin The rendered audio of slurred violin, 7.0 seconds.
slurred violin Piano roll of slurred violin: each note as rendered, pitch against time in beats. C5 E5 G5 1 2

6 voices7.0 srendered in 166 ms-16.0 LUFSplan 44c67039Open in the app

Slurred violin: the bow continues through the slurred notes.
chirp -p tagelharpa -t 70 'D3,A3,D4/1 _ D3,A3,C4/2 D3,A3,E4/2'
Waveform: a tagelharpa drone The rendered audio of a tagelharpa drone, 11.3 seconds.
a tagelharpa drone Piano roll of a tagelharpa drone: each note as rendered, pitch against time in beats. C3 C4 1 2 3

9 voices11.3 srendered in 393 ms-16.0 LUFSplan 8e737673Open in the app

A tagelharpa, the Nordic bowed lyre, holding drones.

Winds and voices

A wind layer is a bore driven by an exciter: a reed on a cylindrical bore (the clarinet's hollow odd harmonics), an air jet over an edge (the flute), buzzing lips (brass) or a reed on a conical bore (the saxophone, every harmonic and a honking low register). Pressure, breath noise and brightness are playable, and notes must stay in the exciter's range, because below it the oscillation stalls or jumps, as on the real instrument.

The voice layer is a source-filter singer: a glottal pulse whose shape follows effort, with breath, jitter and shimmer, through a cascade of five formant resonators on measured adult vowel targets, gliding between them over 25 ms. Consonants are written per note, as onset and coda codes, so A4^on=2^vow=0.75^off=4 sings "bed". The upper formants can cluster near 2.8 kHz, a trained singer's formant, which carries the voice's upper harmonics at sung pitches.

chirp -p tenor-sax -t 100 'D3/8 F G A/4 C4/8 A3 G/2 F/8 D/2.'
Waveform: a tenor saxophone line The rendered audio of a tenor saxophone line, 6.2 seconds.
a tenor saxophone line Piano roll of a tenor saxophone line: each note as rendered, pitch against time in beats. C3 E3 G3 C4 1 2 3

9 voices6.2 srendered in 190 ms-16.0 LUFSplan bb96ff48Open in the app

A tenor saxophone line.
chirp -p voice -t 84 'A3/4^on=2^vow=0.5 C4^on=4^vow=0.5 E4/2^on=2^vow=0.75^off=4'
Waveform: three sung syllables The rendered audio of three sung syllables, 3.9 seconds.
three sung syllables Piano roll of three sung syllables: each note as rendered, pitch against time in beats. G3 C4 E4 1

3 voices3.9 srendered in 90 ms-16.0 LUFSplan 60390ca9Open in the app

"Ba", "da", "bed": consonant codes and vowel positions per note.

Struck things

Modal layers ring physical mode sets on rotating phasors: Bessel ratios for circular membranes, dense near-uniform sets for plates, cymbals and gongs, tuned bars for marimba, vibes and glockenspiel, and a church bell. The striker's hardness bounds the excitation's bandwidth, the strike position weights the modes, and plate-like sets add the chaotic wobble and wash of a real cymbal. Pianos, harps and vibes take a sustain pedal that lets notes ring to the next bar line.

chirp -p marimba -t 116 '[A3/16 C4 E A4]x2 [F3 A C4 F4]x2 [C4 E G C5]x2 [G3 B D4 G4]x2'
Waveform: marimba arpeggios The rendered audio of marimba arpeggios, 6.5 seconds.
marimba arpeggios Piano roll of marimba arpeggios: each note as rendered, pitch against time in beats. C4 C5 1 2 3 4 5 6 7

32 voices6.5 srendered in 684 ms-16.0 LUFSplan 2847bf54Open in the app

Marimba arpeggios: struck bars with their own mode tunings and decays.

6Effects, amps and the master

Every patch, track, bus and the master has an effect chain, and every parameter is a value or a value with modulation: per-note sources (env, vel, key, rand, expr) and per-channel ones (lfo, auto lanes, follow.TRACK, cc.N), in a short form such as cutoff=800+lfo.a*2.

Amps, cabinets and rooms

Saturators run 4× oversampled with first-order antiderivative anti-aliasing, primed so nothing spikes at start-up. amp models tube stages, a tone stack, power-amp sag and the bias shift of hard playing (bloom), voiced as modern, 5150, crunch, clean, Mark and others. cab models speaker cabinets with mic placement, and ir convolves a real cabinet impulse response by uniform partitioned FFT with 2.7 ms of latency, including speaker cone breakup and a stereo room mic.

The reverb is a modulated eight-line feedback-delay network: predelay, four diffusers, eight mutually prime delay lines with RT60 gains, damping and slow modulation, Householder feedback and early reflections, in room, chamber, hall, plate and spring characters. Its wet level is calibrated so mix behaves alike at any decay.

Loudness, measured

Renders report integrated loudness and true peak by ITU-R BS.1770 / EBU R128 (K-weighted, gated, 4× oversampled peak). --loudness -14 normalises without letting the true peak pass −1 dBTP, and with --limit masters up to the target through a soft clipper and a lookahead limiter, the way records get there. Live sessions keep momentary and short-term loudness, and an integrated figure from a constant-memory histogram, so a session can run for days. Every example on this page was normalised to −16 LUFS this way; the two that read −17 stopped at the −1 dBTP ceiling first.

Racks

A rack is a named, reusable chain with macros, rack:tape-echo space=0.8, written anywhere an effect list is. Racks nest and expand at resolve time, so canonical songs and plan hashes contain plain effects.

7Live sessions

Every audio playback is a live session with a control socket, and --loop makes it endless. The engine is the pure chirp-live crate (no threads, clocks or I/O), which is why the web build uses it unchanged. A session renders successive generations, each a fully lowered plan: a loop pass starts a new one sample-exactly at the previous loop end, with a fresh seed, while the old tails ring underneath. Edits are validated before they are accepted, so a bad one never interrupts the music.

A live session over time A live session renders its song as successive generations. Pass 1 plays; at the loop end, generation 2 starts sample-exactly while the tails of pass 1 ring underneath. An edit sent with --when bar is validated first, then starts a new generation at the next bar line, seeked to the same song position, while the old one fades over the crossfade; an xfade measured in bars is an equal-power morph in which the new generation fades in too. Variables change immediately but glide by their smoothing. A pending stop with a fade ends the session on a bar. 1 2 3 4 5 6 7 8 9 10 11 12 generation 1 · pass 1 generation 2 · pass 2 generation 3 · set tempo=132 generation 4 · pass 3 loop end edit: --when bar --xfade 2bar stop --at bar --fade bars
Edits land on the music's grid. Now, at the next bar, or at the loop end; a crossfade measured in bars is an equal-power morph.
chirp --loop --id groove -s groove-live &
chirp live set tracks.drums.score='k . s . k k s .'   # next loop, track by name
chirp live set --when bar --xfade 2bar tempo=126     # a two-bar morph
chirp live var dark=0.8 wet=0.3                      # variables: immediate, smoothed
chirp live mute drums ; chirp live solo bass          # within ~10 ms
chirp live morph other-song --bars 4                  # into another song from its top
chirp stop groove --at bar --fade 4s                  # a musical ending

8MIDI on the same clock

MIDI is a consumer, like a patch. A track with midi instead of patch sends the same notes, timing, voicings, strums and controller lanes to a device, and native and MIDI tracks mix on one timeline. Clock is a song property: 24 PPQN following the tempo map, with song position, Start and Stop. The scheduler is anchored to the audio stream's measured start, t0 = now + latency − written + offset_ms, so a DAW across the room and the patch in the next track line up.

On Linux the scheduler sleeps coarsely, then spins for a margin learned from how much the platform oversleeps, and sends at the deadline. On macOS each message carries its deadline as a host-clock timestamp and is handed to CoreMIDI 50 ms early, so the MIDI server, not a sleeping thread, delivers it on time. The host-clock FFI is the CLI's only unsafe code. Cancelling waits until everything handed over is due, then sends note-offs, All Notes Off and Stop.

MIDI lateness, measured Measured MIDI lateness as a receiver sees it on macOS, from a background session. Sleeping to each deadline gave a mean of 0.2 to 2 milliseconds and a worst case of 8 to 16 milliseconds. Handing CoreMIDI timestamped messages 50 milliseconds early gave a mean of 0.05 milliseconds and a worst case of 0.35 milliseconds. On Linux, sleeping coarsely and then spinning for a calibrated margin keeps the mean under one millisecond. 0 ms 2 ms 4 ms 8 ms 12 ms 16 ms sleep to each deadline mean 0.2–2 worst 8–16 CoreMIDI timestamps, 50 ms ahead mean 0.05 worst 0.35
Timestamps beat sleeping. Lateness as a receiver sees it, from a background session on macOS: the solid bar is the mean, the faint one the worst case.

9In the browser

The web app is the whole engine compiled to WebAssembly. The core crate needed no changes: its dependencies are pure Rust, with no threads, file system, clock or unsafe. It runs in a dedicated Worker that renders ahead of an AudioWorklet, and since GitHub Pages cannot send the headers that SharedArrayBuffer needs, audio crosses a MessagePort in transferred chunks instead.

Chirp in the browser The web app's architecture. The page's main thread holds the editor, the REPL, the live view and the agent. It starts a dedicated Worker that runs chirp-wasm, the same engine compiled to WebAssembly, rendering about one to one and a half seconds ahead. The worker transfers Float32Array chunks of about 2048 frames over a MessagePort to an AudioWorklet, which plays from a queue and reports its position and underruns; an AnalyserNode feeds the live view's spectrum and scope. Nothing needs SharedArrayBuffer, because GitHub Pages cannot send the headers it requires. The library layers chirp's built-ins, the published chirp-config library and the visitor's own files in IndexedDB. The agent talks to Chrome's built-in model or any OpenAI-compatible endpoint. main threadeditor · REPL · live viewagent loop Workerchirp-wasm LiveEnginerenders ~1–1.5 s ahead AudioWorkletplays from a queuereports underruns modelChrome built-inor OpenAI-compatible librarybuilt-ins ⊂ published⊂ your IndexedDB files speakers + AnalyserNodespectrum · scope → live view edits · statustimeline MessagePortFloat32 × 2048 addUserFiles tool calls
One engine, three places. The same chirp-live session drives the CLI, the player and this Worker.
The Chirp web app with the built-in demo song: a YAML editor on the left showing the song's tempo and its drums, bass, pad and guitar tracks, and on the right the live view's track lanes with each track's notes in its own colour; a REPL at the bottom reports the engine version and the library's size.
The web app with the demo loaded: the song as YAML beside the lanes it plays, and the REPL underneath.

The page offers the REPL with the CLI's verbs, a YAML editor that validates as you type and points at the failing path, a live view with lanes, sections, chords and per-track meters, a mix strip, WAV export at a loudness target, and a library that layers the built-ins, a published library and your own files in IndexedDB. A share link carries the song itself, compressed into the URL's fragment, and never reaches a server: every Open in the app link on this page is one.

10Agents in the loop

chirp mcp stdio exposes the CLI's own handlers to agents: chirp plays a score, a song or a library name; chirp_plan and chirp_render validate and render; the live_* tools steer a running session; presets and chirp_schema describe the library and the notation. The same validation that protects a person's edit protects an agent's.

Editing what's playing

The player and the web app each carry an agent that edits the playing song in conversation: "darker and slower", "add a hi-hat on sixteenths", "write a new 90 BPM groove in A minor". Both use one shared tool file. The compact tier offers a few tools whose enums are regenerated from the live library every turn, so names are always valid; a fuller tier takes path edits against the song's JSON Schema. The model makes one call at a time, and engine errors come back to it for repair.

One agent turn One turn of the agent that edits a playing song, in the player or the web app. The operator asks for a change. The model answers with one tool call at a time: here set_tempo, then set_track with a new pad score that misspells a chord quality. The engine validates every edit by resolving and lowering it, so the error, with the failing token and the known qualities, goes back to the model, which repairs its call. A valid edit is queued for the next bar and heard there, in memory, without touching the song file. The model then calls listen, which reports loudness, true peak, levels and CPU, and finishes with done and a summary for the operator. “slower, and a darker pad”1 set_tempo {bpm: 96}2 set_track {track: pad, score: "C3:m9/1 Ab2:mj7/1"}3 score token 2 `Ab2:mj7/1`:unknown chord quality `mj7` (known: maj m …)4 set_track {track: pad, score: "C3:m9/1 Ab2:maj7/1"}5 valid: resolved and loweredapplied at the next bar6 listen {}7 LUFS · true peak · levels · CPU8 done {summary: "Slowed to 96 and darkened the pad."}9 youoperatoryouoperator modelany OpenAI-compatiblemodelany OpenAI-compatible enginelive sessionenginelive session
Errors are input. A misspelt chord quality comes back with the failing token and the valid qualities; the repair is heard at the next bar.

Conversations persist. Editing an earlier message forks a branch, optionally restoring the song as it was then, and switching branches puts that branch's latest song on at the next bar. /loop keeps the agent going after every turn, and /goal keeps it working until it declares the goal complete.

Reproducing records

Agents also improve Chirp itself, by reproducing reference recordings. A climb takes one record, separates it into stems, transcribes a tab into a first Chirp song, and then improves it round by round. Each round renders the song, measures it against the record section by section and instrument by instrument, takes the biggest gap and fixes it in the most general place that closes it. The rule is that the song is the benchmark and the product is Chirp, so a gap should end up as a better default, a parameterised library patch or an engine feature, not a one-song hack. Several agents share a queue that spreads attention across songs and genres, each round is logged as a handoff to whoever climbs it next, and the operator listens in blind A/B comparisons, because the metric has blind spots and ears overrule it.

A reproduction climb The reproduction loop agents run to improve Chirp. Setup: a reference recording is separated into stems and a tab is transcribed into a first Chirp song. Then each round renders the song with the real engine and measures it against the reference, section by section (chroma, rhythm, band balance, loudness, mastering) and instrument by instrument against the stems. The biggest gap gets a fix in the most general place that closes it: an engine feature for all of Chirp, a library patch for every song, or, only when nothing more general works, the song's own parameters. A round that beats the best score is committed and joins the listening playlist, where the operator judges by ear in blind A/B comparisons; either way the round is logged and the next one starts. referencea recording stems + tabseparate · transcribe song.yamla Chirp song renderthe real engine measuresections · stemschroma · rhythm · bandsloudness · mastering biggest gapnamed by the measures fix it, most general first1 an engine feature2 a library patch3 the song itself beats the best?commit · playlistblind A/B by ear setup next round
The song is the benchmark; the product is Chirp. Fixes flow to the most general place that closes the gap.

The rounds change Chirp under rules. A climb that found bowed strings settling into double slip, an octave up, is why the built-ins now sit inside the stable bowing window, pinned by a test. Doubles became two players when per-take intonation, not per-stroke noise, proved to be what decorrelates them. Kit panning became a default after it won across songs. Changing any default means re-scoring every registered climb before and after, genre-dependent fixes become parameters rather than defaults, and a new option at its neutral value must reproduce the old render bit for bit.

·Budgets

MeasureTargetMeasured
Process start to first audio, local sound server< 50 ms3–15 ms
Resolve and lower a cue< 1 ms0.1–0.3 ms
Two-second bell cue, full render with reverb< 20 ms CPU~10 ms
Eight-track demo render≥ 20× real time~40×
MIDI lateness at the receiver, macOS background session—mean 0.05 ms, max 0.35 ms

Measured on x86-64 under load, as recorded in Chirp's design notes. The render times beside each example were measured when this page was built, including loudness normalisation.