HomeJournalThis post

Euclidean Rhythm Generator with Web Audio

Implement and verify a Euclidean pulse pattern in JavaScript, rotate it deliberately, and schedule its hits against the Web Audio clock.

JP
JP Casabianca
AI Engineer and Product Designer · full-stack delivery · Bogotá

A Euclidean rhythm generator turns E(k,n) into an evenly distributed pulse cycle, but rotation and scheduling decide how that structure becomes music. This tutorial implements the Björklund algorithm, verifies its circular gaps, and maps the pattern onto explicit Web Audio target times.

A Euclidean rhythm generator distributes pulse and rest

A Euclidean rhythm generator spreads k pulses across n steps so the gaps are as even as the integers allow. E(3,8), for example, produces three attacks around an eight-step cycle with gap lengths 3, 3, and 2 after a chosen rotation. The algorithm supplies a pattern; tempo, sound, accent, and cultural meaning remain separate creative decisions.

Godfried Toussaint’s paper, The Euclidean Algorithm Generates Traditional Musical Rhythms, connects the distribution procedure to a family of timelines found in musical practice. Use that source with care: a bit pattern is not an ethnographic account, and a browser demo should not relabel a traditional rhythm without historical and cultural context.

This tutorial implements the Björklund construction in JavaScript, makes rotation explicit, derives circular gaps, and schedules hit times against an audio clock. The downloadable fixture is deterministic and runs in Node so its pattern and arithmetic can be tested without pretending that a terminal produces browser audio.

The result is both technical and playable. One pulse ring explains distribution, a second shows rotation, and a three-clock timeline separates UI lookahead from audio scheduling and physical output. That separation keeps musical gesture visible without confusing mathematical correctness with audible timing quality.

Euclidean pulse rings for E(3,8) and E(5,13)@2Two circular step diagrams show E(3,8) and the plus-two rotation E(5,13)@2, whose pulses land at indexes 1, 3, 6, 8, and 11.E(3,8)gaps 3 · 3 · 2E(5,13)@2gaps 2 · 3 · 2 · 3 · 3
E(5,13) rotated by +2 lands at indexes 1, 3, 6, 8, and 11, producing ordered gaps 2, 3, 2, 3, and 3 around the cycle.
Euclidean pulse-ring fixtures
PatternPulse indexesStep countCircular gaps
E(3,8)0, 3, 683, 3, 2
E(5,13)@21, 3, 6, 8, 11132, 3, 2, 3, 3

The top marker is a phase reference, not an intrinsic musical downbeat; the highlighted +2 pulses match the downloadable receipt.

Build the Björklund groups

Inside a Euclidean rhythm generator, the Björklund algorithm begins with k pulse groups and n − k rest groups. It repeatedly distributes the shorter collection across the longer one, carrying any remainder into the next round, until the remainder is one or zero. Expanding the resulting count and remainder tree yields a binary cycle with maximally even spacing.

A compact implementation stores counts and remainders much like the Euclidean greatest-common-divisor procedure. The recursive build step emits rests at one leaf and pulses at the other, then the result is rotated to a documented canonical starting pulse. Canonical rotation is a convenience for comparison, not an intrinsic downbeat.

Validate inputs before recursion: steps must be a bounded positive integer, pulses must be an integer from zero through steps, and rotation must be finite. Define the edge cases. E(0,n) is all rests, E(n,n) is all pulses, and an empty cycle is rejected because it cannot be scheduled.

The core oracle is structural. The result has exactly n entries, exactly k pulses, only zeros and ones, and circular gaps whose largest and smallest values differ by at most one for the tested patterns. That last property describes even distribution; it does not prove a pattern will feel good at every tempo or instrumentation.

Björklund distribution roundsEight initial pulse and rest groups are combined through three rounds into an evenly distributed three-pulse cycle.PULSES1 · 1 · 1DISTRIBUTE10 · 10 · 10REMAINDER0 · 0COUNTS2 · 1BUILD100 · 100 · 10ROTATE10010010
The construction distributes shorter groups across longer groups, then expands and deliberately rotates the cycle.
  1. Start with three pulse groups and five rest groups.
  2. Distribute rests across pulses until the shorter remainder is exhausted.
  3. Record counts and remainders as the Euclidean procedure continues.
  4. Expand the build tree into eight binary steps.
  5. Rotate to the documented phase used by the instrument.

Implement the pattern as pure JavaScript

A Euclidean rhythms JavaScript implementation should keep pattern generation free of DOM and audio state. A pure function from pulses and steps to an array is easy to replay, hash, rotate, render, and compare. It also prevents an animation frame or AudioContext lifecycle from becoming part of the mathematical test. The Euclidean rhythm generator can then expose that pure result to several interfaces.

The lab exposes three operations: bjorklund(k,n), rotate(pattern,offset), and circularGaps(pattern). Rotation uses a normalized modulo so negative and oversized offsets remain deliberate. Gap extraction measures step distance from each pulse to the next, wrapping at the end of the cycle.

Return a receipt that includes the unrotated canonical pattern, rotated pattern, pulse indexes, circular gaps, greatest common divisor, and an evenness check. For the displayed E(5,13)@2 receipt, the performed pulse indexes are 1, 3, 6, 8, and 11, and its ordered circular gaps are 2, 3, 2, 3, and 3. Those five gaps sum to thirteen. Re-running the same arguments must produce the same hash; changing only rotation must preserve gap multiset, pulse count, and cycle length while moving the indexes.

This determinism makes the system composable with seeded randomness for generative art. A seed can choose among bounded pulse counts, rotations, voices, or timbres while the rhythm function remains inspectable. Store both the seed and resolved parameters so an edition can be reproduced without hiding the musical choices behind chance.

Runnable artifact — Deterministic pattern and schedule arithmetic; not browser output latency, musical quality, or ethnomusicological authority.

import assert from "node:assert/strict";
import { createHash } from "node:crypto";

const sha = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
const integerArg = (name, fallback) => { const index = process.argv.indexOf(name); if (index < 0) return fallback; const value = Number(process.argv[index + 1]); if (!Number.isInteger(value)) throw new Error("invalid-argument:" + name); return value; };
const numberArg = (name, fallback) => { const index = process.argv.indexOf(name); if (index < 0) return fallback; const value = Number(process.argv[index + 1]); if (!Number.isFinite(value)) throw new Error("invalid-argument:" + name); return value; };
function rotate(pattern, offset) { if (!Array.isArray(pattern) || !pattern.length || !Number.isInteger(offset)) throw new Error("invalid-rotation"); const shift = ((offset % pattern.length) + pattern.length) % pattern.length; return pattern.slice(shift).concat(pattern.slice(0, shift)); }
function bjorklund(pulses, steps) {
  if (!Number.isInteger(steps) || steps < 1 || steps > 256 || !Number.isInteger(pulses) || pulses < 0 || pulses > steps) throw new Error("invalid-pattern");
  if (pulses === 0) return Array(steps).fill(0);
  if (pulses === steps) return Array(steps).fill(1);
  const counts = [];
  const remainders = [pulses];
  let divisor = steps - pulses;
  let level = 0;
  while (true) { counts.push(Math.floor(divisor / remainders[level])); remainders.push(divisor % remainders[level]); divisor = remainders[level]; level += 1; if (remainders[level] <= 1) break; }
  counts.push(divisor);
  const pattern = [];
  const build = (current) => { if (current === -1) pattern.push(0); else if (current === -2) pattern.push(1); else { for (let index = 0; index < counts[current]; index += 1) build(current - 1); if (remainders[current] !== 0) build(current - 2); } };
  build(level);
  const firstPulse = pattern.indexOf(1);
  return rotate(pattern, firstPulse);
}
function circularGaps(pattern) { const hits = pattern.map((value, index) => value ? index : -1).filter((index) => index >= 0); if (!hits.length) return []; return hits.map((hit, index) => (hits[(index + 1) % hits.length] - hit + pattern.length) % pattern.length || pattern.length); }
function gcd(left, right) { let a = Math.abs(left), b = Math.abs(right); while (b) [a, b] = [b, a % b]; return a; }
const alternate = process.argv.includes("--alternate");
const steps = integerArg("--steps", 13);
const pulses = integerArg("--pulses", 5);
const rotation = integerArg("--rotation", alternate ? 5 : 2);
const bpm = numberArg("--bpm", 112);
const stepsPerBeat = integerArg("--steps-per-beat", 4);
const startAudioTime = numberArg("--start", .25);
if (bpm < 20 || bpm > 400 || stepsPerBeat < 1 || stepsPerBeat > 16 || startAudioTime < 0 || startAudioTime > 60) throw new Error("invalid-schedule");
const canonical = bjorklund(pulses, steps);
const performed = rotate(canonical, rotation);
const gaps = circularGaps(performed);
const stepSeconds = 60 / bpm / stepsPerBeat;
const scheduledHits = performed.map((hit, step) => hit ? { step, audioTime: Number((startAudioTime + step * stepSeconds).toFixed(9)) } : null).filter(Boolean);
const reference = bjorklund(3, 8);
const hostile = {};
for (const [name, run] of Object.entries({ steps: () => bjorklund(1, 0), pulses: () => bjorklund(9, 8), bound: () => bjorklund(1, 257), rotation: () => rotate([1, 0], .5), empty: () => rotate([], 0) })) { try { run(); } catch (error) { hostile[name] = error.message; } }
const core = {
  schema: "euclidean-rhythm-schedule-receipt-v1",
  fixture: "deterministic Björklund pattern and AudioContext target-time arithmetic",
  input: { pulses, steps, rotation, bpm, stepsPerBeat, startAudioTime },
  canonical,
  performed,
  pulseIndexes: performed.map((value, index) => value ? index : -1).filter((index) => index >= 0),
  circularGaps: gaps,
  even: gaps.length < 2 || Math.max(...gaps) - Math.min(...gaps) <= 1,
  gcd: gcd(pulses, steps),
  referenceE3_8: { pattern: reference, gaps: circularGaps(reference) },
  schedule: { stepSeconds: Number(stepSeconds.toFixed(9)), lookaheadIntervalMs: 25, scheduleAheadSeconds: .1, scheduledHits, clocks: ["JavaScript lookahead loop", "AudioContext target time", "device output path"] },
  hostile,
  claimBoundary: "Deterministic pattern and schedule arithmetic; not browser output latency, musical quality, or ethnomusicological authority.",
};
assert.equal(canonical.length, steps);
assert.equal(canonical.reduce((sum, value) => sum + value, 0), pulses);
assert.equal(gaps.reduce((sum, value) => sum + value, 0), pulses ? steps : 0);
console.log(JSON.stringify({ ...core, receiptHash: sha(core) }, null, 2));
console.log("PASS: Björklund counts, circular gaps, rotation, schedule targets, hostile inputs, mutation, and digest verified");

Treat rotation as composition, not cleanup

Two rotations of the same Euclidean cycle share the same circular spacing and can feel very different against a bar, bass line, or visual gesture. Choose the downbeat after hearing the pattern in context. A Euclidean rhythm generator should show the canonical cycle and performed rotation rather than silently spinning the first pulse to step zero.

Represent rotation in the URL, preset, or exported project state. A label such as E(5,13)@2 is more useful than saving only thirteen bits because it preserves the compact construction and the chosen phase. If the pattern is transformed further, store the resolved bits as a checksum or migration aid.

Layering creates polyrhythm generator territory, but avoid implying that every combination has a single repeating bar. Two cycles of 8 and 13 steps realign after their least common multiple when they share the same step duration. Display that horizon and give users a way to solo each voice before judging the composite.

Visual design can make phase tangible. Use a fixed twelve-o’clock reference, number or otherwise label steps outside color alone, distinguish the selected rotation, and animate only when motion adds timing information. Honor reduced-motion preferences and keep a static pattern table available.

Schedule with the Web Audio clock

A Euclidean rhythm generator’s Web Audio sequencer should not rely on setInterval firing at the exact moment a note must sound. The Web Audio API specification provides an AudioContext time coordinate for scheduling audio operations. Use a lightweight JavaScript timer to look ahead and place events slightly into that clock’s future.

MDN’s advanced sequencing guide demonstrates the lookahead pattern: the timer checks often, while the scheduler fills a larger future window. If UI work delays one timer tick, events already committed to the audio timeline can still begin at their target audio times.

Keep three clocks distinct:

  1. The lookahead loop decides when to enqueue more steps.
  2. AudioContext.currentTime defines the scheduled sound time.
  3. The output device and acoustic path determine when a listener hears it.

The Node lab calculates the second clock only. It prints target audio times from BPM, steps per beat, start time, pattern, and rotation. Browser output latency, main-thread stalls, device buffering, and autoplay policy require separate browser measurements; deterministic arithmetic cannot certify them.

Three-clock Web Audio scheduleA lookahead timer repeatedly fills a future AudioContext window, whose scheduled hit targets precede variable device output time.LOOKAHEADAUDIO TIMEOUTPUTFILLFILLFILLscheduled target ≠ heard time
The JavaScript timer decides when to enqueue, AudioContext time owns sound targets, and the device adds a separate output path.
Lookahead loop
Wakes frequently and fills a larger future scheduling window.
Audio time
Provides deterministic target coordinates for AudioNode events.
Output
Adds browser, device, and acoustic latency not certified by the Node fixture.
UI playhead
May render from scheduled targets but remains separate from sound delivery.

Pair sound with an accessible visual instrument

Build the Euclidean rhythm generator instrument as two synchronized but independently useful surfaces. The audible surface creates notes; the visual surface exposes pulse state, current step, rotation, tempo, and transport controls. Neither should be the sole carrier of meaning.

Use native buttons and range inputs where possible, with visible labels and current values. Announce transport changes without sending a live-region message on every step. Offer a static pattern representation such as “hits at 1, 4, 6, 9, 11” and ensure keyboard users can change one parameter without losing focus.

The guidance for accessible data sonification applies in reverse too: sound needs a visual or textual equivalent. A pulse ring can show spacing beautifully, but a list or table makes exact steps inspectable at high zoom and without color. Keep contrast, focus indicators, reduced motion, and muted playback states in the same design system.

For a more physical visual study, turn Web Audio into a cymatics canvas while preserving the rhythm controls outside the canvas. Decorative particles may respond to hits, but transport and pattern state should remain semantic HTML.

Keep synthesis off the scheduling hot path

In a Euclidean rhythm generator, simple oscillator and sample triggers can be scheduled directly with AudioNodes. Custom synthesis or analysis that must run on the render thread belongs in an AudioWorklet, not in repeated main-thread callbacks. Use AudioWorklet for glitch-free custom processing when the sound algorithm requires sample-level state.

An AudioWorklet does not remove the need for lookahead planning. The main application still owns transport, pattern changes, and safe parameter handoff. Version messages between UI and processor, bound queues, and decide whether edits take effect immediately, at the next step, or at the next cycle.

Handle browser lifecycle deliberately. Create or resume the AudioContext from a user gesture, stop scheduled sources on transport reset, cancel visual animation, and restore a coherent next-step pointer after suspension. When the tab returns, do not fire every missed hit; calculate the current cycle position and schedule forward.

Measure late scheduling and dropped visual frames separately. The audio may remain stable while the playhead skips, or the animation may look smooth while sound starts late on a device. A truthful instrument reports those layers without treating one green FPS counter as musical timing evidence.

Export a rhythm receipt readers can replay

A useful Euclidean rhythm generator export contains pulses, steps, canonical pattern, rotation, performed pattern, BPM, steps per beat, start-time policy, voice settings, and application version. Add the circular gaps and schedule targets so another implementation can verify the same cycle without sharing your UI.

Run the lab first with E(3,8), then reproduce the displayed E(5,13)@2 pattern. Check for pulse indexes 1, 3, 6, 8, and 11, ordered gaps 2, 3, 2, 3, and 3, and correct event times. Use the browser to judge sound, interaction, device latency, and musical fit; use the deterministic receipt to judge pattern and schedule arithmetic.

When publishing a preset inspired by a named musical tradition, research and credit the source rather than treating the binary result as free-floating algorithmic novelty. Toussaint’s mathematical comparison is a starting reference, not permission to erase musicians, instruments, or history.

The creative payoff comes from keeping boundaries sharp. Integer distribution provides a repeatable skeleton, Web Audio provides a scheduling coordinate, and art direction provides phase, timbre, hierarchy, and surprise. When those layers remain visible, a small JavaScript generator becomes an instrument people can understand, modify, and trust.