I Spent a Weekend Making Techno With Code. Here’s What I Learned.
An experiment with Strudel — by someone who cannot play an instrument.
Up front, so we’re clear: I’m not going to pretend I know what makes music good. I don’t. I can’t play an instrument — my musical career peaked while trying to match Chester Bennington’s vocals to Numb in the sixth grade and went downhill from there, literally the moment my mom heard my tone-deaf screaming. This isn’t a guide from someone who knows what they’re doing. It’s a weekend experiment with a tool called Strudel, and I’m sharing what I stumbled into — mostly so the next curious person can skip the parts where I was confused.
With that out of the way: you can build a whole evolving track by writing code, layer by layer, and it’s a stupid amount of fun.
What Is Strudel?
Strudel is a live-coding music environment that runs in your browser — a JavaScript port of TidalCycles, the pattern language from the algorave scene, with zero setup. Go to strudel.cc, type, hit play. Everything below runs there as-is.
setcps(0.5)
s("bd*4")A kick, four times a bar. bd is a bass-drum sample; *4 repeats it across the cycle. One note on tempo: cps is cycles per second. With four beats to a cycle, 0.5 cps is 120 BPM. That's the only math in this whole post.
Now let’s actually build something — one layer at a time, because that’s how a track comes together, and it’s also the easiest way to understand what each piece is doing.
Layer 1 — Drums (this is most of your groove)
The thing that surprised me first: the groove lives almost entirely in the drums. Get these right and a track feels good before you’ve added a single melodic note.
Start with the kick and give it real character with a drum machine:
s("bd*4").bank("RolandTR909").bank("RolandTR909") swaps the generic kick for the actual 909 — the drum machine techno is basically built on. Now add hats and a clap:
stack(
s("bd*4").bank("RolandTR909"),
s("hh*8").bank("RolandTR909").gain(0.5),
s("~ cp ~ cp").bank("RolandTR909")
)Two things worth talking through. The hats are hh*8 — eighth notes, driving the track forward. The clap is "~ cp ~ cp", where ~ means silence — so it lands on beats 2 and 4, the backbeat. That backbeat is doing a lot of psychological work; it's the thing your head nods to.
A few things to try right here, because small drum changes change everything:
s("~ hh ~ hh") // offbeat hats — instant house feel
s("hh*16") // double-time hats — more urgent, more energy
s("hh:0 hh:1 hh:2") // ':n' picks different hat samples for variationAnd the trick I liked most — placing hits by position instead of drawing them out:
s("bd").beat("0,7,10", 16) // kick on steps 0, 7, 10 of 16
s("sd").beat("4,12", 16) // snare on the backbeatbeat("positions", steps) says "out of 16 sixteenth-notes, hit these." It's a cleaner way to think about rhythm than counting tildes. You can even nudge a layer toward variation:
s("hh*8").beat("0,2,4,6", 8).sometimesBy(0.3, x => x.speed(2))sometimesBy(0.3, ...) does something 30% of the time — here, occasionally double-speeding a hat into a little roll. That bit of randomness is what keeps a loop from feeling stamped out by a machine. (More on that obsession later.)
Layer 2 — Bass (where it starts to feel like techno)
Now the low end. A plain sawtooth works:
note("c2 eb2 g2 c2").s("sawtooth").lpf(600)But the squelchy, rubber-band acid bass — the sound a thousand tracks chase — has a specific recipe, and it’s worth understanding why it works:
note("c2 c2 c3 c2 eb2 c2 g2 c2").s("sawtooth")
.lpf(200).lpenv(2).lpq(12)lpf(200)— a low cutoff, so most of the brightness is filtered out.lpenv(2)— a filter envelope: every note briefly opens the filter and shuts it. That movement is the "wah."lpq(12)— high resonance, which is the squelch itself (the Roland TB-303 sound).
The bit I had backwards for an embarrassing while: the brightness shouldn’t come from a high static filter — it comes from a low one that the envelope keeps opening and closing. Movement, not volume. Once that clicked, everything sounded more like the records I was trying to copy.
Layer 3 — Melody, and one oscillator becoming ten
A lead is just notes higher up. But here’s where I learned that how you shape a sound matters more than which notes you play.
Get Ashmit Biswas’s stories in your inbox
Join Medium for free to get updates from this writer.
Same melody, four completely different instruments — just by changing the oscillator:
note("c4 eb4 g4 c5").s("sawtooth") // buzzy, bright
note("c4 eb4 g4 c5").s("square") // hollow, retro
note("c4 eb4 g4 c5").s("triangle") // soft, flutey
note("c4 eb4 g4 c5").s("supersaw") // huge, detuned, trance-yThen the envelope — attack, decay, sustain, release — decides whether a note plucks, stabs, or swells:
// a sharp pluck
note("c4 eb4 g4 c5").s("sawtooth").attack(0.01).decay(0.2).sustain(0)// a slow, washy pad
note("c4 eb4 g4 c5").s("triangle").attack(0.4).sustain(0.8).release(0.6)attack is how fast a note fades in, release how slowly it fades out. Fast attack + no sustain = a stab. Slow attack + long release = a pad. Same notes, totally different feel. This was the moment Strudel stopped feeling like a drum machine and started feeling like an instrument.
Layer 4 — Space and grit (effects)
Dry sounds feel like they’re happening inside your laptop. Effects put them in a room:
note("c4 eb4 g4 c5").s("sawtooth")
.room(0.4) // reverb — depth, "size"
.delay(0.5) // echoes — rhythmic space
.distort(1.5) // grit, aggression
.pan(sine) // drifts left↔right over timeroom is reverb (how big the space feels), delay is echo (great for filling gaps in a sparse melody), distort adds dirt. And pan(sine) is a small thing that does a lot — it slowly moves the sound across the stereo field, so it feels alive instead of stuck dead-center. Hold that thought.
Of course it sounded like a robot. The surprising part was the fix.
So I had all four layers — and it was, technically, music. It was also lifeless. Generated. Like a machine reciting what techno is supposed to be.
That wasn’t a shock. If typing stack(...) reliably produced good music, everyone would be doing this and the charts would look very different. The robotic sound is the default — it's the price of admission, not a plot twist.
What I didn’t see coming was what pulled it out of the uncanny valley. I’d assumed it would take the thing I plainly don’t have — musical taste, better chords, theory I never learned. It didn’t. The two things that moved the needle most weren’t musical at all. They were almost mechanical — which means you don’t need to be a musician to get past the robot. You need two structural tricks.
One: a hardcoded loop gets boring fast. A fixed string of notes, identical every bar, turns into wallpaper after eight bars. The fix felt backwards — make it more random, not less:
n(irand(8).segment(16)).scale("c2:minor")
.s("sawtooth").lpf(200).lpenv(2).lpq(12)irand(8) picks random numbers; .scale("c2:minor") snaps every one to the C-minor scale. Different notes every bar, but always in key. The scale is the leash; the randomness is the dog. I expected chaos. To my ear it was the opposite — alive.
Two: nothing should ever sit still. Watching people who actually do this, I noticed they’re never leaving a knob alone. So instead of static numbers, feed parameters a moving signal:
s("hh*8").decay(tri.range(0.02, 0.12)) // hats that breathe
note("c2").s("sawtooth").lpf(sine.range(200, 1200).slow(8)) // a filter that drifts
s("pulse").fm(time).fmh(time) // a riser that climbs forevertri, sine, perlin, time are continuous signals you can plug into almost anything. The day I swapped static numbers for movement, the whole thing came alive — to me, at least. Whatever "soul" I felt was missing, that was it: not a missing layer, the absence of motion.
And notice what neither fix required: a single thing I learned in a music class I never took. That’s the part that actually surprised me.
Putting it together (and making it evolve)
Stack the layers and you have a loop. But a track isn’t a loop — it’s layers coming in and out over time. A few ways to get there:
// '<...>' steps through options each cycle — the bass changes every bar
note("<c2 eb2 g2 f2>").s("sawtooth").lpf(300)// 'every' does something on a schedule — a fill every 4th bar
s("bd*4").every(4, x => x.fast(2))// 'mask' gates a layer on/off — hats only in the second half
s("hh*16").mask("0 1")
That’s the whole arrangement game: decide what’s playing, when, and let things enter, build, drop, and strip back. (This is the rabbit hole I’m still happily lost in.)
The whole thing, in one block
Stack everything we just built and you’ve got a complete, living track. Paste this into strudel.cc and hit play:
setcps(0.55) // ~132 BPMstack(
// drums
s("bd*4").bank("RolandTR909"),
s("hh*8").bank("RolandTR909").gain(0.5)
.decay(tri.range(0.02, 0.12)), // hats that breathe
s("~ cp ~ cp").bank("RolandTR909").gain(0.7), // acid bass — generative, low filter + envelope + resonance, drifting cutoff
n(irand(8).segment(16)).scale("c2:minor")
.s("sawtooth")
.lpf(sine.range(180, 900).slow(8)) // the filter moves on its own
.lpenv(2).lpq(12)
.gain(0.8), // lead — a square 'stab' in the same key, with echo and space
n(irand(8).segment(8)).scale("c4:minor")
.s("square")
.attack(0.01).decay(0.18).sustain(0)
.delay(0.4).room(0.3)
.degradeBy(0.3) // leave gaps
.gain(0.4)
)
Nothing in there is hardcoded, and nothing sits still — the notes re-roll every bar (always in key), the hats breathe, the filter drifts on its own. That’s the entire lesson in one block. From here it’s yours: change the scale, swap the oscillator, push the tempo, mute a layer. Break it. That’s the fun part.
Where I learned all this
Honestly? By studying the live-coding sets of Switch Angel — frame by frame, reading the actual code off the screen as she built tracks. I’m a beginner who learned by copying someone who very much isn’t. If you want into Strudel, skip the docs and watch someone perform with it. Go watch her; she’s brilliant.
The thing it turned into
I wired all of this into a small project I call musaic — a generative DJ in the browser. Hit “new track” and it rolls a fresh tempo, key, bassline, and sound palette, holds that as the track’s identity, then arranges it on the fly. No two tracks the same. Open source and very unfinished: github.com/ashmitb95/musaic.
Go make some noise
The barrier is basically gone. strudel.cc — no install, no account, no instrument. Paste s("bd*4") and start stacking.
Two things I’d tell myself on day one — not as music advice (I have none), just what worked for me: don’t hardcode anything, and don’t let a number sit still. Everything past that is taste, and I’m still working on mine.
Cheers!







