Live-coding with Strudel
This is a beginners’ Strudel worksheet designed for Peckham Digital’s 2025 Music x Code programme. Thanks to Lucy Cheesman for providing the foundations of this workshop!
The purpose of this guide is to start you on your live-coding journey with Strudel. There are many different ways to do live-coding, and the approach outlined here is by no means the best or only approach. However, we hope that it will provide you with the tools necessary to start live-coding music for yourself.
Most of all, we encourage you to experiment - there is no right or wrong way to live-code!
Getting started
We can write and run code interactively in this worksheet - press the play button or hit Ctrl
+Enter
. Hit the stop button or Ctrl
+.
to stop the sound.
Later you can move on to using the main Strudel editor (click go to REPL
in the top right), but it works in the same way as the code snippets on this page.
Feel free to edit any of the code you see - you can always refresh the page to get the original worksheet back.
The basics
This is how we write a basic pattern in Strudel.
sound
is a function that tells strudel we want to play a sound- Inside the speech marks we write the names of the sounds we want to play.
sound("bd hh sd oh")
These short names correspond to different parts of a standard drum kit:
bd
= Bass drum
sd
= Snare drum
hh
= Hi-hat (closed)
oh
= Hi-hat (open)
- You play the sound by pressing the play button (or
Ctrl
+Enter
) - You can stop the sound by hitting
Ctrl
and.
- The sounds have to load, so you might not hear them at first!
What happens if we put more sounds in the pattern?
sound("bd hh sd oh hh bd oh sd")
Even more?
sound("bd hh sd oh hh bd oh sd hh sn oh bd")
The pattern becomes much faster and denser - this is because Strudel has a consistent ‘cycle’ of time running in the background. You might find that adding ._spiral()
to the end of your code helps you to visualise this:
sound("bd hh sd oh") ._spiral()
Any sounds in the pattern will run within that cycle, and Strudel will try to space them evenly in time. (Unless we tell it not to… we’ll learn how to do that later).
This means that the pulse of the pattern is dictated by the number of elements:
sound("hh bd sd")
vs.
sound("hh bd hh cp hh bd hh")
Later on when we learn how to play multiple patterns at once this gives us some opportunities to play with rhythm.
Now try writing your own pattern with some of these other sounds:
insect wind jazz metal east crow casio space numbers
//your code here
Mininotation
The green writing inside the speech marks has its own set of rules. We call this the ‘mininotation’ and it’s the core of how Strudel generates patterns.
We can speed things up with *
sound("hh")
sound("hh*4")
sound("hh*32")
Or slow them down with /
- the snare only sounds every other cycle:
sound("bd sd/2")
We can choose different sounds from a set using :
. The computer starts counting at 0
, so casio:0
is the same as casio
.
sound("casio casio casio casio")
sound("casio casio:1 casio casio:5")
An alternative way to do this is with n
:
n("0 1 0 5").sound("casio")
We can add a rest using ~
or -
:
n("0 1 ~ 5").sound("casio")
We can create more variation with sub-sequences.
We do this by breaking our steps down into mini patterns using []
:
sound("bd sd bd hh")
sound("bd sd [bd cp] [hh oh]")
Let’s have a look at how that looks visually:
sound("bd sd [bd cp] [hh oh]")
You can also use *
or /
to speed up or slow down sub-sequences:
sound("bd sd [bd cp]/2 [hh oh]*2")
You can nest sequences within sequences for dense patterns:
sound("bd bd [hh hh] [hh [hh hh]]")
You can also use <>
to play events once per cycle:
sound("bd bd <hh cp> sd")
Let’s add some easy randomness using ?
.
Putting ?
after a sound means there is a 50% probability that the sound will play.
sound("hh*8?")
Selecting functions
Strudel has a lot of functions (To check them out click go to REPL > Reference
) - this can be a problem for live performances:
- it’s not practical to memorise all these
- in the moment on stage we want to simplify our decision making
- some of them require complex syntax and/or a lot of typing
So let’s choose some favourites to work with. I’m going to go through a few of mine here and how I use them, but if you’re more experienced you might have your own ideas!
Working with short sounds
Let’s start by writing some simple patterns and slowly adding complexity.
{}
- for combining patterns with different numbers of elements:
$: sound("{bd bd/1 bd/2 bd/3, ~ ~ hh [~ cp] hh*3}")
$: sound("{bd bd/1 bd/2 bd/3, ~ ~ hh [~ cp] hh*3}") $: sound("metal(3,8)")
hurry
$: sound("{bd bd/1 bd/2 bd/3, ~ ~ hh [~ cp] hh*3}") $: sound("metal(3,8)") .hurry("<1 1 2 4>")
Density (aka fast
/slow
)
$: sound("{bd bd/1 bd/2 bd/3, ~ ~ hh [~ cp] hh*3}") .fast("1 <1 2>") $: sound("metal(3,8)") .hurry("<1 1 2 4>")
You’ll notice here that we’re using $:
before each sound
- this lets us play multiple sounds within the same block of code.
Exercise
Try writing some patterns using very simple functions and just a few short sounds.
Tips:
- You can use the functions/sounds I’ve used above, or use some others if you know them
- Try to make something you like the sound of without writing a lot of code
- Don’t be afraid to delete and start again - imagine you are learning piano and need to practice your scales!
- Try to really limit the number of sounds for now
- If you really want to try some other sounds, how about using some of these:
insect wind jazz metal east crow casio space numbers
// your code here
Working with patterns
Next we’ll try manipulating the patterns we’ve described above.
iter
cycles through the pattern starting at different points, it’s great for adding variety over time
$: sound("{bd bd/1 bd/2 bd/3, ~ ~ hh [~ cp] hh*3}") .fast("1 <1 2>") $: sound("metal(3,8)") .hurry("<1 1 2 4>") .iter("4")
jux
lets us apply a transformation in one speaker only giving us a stereo effect
$: sound("{bd bd/1 bd/2 bd/3, ~ ~ hh [~ cp] hh*3}") .fast("1 <1 2>") $: sound("metal(3,8)") .hurry("<1 1 2 4>") .jux(iter("4"))
sometimes
/often
/every
/etc lets us apply transformations only some of the time
$: sound("{bd bd/1 bd/2 bd/3, ~ ~ hh [~ cp] hh*3}") .fast("1 <1 2>") $: sound("metal(3,8)") .hurry("<0.5 1 1 2>") .jux(iter("4")) .every(2, fast(2))
chunk
is another good way to add interest over time - it divides the patterns into n chunks and applies a transformation to a different chunk each cycle
$: sound("{bd bd/1 bd/2 bd/3, ~ ~ hh [~ cp] hh*3}") .fast("1 <1 2>") $: sound("metal(3,8)") .hurry("<0.5 1 1 2>") .jux(iter("4")) .every(2, fast(2)) $: sound("wind*8?") .legato("1") .chunk(4, hurry("<2 0.5>"))
The last function I want to look at here is off
, which can be used to add another layer of complexity to your pattern.
Here, the pattern is delayed by 1/8th of a cycle, made faster with speed(2)
, then played back on top of the original pattern:
$: sound("bd cp [hh oh] bd") .fast("1 <1 2>") .off(1/8, x=>x.speed(2))
Note: The second argument to off
, x=>x.speed(2)
, is known as a lambda function. Essentially, it’s telling off
to apply .speed(2)
to the original pattern.
Sound banks
By default, Strudel includes samples from a range of classic drum machines, such as the Roland TR-808.
If you go to the REPL, then go to sounds
> drum-machines
, you can see that these samples are all prefixed with drum machine names: RolandTR808_bd
, RolandTR808_sd
, RolandTR808_hh
etc. The number in brackets next to each sample indicates the number of samples within each set - for example, RolandTR808_bd(25)
contains 25 samples.
You could bring these in like this:
sound("RolandTR808_bd RolandTR808_hh RolandTR808_sd RolandTR808_hh")
But a quicker way is to use .bank(name)
, which simply adds name_
to the beginning of each sample:
sound("bd hh sd hh").bank("RolandTR808")
Here are a few others to try:
- RolandTR909
- compurhythm8000
- akailinn
- emusp12
You can use n
to select different sounds from each sample set:
n("<1 2> 1 <2 3> 1") .sound("bd hh sd hh") .bank("RolandTR808")
Exercise
Take your pattern from the first exercise and try applying some pattern manipulations.
Tips:
- You can use the functions I’ve used above, or use some others if you know them
- The process is to start with something simple and iteratively add complexity
- Again, try to limit the sounds you’re using to just a few samples
- Don’t worry too much about your code, just play about with different combinations and see if you can find something you like the sound of!
// your code here
Working with longer sounds
Longer samples require a different approach, and can cause us some problems in on-the-fly performances if we’re not careful.
Strudel doesn’t come with many long samples so we’re going to load some in from the internet for these examples - for more info on this, see Making sound > Samples > Loading Custom Samples
.
loopAt
lets us sync a sample to a given number of cycles. This is especially helpful for working with loops:
samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) $: sound("rhodes") .loopAt(2)
As well as a basic loop we can play about with the parameters of loopAt
:
samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) $: sound("rhodes*4") .loopAt("<4 <1 2>>") .cut(1) .iter(4) .chop("<1 1 2 4>") $: sound("{bd bd/1 bd/2 bd/3, ~ ~ hh [~ cp] hh}") .every(4, (slow(2)))
Here, cut
is used to stop playing the sample as soon as the next sample is triggered.
An alternative to loopAt
is fit
, which will fit a sample to its event duration:
samples('github:yaxu/clean-breaks') s("amen/4").fit().chop(16)
This fits the break into 4 cycles and chops it into 16 pieces.
The chops are not audible yet, because we’re not doing any manipulation. So let’s add randomized doubling with ply(2)
and reversing with mul(speed(-1))
:
samples('github:yaxu/clean-breaks') s("amen/4").fit().chop(16).cut(1) .sometimesBy(.5, ply(2)) .sometimesBy(.25, mul(speed(-1)))
If we want to specify the order of samples, we can replace chop
with slice
:
samples('github:yaxu/clean-breaks') s("amen/4").fit() .slice(8, "<0 1 2 3 4*2 5 6 [6 7]>*2") .cut(1).rarely(ply("2"))
If we use splice
instead of slice
, the speed adjusts to the duration of the event:
samples('github:yaxu/clean-breaks') s("amen") .splice(8, "<0 1 2 3 4*2 5 6 [6 7]>*2") .cut(1).rarely(ply("2"))
Note that we don’t need fit
, because splice
will do that by itself.
Exercise
Try creating a pattern using some longer samples. Tips:
- Feel free to use the functions above, or if you know some others then give them a go
- Can you create complementary pattern of short sounds? Use your ear to decide what sounds good
- Iterate on your patterns using the techniques you know
- Think about chaining together simple functions to build complexity over time
- Again, try to really limit the number of sounds you’re using - this is about getting used to what you can do with the functions.
- If you’re feeling confident try to do it without looking at the reference
- Remember - if you don’t like where it’s going, delete it and start again!
- Even if you like where it’s going try to get used to deleting and re-creating your patterns
- Now we’ve loaded in the TidalCycles samples here are a few more longer sounds you might want to play about with:
pebbles foo birds
samples('github:tidalcycles/dirt-samples') //your code here
Effects
You can use effects to change the character of your sounds. The simplest of these is gain
, which controls the amount a signal is amplified:
s("hh*8").gain(".4!2 1 .4!2 1 .4 1").fast(2)
speed
changes the speed a sample is played back. Providing a negative value will play the sample backwards:
s("bd sd [hh oh] sd").speed("1 2 -1 -2")
hpf
applies a high-pass filter, which cuts out the lower frequencies of a sound:
sound("bd bd [hh oh] sd") .hpf("100 1000 5000 7500")
Similarly, lpf
cuts out the higher frequencies:
sound("bd bd [hh oh] sd") .lpf("20 100 1000 5000")
Note that lpf
and hpf
work best with values in the range 20 - 20000, which is the range of frequencies typically audible to the human ear.
You can use pan
to move the sound between the left and right channels, where 0 is the left channel and 1 is the right channel:
sound("bd bd [hh oh] sd") .pan("0 0.25 0.75 1")
You can create an echo effect using delay
, along with a value from 0 to 1 to control the level of the delay signal:
s("cp cp").delay("<0 .25 .5 1>")
You can use delaytime
to change the speed of the delay:
s("cp cp").delay(.25).delaytime("<.125 .25 .5 1>")
You can apply a reverb effect with room
:
s("cp cp").room("<.125 .25 .5 1>")
Signals
Until now, we’ve been using numbers to control our patterns. However, you can also use signals which change smoothly over time.
For example, you can use saw
to create a signal that rises smoothly from a lower value (500) to a higher value (5000):
s("cp*2").delay(.25).lpf(saw.range(500, 5000).slow(4))
Here, .slow(4)
means that it takes four cycles for the signal to reach it’s maximum value.
You can also use signals with different shapes - try using tri
or square
instead of saw
and see how that changes the sound!
Exercise
Take your pattern from the previous exercise and try applying some effects. If you’re feeling confident, try using signals to change the effects!
Tips:
// your code here
Notes
So far, we’ve used Strudel to create rhythmic patterns, like beats. This is fine by itself, but you might be interested in melodic patterns, or want to combine rhythm and melody to add some variety to your music.
The basic building block of a melody is a note. In Western musical notation, a note describes both the pitch of a sound - how high or low it is - and the duration - how long it is played for.
There are a few different ways to describe notes in Strudel. One way is to use numbers:
note("48 52 55 59").sound("piano")
You can also use decimal numbers here, like 55.5, for small variations in pitch.
Another way is to use the letters A - G, which is more common to Western musical notation:
note("c e g b").sound("piano")
To play a black key before a white key, we add the ‘flat’ symbol (b
) to the letter:
note("db eb gb ab bb").sound("piano")
To play the black key after the white key, we add a ‘sharp’ symbol (#
) to the letter:
note("c# d# f# g# a#").sound("piano")
You can control which octave the note is played in by adding the numbers 1 - 8 to the letter, with 1 being the lowest and 8 being the highest:
note("c2 e3 g4 b5").sound("piano")
To control the length of the note, use clip(n)
, which multiplies the duration by n
:
note("c2 e3 g4 b5") .clip(.5) .sound("piano")
Earlier, we used a signal to control the parameters of an effect. You can also use signals in place of ordinary patterns - for example:
note(saw.range(40,52).segment(16)).sound("tri")
The difference here is that we’ve used segment
to sample the signal at 16 points over the course of one cycle. This is used to turn a continuous signal, which changes smoothly, into a discrete signal, which changes in ‘steps’.
Exercise
Try to create a melody you’re happy with using note
, sound
, clip
, and some of the functions you’ve learned above (such as fast
, iter
, or off
).
If you’d like to try something other than the piano
sound, you can find more sounds in the REPL under sounds
> synths
. Some of the sounds might take time to load!
//your code here
Chords
A chord is a set of notes played together. We can achieve that by separating each note with a comma (,
):
note("53,57,60").sound("piano")
You could also use note letters instead of numbers here, i.e. f3,a3,c4
.
We can use <>
to alternate between chords:
note("<[53,57,60] [48,51,55]>").sound("piano")
A more convenient way to think about chords is in terms of the distance (or interval) of each note from the root note, which is typically the lowest note of the chord.
For instance, if we take 53,57,60
, the lowest note is 53
, or f3
. The distance from 53 to 57 is 4, and the distance from 53 to 60 is 7. So, we could also specify this chord as:
note("0,4,7".add("53")).sound("piano")
This is known as a major chord, which is usually described as sounding ‘happy’. 0,3,7
is a minor chord, which is usually described as sounding ‘sad’:
note("0,3,7".add("53")).sound("piano")
So, we could also write our alternating chord sequence as:
note("<[0,4,7] [0,3,7]>".add("<53 48>")).sound("piano")
Exercise
Try to make a chord sequence you’re happy with using the code below as a starting point:
note("<[0,4,7] [0,3,7]>".add("<c a f e>")).sound("piano")
You can find more ways to work with chords in this tutorial.
Scales
Another way to sequence notes is from a scale. A scale is a series of notes, ordered by pitch, with specific intervals between the notes.
For example, a C major scale:
n("0 1 2 3 4 5 6 7") .scale("C:major") .clip(1) .sound("piano")
Instead of C:major, try:
- C:major
- A2:minor
- D:dorian
- G:mixolydian
- A2:minor:pentatonic
- F:major:pentatonic
If you have no idea what these scales mean, don’t worry. These are just labels for different sets of notes that go well together.
You can use mininotation with n
to vary which notes of the scale are chosen:
n("0 <5 4> <3 2> 7, <[0,3,7] [0,4,7]>") .scale("A:minor") .clip(1) .sound("harp")
Exercise
Try applying what you’ve learned so far to create a musical piece. For a quick overview of the functions referenced here, check out the Recap page!
You might find the comments below helpful to organise your code:
// Drums // $: // Longer samples // $: // Notes // $: