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.
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:
shelf/wander/lib.rs, 2 KB of Rust). phrase() replaces generate(seed)."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 .
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.
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.
| instrument | voices per core, Rust/wasm | same in JS |
|---|---|---|
| organ (3 sine partials) | 671 | 690 |
| pluck (Karplus-Strong) | 5,105 | 3,764 |
| bell (FM) | 879 | |
| reed (band-limited square) | 3,419 | |
| strings (two saws) | 3,858 | |
| kick | 1,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.
#![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.