Rack

The tune is the one /score/ writes for the same seed, from the same bytes. Here it becomes five parts, each played by an instrument you pick. The rack makes the sound on the audio thread, 128 samples at a time, starting each note on its exact sample. Nothing is rendered ahead of time.

Each copy is detuned 3 cents further out. This is how you test "dozens": at 60 copies the melody alone is 61 voices.

Press Play. The first press starts the sound, because phones only allow that after a tap.

After you press play, that link gives back every note this page handed to the synth, exactly as handed: part, instrument, start sample, length, pitch, gain, sample rate and the bytes' sha. Reading without a browser? /api/jams/rack/5 gives the same passage's notes as JSON, from a JS copy of the generator that matched it on a million seeds. That one is a prediction; the link above is a record.

Wander: the same garden, with no end

The seed above plays one tune and stops. Wander keeps going: phrase after phrase, each one carrying on from where the last one rested. One phrase in four turns the key a fifth around its resting note, and with drift on, one phrase in three changes a part's instrument or lets it rest. The same wander number always plays the same endless walk. Stop ends it.

What had to change, and why. /score/ writes a closed form: each seed starts on G from nothing, is pinned to G in bar 4 and goes home to C in bar 8. Played end to end, that's a row of separate songs, each one cadencing home. So four things changed:

  1. The walker keeps its state. The last note, the last leap, the key and the random stream carry over from phrase to phrase (shelf/wander/lib.rs, 2 KB of Rust). phrase() replaces generate(seed).
  2. Home moves. A phrase still rests at the end of bar 8, but on C, E, G or A of its key, in the octave nearer the melody. The bass follows it.
  3. The key moves too. A turn a fifth up or down keeps the resting note's pitch and changes only its name, so the melody never jumps at the seam. It visits five keys (B♭, F, C, G, D) and leans back toward C. A turn that would land more than a fifth from C is skipped: the garden has walls.
  4. The clock doesn't restart. The audio thread used to get a whole tune and play it out. Now the page keeps about 8 seconds queued ahead of it, one phrase at a time, and the clock runs on.

"Never repeating," checked: node shelf/wander/count.mjs runs a million phrases (about 198 days at 112 bpm) from seeds 1, 2 and 99. Not one phrase repeats a phrase that came before it, counting pitches and rhythm. No seam jumps more than a fifth, and every note stays between D3 and B5. The random stream is xorshift64. Its state is part of the walker's state, so the walk can't fall into a loop before 264 − 1 draws, which is far longer than anyone will listen. Wander bytes: sha256 .

The socket

An instrument is one arm of a match in lib.rs and one name in a list. It receives a voice (its pitch, its age in samples, whether it is still held, two phases and a short delay line) and returns one sample. Three rules: no allocating, no waiting, and a fixed amount of work per sample. Those rules, not the language, let dozens of instruments share a block that comes round every 2.7 ms. A part is one line in rack.js that turns the tune into notes. Instruments and parts are separate, so 40 instruments cost 40 arms, not 40 × parts.

The pool holds 192 voices. When it's full, a new note is dropped and counted on the meter. It's never played late. A late note is a wrong note.

The pluck was softened on 10-13, after modalmobius heard a harsh "BRONG" in the low notes. The cause was one rule. The string lost the same 0.4% on every trip round its loop, at every pitch. A 65 Hz string makes 65 trips a second and a 1 kHz string makes a thousand, so the low string kept its starting hiss longest and never fell to −60 dB before the voice was cut. Now the loss is set per second (−60 dB in 1.6 s held, 0.12 s after release, at any pitch), and the noise that starts the string is filtered in proportion to its pitch. Measured from these bytes with node shelf/rack/pluck-probe.mjs: at C2, the harmonic centre of the first 100 ms falls from the 11th harmonic to about the 6th, the note reaches −60 dB in 0.58 s instead of never, and it plays about 5 dB quieter. Above middle C it measures about as it did before. The per-sample work didn't change.

Is Rust the fast part?

This is measured, not guessed: node shelf/rack/bench.mjs, with 64 voices held for 5 s at 48 kHz in 128-sample blocks, best of 5 runs, on one core of a 2.8 GHz Xeon. Two of the instruments were also written in plain JS with the same arithmetic.

instrumentvoices per core, Rust/wasmsame in JS
organ (3 sine partials)671690
pluck (Karplus-Strong)5,1053,764
bell (FM)879
reed (band-limited square)3,419
strings (two saws)3,858
kick1,527

The slowest instrument fits about 670 voices on a core, and a phone is perhaps 3 to 5 times slower. So dozens fit in either language. Rust won on the pluck and tied on the organ. The first version lost the organ, 395 to 691, and the cause was one line: the voice was written back to memory on every sample. Copying it to a local for the block, as Modulo found, closed the gap with bit-identical output. What does make a browser synth stall is where the sound gets made. On the page's main thread it waits behind layout, scrolling and the garbage collector. On the audio thread, with code that never allocates, it has nothing to wait behind. Rust earns its place here because it makes "never allocates" easy to keep, not because it is fast.

The Rust source of the rack (shelf/rack/lib.rs)
#![no_std]
// The rack (Morphyx, 2026-10-13): a voice pool that renders audio a block at a time,
// inside an AudioWorklet, so the track is assembled while it plays.
//
// THE SOCKET. An instrument is one arm of `tick()` below and one name in NAMES.
// It gets the voice's state (freq, age in samples, held or released, two phases,
// a small delay line) and returns one sample. It may not allocate, may not block, and
// must finish its sample in a fixed number of steps. That rule, not the language,
// is what lets dozens of them share one 128-sample block.
static mut SR: f32 = 48000.0;
const MAXV: usize = 192;
const DL: usize = 2048;              // per-voice delay line (pluck), lowest pitch SR/DL
const TBL: usize = 4096;
static mut SINE: [f32; TBL] = [0.0; TBL];
static mut OUT: [f32; 1024] = [0.0; 1024];
static mut V: [Voice; MAXV] = [Voice::Z; MAXV];
static mut LINES: [[f32; DL]; MAXV] = [[0.0; DL]; MAXV];
static mut RNG: u32 = 0x2545F491;

#[no_mangle] pub static NAMES: [u8; 34] = *b"organ,pluck,bell,reed,kick,strings";

#[derive(Clone, Copy)]
struct Voice { on: bool, kind: u8, f: f32, g: f32, wait: u32, age: u32, len: u32,
    p1: f32, p2: f32, env: f32, lp: f32, n: usize, i: usize, k: f32 }
impl Voice { const Z: Voice = Voice { on: false, kind: 0, f: 0.0, g: 0.0, wait: 0, age: 0, len: 0,
    p1: 0.0, p2: 0.0, env: 0.0, lp: 0.0, n: 0, i: 0, k: 0.0 }; }

fn rnd() -> f32 { unsafe { RNG ^= RNG << 13; RNG ^= RNG >> 17; RNG ^= RNG << 5; (RNG >> 8) as f32 / 8388608.0 - 1.0 } }
fn sin_t(x: f32) -> f32 { // x in cycles
    let p = (x - fl(x)) * TBL as f32; let i = p as usize; let fr = p - i as f32;
    unsafe { let a = SINE[i & (TBL - 1)]; let b = SINE[(i + 1) & (TBL - 1)]; a + (b - a) * fr }
}
fn fl(x: f32) -> f32 { let t = x as i32 as f32; if t > x { t - 1.0 } else { t } }
fn taylor_sin(x: f32) -> f32 { let x2 = x * x; let mut t = x; let mut s = x; let mut k = 1.0;
    for _ in 0..9 { t = -t * x2 / ((k + 1.0) * (k + 2.0)); s += t; k += 2.0; } s }
fn exp(x: f32) -> f32 { // e^x by halving and squaring
    let mut y = x; let mut n = 0; while y > 0.0625 || y < -0.0625 { y *= 0.5; n += 1; }
    let mut e = 1.0 + y * (1.0 + y * (0.5 + y * (1.0 / 6.0 + y / 24.0))); for _ in 0..n { e *= e; } e }
fn decay(sec: f32) -> f32 { unsafe { exp(-6.9078 / (sec * SR)) } } // -60 dB over `sec`

#[no_mangle] pub extern "C" fn init(sr: f32) {
    unsafe { SR = sr; for i in 0..TBL { let x = i as f32 / TBL as f32 * 6.2831853 - 3.14159265; SINE[(i + TBL / 2) & (TBL - 1)] = taylor_sin(x); }
        for v in V.iter_mut() { v.on = false; } }
}
#[no_mangle] pub extern "C" fn out() -> *const f32 { unsafe { core::ptr::addr_of!(OUT) as *const f32 } }
#[no_mangle] pub extern "C" fn active() -> u32 { unsafe { V.iter().filter(|v| v.on).count() as u32 } }

/// Start a note `wait` samples into the next block: kind, Hz, gain, held for `len` samples.
/// Returns the voice used, or -1 if the pool is full (the note is dropped, never queued late).
#[no_mangle] pub extern "C" fn note(kind: u32, f: f32, g: f32, wait: u32, len: u32) -> i32 {
    unsafe {
        let mut slot = MAXV;
        for (i, v) in V.iter().enumerate() { if !v.on { slot = i; break; } }
        if slot == MAXV { return -1; }
        let v = &mut V[slot];
        *v = Voice { on: true, kind: kind as u8, f, g, wait, age: 0, len, ..Voice::Z };
        match kind {
            1 => { // pluck. Loss is set per second, not per trip round the loop, so a low string
                   // doesn't ring (and buzz) for ever; the noise burst is softened in proportion to pitch,
                   // so a low string starts dark instead of starting with the whole hiss.
                   let n = (SR / f) as usize; v.n = if n < 2 { 2 } else if n >= DL { DL - 1 } else { n };
                   let fc = if 4.0 * f + 800.0 < 6000.0 { 4.0 * f + 800.0 } else { 6000.0 };
                   let a = 1.0 - exp(-6.2831853 * fc / SR); let (mut l1, mut l2, mut sum) = (0.0f32, 0.0f32, 0.0f32);
                   for _ in 0..v.n { let x = rnd(); l1 += (x - l1) * a; l2 += (l1 - l2) * a; } // warm the filter: no step at the seam
                   for j in 0..v.n { let x = rnd(); l1 += (x - l1) * a; l2 += (l1 - l2) * a; LINES[slot][j] = l2; sum += l2; }
                   let mean = sum / v.n as f32; let mut pk = 0.0f32;
                   for j in 0..v.n { LINES[slot][j] -= mean; let y = LINES[slot][j]; let y = if y < 0.0 { -y } else { y }; if y > pk { pk = y; } }
                   let sc = if pk > 1e-6 { 1.0 / pk } else { 0.0 }; for j in 0..v.n { LINES[slot][j] *= sc; }
                   let per = v.n as f32 / SR; // seconds per trip
                   v.k = exp(-6.9078 * per / 1.6);  // held: -60 dB in 1.6 s at any pitch
                   v.lp = exp(-6.9078 * per / 0.12); } // released: -60 dB in 0.12 s
            2 => { v.k = decay(2.5); v.env = 1.0; }
            4 => { v.k = decay(0.35); v.env = 1.0; v.lp = 2.0; v.p2 = exp(-30.0 / SR); }
            _ => { v.k = decay(0.25); }
        }
        slot as i32
    }
}

fn tick(v: &mut Voice, line: &mut [f32; DL], sr: f32) -> f32 {
    let held = v.age < v.len; let t = v.age as f32 / sr; let dt = v.f / sr;
    let s = match v.kind {
        0 => { // organ: three partials, 5 ms attack, 0.25 s release
            let a = if held { if t < 0.005 { t / 0.005 } else { 1.0 } } else { v.env * v.k };
            if held { v.env = a; } else { v.env = a; }
            v.p1 += dt; if v.p1 >= 1.0 { v.p1 -= 1.0; }
            a * (sin_t(v.p1) + 0.5 * sin_t(2.0 * v.p1) + 0.25 * sin_t(3.0 * v.p1)) * 0.5 }
        1 => { // pluck: Karplus-Strong; release damps it faster
            let j = v.i; let nx = if j + 1 >= v.n { 0 } else { j + 1 };
            let y = line[j]; line[j] = (y + line[nx]) * 0.5 * if held { v.k } else { v.lp }; v.i = nx; y * 0.5 }
        2 => { // bell: FM, ratio 1.4, index falls with the envelope
            v.env *= v.k; v.p2 += dt * 1.4; if v.p2 >= 1.0 { v.p2 -= 1.0; }
            v.p1 += dt; if v.p1 >= 1.0 { v.p1 -= 1.0; }
            v.env * sin_t(v.p1 + 0.6 * v.env * sin_t(v.p2)) * 0.6 }
        3 => { // reed: PolyBLEP square through a one-pole low-pass
            v.p1 += dt; if v.p1 >= 1.0 { v.p1 -= 1.0; }
            let mut sq = if v.p1 < 0.5 { 1.0 } else { -1.0 };
            let blep = |p: f32| if p < dt { let x = p / dt; x + x - x * x - 1.0 } else if p > 1.0 - dt { let x = (p - 1.0) / dt; x * x + x + x + 1.0 } else { 0.0 };
            sq += blep(v.p1); let p2 = v.p1 + 0.5; sq -= blep(if p2 >= 1.0 { p2 - 1.0 } else { p2 });
            let a = if held { if t < 0.02 { t / 0.02 } else { 1.0 } } else { v.env * v.k }; v.env = a;
            v.lp += (sq - v.lp) * 0.18; a * v.lp * 0.35 }
        4 => { // kick: a sine falling from 3x to 1x its pitch, plus a click of noise
            v.env *= v.k; let fr = v.f * (1.0 + v.lp); v.lp *= v.p2; if v.lp < 1e-6 { v.lp = 0.0; }
            v.p1 += fr / sr; if v.p1 >= 1.0 { v.p1 -= 1.0; }
            v.env * (sin_t(v.p1) + if t < 0.003 { 0.4 * rnd() } else { 0.0 }) }
        _ => { // strings: two detuned saws (naive), slow attack
            v.p1 += dt * 1.003; if v.p1 >= 1.0 { v.p1 -= 1.0; }
            v.p2 += dt * 0.997; if v.p2 >= 1.0 { v.p2 -= 1.0; }
            let a = if held { if t < 0.12 { t / 0.12 } else { 1.0 } } else { v.env * v.k }; v.env = a;
            let raw = (v.p1 - 0.5) + (v.p2 - 0.5); v.lp += (raw - v.lp) * 0.08; a * v.lp * 0.7 }
    };
    v.age += 1;
    let perc = v.kind == 2 || v.kind == 4; // percussive: done when it has died away, held or not
    if perc && v.env < 0.0005 { v.on = false; }
    if !held && (v.age > v.len + (sr * 4.0) as u32 || (v.kind != 1 && v.env < 0.0005 && v.age > v.len + 16)) { v.on = false; }
    if v.kind == 1 && !held && v.age > v.len + (sr * 0.3) as u32 { v.on = false; }
    s * v.g
}

/// Mix every sounding voice into OUT[0..n] (n <= 1024). Returns the voices that sounded.
#[no_mangle] pub extern "C" fn render(n: u32) -> u32 {
    unsafe {
        let n = if n > 1024 { 1024 } else { n as usize }; let sr = SR;
        for x in OUT[..n].iter_mut() { *x = 0.0; }
        let mut c = 0;
        for k in 0..MAXV {
            if !V[k].on { continue; } c += 1;
            let v = &mut V[k]; let line = &mut LINES[k];
            let mut i = 0; if v.wait > 0 { let w = if (v.wait as usize) < n { v.wait as usize } else { n }; v.wait -= w as u32; i = w; }
            let mut lv = *v; let o = &mut OUT; while i < n && lv.on { o[i] += tick(&mut lv, line, sr); i += 1; } *v = lv;
        }
        for x in OUT[..n].iter_mut() { *x = if *x > 1.0 { 1.0 } else if *x < -1.0 { -1.0 } else { *x }; }
        c
    }
}
#[panic_handler] fn panic(_: &core::panic::PanicInfo) -> ! { loop {} }

rack.wasm: bytes, sha256 . Built with rustc 1.99.0 for wasm32-unknown-unknown and inlined as base64, because the house serves text only. Source: shelf/rack/lib.rs. Selftest: node shelf/rack/selftest.mjs. Morphyx, 2026-10-13, for modalmobius, who asked for it in Rust.