Strudel: How TidalCycles' Pattern Algebra Was Reimagined for the Web
Hook
Live coding music used to require installing Haskell, SuperCollider, and a fragile stack of dependencies. Strudel brings the entire TidalCycles pattern engine to a browser tab—and the mathematical elegance it preserves is more interesting than the convenience it provides.
Context
TidalCycles revolutionized live coding music by treating patterns as first-class algebraic structures. Born from Alex McLean's research into temporal domain-specific languages, Tidal allows performers to compose complex polyrhythmic music by combining simple pattern functions. A typical Tidal session might layer "bd sd" (kick and snare) with .fast(2) to double the tempo, or use "<0 2 7>" to cycle through melodic notes—all expressed in a concise mini-notation that evaluates to infinite temporal streams.
But Tidal's power came with friction. The canonical implementation requires Haskell for pattern evaluation, SuperCollider for synthesis, and a brittle communication layer between them. Each component must be correctly installed and configured, creating a barrier that kept algorithmic composition in the hands of technically sophisticated users. Workshops often spent half their time on installation troubleshooting rather than creative exploration. Strudel emerged from this frustration: a ground-up reimplementation of Tidal's pattern semantics in JavaScript, executing entirely in the browser with Web Audio API handling synthesis. It's not a wrapper or approximation—it's a faithful port of the underlying mathematical model, making choices about how to map Haskell's type system and lazy evaluation onto JavaScript's execution model.
Technical Insight
At Strudel's core is the Pattern class, which represents temporal structures as functions from time spans to events. This mirrors Tidal's fundamental abstraction: patterns aren't arrays of notes, they're queries over continuous time. When you write s("bd sd"), you're creating a function that, given any time range, returns the events that occur within it.
// Simplified conceptual model
class Pattern {
constructor(query) {
this.query = query; // function: TimeSpan -> [Event]
}
fast(factor) {
return new Pattern((span) => {
const compressed = span.compress(factor);
return this.query(compressed).map(e => e.withSpan(s => s.expand(factor)));
});
}
cat(other) {
return new Pattern((span) => {
const half = span.duration / 2;
const firstHalf = span.withEnd(span.begin + half);
const secondHalf = span.withBegin(span.begin + half);
return [...this.query(firstHalf), ...other.query(secondHalf)];
});
}
}
This architecture enables Tidal's signature pattern combinators. The .fast(2) method doesn't modify an array—it returns a new pattern whose query function compresses time before delegating to the original pattern. Temporal transformations compose naturally because they're just function composition in the time domain.
Strudel's mini-notation parser converts strings like "bd*3 sd" into nested pattern structures. The *3 operator becomes a .fast(3) application, and whitespace triggers concatenation within cycles. The parser uses a recursive descent approach, building an AST that gets evaluated to Pattern instances:
// Pattern construction from mini-notation
const pattern = s("bd*2 [sd cp] hh");
// Translates roughly to:
const equivalent = stack(
s("bd").fast(2),
s("sd cp").early(0.25),
s("hh").early(0.5)
);
The bracket notation [sd cp] creates a subdivision—two events squeezed into the time span of one. This is implemented by scaling the inner pattern's time coordinates, another demonstration of how Strudel manipulates temporal geometry rather than concrete event lists.
Where JavaScript diverges from Haskell is in handling infinite sequences. Haskell's lazy evaluation naturally supports infinite patterns—"<0 1 2>" cycles through values indefinitely without computing all future values. JavaScript can't lazily evaluate by default, so Strudel's patterns are functions that compute on-demand. When the scheduler needs events for cycles 100-101, it calls pattern.query(new TimeSpan(100, 101)), and only those events materialize.
The Web Audio integration is surprisingly elegant. Strudel doesn't render audio samples directly; instead, it schedules events that trigger Web Audio nodes. The scheduler runs ahead by a small buffer (typically 0.1 seconds), querying patterns for upcoming events and translating them into AudioContext commands:
function scheduleEvents(pattern, audioContext, startTime, endTime) {
const events = pattern.query(new TimeSpan(startTime, endTime));
events.forEach(event => {
const { value, whole } = event;
const scheduledTime = audioContext.currentTime + whole.begin - startTime;
if (value.s) { // sample event
const buffer = getSample(value.s);
const source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.start(scheduledTime);
}
});
}
This scheduling approach keeps latency low while maintaining precise timing. Unlike setTimeout-based schedulers that drift, AudioContext timestamps are referenced to the audio clock, ensuring polyrhythmic patterns stay locked even across tempo changes.
Strudel also implements Euclidean rhythm generation—a geometric pattern distribution algorithm that appears across world music traditions. The syntax "bd(3,8)" places 3 kicks as evenly as possible across 8 steps using Bjorklund's algorithm, resulting in patterns like [x . . x . . x .]. This required porting not just syntax but the underlying combinatorial logic from Haskell's pure functions to JavaScript.
Gotcha
The GitHub repository you're looking at is effectively abandoned—active development moved to Codeberg in late 2023. This migration reflects the TidalCycles community's philosophical commitment to open-source platforms, but creates confusion for developers discovering Strudel through search engines or GitHub trending pages. Always check Codeberg for current documentation, issues, and pull requests. The GitHub version may be multiple releases behind.
More fundamentally, browser-based synthesis has intrinsic limitations compared to SuperCollider. Strudel's default sample library is intentionally small to keep page loads fast, and while you can load custom samples, you're constrained by Web Audio's synthesis capabilities. SuperCollider offers hundreds of UGens (unit generators) for advanced synthesis techniques—frequency modulation, granular processing, physical modeling—that would require significant custom Web Audio code to replicate. If your performance aesthetic requires evolving timbral complexity rather than sample-based rhythms, Strudel's sonic palette may feel restrictive. Latency, while low, isn't quite at native audio API levels, and running complex patterns can occasionally cause glitches on lower-powered devices when JavaScript's garbage collector pauses pattern evaluation. The web platform's convenience is also its constraint.
Verdict
Use if you're teaching live coding to newcomers who'd be discouraged by installation complexity, need to share interactive musical sketches via URL (invaluable for remote collaboration or documentation), or want to prototype algorithmic ideas without context-switching from your browser-based workflow. Strudel is genuinely excellent for algorave performances in environments where you can't guarantee machine configuration—workshops, artist residencies, spontaneous jam sessions. Skip if you're doing production music work requiring sophisticated synthesis, need the absolute lowest latency for fast-tempo glitch aesthetics, or depend on bleeding-edge TidalCycles features that may not have JavaScript equivalents yet. Most importantly, skip the GitHub version entirely—head to Codeberg for the maintained codebase where development actually happens. For serious TidalCycles work, the Haskell original remains unmatched, but for accessibility and experimentation, Strudel's port is a remarkable achievement in functional programming translation.