Skip to content

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.

strudel-term running in a terminal, showing the code pane, a piano roll with coloured notes and a playhead, the mixer with reverb and flanger rows, and an oscilloscope.

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-term

Getting 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.