Examples
# Shot a preset under a loud beat, at a custom sizecargo run -p standalone --example shot -- --preset "Drift" \ --set bass=1,onset=1,beat=1 --size 960x540 --out pulse.png
# Both sides of a gate, e.g. `select(treb > 0.55, 12, 8)` on kaleido_order.# The subject is a docs/examples file rather than a shipped preset on purpose:# NO preset in the library binds `tempo`, so the tempo-gate example this# replaces named a file that had been deleted and a variable nothing uses.cargo run -p standalone --example shot -- \ --preset-file docs/examples/tuning/step-2-naive-bands.toml \ --set treb=0.2 --out below-the-gate.pngcargo run -p standalone --example shot -- \ --preset-file docs/examples/tuning/step-2-naive-bands.toml \ --set treb=0.9 --out above-the-gate.png
# Labeled contact sheet of the whole librarycargo run -p standalone --example shot -- --all --out gallery/
# Metrics report as a text table, or JSON for parsingcargo run -p standalone --example shot -- --reportcargo run -p standalone --example shot -- --report --json > report.json
# Beat filmstrip from a synthesized click track (no asset needed)cargo run -p standalone --example shot -- --preset "Drift" \ --signal click:120 --strip 8 --out click.png
# The frame a reseed actually fired on, at the tier the app starts incargo run -p standalone --example shot -- --preset-file presets/attractor_ink.toml \ --signal click:120 --at 44,46,48,54 --tier rich --out reseed.png
# ...or from the one synthesized kind with dynamicscargo run -p standalone --example shot -- --preset "Drift" \ --signal dynamic:110 --strip 8 --out groove.png
# Filmstrip from a real clip (16-bit PCM WAV)cargo run -p standalone --example shot -- --preset "Heartfall" \ --audio assets/test/clip.wav --strip 8 --out clip.pngFour committed scripts drive shot, all self-documenting in their headers — run them with
node, no arguments needed:
| Script | What it renders | Output |
|---|---|---|
scripts/tuple-sheets.mjs | one labeled contact sheet per attractor family, a cell per roster entry — the menu a tuple curation judges | target/, not committed |
scripts/tuple-paths.mjs | one filmstrip per candidate tuple_from/tuple_to pair, a cell per morph step; pairs the engine refuses a walk for are skipped rather than rendered as identical cells | target/, not committed |
scripts/docs-shots.mjs | every documentation still, from an inline manifest naming the preset file, stimulus, hop, size and tier behind each one | docs/images/, committed |
scripts/docs-clip.mjs | the two artifacts that are not stills — the demo clip and the 2:1 social preview — from a manifest of the same shape, over a stimulus WAV it synthesizes into target/ | docs/images/, committed |
The first two read the roster straight out of core/src/render/scenes/particles/family.rs, so a
roster edit needs no script edit, and their output is a scratch artifact — re-run them when a
judgement is owed.
The last two are the odd ones: their output is committed, so the manifest is the provenance record
for every picture in the documentation, and swapping which preset represents a family is one line
plus a re-run (ADR-0100). Each
writes only under docs/images/ and refuses an entry pointing anywhere else. Neither is a CI gate
and neither must become one — renders are not byte-reproducible across machines, so freshness is a
close-ceremony sweep duty rather than a check.
They are separate because docs-shots.mjs writes only PNG, spawns only cargo, and needs nothing
installed beyond the toolchain, while a clip writes a container through an external encoder:
docs-clip.mjs needs ffmpeg on PATH and stops before rendering anything without one. Its
still also carries a byte budget — GitHub refuses a social preview over 1 MB — so that entry
quantizes to a 256-colour palette and fails the run if it is still over.
--signal kinds: click:<bpm>, bass:<hz>, treble:<hz>, noise:<seed>,
chord, dynamic:<bpm>, and the three stereo ones below — pan:<p>,
wide:<seed>, split:<p>. The synth path needs no committed asset. --audio
reads uncompressed 16-bit PCM WAV only (a hand-rolled reader — no decoder
dependency); other encodings are a followup.
dynamic:<bpm> — the one kind that rises and falls
Every other kind is a steady tone or steady noise, and the band report says
so: bass:60 reads min/mean/max 0.187 / 0.187 / 0.187 — zero variance — and
chord 0.058 / 0.059 / 0.060. A filmstrip of those exercises the DSP with
material that never changes, which is not what any preset is authored against.
click:<bpm> has real transients but peaks at bass ≈ 0.011, far below anything
a shipped preset is gained for.
dynamic:<bpm> is three layers on a beat grid — a pitch-dropping kick every
beat (bass), eighth-note hats (treble), a harmonic pad that swells across each
beat (mid) — under an 8-beat phrase that builds for six beats and rests for
two. Measured at 110 BPM through the real analyzer:
| band | min | mean | max | max / mean |
|---|---|---|---|---|
| bass | 0.0035 | 0.0399 | 0.1063 | 2.67 |
| mid | 0.0005 | 0.0062 | 0.0189 | 3.07 |
| treb | 0.0000 | 0.0059 | 0.0320 | 5.45 |
against noise:<seed>’s 1.78 / 1.15 / 1.07 — and it was the liveliest kind there
was. Like every generator here it is a pure function of its arguments, so a
filmstrip of it is reproducible.
Those are raw magnitudes, so since ADR-0049 they describe bass_raw /
mid_raw / treb_raw. Through the normalizers the same clip reads:
| variable | min | mean | max |
|---|---|---|---|
bass | 0.035 | 0.661 | 1.000 |
mid | 0.031 | 0.575 | 1.000 |
treb | 0.002 | 0.281 | 1.000 |
onset | 0.001 | 0.145 | 1.000 |
The crest factors above are what normalization preserves — it divides by a
slowly-moving peak, so a clip’s dynamics survive while its absolute level does
not. Note every variable reaches 1.000: full scale is a state real material
visits, not a corner.
The waveform trace is levelled the same way
(ADR-0139)
and is absent from the table above for a different reason than the *_raw twins
are: it is not reachable from a preset expression at all, because the grammar is
scalar. What it has instead is an escape hatch the band variables lack —
waveform_gain is the divisor, so waveform[i] * waveform_gain is the amplitude
the analyzer actually read. That is what makes a capture of a wave_mode figure
independent of the fader it was taken at, and it is why the foobar component and
the standalone draw one picture from one track while tapping their streams on
opposite sides of the output volume.
It exercises dynamics. It is not evidence about real loopback levels. A preset that looks right under
dynamic:110is a preset that survives material which rises and falls — that is all this says. Nothing synthesized can tell you whether your gains match what your music actually produces; only--audioon real material does — see the reference range below. Do not read a lively filmstrip as a calibration check.
The stereo kinds: pan, wide and split
Every kind above builds one mono buffer and duplicates it into both channels, so under all of them the stereo field (ADR-0215) reads a flat zero — which is true, and is also why a working stereo implementation and a broken one were indistinguishable under the whole harness until these three existed. Each is a pure function of its argument, like every generator here.
| kind | what it is | what it reads |
|---|---|---|
pan:<p>, p in -1..1 | broadband seeded noise at per-channel gains `L = (1-p)/(1+ | p |
wide:<seed> | independent seeded noise per channel, from two streams derived from seed | spread ≈ 0.500, balance ≈ 0.000 |
split:<p> | a centred 80 Hz sine under an 8 kHz tone panned to p | bass_balance ≈ 0, treb_balance ≈ p, and a whole-mix balance strictly between them |
pan and split are the two worth understanding. pan is one waveform at two
gains, so the RMS ratio is p algebraically and the correlation is exactly
1 — the numbers it produces are properties of the construction, not fitted ones.
split is the case a whole-mix scalar cannot express: the bass is centred
while the treble is thrown to one side, which is the look per-band balance exists
for, and balance alone would report some intermediate position belonging to
neither layer.
pan’s two endpoints are the exception to its own spread row. At p = ±1
one gain is exactly 0, so one channel is exactly silent — and a correlation
against silence is undefined rather than perfect. The analyzer reports that as
spread = 0.5, the fully-decorrelated midpoint, on the reasoning that a hard
pan is a wide image and not a narrow one. So pan:1 prints balance 1.000 with
spread 0.500, while pan:0.999 prints spread 0.000 like every other
interior position: at 0.999 the quiet channel is a scaled copy of the loud one
rather than silence, and a scaled copy correlates exactly. Use an interior p
to exercise a spread-gated preset against a narrow image.
Its treble layer is deliberately the louder of the two (0.6 against 0.3), which is arithmetic rather than taste: a band’s value is a mean over its linear bins, and the treble band spans ~600 of them against the bass band’s ~10, so an equal-amplitude tone up there averages down to about the silence floor and the band reads as having no position at all.
# The stereo field, printed beside the band rowscargo run -p standalone --example shot -- --signal pan:-0.8 --out pan.pngcargo run -p standalone --example shot -- --signal split:-0.7 --out split.pngWhat those two print, measured over their 355 past-warm-up hops:
| row | pan:-0.8 min / mean / max | split:-0.7 min / mean / max |
|---|---|---|
balance | −0.800 / −0.800 / −0.800 | −0.381 / −0.357 / −0.336 |
spread | 0.000 / 0.000 / 0.000 | 0.137 / 0.140 / 0.144 |
bass_balance | −0.800 / −0.800 / −0.800 | −0.000 / −0.000 / 0.000 |
mid_balance | −0.800 / −0.800 / −0.800 | 0.000 / 0.000 / 0.000 |
treb_balance | −0.800 / −0.800 / −0.800 | −0.700 / −0.700 / −0.700 |
pan is flat to three decimals in every row because a gain ratio does not vary
over time. split’s mid_balance is 0.000 for a different reason than its
bass_balance is: no energy lands in the mid band at all, so it is under the
silence floor and reads exactly zero rather than being centred. And its spread
is 0.140 rather than 0 — the two channels carry the same two layers at
different proportions, which is a real decorrelation, not a rounding.
What real material actually produces
Measured 2026-07-27 through --audio on three local clips, none committed (see
the note above about assets/test/). All three were peak-normalized to −1 dBFS
first, because two of them arrived 20–26 dB under-levelled and every band read
zero — a level problem in the file, not a fact about the music.
These are raw magnitudes, so they now describe
bass_raw/mid_raw/treb_raw(ADR-0049). They used to be “the numbers to calibrate a gain against”, and for the normalizedbass/mid/trebthey no longer are — that is the entire point of normalizing. Calibrate those against the0–1table in Expression language and they will hold on material like this without re-tuning. This section is now the reference for the*_rawvariables, and for understanding why the normalized ones exist: look at how far apart the three rows below are.
| material | RMS | bass min / mean / max | mid | treb |
|---|---|---|---|---|
| electric-guitar loop, ~101 BPM, no drums | −17.7 dBFS | 0.000 / 0.000 / 0.004 | 0.000 / 0.002 / 0.013 | 0.000 / 0.000 / 0.000 |
| hi-hat percussion loop, ~102 BPM | −21.1 dBFS | 0.000 / 0.000 / 0.001 | 0.000 / 0.000 / 0.002 | 0.000 / 0.002 / 0.011 |
| trap with 808 sub, ~140 BPM | −19.9 dBFS | 0.000 / 0.007 / 0.190 | 0.000 / 0.001 / 0.026 | 0.000 / 0.001 / 0.006 |
The shape of it matters more than any single number. The 808’s bass peak
(0.190) sits right on a full-scale 60 Hz sine (0.187) — the analyzer is not
quietly attenuating anything. Its mean is 0.007, about 25× lower, because
real material is transient and spectrally sparse in a way no steady generator is.
Everything else here reads lower still: a guitar loop with no drums puts
essentially nothing in bass or treble, which is correct and is what most material
does in most bands most of the time.
So, in descending order of how far a stimulus is from real music — on the raw
scale, i.e. what bass_raw sees:
| stimulus | bass_raw it produces | vs. a real mean |
|---|---|---|
--set bass_raw=0.8 | 0.800 | ~100× too hot |
--signal bass:60 (full-scale sine) | 0.187 | ~25× too hot |
--signal dynamic:110 | mean 0.040, max 0.106 | ~6× too hot, right order for peaks |
| real music (above) | mean 0.000–0.007, max up to 0.190 | — |
This ladder is exactly what ADR-0049 abolished for the normalized variables.
Its four rows span three orders of magnitude, and picking a threshold meant
knowing which rung you were standing on — which is why nine shipped mechanisms sat
dead for months. On the normalized scale every rung that carries real dynamics
lands in the same 0–1 range, so bass > 0.8 means “near this material’s own
peak” whether the material is an 808 at −1 dBFS or a quiet guitar loop. The ladder
survives here because *_raw still climbs it.
Two practical consequences, both of which still apply to *_raw and to bin():
- Calibrate against a mean, not a peak, for anything continuous — a size, a
zoom, a hue drift. Those spend their life near the mean, so a gain tuned to look
right at
0.19barely moves. - Calibrate against a peak for anything percussive — a flash, a burst, a beat-latched accent. Those exist to fire on the hit, and the hit really does reach a full-scale tone’s level.
The shipped library predates this measurement and is gained against the older figures; whether it needs a re-gain pass is design-backlog 0020, not something to fix preset-by-preset.
Test audio is added manually and never committed. Drop a 16-bit PCM WAV into
assets/test/— that folder is gitignored (only its README is tracked), so no licensed audio lands in the repo. Use your own or a royalty-free / CC0 clip; factory-library samples are fine to point at on disk but must not be committed. The--signalpath needs no file, so the whole audio pipeline can be validated without adding anything.
The --report --json schema is a nested object of numbers keyed by
family/preset: per-band reactivity, reactivity_low and reactivity_footprint,
animation, drive, count, rate, coverage, level, frame_cost, transient (rise_frames /
fall_frames as integers plus their ratio), reachability, the pairwise
pixel/shape distinctness matrices, and near_duplicates. Beside source
and tier at the top level sits machine — adapter, profile and
software — which every frame_cost below it was taken on.
frame_cost is an object: ms_per_frame, the width and height it was
taken at, the budget_ms it is compared against and over_budget, the text
block’s ! as a boolean (the frame-cost block). It is
omitted on a software adapter rather than written as a fake number, the
way in_frame_geometry is omitted where no line seam measured.
rate is an object, not a bare number: mean, settled, and
measured_at_px — the size it was captured at, which is not the one the columns
beside it use (the two motion readings). A consumer
that drops measured_at_px and compares a rate against a differently-sized run
is reading a different statistic.
count is an object for the same reason: mean, and the schedule it was read
under — frames (48) and frames_per_beat (5). mean is written at full
precision rather than to four places, so a preset that reads no clock shows an
exact 0.
reachability carries dead_branches, unapproached_ceilings and
saturated_clamps counts, the full gates list (each with param, source,
kind, and one of always / peak_fraction_of_bound / occupancy), a holds
list (each with param and edge — see held bindings),
and a probe object naming the signal, BPM and duration they were observed
under. holds is the one member read off the compiled preset rather than
observed, so the probe provenance does not apply to it.
kind is "select", "compare", "clamp" or "saturated" — matching the
GATE / COMP / CEIL / SAT lines above — and dead_branches counts the
first two together. Keep the provenance when you consume it: a flag only ever
means not observed under this stimulus.
Built from 13c7582 at version 0.158.0. This site tracks main and is not versioned per release.