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).tempoMapbreakpoints are beats, unchanged. - Any binding name will do. The marker list is recognised by its value, not by its name:
landmarks = form.markers […]andcues = form.markers […]are the song's timeline exactly asmarkers = …is (the naming law — Language Foundations). A binding calledmarkersshadows the stdlib name, which is why the examples qualify the call asform.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 importedsong { … }; onlyimport "kit" (markers)adopts one. meter = p/qis a literal time signature (numerator 1–64, denominator a power of two ≤ 32) — a reserved declaration liketempo/key, not a builtin. Bar length isp·4/qquarter-note beats; bpm keeps meaning quarter notes, sotempo/tempoMap/ms/Hzare unaffected. One cycle = one bar: pattern cycle length,Nc/barsliterals,arrangeweights, scene-launch quantization, and the rulers all follow.- With the default transport origin,
|> onceplays the first complete pattern period; seeking or joining past it plays nothing. AddphraseOrigin "entry"to play it once per activation instead. Tracks kept across scenes do not reactivate; see Arrange a song §Phrase origins. - Nonzero
atplacement and entry origin are incompatible.at 0barsremains 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 theattach = "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 drawstempoMapbreakpoints as handles andmeterMapchanges as labelled ticks — Write a song as a timeline § In Studio. - Export — standard WAV
cue+LIST-adtl/lablchunks in every offline render (nativerender_export/render_export_arrangementviaoffline::append_marker_cues, browserruntime/src/export.ts'sappendMarkerCuestwin), 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_markersinlower.rs(theextract_tempopattern — except thatextract_markersmatches themarkershead on any binding, origins-gated like theautomatelane walk);at/oncelower inlower/track.rs(BuiltinKind::AtBar/OnceTrack); the ambient bar length is a thread-local (lower::bar_beats(), installed per compile like key/tuning) feedingcycle_beats_for,cycle_seconds_at,Nc/barsconversions. - Clocks:
engine/src/transport.rs::TempoClock(set_bar_beats,MeterSegment) is the audio clock everywhere; the page-sideruntime/src/tempo.ts::TempoClock(beatAtBar/barAtBeat, thebarBeatsfield,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 aplay_oncecursor 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#
meterMapchanges 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 spanscycle_beats / bar0bars, stretched per segment by the clock'sMeterSegmentintegrators —track_beat_to_globalinengine/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_boundarymap branches).- The engine viz frame's
songClock.barSecondsis 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'ssecondsPerBeat/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
meterMapgoes through the bar axis, neverbar · bar_beats: exact-seek checkpoints, a written song'ssectionPosend, 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
|> atanchor inside a loopingarrangeschedule 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/4to7/8at 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).