You already know Strudel, or you are about to. This page is about what happens to your code once it is on the site: how a song is read, which of its lines become controls and pictures, what a DJ can do with it that your code never has to handle, and how to write a song that takes advantage of all that. You write the song; the site turns it into an instrument someone else can play.
A song here is plain Strudel. Anything that plays on strudel.cc plays here, with the few exceptions under the sound palette and in the reference. If you are new to it, here is the part of the language that nearly every song on the site is made of.
setcps(124/60/4) // the tempo: 124 beats a minute, one cycle per bar
KICK: s("bd*4").bank("RolandTR909") // four kicks a bar, from the 909
CLAP: s("[~ cp]*2") // rest then clap, twice: claps on two and four
HATS: s("hh(3,8)") // three hats spread evenly over eight steps
BASS: note("*2") // one note per bar, four bars in a row, twice as fast
.s("sawtooth") // played on a sawtooth synth
.lpf(900) // through a low-pass filter at 900 Hz
.gain(0.5) // at half level
LEAD: n("0 2 4 7").scale("A4:minor") // scale steps instead of note names
.room(0.4) // in a little reverb
The text inside double quotes is mini-notation, Strudel's language for rhythm.
Space separates steps in a bar, ~ is a rest, *4 repeats a step four times inside its
space, [ ] groups steps so they share one space, <a b> plays one of its items per
cycle in turn, and (3,8) is a Euclidean rhythm, three hits spread over
eight steps. s() picks a sample or a synth, .bank() picks the drum machine the drum names
come from, note() plays note names and n() with .scale() plays scale
degrees. .lpf(), .room() and .gain() are a filter, a reverb and a level. And
setcps() sets the tempo in cycles per second, which is
why songs write it as beats per minute divided by sixty and by four: one cycle is then one bar of
four beats, which is what the whole site assumes.
That is where this guide stops teaching Strudel and strudel.cc's own workshop takes over: it covers every function in the lines above, and hundreds more, with sound. Learn it there, write here.
There are four ways to get a song onto the deck, and all of them end in the same place: a song of yours in My songs.
.strudel, .js, .str or .txt, or a pattern export from strudel.cc as .json, with any number of patterns in one file. So you can write on strudel.cc, where the whole language is documented and the REPL is quick, export, and import here. Export goes the other way, with all your songs in one strudel.cc-format file.beats/ and reload; it appears beside the demo beats First Light, Low Tide and Clockwork. The repository README has the details.Then shape it for the stage, which is what the rest of this page is about: a header, labels, named knobs, a switch or two, suggestions, notes for the song map, a visual or two and a 32-bar form. None of it is required. A bare pattern plays. But each one gives a listener something to hold.
The site reads the code before it runs it, the way you would read a score, and builds the deck from what it finds. Nothing is declared, nothing is configured: the code is the configuration.
| You write | The listener gets |
|---|---|
DRUMS: s("bd*4") | A channel called DRUMS in the mixer: a level with a real meter, mute, solo and a capture button. The first twelve channels answer to the keys 1 to 9, 0, - and =. |
$: note("c2 g2").s("sine") | A channel too, named from the first sound it plays (here SINE), or "track" if there is none. |
_PAD: … or PAD_: … | A muted layer: a channel that comes up muted, ready to be brought in live. |
const pressure = slider(1200, 300, 4000) | A knob called PRESSURE, 300 to 4000, with a sub-label saying what it feeds. The same slider is drawn in the code; knob and slider are one control. |
.lpf(slider(900, 200, 4000)) inline | A knob named after its track and the parameter, BASS · lpf: accurate, less inviting. |
const mood = 0 with a header line mood: 0 minor, 1 major | A switch with named positions. Turning it re-runs the song in that version. |
const part = 0 // 0-2 | A switch with positions 0 to 2, from the comment beside it. |
setcps(122/60/4) or setcpm(122/4) | The tempo shown as 122 BPM, and the tempo the other deck syncs to. |
@try Label: `find` -> `replace` in the header | A Try chip over the code, up to four. One tap makes the change and runs it; another puts it back. |
| A comment in words just above a part | That part's note in the song map, beside the code. |
._punchcard(), ._scope() and the other inline visuals | A drawing under the line, moving while that part plays, dim while it is muted. |
The opening comment with @title, @by and some plain lines | The song's name on the deck chip and the curtain, its credit, and its notes in the help sheet and in search results. |
Here is a whole song that uses one of each, and what the site makes of every line.
The comment at the top is the header. @title names the song; the two plain
lines are its notes, written for whoever is about to play it; the @try line becomes a chip that swaps
the bass from a sawtooth to a triangle, inside BASS only. setcps(118/60/4) makes the
deck say 118 BPM. const pressure = slider(1200, 300, 4000) is a knob named PRESSURE, and because it goes
straight into .lpf() its readout is in hertz and its travel is logarithmic, so the low end is not
crammed into the first few degrees of turn. DRUMS: is a channel that the No drums pad
recognises by name; the ._punchcard() at the end of its chain draws the drum pattern under the line.
BASS: is a second channel with a ._scope() drawing its wave. _PAD: is a third
that starts muted: the mixer shows it dim, and a listener brings it in when the lights come up, as the notes suggest.
The first comment in the file is the song's identity. The site reads four kinds of line from it and drops the rest.
/*
@title Night Ferry
@by Your Name
A slow one for late crossings. Everything is built from four chords.
Mute DRUMS and the bass carries it alone.
mood: 0 minor, 1 major
@try Glassy lead: `s("sawtooth")` -> `s("triangle")` in LEAD
@try Double-time hats: `hh*8` -> `hh*16`
*/
@title and @by are the name and the credit, shown on the deck chip, the curtain, the song list and in a video clip. Strudel's shorthand // "night ferry" @by yourname on one line works too. Without one of these the song has no title of its own.mood: 0 minor, 1 major, names the positions of a switch (below). It can carry on over the following lines as long as they begin with a digit. Legends are shown in the help sheet with the notes, but kept out of the page description.@try lines become the chips (below).
Any other @tag is dropped, so a @license or @tempo line from another tool does no
harm but is not shown. The tempo is only ever read from setcps() or setcpm() in the code.
A label is a name, a colon and a pattern at the top level of the file: BASS: note(…). Each becomes a
channel in the mixer, and the name is what the listener sees, so choose it the way you would name a track in a
session. Leading and trailing underscores are stripped, camelCase is split into words, duplicates are
numbered, and the result is shown in capitals: subBass: reads SUB BASS.
A label that starts or ends with an underscore, _ACID: or ACID_:, is a muted layer: it
comes up muted, and Reset mutes it again. This is the simplest performance feature you can write.
Three or four muted layers let a DJ build the song up in the mixer over a few minutes, and the song still reads as a
complete piece when every layer is in.
Keep drums in their own tracks, with plain names. The No drums pad finds percussion by name (drum, kick, bd, hat, hh, oh, clap, cp, snare, sd, perc, break, click, rim, cymbal, tom) or, failing that, by what the track plays in its first eight bars; if a track is mostly drum hits it counts. A DRUMS track that also carries the bass line takes the bass out with the drums. Separate tracks also make better snapshots, since a capture takes one channel alone, and a cleaner arrangement strip, which shows one row per label.
What a listener gets from each label: a fader with a meter, M and S, the keys 1 to 12, a ● to capture it, a row on the strip, a name to click in the code to mute it, and an entry in the song map. A song with no labels at all is one pattern: it plays, but there are no strips, no rows, nothing to capture and nothing for No drums to act on.
A song built as floors, each part on its own, two of them waiting with the lights off:
A knob is the main way someone who has never seen Strudel touches your song. Signed-out visitors can turn knobs on
the featured beats, so a knob is often the first thing a stranger does with your music. The site makes every
slider(value, min, max) a knob; your job is to make each one a musical idea.
Name it with a const. const air = slider(0.35, 0, 0.9) shows as AIR
with a sub-label naming up to two of the parameters it feeds. An inline slider is named after its track and parameter,
which is accurate but says nothing about the music. Names that are musical ideas work best: PRESSURE, DARKNESS,
ENERGY, SPACE, DRIVE, AIR, CHAOS, TENSION. A knob called LPF is a technical fact; a knob called DARKNESS is an
invitation.
One const in several places is a macro. Use energy on the bass
filter, again scaled for the hats' level, and again for the pad's reverb, and one gesture changes three things at
once. The site lists every track a knob shapes in its tooltip (Shapes BASS, HATS, PAD). Two to five strong
knobs beat twelve subtle ones, and every position of each should sound good: a visitor will turn it all the way both
ways.
The readout follows the sound. A knob that reaches a filter, directly
(.lpf(cut)) or through .range(), .rangex(), .add(),
.mul() and the like with plain numbers (.lpf(haze.range(400, 3200))), reads in hertz or
kilohertz, showing the frequency the filter actually receives, not the slider's own number. A knob fed straight into
a frequency parameter, with a minimum above zero and a range of four times or more, also gets a logarithmic travel,
so 300 to 4000 feels even across the turn. A mapped knob keeps the curve its mapping gives it.
Write the range as plain positive numbers. The minimum and maximum are read only when they are
number literals: slider(0.5, 0, 1). A negative literal or an expression is not read, so
slider(0, -12, 12) becomes a knob from 0 to 12 and slider(1, 0, max * 2) a knob from 0 to 1.
Put the maths inside the chain instead: .add(detune.sub(12)). A fourth argument sets a step.
Turning a knob rewrites the number in the code without re-running the song, so a knob is instant and safe. Positions on a beat are remembered in the listener's browser, and Reset puts every knob back to the value you wrote, so write the value you want people to hear first.
A switch is a plain whole number at the top level, const section = 0, whose meaning you spell out:
either in the header as a legend, section: 0 intro, 1 groove, 2 drop, or in a comment after the
statement that starts with a range, // 0-2. It appears before the knobs on the deck as a stepped control
with the option's name as its readout.
Because a switch re-runs the song (a moment after the turn settles), it can change anything: the scale, the drum pattern, which parts exist. That makes it the control for real versions, not fine adjustments: intro, groove and drop; minor and major; sparse and full; hook and verse. A knob is for things that should change while the music carries on; a switch is for things that need a new run.
The rules are short. Positions must count up by one from the first, there must be at least two, and the value in the code must be one of them; a number without a legend is just a number. Index lists with it, which is the one place a plain number does what a slider cannot:
const section = 0 // 0-2 DRUMS: s(["bd*2, hh*4", "bd*4, [~ cp]*2, hh*8", "bd*4, cp*2, hh*16"][section])
Two switches in one song, one from a header legend and one from a comment, with a scale and a whole arrangement hanging off them:
A @try line in the header is a change the listener can hear with one tap and put back with another.
Treat them as lessons hidden in the song. Each should teach one Strudel idea: swap a bank, double a subdivision,
move an octave, change a rhythm's shape. They also appear in the first-steps list for new visitors, so for many
people a @try is the first edit they ever make.
@try 808 kit: `RolandTR909` -> `RolandTR808`
@try Busier: `hh*8` -> `hh*16`; `bd*4` -> `bd*8`
@try Glass bell: `s("triangle")` -> `s("sine")` in BELL
`find` -> `replace` in backticks. The arrow → works too.; joins several changes into one tap.in NAME keeps the change inside one track. Without it, every place the find text appears outside comments is changed.
Pick text that is unique to the spot you mean. A chip is greyed, with the hint that that part of the
code has changed since, when the code no longer says find, when the replace text already
appears somewhere else in scope (so the site could not tell the change from its reversal), when two of its changes
would overlap, when the named track is missing, or when it would rewrite a knob's or a switch's number, which belongs
to the deck. So `0.5` -> `0.8` is a poor suggestion and `.room(0.5)` -> `.room(0.8)` is a
good one, as long as only one part has that reverb. A suggestion is read from the code itself, so it still works
after the listener has turned knobs or edited other lines.
Four suggestions, one idea each, the last scoped to one track:
Beside the code (or behind Map when the window is narrow) the song map lists the song's sections: setup (the header, the tempo, sample packs), knobs and switches, parts, and the helpers parts share. Each entry describes itself from the code: the sounds it names, its effects in plain words, the knobs it uses. A click glides the code there, and the entry's card offers Edit this part.
A comment in words becomes a part note, shown first on the card. The site takes an own-line comment that ends just above the part, or sits inside it, or follows it directly with a blank line after. It must contain a letter and not look like code; rows of dashes and commented-out code are ignored, so only real notes show; and it is cut at 160 characters. Header comments are never notes.
A good note says what the part is for and what to do with it, not what the code is: the bass follows the kick and ducks under it, bring these in when the room is ready, the only knob: how far away the keys sit. The code already says it is a sawtooth through a filter; the map reads that out for you.
// a dip on every beat, so the chords breathe with the kick
const duck = "[0.5 1]*4"
// a bass line that walks between two chords
BASS: note("*2").s("square").lpf(600).gain(0.45)
Every part and helper with its own note, so the map reads like liner notes:
Six calls draw a live picture under the line they end: ._punchcard() (a grid of hits, best on drums),
._pianoroll() (notes over time, best on a melody), ._scope() (the waveform, best on a bass),
._spectrum(), ._spiral() and ._pitchwheel(). Each is an
inline visual: it lives under its own line on the stage, dims
while its track is muted, and is drawn into video clips. Put the call last in the chain
so it sits under the part it belongs to, and give the first three a width if the default 640 is too wide for the line.
One or two per song reads best; a drawing under every line is a wall. A punchcard on the drums and a scope on the bass is a good default, with a pianoroll on a lead if the song has one. The stage lights up mini-notation word by word as each event sounds, so the more of your song lives in quoted patterns, the more the stage dances; long patterns built in JavaScript light up less.
The versions without the underscore, .punchcard(), .scope() and the rest, plus
.tscope(), .fscope() and .wordfall(), draw on a canvas behind the whole stage
at half strength rather than under their line, and are left out of clips. They work, but the inline ones are the
ones the site is built around. Hydra is not included.
The arrangement strip under the stage shows which tracks play in each of 32 bars, one cycle per bar. A song that is the same loop for ever shows 32 identical bars, and a listener who trims it is editing a loop. A song with a shape shows that shape, and trimming and rearranging become musical decisions: cut the intro, keep only the drop, move the break earlier.
A shape that works for most things: bars 1 to 8 drums alone or nearly, 9 to 16 everything in, 17 to 24 the fullest stretch or a lift, 25 to 28 a break, 29 to 32 the way out. Techno tends to climb in straight lines, one element every four or eight bars and nothing leaving. Rap and trap want a hook and a verse that are the same length and swap cleanly, with the keys thinner under the verse. Experimental music can ignore all of it, but even there eight bars of something before the strangeness lets a DJ get in. Treat the shape as guidance, not a rule: the point is that the 32 bars tell a story.
Three ways to write it, in plain Strudel:
BASS: note("*2").s("sawtooth").lpf(900)
.mask("<0!8 1!24>")
HATS: s("<[~ hh]*4 [~ hh]*4 [~ hh]*4 [hh*2 hh*3 hh*4 oh]>")
all(x => arrange([8, x.ribbon(0, 8)], [16, x.ribbon(8, 16)], [8, x.ribbon(24, 8)]))
.mask() is the one the strip understands best, because it is what the strip writes. When a listener
silences a track for a stretch, the site adds a 32-step mask to the end of that track's chain, or rewrites one that
is already there. So a mask must be the last call in its chain, after the visual too, for
Silence and Bring back to find it; a mask buried in the middle is left alone and a
second one is added after it. Likewise the site recognises its own trim line by its exact form, so if you hand-edit
one keep the same shape. The strip sees the first 32 bars; a longer form plays, but only those bars are shown and
trimmed.
A whole shape in one loop, written with masks, so the strip shows it from the first bar:
A snapshot takes one channel alone, as it sounds now, for 1, 2, 4 or 8 bars, and keeps it on a pad to bring back in time, or to pin as a looping channel under another song. So a track that sounds good on its own is a gift: a riff, a vocal chop, a fill, a chord stab. Write at least one, give it its own label, and keep it musically separate from the parts around it: a capture takes that channel alone, with its own effects, and nothing else.
Think about length. A riff that takes two bars to say itself needs a four-bar capture to loop cleanly; a one-bar
drum pattern loops at one. Alternation that cycles every four bars, <a b c d>, captures whole at
four or eight bars and oddly at two. The listener chooses the length, but the song decides what lengths make sense.
Two decks play two songs at once, kept in time, with a crossfader between them, pads on the master and a mixer on each. None of this is yours to code, but knowing it is there changes what you write.
setcps(bpm/60/4) or setcpm(bpm/4), so the deck shows a BPM and Sync can bring the other deck in at it. A song without one has no tempo to sync to. The tempo knob bends a deck by up to a quarter either way, so two songs a little apart in tempo still meet._LAYER: tracks give a DJ a song that grows in the mixer over minutes, which is how a set is built.Before any song runs, the site loads a set of sounds every song can rely on. Nothing in this table needs a samples() line.
| Sounds | How to reach them |
|---|---|
| The drum machines | .bank("RolandTR909"), "RolandTR808" and dozens more from the tidal-drum-machines collection, with the usual drum names: bd sd hh oh cp rim cr lt mt ht. The short aliases work too. |
| Dirt-Samples | The classic TidalCycles pack: s("casio"), breaks, hits and voices. Use :n or .n() to pick a sample within a name. |
| Piano, VCSL, mridangam | A sampled piano, the VCSL orchestral and percussion library, and a mridangam, each by its sample names. |
| The uzu drumkit | Strudel's own kit, by its drum names. |
| The synths | sine, triangle, square, sawtooth, supersaw, and the noises white, pink, brown and crackle. |
| General MIDI soundfonts | Every gm_ instrument: gm_epiano1, gm_pad_halo, gm_lead_2_sawtooth and the rest. Each loads the first time it is played. |
| ZZFX | Strudel's small synthesised effects, by their names. |
Anything else is a sample pack, loaded with samples('github:user/repo') or
from any address that allows cross-origin requests; there is no list of approved packs. Packs on GitHub,
felixroos.github.io and Shabda are kept on the device once heard, so a song plays offline the second time. The site
waits about seven seconds for a pack; after that the song starts without it, the status says Still waiting for
sample pack, and the missing sounds come in when the pack arrives. So keep packs small and public, name only the
ones the song plays, and credit them in the header. The About page credits the packs the site's own beats use;
your song's notes are where you credit yours.
The sounds strudel.cc lists on its samples page are the same ones,
and that page explains samples() in full.
Four short songs, each a working example of a style and of the ideas on this page. Open any of them in the player, and Save a copy keeps it to take apart.
Ballast is a long straight build at 134: the kick is there from the first bar and never leaves, the hats arrive at
bar 5, percussion at 9, the pad at 17. Listen for how PRESSURE opens the bass and the pad together, which is the
macro idea from above, and for how the .mask() at the end of each later chain is what
draws the climb on the strip. ACID waits muted for the second half, a layer for the mixer. The two
suggestions each swap one word.
Half Light is half-time at 140: a kick that lands late, a snare on three, hats that roll in sixteenths and double
up every other bar through <…> alternation. The sub slides between two notes on a sine, and WEIGHT
is its level. The part switch is a hook and a verse that are the same length, with the keys thinner under
the verse, and STAB stays muted until the hook needs it. Room is left for a voice.
Paper Street is plain house at 124: four on the floor, a clap on two and four, open hats on the off-beats, piano chords on top. Listen for the form: the first eight bars and the last eight are drums only, so it mixes in and out on the other deck, which is the DJ advice made audible. FILTER is one macro on the bass and the chords, and the second suggestion rewrites a whole mask to bring the bass in from the start.
Loose Teeth lines nothing up on purpose: kicks in threes over eight, hats in fives, a bass line seven bars long so
it drifts against everything. CHAOS drops notes at random, and the texture switch adds grit from a noise
track. It is short and odd, which makes it good snapshot material, and its spiral and pitch wheel show the two
visuals the other sketches do not use.
@title, @by, and a sentence or two of notes written for a listenersetcps(bpm/60/4) or setcpm(bpm/4), one cycle per barconst sliders, with plain positive numbers for the range, musical across the whole turn// 0-2 comment@try lines that each teach one idea, with find text unique to the spot.mask() the last call in its chainslider() used as an indexThen share it: Share explains the link, and Let the site feature this offers it for the community shelf.