See a repeating pattern around a wheel to compare event positions, accents and relationships between cycles.
What it is#
The module's rhythm drawn as a circle: one revolution per bar, a beat ring inside, one ring of attack-to-attack arcs per audible track (a pattern longer than the bar laps onto concentric sub-rings, so a group that crosses the barline visibly continues), the attacks joined into a polygon, a hand per ring, and an n / m count of the pulse inside the sounding group. It lives as a compact wheel ▸ dock on any bench track cell. The Timeline panel's wheel mode is off the switch for now (the panel offers cycles | map; a session saved with tlmode=wheel reopens on the cycle lanes), so the Timeline surface below describes the mode as built, not a control you can currently reach. Renderer only — no engine or compiler seam.
Surface#
Timeline ▸ wheel (hidden for now — see above). The chip beside cycles/map (persisted as tlmode=wheel; the older tlmap=1 still reads as the map). The header:
- unit chips
bar · cycle · 2 bars · 4 bars(persisted astlunit=):barfolds everything to one bar (longer patterns lap, shorter repeat);cyclelets each ring revolve at its own period — the polyrhythm view — and a cross-rhythm (3 : 4) addsrealign every 12 beats · next in 5.2; the phrase units put two or four bars on one turn; - the meter (
4/4,7/8; under ameterMap,meter map · N beats here, folded at the bar the transport is in); - one chip per ring in its Timeline lane colour — click to focus a ring (the count follows it, the others fade); the default focus is the busiest ring;
- the count
n / mwith the ring's grid in words (8ths,16ths,8th triplets,5:4, orfreewhen the attacks share no grid up to 96/beat).
On the wheel: hover a group for name · 3 8ths · beat 4 → 6 · lap 1/2 · pitch · vel · chord of N; click a group to move the caret to its mini-notation atom (honoured only inside a !"…" literal of the live buffer); click a beat-ring pulse to seek there. Stopped, the static picture stays and the live count is parked unless a cycle is being inspected.
Bench ▸ track cell ▸ wheel ▸. One ring, its polygon, the count; while the transport is stopped, ←/→ step the hand a grid unit at a time and atom ↗ jumps to the atom under the hand.
Both wheels share a cycle picker with the pattern roll and Bay explorer. Previous/next or selecting a pattern cycle parks the hand for inspection and emits no transport command. Follow resumes the track's engine clock. Ring focus follows the selected consuming track; selecting a wheel group shares its source atom with the written/result views. State belongs to the live document, and source anchors expire on edits. See Pattern rolls and phrase editing.
-- examples/songs/pyramid.volta — the study the wheel was built on:
-- 3-3-4-3-3 on eighths over two bars of 4/4, so the chord ring laps twice
-- and the four carries across the barline.
piano = patch {
source = saw (freq pitch) + (triangle (freq (pitch + 12Semitones)) |> mods.gain 0.25),
ampEnv = adsr 4ms 900ms 0.15 500ms,
cutoff = 1400Hz,
gain = vel * 0.45
}
pyramidChords = !"<[[f#3,a#3,c#4] ~ ~ [e3,g#3,b3] ~ ~ [e3,g#3,c#4] ~] [~ ~ [f#3,a#3,c#4] ~ ~ [g#3,c#4,e4] ~ ~]>"
pulse = sound "bd bd bd bd" |> gain 0.6
chords = play piano pyramidChords
How it's implemented#
- Model —
ui/src/cyclewheel/model.rs:WheelModel::build(info, unit, bar_beats)overModuleInfo. Attacks are sounding onsets (a chord's simultaneous events are one attack); a group runs attack → next attack, wrapping the last to the first; the held duration is a lighter inner band. A ring folds at the smallest whole number of both its period and its revolution (fold_beats, LCM search; pastMAX_LAPS = 8laps the ring draws one revolution, cut, and says so); the grid is the coarsest denominator (1…96 per beat) every attack, the period and the revolution sit on — exact, because IR onsets are grid-exact and swing/nudge/humanize are scheduler-side track fields. Chooser tracks fold every deck and the renderer shows the engine-latched one (TrackFrame::active_alternative, the Timeline's rule).count_at/hand_atgive the readout and hand for an absolute beat. - Renderer —
ui/src/cyclewheel/mod.rs:WheelUi::ui(Timeline mode) andWheelUi::compact_ui(bench dock). Arcs are quad meshes (an annular sector is concave; epaint's filled paths are convex-only). Each ring's hand and count run on the TRACK's own beat axis — the frame's per-trackcycle+phase× period, which already carryclock (p/q)ratios and|> atanchors — so a track that is not running has no hand; the beat ring runs onsong_clock.beat. Both are extrapolated ≤ 120 ms between viz frames atbar_len / bar_secondsand frozen when the frame time stops advancing (a held transport must not drift). The attack flash ridesactive_eventson the group the hand is in (plain tracks only — chooser decks index their own event lists); the audibility gate is the playing- step highlight's (stripsmute/solo). Under phrase units the beat ring carriesp × npulses; a pulse click seeks through the meter map. Colours:timeline:: lane_colorper ring, theme tokens elsewhere; low-power drops glow, trail and the between-frame repaint. Every readout carries awidget_infoname (cycle wheel · N rings · bar · 4/4,cycle count n / m,focus ring <track>,wheel step forward/back). - Mounts —
ui/src/timeline.rs(TimelineMode, the chips, the mount),ui/src/app/prefs.rs(tlmode/tlunitshell state),ui/src/app/ rightcol.rs→navigate::jump_to_live_atom(the click-to-atom gate viaplayhead::in_pattern_literal),ui/src/bench.rs(open_wheel_dock/wheel_states, the chip besidedyn ▸,pending_mod_jumpfor the atom). - Harness —
VOLTA_TIMELINE_MODE=wheel(+VOLTA_WHEEL_UNIT=) parks the Timeline on the wheel for volta-shot;VOLTA_BENCH_WHEEL=<track>captures the compact dock aswheel.png. Tests:ui/tests/cycle_wheel.rs(mode switch + persistence, the count under a driven clock across the barline-crossing four, the atom-jump gate, the bench dock + stepper) and the model's unit tests (Pyramid Song, euclid, chord, alternation, triplets, free onsets, 3:4 fold, lap cap, choosers, phrase units). - Song —
examples/songs/pyramid.volta(goldenpyramid.json; picker tagstudy).
Static export (a shareable frame set)#
volta-shot is deterministic, so a frame sequence is a build artefact:
for i in $(seq 0 59); do
VOLTA_TIMELINE_MODE=wheel cargo run --profile fast -p volta-studio --bin volta-shot -- \
examples/songs/pyramid.volta --views source --frames $((120 + i * 3)) --settle 0 \
--size 1000x1000 --out-dir /tmp/wheel/$i
done
ffmpeg -framerate 20 -i /tmp/wheel/%d/source.png -vf "crop=560:560:960:260" wheel.gif
(--frames is the transport warm-up in 60 Hz frames, so consecutive shots are 50 ms of song apart; adjust the crop to the Timeline pane.)
Notes & limits#
- Groups are attack-to-attack (what one counts), not event durations; a held note shows as the inner band. Overlapping notes of different lengths keep only the longest as the band.
- Past eight laps in
barmode the ring is cut at one revolution and its chip says so; switch to2 bars/4 barsorcycle. - A
cycle_beats = 3track in 4/4 folds at 12 beats (three laps) and the header reports the realignment; periods that are plain multiples of each other (4 : 8) say nothing. - Under a meter map the unit is the bar at the transport's position, so the fold can change mid-song; the header names the bar length.
- Arrangement-walk tracks (
arrange/at/once) are drawn as loops of their pattern — the wheel does not know the schedule. - Swing/nudge/humanize move the audible attack after the drawn vertex; no ghost vertex yet (Read a rhythm as a circle §5).
- The browser build has the wheel (the viz frame reaches the wasm UI); the exact trigger-tick ring does not, and is not needed.