VOLTAreference

Reference / Compose and perform

Schedule events on a timeline

Compose and perform

Open in the workbench →
On this page · 6 sections

Place an event at a particular point or play it once when a repeating pattern is not the right description.

What it is#

The absolute-time axes on top of Volta's cycle world: named markers, absolute track placement (|> at), one-shot tracks (|> once), and a module meter (meter = 7/8) that sets the bar/cycle length. All of it rides one seam — beatAtBar/barAtBeat on the tempo clock — so every conversion holds under a tempo map.

Surface#

meter   = 7/8                                   -- a bar = 7 eighths = 3.5 beats (default 4/4)
markers = form.markers [[0, "intro"], [8, "verse"], [37, "fix the snare"]]

crash = play cym !"c5" |> at 37bars |> once     -- one hit, at bar 37
enter = play kit beat  |> at 8bars              -- silent until bar 8, loops after
meter = meterMap [[0, 4/4], [32, 7/8], [40, 4/4]]   -- meter changes on whole bars
  • Bars are 0-based notational bars (fractional allowed in markers/at). tempoMap breakpoints are beats, unchanged.
  • Any binding name will do. The marker list is recognised by its value, not by its name: landmarks = form.markers […] and cues = form.markers […] are the song's timeline exactly as markers = … is (the naming law — Language Foundations). A binding called markers shadows the stdlib name, which is why the examples qualify the call as form.markers; that is the only place the name still matters. A module has ONE timeline, so a second marker list in the same module is an error naming both bindings — merge them. An imported module's marker list stays a value (Names and library imports §6), like an imported song { … }; only import "kit" (markers) adopts one.
  • meter = p/q is a literal time signature (numerator 1–64, denominator a power of two ≤ 32) — a reserved declaration like tempo/key, not a builtin. Bar length is p·4/q quarter-note beats; bpm keeps meaning quarter notes, so tempo/tempoMap/ms/Hz are unaffected. One cycle = one bar: pattern cycle length, Nc/bars literals, arrange weights, scene-launch quantization, and the rulers all follow.
  • With the default transport origin, |> once plays the first complete pattern period; seeking or joining past it plays nothing. Add phraseOrigin "entry" to play it once per activation instead. Tracks kept across scenes do not reactivate; see Arrange a song §Phrase origins.
  • Nonzero at placement and entry origin are incompatible. at 0bars remains an identity in either modifier order.

What markers feed#

  • Song map — a flag lane on the Timeline ruler, Map and Arrange alike (ui/src/conductor.rs, the same Rust UI desktop and browser); click = seek to the marker's bar and select it. A marker is a handle: dragging it (whole bars, Shift for quarter bars), ←/→ on a selected one, or its inspector field rewrites that row's bar literal as one undo step, and the notice counts the attach = "marker" placements the recompile re-snapped. A marker list not written in the song's own document draws as a non-handle that says so. The tempo and meter lane under the ruler draws tempoMap breakpoints as handles and meterMap changes as labelled ticks — Write a song as a timeline § In Studio.
  • Export — standard WAV cue + LIST-adtl/labl chunks in every offline render (native render_export/render_export_arrangement via offline::append_marker_cues, browser runtime/src/export.ts's appendMarkerCues twin), so DAWs and wave editors show the song's landmarks.

How it's implemented#

  • Compiler: Module.{meter, markers} + Track.{anchor_bars, play_once, phrase_origin} (ir.rs; serde defaults keep 4/4 modules byte-identical). extract_meter/extract_markers in lower.rs (the extract_tempo pattern — except that extract_markers matches the markers head on any binding, origins-gated like the automate lane walk); at/once lower in lower/track.rs (BuiltinKind::AtBar/ OnceTrack); the ambient bar length is a thread-local (lower::bar_beats(), installed per compile like key/tuning) feeding cycle_beats_for, cycle_seconds_at, Nc/bars conversions.
  • Clocks: engine/src/transport.rs::TempoClock (set_bar_beats, MeterSegment) is the audio clock everywhere; the page-side runtime/src/tempo.ts::TempoClock (beatAtBar/barAtBeat, the barBeats field, meterBarBeats([p,q]), installMeter) is its pacing twin for the browser host. Cross-runtime pin-tested; all bars↔beats conversion routes through them.
  • Scheduling: the engine offsets a track's global origin by anchor_beat_of(anchor_bars) and stops a play_once cursor after cycle 0 (engine/src/lib.rs) — composing with polytempo (clock (p/q) warps inside the anchor).
  • UI: ModuleInfo::{bar_len(), meter_label(), markers} (ui/src/moduleinfo.rs) drive the song map (ui/src/songmap.rs), timeline, bench strip, and arrangement walk — the one Rust workbench, desktop and browser alike.

Notes & limits#

  • meterMap changes land on whole bars (strictly increasing; bar 0 implied 4/4 unless the map opens there). Under a map, unwarped tracks' cycles follow the bar axis (a cycle spans cycle_beats / bar0 bars, stretched per segment by the clock's MeterSegment integrators — track_beat_to_global in engine/src/lib.rs); clock (p/q) tracks keep uniform beat-axis cycles and drift against the bar grid, exactly as they do against 4/4. Scene quantization moves to the bar axis (nextBarTime / next_bar_boundary map branches).
  • The engine viz frame's songClock.barSeconds is one bar of the bar-0 meter (3.5 beats in 7/8); a consumer divides it by that meter's beats, never by 4. The page host's secondsPerBeat/djTempoClock (runtime/src/native-runtime.ts, which also installs the module's bar axis on the DJ clock), the desktop Link/OSC tempo and the workbench backdrop's beat/downbeat lanes (ui/src/signal.rs, downbeats on the module's own bar axis) all do.
  • Everything that turns a bar into a beat under a meterMap goes through the bar axis, never bar · bar_beats: exact-seek checkpoints, a written song's sectionPos end, the arrangement walk's ramp lengths, capture → regions, attach = "marker" snapping, and the DAWproject/SMF interchange.
  • Swing is defined on the pattern's own subdivisions (phase-domain), so it is meter-blind by construction.
  • An |> at anchor inside a looping arrange schedule stays on the absolute axis — it does not re-fire per loop pass.

A meter edit on a live transport#

A hot swap (a live recompile) that changes meter or meterMap moves the running transport onto the new bar axis at once, without restarting it:

  • The beat axis is kept; bars re-derive. The playhead's beat and sample do not move, and the bar at every beat is what a fresh load of the new module would read at that beat, counted from beat 0. Editing 4/4 to 7/8 at beat 9 puts the playhead in bar 9/3.5 ≈ 2.57, and the next downbeat is beat 10.5. The display bar number can jump; the sound does not. This is the tempo map's recompile rule (set_map: the new authored map reads on the absolute axis from here on) applied to the other axis.
  • A phrase origin is a bar, so it re-derives too. An entry-origin track that entered on bar 1 keeps bar 1 as its origin, which under the new meter is a different beat. Its cycles therefore stay on the new downbeats, in step with every transport-origin track and the click, instead of drifting half a beat off them. Every running track's cursor is re-seated on the cycle that holds the playhead (past onsets are skipped, as on any swap).
  • Everything that reads bars follows: event scheduling (under a map, cycles follow the new map's bars), scene quantization and the queued stop-all, the arrangement walk (it re-plans from the bar the playhead now reads), songPos/sectionPos, marker waits and the click accent.
  • A queued boundary re-quantizes. A scene change or stop-all stamped before the swap was waiting for "the next downbeat"; after a meter edit it lands on the next downbeat of the new meter.
  • A swap that keeps the meter changes nothing on the bar axis (sample-identical onsets). The transport loop is stored in beats and stays put.

Mechanism: PreparedScheduling carries the new module's bar axis (transport::MeterAxis), built on the control thread; the audio thread exchanges it into the live TempoClock and retires the old segments to the graveyard, so the swap allocates nothing (rt_safety::a_meter_changing_swap_allocates_nothing_on_the_callback). Pinned by engine/src/meter_swap_tests.rs.

The transport's own start does not build its clock on the callback either: PreparedScheduling also carries the module's clock at origin 0 (tempo map, meter, meter map — TempoClock::for_module) and a spare copy. A start re-seats the spare in place at its origin (TempoClock::restart_from, the same bits a fresh clock builds there); a stop-all parks the running clock as the next start's spare, or retires it when a spare is already waiting (rt_safety::starting_and_stopping_the_transport_allocates_and_frees_nothing).