Use simulated motion as a control source for musical changes that evolve differently from a simple repeating LFO.
Seeded, deterministic physics/emergence simulations as control signals: bounceSim, bouncePat, flock, flockAgent, ca, orbit. Design spec: Motion from physical simulations. Demo song: examples/songs/emergence.volta.
Everything here replays bit-identically within a backend (the randomness is a pure function of the module seed and the sim's identity), and outputs are range-analyzed (refine.rs knows bounce/ca ⊂ [0, 1] and flock/orbit ⊂ [-1, 1]). Physical coefficients and rates accept live controls; counts, agent selection and CA rule/seed/layout remain structural. Coefficients and layout numbers are dimensionless. Rates accept Hz, periods or numeric rate lanes. Those rules also apply through aliases, imports, returned functions, records and lists; named options and trailing defaults bind the same way. orbit keeps both its bare signal and callable rate through those values. Wrong units and live layout values fail during checking, even in unused calls.
Prepared coefficients and layout values must be finite. Bounded coefficients clamp before conversion to engine precision; counts and cell widths round and clamp to their documented bounds. Fixed rates must remain positive and finite in engine precision. Native and JSON preparation apply the same rules, reject unknown models, unsupported ports and invalid output/port units, and retain the installed performance when a replacement fails.
1. bounceSim gravity elasticity [rate] → Sig Control Normalized#
A ball dropped at note-on: height in [0, 1], geometrically accelerating hops that converge to rest, looping at rate (default once per cycle; accepts Hz, cycle fractions, seconds). Higher gravity (0–1) converges sooner (rest fills the remainder — the fill-then-space gesture); elasticity (0.05–0.98) is the hop-to-hop time/energy ratio. Note-anchored — it behaves like an envelope, restarting phase where the note starts; a glide renote keeps the original anchor. Literal rates use the original closed form; live rates integrate phase sample by sample (0–100 Hz), so a rate change does not restart the bounce. Gravity and elasticity are also live, clamped to their stated bounds.
cutoff = const 400Hz + bounceSim 0.5 0.8 * 3kHz -- filter fill
pitchMod = bounceSim 0.55 0.72 * 14Semitones -- falling with the rhythm
2. bouncePat gravity elasticity [pitch] → Pattern Pitch#
The same ball's floor impacts as a one-cycle pattern — a compile-time generator like rwalk, so the accelerating fill is real events on both backends, with velocities decaying by impact energy (clamped to ≥ 0.2) and impacts below 3 % energy dropped (≤ 48 events). Default pitch ~C3. Because it shares bounceSim's closed form, using the same constants in both lines the audio's fall up with the rhythm exactly:
tom = patch { source = sine (freq pitch + pitchMod),
ampEnv = adsr 0ms 90ms 0 60ms,
pitchMod = bounceSim 0.55 0.72 * 14Semitones,
gain = vel * 0.6 + 0.2 }
bounceT = play tom (bouncePat 0.55 0.72 ~D2)
3. flock n cohesion [rate] / flockAgent n cohesion i [rate] → Sig Control Bipolar#
1-D boids: n agents (2–16) with cohesion pull toward the centroid, separation push off the nearest neighbour, a soft cubic wall, damping, and seeded per-step stirring noise. flock reads the centroid; flockAgent reads agent i (0-based, < n). rate (default 0.25 Hz) scales the motion.
Same-parameter flocks are the same flock: the seed derives from (n, cohesion, rate), so agents read anywhere in the module move together. That's the ensemble trick — voices that are individuals but travel as a group:
pad = patch {
source = saw (freq pitch + flockAgent 5 0.65 0 * 9Cents)
+ saw (freq pitch + flockAgent 5 0.65 1 * 9Cents)
+ saw (freq pitch + flockAgent 5 0.65 2 * 9Cents),
cutoff = const 700Hz + flock 5 0.65 * 500Hz,
ampEnv = adsr 300ms 500ms 0.8 800ms, gain = vel * 0.4
}
Free-running on absolute time (continuous across notes, like drift), looping every 120 s.
4. ca rule seed [cells] → Sig Control Normalized#
An elementary cellular automaton (Wolfram rules 0–255) on a wraparound row of cells (2–64, default 16). The row steps once per cycle; within the cycle it is scanned left→right as a 0/1 gate — an evolving micro-rhythm (rule 110 and rule 30 are the classics). seed = 0 starts the single centre cell; any other value seeds a ~50 % random row from that number alone (module-seed-independent, so the author's choice is pinned). Free-running — the scan is not re-locked to the bar on a transport restart (a grid-locked variant is deferred; Motion from physical simulations §6).
Seeds round and wrap modulo 2³²; integer expressions reduce exactly before any floating-point conversion. The engine retains all 32 bits. This corrects the former loss of seed bits above 2²⁴: previously rounded seeds can produce a different row; exactly representable seeds retain their trajectories.
hat = patch { source = noise |> hpf 6kHz 1, ampEnv = adsr 0ms 40ms 0 30ms,
gain = ca 110 7 * 0.55 + 0.05 }
hatT = play hat !"c6*16"
5. orbit [rate] → Sig Control Bipolar#
A softened-gravity three-body system; the output is body 0's x-coordinate through a soft limiter — bounded, chaotic, *near*-periodic. Bare orbit is the default-rate (0.08 Hz) signal, the same bare-vs-applied duality as sine; orbit 0.15Hz speeds the system up. Each occurrence is an independent system (semantic binding and occurrence identity, including @identity). Free-running, 120 s loop.
pitchMod = orbit * 7Cents -- organic vibrato
cutoff = const 900Hz + orbit 0.15Hz * 400Hz
Live flock cohesion/rate and orbit rate change coefficients on retained 60/120 Hz worlds. Flock rate clamps 0.0025–10 Hz, orbit 0.0008–3.2 Hz. Live worlds begin at voice/control onset, preserve seed, positions and step index, and do not loop-reset or replay skipped history. Use an explicit shared control for movement that persists across notes. Constants retain the legacy absolute time and 120-second loop described above. At ordinary audio rates each world advances at most one internal step per sample; storage never grows.
6. Determinism & parity (the fine print)#
- Reference DSP:
engine/src/sim.rs(SimState,FlockWorld,OrbitWorld,CaWorld, theMulberry32PRNG and the statelesshash_bistirring draw). The one Rust engine renders sims everywhere — desktop, the browser AudioWorklet and the plugin target — so a given seed traces the same trajectory sample-for-sample on every backend (One audio engine across hosts). Replay is exact. - Sims work in macro modulation and persistent effect controls. Those instances run across notes;
@shared "identity"explicitly shares one world between destinations. Independent persistent instances use independent identities. Same-parameter flock sharing inside legacy voice graphs remains unchanged. - New persistent simulation state starts at local time zero after a restart or seek. A seek resets at its destination and performs no skipped-history replay. Compatible edits retain state; a changed graph or seed resets it.
- Legacy voice flock/orbit catch-up is bounded by their 120-second loop; legacy cellular automata advance from their own generation history. The new local reset policy avoids timeline-length catch-up for persistent automata. Fixed-capacity flock and automaton storage avoids per-step/reset allocation.
Historical#
Until 2026-07-24 a second, TypeScript Web Audio engine mirrored every formula (simsource.ts, drift-style baked ConstantSourceNode curves) and only bounce/ca matched it bit-for-bit; it was deleted, see git history.
7. Files#
compiler/src/builtins.rs (SimKind) · compiler/src/builtins/controls.rs (the descriptors) · ir/src/ir.rs (NodeKind::Sim) · compiler/src/lower/expr/controls.rs::lower_sim · compiler/src/lower/pattern.rs::resolve_bounce_pat · compiler/src/refine.rs · engine/src/sim.rs · engine/src/graph.rs (SimNodeKind, CompiledNode::Sim) · engine/src/voice/eval.rs · runtime/src/types.ts (IR type mirror) · runtime/src/docs-data.ts · tests: engine/tests/sim_sources.rs, compiler/src/lower/tests.rs, golden emergence.