A hands-on introduction to live-coded music in the browser
Strudel is a JavaScript-derived live-coding environment that lets you make music directly in a browser REPL. Each snippet you evaluate becomes part of a real-time musical texture. This course takes you from your first drum hit to expressive synthesis and full performances.
Youβll type every example into the REPL yourself. At the end of each module youβll have short βfree-playβ challenges to cement the ideas.
- The Strudel REPL or a local install
- Headphones or speakers
- Curiosity and willingness to experiment
Understand how Strudel represents rhythm and repetition through pattern notation.
| Concept | Meaning | Example |
|---|---|---|
s() |
choose a sound sample | s("bd") |
* |
repeat | s("bd*4") |
< > |
alternate patterns | s("<bd sd>") |
~ |
silence | s("bd ~ sd ~") |
, |
layer simultaneous sounds | s("bd, hh*4") |
s("bd hh bd hh")
s("bd*2 hh*2").gain(0.8)
s("<bd sd> hh <hh hh*2>")-
Add a second voice:
s("bd ~ bd ~") s("~ hh ~ hh").gain(0.7)
-
Slow and speed patterns:
s("bd sd hh hh").slow(2) s("bd sd hh hh").fast(2)
Create a two-bar groove that uses < > to alternate variations, includes one silence ~ per bar, and layers at least two instruments.
Control how Strudel plays and transforms audio samples.
| Function | Description |
|---|---|
bank(name) |
selects a folder of samples |
n() |
selects index within a bank |
.begin() / .end() |
play fraction of the sample |
.loop(1) |
loop playback continuously |
s("bd sd [~ bd] sd, hh*6")
.bank("RolandTR909")
.n("<0 1>")
.gain(0.7)s("rave").begin("<0 .125 .25 .375 .5 .625 .75 .875>").fast(2)-
Vary
.begin()and.end()to βremixβ a breakbeat:s("loopAmen").begin("<0 .25 .5 .75>").end("<.25 .5 .75 1>")
-
Loop continuously:
s("casio").loop(1).gain(0.6)
Using a single loop sample, make a breakbeat that feels like a new groove by changing .begin() values each bar.
Use pitch, scales, and simple harmony.
| Function | Use |
|---|---|
note() |
define melodic pattern |
.s() |
choose instrument |
.fast() / .slow() |
control rhythmic density |
note("c e g b").s("saw").gain(0.7)note("c4 e4 g4 b4").s("square").slow(2)
note("c3 e3 g3 b3").s("square").fast(2).gain(0.5)note("60 64 67 71").s("piano").gain(0.8)-
Layer melodic voices:
note("c e g").s("saw") note("c e g").s("pulse").slow(2)
-
Mix
.fast()and.slow()to create counterpoint.
Compose a two-layer arpeggio where one voice plays double speed and the other half speed, harmonized in a different octave.
Excellent β hereβs Part 2 of the Strudel self-study workshop in Markdown, continuing seamlessly from Module 3.
Learn to shape loudness and articulation using amplitude envelopes and dynamic controls.
| Function | Purpose |
|---|---|
.attack() |
time before peak volume |
.decay() |
time to fall from peak to sustain level |
.sustain() |
steady volume during note |
.release() |
fade-out time |
.gain() |
exponential loudness scaler |
.velocity() |
per-event intensity |
.postgain() |
final volume trim after FX |
note("c3 e3 g3 b2")
.s("saw")
.attack("<0 .05 .1 .2>")
.decay("<.1 .2 .3 .4>")
.sustain("<0 .25 .5 .75 1>")
.release("<.2 .4 .6 1>")
.gain(0.9)-
Create per-note accents:
s("hh*8").gain(".4!2 1 .4!2 1 .4 1").velocity(".4 1").fast(2)
-
Compare
.gain()vs.velocity()β gain affects amplitude after synthesis, velocity affects instrument expression.
Write a four-chord progression whose ADSR parameters evolve slowly across eight bars.
Try giving each chord distinct attack/decay/release values to create breathing motion.
Sculpt timbre with filters and modulation.
| Function | Description |
|---|---|
.bpf(freq) |
band-pass filter center frequency |
.bpq(q) |
resonance (bandwidth sharpness) |
.fm(index) |
FM modulation depth |
.fmh(ratio) |
harmonicity ratio |
.fmattack(), .fmdecay(), .fmsustain(), .fmenv() |
envelope controls for FM |
.wt(), .warp(), .warpmode() |
wavetable morphing |
.wtrate(), .wtdepth(), .wtskew() |
LFO modulation of wavetable position |
note("c e g b").s("sine")
.bpf("<500 1000 2000 4000>")
.bpq("<0 .5 1 2>")note("c e g b g e").s("sine")
.fm("<0 1 2 8 16>")
.fmh("<1 2 1.5 1.61>")Add envelopes for expressiveness:
note("c e g b g e").s("sine")
.fm(4)
.fmattack("<0 .05 .1 .2>")
.fmdecay("<.01 .05 .1 .2>")
.fmsustain("<1 .75 .5 0>")
.fmenv("<exp lin>")-
Sweep filters by patterning frequency values.
-
Combine FM and filter modulation for metallic textures.
-
Try a wavetable sweep:
s("basique").bank("wt_digital") .note("F1") .wt("0 0.25 0.5 0.75 1") .warp("<0 .25 .5 .75 1>") .warpmode("<asym bendp spin logistic sync wormhole>*2")
Design a bass patch that evolves between two timbres.
Hint: alternate .warpmode("fold") and "spin" while sweeping .warp().
Add depth, polish, and motion with effects.
| Effect | Control | Notes |
|---|---|---|
.reverb(amount) |
0 β 1 | spaciousness |
.chorus(depth) |
0 β 1 | detuned shimmer |
.compressor(settings) |
"threshold:ratio:knee:attack:release" |
dynamics |
.distort(amount) |
> 0 | harmonic saturation |
.postgain() |
compensates output level |
note("d f a c").s("sawtooth")
.chorus(0.5)
.reverb("<0 .2 .4 .6>")
.gain(0.7)s("bd sd [~ bd] sd, hh*8")
.compressor("-20:20:10:.002:.02")
.postgain(1.3)-
Animate reverb send:
note("a f c d").s("pad").reverb("<0 .3 .8 .3>").gain(0.8)
-
Stack multiple effects and adjust
.postgain()to avoid clipping.
Design an evolving ambient pad: use modulation and reverb so rhythm melts into texture. Layer at least two oscillators with different chorus depths for stereo width.
Excellent β hereβs Part 3, completing the self-study workshop Markdown with Modules 7β10 and the closing sections.
Learn how to transform, remix, and randomize patterns so your music constantly evolves.
| Function | Purpose |
|---|---|
.rev() |
Reverse a pattern |
.every(n, fn) |
Apply fn every n cycles |
.off(offset, fn) |
Delay then apply fn |
.stack(n) |
Duplicate layer with phase offsets |
.add() / .combine() |
Merge patterns together |
// reverse every third cycle
s("bd sd hh hh").every(3, p => p.rev())// accent hats slightly off the grid
s("hh*8").off(0.125, p => p.gain(0.5))// triple-layer hi-hat texture
s("hh*8").stack(3)- Add
.rev()to one instrument while another keeps time. - Use
.every()to swap drum patterns each bar. - Combine these techniques with
.off()to make micro-variations.
Build a groove that never quite repeats:
alternate fills with .every(4, p => p.rev()), offset the snare by 0.25, and layer a polyrhythmic hi-hat using .fast(3) against .slow(2).
Make your music reactive β tweak parameters live from the REPL.
| Function | Purpose |
|---|---|
slider(value, min, max, step) |
Creates a UI control |
.markcss(css) |
Visually mark events |
.source(fn) |
Define a custom WebAudio node |
// global mix control
const MIX = slider(0.6, 0, 1, 0.05)
note("c e g").s("sine").gain(MIX)// live filter cutoff
const CUT = slider(1200, 200, 8000, 50)
note("c e g b").s("sine").bpf(CUT).bpq(1.2).gain(0.7)// visually mark notes
note("c4 a3 f4 e4").s("sine").markcss('text-decoration:underline')// custom WebAudio source
source(ctx => {
const osc = ctx.createOscillator()
osc.type = "sawtooth"
const gain = ctx.createGain()
gain.gain.value = 0.2
osc.connect(gain)
osc.start()
return gain
})
.attack(0.01).decay(0.2).sustain(0.2).release(0.3)- Assign sliders to multiple parameters (e.g. filter + reverb).
- Use different sliders to cross-fade between two instruments.
- Mark active parts visually with
.markcss()for performance clarity.
Build a performance patch with two interactive sliders β one for cutoff, one for mix. Perform a slow evolution by hand over several cycles.
Combine everything into a cohesive piece.
- Each
note()ors()line is a voice. - Timing is cyclic β use
.slow()and.fast()for phrasing. - Think in layers rather than tracks.
// Bass
note("c2(3,8) g1(3,8) c2(5,8)").s("sine").gain(0.8)
// Chords
note("<c3e3g3 a2c3e3 f2a2c3 g2b2d3>").s("saw").slow(2)
.attack(0.02).decay(0.3).sustain(0.5).release(0.4).gain(0.6)
// Drums
s("bd sd [~ bd] sd, hh*8").gain(0.9)- Add
.bpf()to chords for movement. - Apply
.compressor()to the drums. - Use
.off()to stagger the bass slightly behind the beat.
Compose a two-minute section that introduces, builds, and releases energy. Add or remove layers live to create arrangement dynamics.
Perform confidently in real time.
| Tip | Meaning |
|---|---|
| Re-evaluate lines | Start/stop parts live |
Use clearAll() |
Silence everything instantly |
Keep gain β€ 1 |
Avoid clipping |
| Structure in cycles | Plan phrases beforehand |
// Base groove
s("bd ~ bd ~"), s("~ hh ~ hh").gain(0.6)
// Add snare later
s("~ sd ~ sd").gain(0.8)
// Bring in bass
note("c2(3,8) g1(3,8) c2(5,8)").s("sine").fm(2).fmh(1.5).gain(0.8)
// Pad swell
note("<c3e3g3 a2c3e3 f2a2c3 g2b2d3>").s("saw")
.chorus(0.4).reverb(0.5)
.attack(0.01).decay(0.3).sustain(0.5).release(0.6).slow(2)- Practice muting/unmuting parts with line evaluation.
- Change pattern parameters live without stopping the clock.
- Gradually introduce polyrhythms for a finale.
Perform a 3-minute improvised jam. Introduce ideas slowly, develop texture, and resolve into silence. Use everything youβve learned: filters, FM, effects, ADSR, and pattern transformation.
| Category | Functions |
|---|---|
| Sound | s(), bank(), n(), begin(), end(), loop() |
| Pitch | note(), scale(), chord() |
| Timing | fast(), slow(), off(), stack(), rev() |
| Dynamics | gain(), velocity(), attack(), decay(), sustain(), release(), postgain() |
| Modulation | fm(), fmh(), wt(), warp(), warpmode() |
| Filters | bpf(), bpq() |
| FX | chorus(), reverb(), compressor(), distort() |
| Interaction | slider(), markcss(), source() |
| Pattern Code | Effect |
|---|---|
"bd*4" |
repeat kick 4 times |
"<bd sd>" |
alternate each cycle |
"[bd sd]" |
play both in sequence within one cycle |
"bd, hh" |
layer simultaneous patterns |
"~" |
silence rest |
Youβve now touched every major part of Strudel: pattern syntax, sample banks, synthesis, filters, FX, and interaction. Keep your REPL open and treat it as an instrument β not a file. The best way to improve is to play with time and texture every day.