Shipped · 2026
strudel-term
A terminal client for Strudel, the live coding environment for music. An editor, piano roll, oscilloscope and mixer in one pane, with Strudel's pattern engine and synth engine running in Node and no browser anywhere. The language and the sound are Strudel's; this is the interface around them.

Runs in the browser. Nothing is installed and nothing is sent anywhere.
JavaScript · Node · Web Audio · TUI · Strudel
Strudel is a live coding environment for music, normally run in a browser. This is the same pattern engine in a terminal: you type JavaScript, it plays, and the piano roll, oscilloscope and mixer sit under the code in one pane.
Worth saying plainly, because it is most of the project: nearly everything that
makes sound here is Strudel’s work, not mine. The pattern language, the
scheduler, the mini-notation parser and the transpiler are @strudel/core and
its siblings, roughly 22,600 lines. The synth and sampler engine is superdough,
another 9,300. Strudel is by Felix Roos, Alex McLean and its contributors, and
grew out of TidalCycles. What I wrote is about 5,400
lines of terminal client on top: the interface, the editor, a realtime scheduler
and the renderers. It invents no notation, and a pattern written here runs in
the browser REPL unchanged. It is AGPL, as Strudel is.
Type and it plays. The buffer re-evaluates as you type and only when it parses, so a half-finished edit never reaches the speakers or the file. The last working pattern keeps going until the new one is valid.
Install it with Homebrew:
brew tap apolyaker92/tap
brew trust apolyaker92/tap
brew install strudel-termGetting audio out of Node
Strudel’s synth engine, superdough, expects the browser’s Web Audio API.
node-web-audio-api provides that in Node through a Rust backend, which is what
makes running without a browser possible at all.
It also produced the strangest bug in the project. After a minute or two of playing, the audio would start dropping out at random. Disconnected audio nodes stay in the render graph until V8 collects their JavaScript wrappers, and those wrappers are tiny: the audio buffers live on the Rust side, so the heap sits around 15MB and V8 never has a reason to run. Dead nodes pile up until render load passes realtime and the output breaks up.
The fix is to force collection on a timer, which meant re-executing the process
with --expose-gc at startup. Measured over a minute: render load fell from 1.23
to 0.49, with zero underruns and a worst-case pause of 7.3ms against a 150ms
scheduling lookahead.
Editing notes by rewriting the source
The piano roll is editable. Arrow keys move a note and the change is written into your code, not into a separate model that then has to be synchronised.
Strudel’s transpiler tags every mini-notation string with its source offset, and
each event carries the locations that produced it, so a note on screen traces
back to the exact characters that made it. Moving it is a character replacement
at a known range, which means brackets, chords and alternation all work:
"c4 [eb4 c5] ~ g4" is editable without understanding what the brackets do to
timing, because each slot carries its own range.
Counts aren’t editable, on purpose. The 3 in c4*3 is a repeat count rather
than a pitch, and treating it as a slot would let an arrow key silently change
the rhythm.
Effects that superdough does not have
Reverb and delay are sends, merged into each event. A flanger and a chorus are not: superdough implements neither, and there is no way to add one per voice without owning its voice graph. They run on the master output instead, as a delay line swept by an oscillator, with sweep rate, depth and feedback adjustable while the pattern plays.
Auditing which controls actually do anything turned up seven that Strudel accepts and superdough ignores entirely. They evaluate, they change the event, and the sound doesn’t change. Completion now flags them, so you don’t end up chasing them by ear.
Testing something that makes noise
Almost none of this can be tested by listening in CI. The suite renders patterns
through an OfflineAudioContext and asserts on the samples, so a pattern that
produces events nothing can play fails the build. That check exists because two
broken example files shipped: one ended in an uncalled .rev, which left a
function where a value belonged, and the whole thing only surfaced when the
repository was cloned fresh rather than run from the working copy it was written
in.
240 tests run on Node 22 and 24 in CI.