Skip to main content

strudel

Write a live-coded track as a Strudel pattern — mini-notation, synths, effects, song structure — and check it with the verdict. Use whenever the workspace holds a .strudel file or the user asks for a beat, a groove, a bassline, a loop, a track, or music in the pane.

Jump to install

Source facts

Repository
autonomous-ai/openharness
Last source activity
September 22, 2026 at 08:16
Detected SKILL.md language
English
Stars
822
Forks
67

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
strudel
description
Write a live-coded track as a Strudel pattern — mini-notation, synths, effects, song structure — and check it with the verdict. Use whenever the workspace holds a .strudel file or the user asks for a beat, a groove, a bassline, a loop, a track, or music in the pane.
# Strudel Strudel is TidalCycles in JavaScript. A track is **one expression** that describes a cycle; the REPL plays that cycle forever until you change it. You write one file, `track.strudel`, and the pane plays it — a save hot-swaps the pattern on the next cycle without stopping the transport. The whole file is evaluated as ECMAScript 2022 (top-level `await` allowed), so JavaScript syntax rules apply: balanced brackets, balanced quotes, `//` and `/* */` comments. There is no module loader — **never write `import` or `require`**. Everything Strudel offers is already in scope. Check after every save: ```sh python3 "$STRUDEL_TOOLCHAIN/verdict.py" # track.strudel python3 "$STRUDEL_TOOLCHAIN/verdict.py" b.strudel ``` It parses the file with the same parser the REPL uses, checks offline sound sources, and writes `.harness/verdict.json`, which is what the pane header shows. It does not listen to the music. **Playing is the pane's job; musical quality needs listening and judgment.** ## Offline first: use synths, not samples The REPL preloads sample *maps* from GitHub at start and fetches each `.wav` the first time it is triggered. So `s("bd sd hh")` needs the internet, twice. These sound sources need nothing: | Source | Names | |---|---| | Oscillators | `sine` `sawtooth` `square` `triangle` (aliases `sin` `saw` `sqr` `tri`) | | Richer | `supersaw` (`unison`, `spread`, `detune`), `pulse` (`pw`, `pwrate`, `pwsweep`) | | Noise | `white` `pink` `brown` (hard → soft), `crackle` (with `density`) | | Drum | `sbd`, a synthesised bass drum | | Chip | `zzfx` `z_sine` `z_sawtooth` `z_triangle` `z_square` `z_tan` `z_noise` | Build drums out of those: `s("sbd*4")` is the kick, short noise is the snare and the hats. ```js stack( s("sbd*4").decay(0.24).lpf(280), s("~ white ~ white").decay(0.11).sustain(0).hpf(1200).gain(0.5), s("white*8").decay(0.03).sustain(0).hpf(7000).gain("[0.3 0.14]*4") ) ``` Reach for `bd sd hh oh rim cp` and `.bank("RolandTR909")` only when the user wants real drum machines and has a connection. Say so when you do; the verdict warns about it too. If you set `note()` and no `s()`, the sound defaults to `triangle` — already offline. ## Mini-notation Everything inside a double-quoted string is mini-notation. Single quotes are a plain string, not a pattern. | | | |---|---| | `a b c` | a sequence; the whole thing still takes one cycle, so more events = shorter events | | `~` or `-` | a rest | | `[a b]` | a subdivision — one slot of the outer sequence, nests as deep as you like | | `<a b c>` | one per cycle. `<a b c>` is exactly `[a b c]/3` | | `a*2` | faster / repeat within the slot (decimals allowed: `*2.75`) | | `a/2` | slower — spread over 2 cycles | | `a, b` | in parallel (a stack), at the top level or inside `[ ]` | | `a!3` | replicate: `a a a`, *without* speeding up. Bare `!` means 2 | | `a@3` | elongate: this one gets 3 slots' worth of time. Default weight 1 | | `a?` `a?0.1` | drop it 50% / 10% of the time | | `a\|b` | pick one at random each cycle | | `a(3,8)` `a(3,8,1)` | Euclid: pulses, steps, and an optional rotation | | `{a b c, x y}` `{a b c}%4` | polymeter | | `bd:2` | second value — for `s()` that is the sample index, a third is gain | ```js s("bd [hh hh] sd [hh bd]") // subdivision note("<c2!2 g1!2 as1!2 f1!2>(3,8)").s("sawtooth") // 8 bars of euclidean bass s("[bd sd]*2, white*8") // two voices in one string n("<4 [3@3 4] [<2 0> ~] ~>").scale("d4:minor") // weights and nesting ``` ## Notes, scales, chords - `note("c3 e3 g3")` — letters a–g, `b`/`#`, optional octave 0–9, default octave 3. MIDI numbers work too (`note("48 55")`, 69 = A4). - `n("0 2 4").scale("c4:minor")` — degrees of a scale, zero-indexed, negatives wrap backwards. The scale name takes **no spaces**: `"c:minor"`, `"f#4:mixolydian"`, `"a:harmonic:minor"`. - `[c3,eb3,g3]` inside a pattern is a chord — a stack of notes in one slot. - `n("0 1 2 3").chord("<C Am F G>").voicing()` builds voicings from chord symbols. - `.arp("0 [0,2] 1 [0,2]")` spreads a stacked chord over time. It takes a **pattern of indices**, not mode names like `"up"`. ```js note("<[c3,ds3,g3] [g2,as2,d3] [as2,d3,f3] [f2,gs2,c3]>") .s("supersaw").unison(5).spread(0.45).attack(0.5).release(1.2).room(0.6) ``` ## Shaping the sound - **Envelope** `attack`/`att`, `decay`/`dec`, `sustain`/`sus`, `release`/`rel`, or all four at once as `.adsr(".1:.1:.5:.2")`. `clip` (alias `legato`) multiplies note length. - **Filter** `lpf` (aliases `cutoff`, `lp`), `hpf`, `bpf`; resonance is `lpq` (alias `resonance`, 0–50), `hpq`, `bpq`. `ftype` picks `12db` / `ladder` / `24db`. - **Filter envelope** `lpattack` `lpdecay` `lpsustain` `lprelease` `lpenv` (short: `lpa` `lpd` `lps` `lpr` `lpe`). `lpenv` is the depth and may be negative. - **Space** `room` (0–1) with `roomsize` (alias `rsize`/`size`, 0–10 — change it sparsely); `delay`, `delaytime`/`dt`, `delayfeedback`/`dfb`. `orbit(n)` gives a voice its own reverb and delay bus; patterns on the same orbit share one. - **Colour** `distort` (prefer it over the deprecated `shape`), `crush` (1 crunchy … 16 clean), `coarse`, `vowel("<a e i o u>")`, `phaser`. - **Level and place** `gain`, `postgain`, `pan` (0 left … 1 right). Many of these take several values through `:` — `lpf("1000:10")` is cutoff and resonance, `delay("0.65:0.25:0.9")` is level, time and feedback, `room("0.9:4")` is level and size. ```js note("c3 bb2 f3 eb3").s("sawtooth").lpf(600).adsr(".1:.1:.5:.2") note("c2 e2 f2 g2").s("sawtooth").lpf(300).lpa(0.5).lpenv("<4 2 1 0 -1 -2 -4>/4") ``` ## Moving the pattern `rev` `palindrome` `iter(4)` `chunk(4, f)` `ply(2)` `off(1/16, f)` `superimpose(f)` `layer(f)` `jux(rev)` `juxBy(0.5, f)` `segment(16)` `struct("x ~ x x")` `mask("1 0")` `degradeBy(0.3)` `sometimes(f)` `sometimesBy(0.4, f)` `often` (0.75) `rarely` (0.25) `almostAlways` (0.9) `someCyclesBy(p, f)` `every(4, f)` `slow(2)` `fast(2)` `euclid(3,8)` `euclidRot(3,8,1)` `euclidLegato(3,8)` `add(note("12"))` `echo(3, 1/8, 0.5)` `linger` `swingBy(1/3, 4)` `hurry(2)`. ```js n("0 1 [4 3] 2 0 2 [~ 3] 4").s("triangle").jux(rev) s("white*8").sometimesBy(0.4, x => x.hpf(2000).gain(0.4)) n("0 [4 <3 2>] <2 3> [~ 1]".off(1/16, x => x.add(4))).scale("c5:minor").s("triangle").dec(0.1) ``` ## Signals as modulators `sine` `cosine` `saw` `isaw` `tri` `square` `rand` `perlin` run 0…1 (the `…2` variants, `sine2` `saw2` `rand2`, run −1…1); `irand(n)` is called as a function. Shape them with `.range(lo, hi)` (or `.rangex` for exponential) and slow them with `.slow(n)`. ```js note("<[c2 c3]*4 [bb1 bb2]*4>").s("sawtooth").lpf(sine.range(100, 2000).slow(4)) n(saw.range(0, 8).segment(8)).scale("c:major") n(irand(8)).struct("x x*2 x x*3").scale("c:minor") ``` **The catch worth knowing:** a signal is sampled once per *event*, not continuously. A pad held for a whole cycle gets one filter value per note, not a sweep. Add events to hear the movement: ```js s("supersaw").seg(16).lpf(tri.range(100, 5000).slow(2)) ``` Envelopes *are* continuous in time: `attack`/`decay`/`release`, the pitch envelope (`penv`, `pattack`, `pdecay`), the filter envelopes, `tremolo`, `phaser`, `vib`. ## Putting a track together **Tempo.** `setcpm(120/4)` — cycles per minute, and with one bar per cycle that is 120 bpm in 4/4. `setcps(x)` is the same thing per second; the default is 0.5 cps, a 2-second cycle. Set it on the first line so the file says its own tempo. **Layers.** `stack(a, b, c)` plays voices together and is the shape to reach for. The `$:` label form does the same thing one line at a time, and `_$:` mutes a line — handy when the user asks to drop a part: ```js setcpm(120/4) $: s("sbd*4").decay(0.24) $: note("<c2 g1>(3,8)").s("sawtooth").lpf(800) _$: n("0 3 7").scale("c4:minor").s("triangle") // muted ``` **Name every voice.** The pane draws one lane per `stack` argument (or `$:` line) — its notes scrolling under a playhead, an activity light, Mute and Solo — and titles the lane from the comment directly above the voice, up to the first ` — `, `:` or `.`. So give every voice its own argument and a title line, and keep what it does after the dash: ```js stack( // Kick — four on the floor s("sbd*4").decay(0.24), // Bass — three pulses in eight, filter opening over four bars note("<c2 g1>(3,8)").s("sawtooth").lpf(sine.range(220, 1600).slow(4)) ) ``` A named label (`bass: note(...)`) is its own title. Without a comment the lane is named after the first sound it plays. **Structure.** A cycle is a bar. Longer form comes from `<>` (one per cycle), `!` and `@` to hold a chord for two bars, `/n` to stretch, and `every(n, f)` / `someCyclesBy` to vary. Eight bars of harmony is `<[chord]!2 [chord]!2 [chord]!2 [chord]!2>`. - **Intro** — one or two voices, filter closed, no lead. `.lpf(400)` on everything is a whole intro. - **Groove** — kick, backbeat, hats, bass, then harmony. Give each voice its own register and one clear rhythmic idea; do not let two voices play the same subdivision at the same volume. - **Break** — `.mask("<1 1 1 0>")` drops a voice for the fourth bar of four; `every(4, x => x.fast(2))` doubles it instead. A break is one voice leaving, not everything changing. - **Lift** — open the filter, add `.jux(rev)`, add an octave with `.sometimes(add(note("12")))`.
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub