Skip to content

Examples

Terminal window
# Shot a preset under a loud beat, at a custom size
cargo 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.png
cargo 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 library
cargo run -p standalone --example shot -- --all --out gallery/
# Metrics report as a text table, or JSON for parsing
cargo run -p standalone --example shot -- --report
cargo 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 in
cargo 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 dynamics
cargo 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 "Perseids" \
--audio assets/test/clip.wav --strip 8 --out clip.png

Three committed scripts drive shot, all self-documenting in their headers — run them with node, no arguments needed:

ScriptWhat it rendersOutput
scripts/tuple-sheets.mjsone labeled contact sheet per attractor family, a cell per roster entry — the menu a tuple curation judgestarget/, not committed
scripts/tuple-paths.mjsone 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 cellstarget/, not committed
scripts/docs-shots.mjsevery documentation image, from an inline manifest naming the preset file, stimulus, hop, size and tier behind each onedocs/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.

docs-shots.mjs is the odd one: its 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). It writes only under docs/images/ and refuses an entry pointing anywhere else. It is not a CI gate and must not become one — renders are not byte-reproducible across machines, so freshness is a close-ceremony sweep duty rather than a check.

--signal kinds: click:<bpm>, bass:<hz>, treble:<hz>, noise:<seed>, chord, dynamic:<bpm>. 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:

bandminmeanmaxmax / mean
bass0.00350.03990.10632.67
mid0.00050.00620.01893.07
treb0.00000.00590.03205.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:

variableminmeanmax
bass0.0350.6611.000
mid0.0310.5751.000
treb0.0020.2811.000
onset0.0010.1451.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:110 is 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 --audio on real material does — see the reference range below. Do not read a lively filmstrip as a calibration check.

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 normalized bass / mid / treb they no longer are — that is the entire point of normalizing. Calibrate those against the 0–1 table in Expression language and they will hold on material like this without re-tuning. This section is now the reference for the *_raw variables, and for understanding why the normalized ones exist: look at how far apart the three rows below are.

materialRMSbass min / mean / maxmidtreb
electric-guitar loop, ~101 BPM, no drums−17.7 dBFS0.000 / 0.000 / 0.0040.000 / 0.002 / 0.0130.000 / 0.000 / 0.000
hi-hat percussion loop, ~102 BPM−21.1 dBFS0.000 / 0.000 / 0.0010.000 / 0.000 / 0.0020.000 / 0.002 / 0.011
trap with 808 sub, ~140 BPM−19.9 dBFS0.000 / 0.007 / 0.1900.000 / 0.001 / 0.0260.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:

stimulusbass_raw it producesvs. a real mean
--set bass_raw=0.80.800~100× too hot
--signal bass:60 (full-scale sine)0.187~25× too hot
--signal dynamic:110mean 0.040, max 0.106~6× too hot, right order for peaks
real music (above)mean 0.0000.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.19 barely 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 --signal path 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, rate, coverage, level, transient (rise_frames / fall_frames as integers plus their ratio), reachability, the pairwise pixel/shape distinctness matrices, and near_duplicates.

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.

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 a8ce055 at version 0.115.0. This site tracks main and is not versioned per release.