Music as code
Strudel — live-code your rack
Sequence your rack by typing. One track, built twice — a song, then the same song made to wander while it moves the filters.
Video coming soonStrudel is a live-coding language that runs in a browser. Here it is not making the sound — your modules are. Strudel sends MIDI notes and CC; the rack does the rest.
Below is the actual code from the video: one track, two passes. The first lays down three voices, a drum kit and an arrangement. The second lets the notes wander and sends CC42 out to move the lead’s timbre on Ogham and the Alchemy Lab effect it plays through. Both files are downloadable below — paste one into strudel.cc in Chrome, point the device names at your own gear, and it will play. The play-out from the end of the video, with its visuals, is recorded_visual.js.
1 · The song
Three voices, a drum kit and an arrangement — the whole track on one screen, and not a note of it made in the browser.
const WS = 'Workshop System MIDI'
const OC = 'Phazerville'.midi(WS) or .midi(OC).const prog = "<0 0 3 5 0 0 5 4>"
const bass = n("0 ~ ~ ~ 0 ~ 3 ~".add(prog))
.scale("A2:minor").midichan(4).midi(OC)n() plays scale degrees, not fixed notes — 0 is the root, so with .scale("A…:minor") everything lands in key. prog is a chord move in < >, one per cycle, and .add(prog) walks the part through it. That is the difference between a song and a loop, and it costs one word.const pad = n("0 ~ ".add(prog))
.scale("A3:minor").midichan(3).midi(OC)
const lead = n("0 3 5 3 0 3 5 3".add(prog))
.scale("A5:minor").midichan(1).midi(WS)const kick=36, snare=37, closedHat=38, openHat=39
const drums = stack(
note(kick ).struct("x x"),
note(snare ).struct("~ x"),
note(closedHat).struct("x x x ~ x x x ~"),
note(openHat ).struct("~ ~ ~ ~ ~ ~ ~ x")
).midichan(10).midi(OC).struct() is a step grid — x a hit, ~ a rest — which reads like a drum machine and keeps the rhythm separate from the sound. All on channel 10, into Beatsi.const intro = stack(drums, pad)
const verse = stack(drums, bass, pad)
const chorus = stack(drums, bass, pad, lead)
const outro = stack(bass, pad)
arrange(
[4, intro],
[8, verse],
[8, chorus],
[4, outro],
).cpm(35)stack() layers parts into sections, and arrange() plays the sections in order as [bars, section]. cpm(35) sets the tempo — 35 cycles a minute. Twenty-four bars, and the track is finished.2 · Movement
The same song, now it never plays the same way twice — and the lead's tone and the effect it plays through move while it does.
const bass = n("0 _ ~ ~ 0 ~ 3 ~ | 0 _ ~ ~ 3 ~ ~ 5".add(prog))
.scale("A2:minor").midichan(4).midi(OC)| means pick one of these each cycle, so the bass stops repeating exactly. _ holds the note before it for another step — which only sounds different if the voice reads gate *length*, so on this rig it depends which way the A-142-3's AD/AR toggle is set.const lead = n("0 3 5 3 0 3 5 3 | 0 3 4 5 3 2 1 5".add(prog)
.sometimesBy(0.4, x => x.add(choose(5,7))))
.scale("A5:minor").degradeBy(slider(0.313, 0, 1)).midichan(1).midi(WS).sometimesBy(0.4, …) takes a chance on each note — about 40% of them jump up 5 or 7 scale degrees, whichever choose() picks: a sixth or a full octave, because degree 0 is the root. .degradeBy() drops notes entirely, and slider() puts a fader in the code so you can thin the melody out by hand while it plays.const drums2 = stack(
note(kick ).struct("x x x x"),
note(snare ).struct("x x"),
note(closedHat).struct("x x x*2 ~ x*2 x x ~"),
note(openHat ).struct("~ ~ ~ x ~ ~ ~ x*2 | ~ ~ ~ x ~ ~ ~ x*4"),
).midichan(10).midi(OC)x*2 fits two hits into one step, which is how the hats get their double-time feel without a second grid.const lfo1 = isaw .slow(4)
const lfo2 = perlin.slow(4)isaw because you are not calling it — you are naming a shape: a ramp that falls from 1 to 0. .slow(4) stretches one fall over four cycles. perlin is smooth random, so it wanders instead of marching.const mod1 = v => ccn(42).ccv(v).midichan(2).midi(WS)
const mod2 = v => ccn(42).ccv(v).midichan(1).midi(WS)
const mods = stack(
mod1(lfo1.range(1,0).segment(32)),
mod2(lfo2.range(0,1).segment(32)),
)v => … is a small function — a recipe with a hole in it — and everything after the arrow is an address that never changes: controller 42, a channel, a device. Then .range() sets depth and direction, and .segment(32) takes 32 snapshots a cycle, because a continuously moving value has to be chopped up before MIDI can carry it. At cpm(35) that is about 19 messages a second.const lead2 = n(irand(8).segment(16)).degradeBy(0.6)
.scale("A5:minor").midichan(1).midi(WS)
const bridge = stack(drums2, pad, lead2, mods)irand(8) is pure chance and .segment(16) takes sixteen of them a cycle — the same idea as the LFOs above, sampling something continuous into countable events. It only plays in the bridge, so that lift is different every pass.const intro = stack(drums, pad)
const verse = stack(drums, bass, pad, mods)
const chorus = stack(drums2, bass, pad, lead, mods)
const outro = stack(bass, pad)
arrange(
[4, intro], [8, verse], [8, chorus],
[4, bridge], [8, chorus], [4, outro],
).cpm(35)mods joins the verse and the chorus, so the sweeps run from the verse on; the chorus takes the busier kit; and four bars of bridge come before the chorus plays again. Thirty-six bars in all.