VOLTAreference

Reference / For contributors

Temporal core

For contributors

Open in the workbench →
On this page · 5 sections

The volta-time Rust crate implements Stage 0 of Unified Timeline: reusable content, independent regions of every anchor domain, bounded queries, per-receiver indexes with prepared simultaneity budgets, and a checked bridge to the engine's tempo clock.

The language surface it was built for shipped in Stages 2 and 3 — Write a song as a timeline is the canonical doc for region, automate and song { timeline }. The crate resolves and validates; it does not deliver. The engine prepares each region into an ordinary track plan and schedules it through the one note path it already had, and builds the plan here in parallel to check that every placement is representable on the checked rational axis and that the whole plan fits its budget. That divergence from the design's §8 sketch is deliberate: this core carries gates, not the per-event musical data (velocity lanes, swing, glide, the sampler, MIDI out) the shipped note path already delivers correctly. Queries, lazy repeats and the projection API below are therefore a contributor-facing foundation, not the live scheduler.

Exact coordinates#

Ratio is a reduced signed i64 numerator over a positive u32 denominator. Arithmetic uses i128 intermediates, reduces before checking storage bounds, and returns errors for overflow or zero division. It never rounds to a PPQ grid or silently falls back to a float. Decimal parsing accepts a sign, decimal point and exponent directly from token text. Tokens are limited to 64 bytes; intermediates that cannot fit i128 are rejected. JSON uses a string such as "9007199254740993/1", preserving values through JavaScript. This is the rational wire format; the rest of the spike is a Rust API, not a versioned IR.

Position<D>, nonnegative Duration<D> and nonempty half-open Span<D> keep Beats, Bars and Cycles separate. Signed positions support pickup material and negative source offsets. Spans validate both endpoints and their duration; even an intersection can return overflow when its new rational denominator is unrepresentable. Ratio::from_f64_exact accepts exactly representable existing binary metadata, such as legacy meter values; it is not a decimal token parser.

Seconds holds finite signed elapsed time from song zero. SourceSamples retains signed integer frames and their positive asset sample rate; asset identity remains with the caller. Frame advances use checked integer arithmetic. Conversion to seconds rejects frame magnitudes above 2^48 before casting to a float. AuthoredPosition distinguishes beats, bars, seconds and source samples, and a resolved position retains that original value and domain.

AuthoredSpan is the same distinction for a whole placement, stored on the region beside the beat span a conductor resolved it to. PlanAxis means the placement was written directly on the plan's own axis, so re-resolving it is a no-op; re_resolves() says whether a tempo or meter edit moves a placement. Conductor::resolve_span maps any variant onto beats and Conductor::place re-places one region, keeping its identity, lineage, seed and policies. Beats and bars stay exact through the meter map. Seconds and source frames have no exact rational beat — the inverse of a tempo integral is not rational — so their beat span is rounded onto ELAPSED_ANCHOR_TICKS_PER_BEAT.

That grid is an error budget, not a display detail, because the scheduler reads the resolved beat: a gate inside an elapsed-anchored region reaches sample_boundary through sample_at_beat, never through the authored seconds. One tick is samples_per_beat / ticks_per_beat samples, worst at the slowest tempo and highest rate this conductor accepts (20 BPM, 384 kHz → 1,152,000 samples per beat). At 2^23 ticks that is 0.137 samples per tick, and resolve_beat picks whichever neighbouring tick lands nearest the authored sample, so the measured worst case over the whole 20–1000 BPM × 8–384 kHz envelope is 0.064 samples — an order below sample_boundary's own half-sample rounding, which every domain pays. conductor:: elapsed_anchor_error_stays_under_a_tenth_of_a_sample walks that envelope.

A consumer that needs a region's own boundaries exactly does not go through the beat at all: Conductor::authored_sample_span resolves the authored elapsed time straight to samples. The authored value remains the coordinate a tempo edit re-resolves from.

MeterMap prepares ordered whole-bar changes beginning at zero. Bar positions and anchored durations convert exactly through its piecewise map. Two 4/4 bars followed by 7/8 place bar 2 at beat 8 and bar 3 at beat 23/2. Beat-anchored regions retain their beat axis. The first/last meter extends before/after the listed changes. The engine's conductor snapshot prepares this exact meter map from its existing meter segments, rejecting values that cannot be preserved.

sample_boundary rounds an absolute finite sample coordinate to nearest, with exact halves away from zero and no epsilon snapping. Inputs must lie within ±2^48 frames; float spacing is at most 1/16 sample there. Rounding error against that supplied float is at most half a sample. Adjacent boundaries must use the same absolute input, never accumulated rounded deltas. Conductor accuracy is tested separately against the high-precision vectors described below.

Engine conductor#

TempoClock::temporal_conductor() prepares an immutable Conductor<TemporalClock> snapshot off the callback. It reuses the engine's existing stable closed-form tempo integral and inverse. Segment lookup in the snapshot is binary. Rebuild the snapshot after tempo, meter or seek changes; old snapshots stay unchanged.

The prepared range includes both endpoints of ±1,000,000 beats. It accepts 8–384kHz output sample rates, 20–1000 BPM, up to 1,024 tempo/meter segments, and a beat-zero stream origin within ±2^40 frames. Tempo segments begin at beat zero and must be finite, ordered and continuous in sample time. Meter changes remain whole bars with 1/8–256 quarter-note beats per bar. Unsupported legacy values produce an error; the adapter never silently clamps them.

Beat and bar anchors resolve through the tempo map while retaining an exact beat position. Seconds and source-frame anchors resolve at elapsed time from song zero, independent of tempo edits, and carry only an approximate inverse beat for display. That estimate never replaces the authored coordinate. Source-frame anchors interpreted by this conductor are elapsed frame time from song zero; audio-region placement and asset lookup remain separate future work.

Before the first tempo anchor, the engine and snapshot hold its initial BPM. This also applies to inverse lookup, negative seeks and short musical advances crossing song zero. Previously the engine extrapolated the first ramp backwards, which could cross zero BPM and disagree with pickup placement. Forward tempo ramps retain their existing integral and live-update behavior.

map_events projects a query's whole, boundary-limited and visible gates into absolute sample spans in a preallocated SampleEventBuffer. It preserves input order and musical/delivery identities, clears results on any error and neither allocates nor frees memory. Distinct exact instants can share a rounded sample; a short gate can become an empty sample span without an implicit stretch. This API does not schedule voices: the shipped engine admits onsets positionally on the sample axis instead, which is why it needs no delivery ledger — a seek, a transport loop and a hot swap land the same set of onsets by construction. A policy for gates that collapse to an empty sample span is still open.

Content, placement and identity#

Content stores one positive source period and uniquely identified event gates in local cycles. Onsets lie within that period; held gates may cross it. One cycle is one local unit, independent of the full source period. Event values remain with the consumer, addressed by ContentId and SourceEventId.

Region binds content to a stable track, finite parent span, positive affine TimeMap, repeat policy and explicit note boundary. Track is the receiver a play binding already is and carries an OverlapPolicy; Mix is the shipped behavior and the only policy the query honors, and Replace stays a recorded placeholder — no language surface selects it, and the engine prepares every track as Mix. (Lane names the automation row, which shipped in Stage 2 for macro destinations and is not modelled here: a prepared lane is the engine's own PreparedLane, evaluated analytically, and the dense fixture below still has no curve readers.) Entry phase uses the placement as its clock origin; transport phase keeps the parent's origin. Offsets and rates compose exactly. Repeat::Once contributes its one stored period; Repeat::Loop queries signed iterations arithmetically. Negative periodic offsets use mathematical modulo. Neither policy materializes repeats.

Onsets are admitted in the region's half-open interval. Clip releases gates at its final edge; LetRing retains admitted gate lengths and indexes their possible tails. A seek into a held gate returns its full mapped whole, its boundary-limited gate, and the query's visible intersection. has_onset() is false when the query starts inside that gate. The shipped playback path does not consume these queries; it reaches the same contract positionally (Write a song as a timeline § What the engine does).

split keeps the left editor ID, allocates the supplied right ID, and preserves the original admission span, source mapping, seed and playback lineage. The prepared plan requires the entire continuity family with no gaps or overlaps. Queries coalesce a held event across the seam. duplicate creates a separate occurrence with ordinary boundaries and the same musical seed.

An EventKey includes track, occurrence path, content, source event, signed iteration and delivery epoch. Its separate MusicalKey excludes placement identity and delivery epoch. Duplicate notes therefore both sound, while a seek does not imply rerolling pure choices.

An OccurrencePath is outermost placement first, with the continuity lineage as its leaf; it is a fixed MAX_OCCURRENCE_DEPTH array, so an event key stays Copy and a query never allocates. Region::nested_in prefixes a placement onto a path while the caller composes the matching time maps with TimeMap::then, so two placements of one group deliver the same musical choices at composed times as distinct occurrences. Exceeding the depth is an error, never a silently truncated identity. There is no group syntax — that is Stage 3 — only the identity and composition law a group needs. IDs are flat numbers supplied by the caller; source serialization and a random evaluator are not implemented here. Source-ID persistence through formatter edits shipped in Stage 2, in the compiler rather than in this crate: a region with no explicit id is given a document-local one derived from the owning binding and its position in the region list, so whitespace, comments and a formatter round trip cannot change musical identity (Format and modernize source).

RegionProjection exposes the same region/content/track IDs with an arrangement span and unwrapped cycle cursor. Tests use one selected region in both views. This is a shared read model; it does not add a new control to Studio.

Preparation and query limits#

PreparedTimeline owns immutable content, a balanced interval index over every explicit region, and one more such index per track. It allocates, sorts and validates during preparation. Its storage is independent of virtual repeat count and independent track periods. query_track answers one receiver without walking the whole song and costs measurably less work; an unknown track is empty, not an error. QueryBuffer allocates once and is reused; query paths neither allocate nor deallocate. Their result order uses exact onset time, then stable event identity. Distinct instants remain ordered when they round to the same sample.

Preparation also sweeps the prepared extents for maximum simultaneity: regions and gates, per track and in total, with the placement that reached each peak. Region counts are exact. A gate count is a reservation bound, not the exact steady state — one looping region is charged sum over its events of ceil(gate / period), which is the most repetitions of its own gates that can be alive at once and is computed in O(events) with no unrolling. It is deliberately loose where a pattern's notes never overlap. prepare_within rejects a plan over a declared PlanBudget, returning a BudgetViolation naming the kind, track, responsible region, required and allowed counts; PrepareError keeps a numeric failure distinguishable from a budget one.

BoundStage 0 ceiling
Explicit regions16,384
Unique contents / meter changes (MAX_METER_CHANGES)4,096 each
Events per content4,096
Total stored events262,144
Occurrence path depth4
Query hit slotsCaller-selected, at most 65,536
Query work unitsCaller-selected, at most 1,048,576

Inspection is bounded per content. A content's events are sorted by onset and indexed by its longest gate, so a query bisects the onsets that can still be sounding inside its window — [start − max_gate, end) — instead of scanning the list. Repetition is iteration-major: the range of repetitions that can reach the window is computed once and charged before any of it is walked, so a query spanning a million periods fails immediately rather than after a million steps. A work unit charges an index node visit, a bisection, an inspected source event or a candidate repetition. Two bounded in-place sorts order and coalesce the hits; these are limited by hit capacity, not counted as work units. Split fragments consume slots before coalescing. Capacity, work and numeric errors clear the result and return an error, never a partial schedule.

These are safety ceilings, not a guarantee that a maximum-size query fits an audio callback — see the measured dense fixture below, where the synthetic worst case does not. Engine ownership, deferred plan destruction and overload policies remain integration work. A failed query must not be treated as successful silence by a runner.

Verification#

cargo test -p volta-time --locked runs exact arithmetic and JSON cases, a materialized-schedule oracle, partition invariance, split/seek/duplicate laws, meter changes, sample edges, lazy distant seeks, interval lookup and a separate allocation/deallocation gate. tests/plan.rs adds the Stage 0b contracts: authored anchors of all four domains surviving tempo and meter edits, mixed domains in one prepared plan, elapsed-anchor tick quantization, bounded inspection over a content at the event ceiling (with a partition-invariance check that the bisection did not drop a gate that started periods ago), per-track indexes, prepared budget violations by region ID, and nested occurrence paths against a directly placed control. It also checks that different coordinate domains cannot be added. The package is included in the fast and workspace gates.

cargo test -p volta-engine --lib transport:: covers existing clocks and the conductor adapter. Its shared portable contract includes 1,890 vectors generated with an independent 80-digit decimal integral: constant, rising, falling, nearly flat and 1,024-segment tempo maps, three rates and three stream origins. Across that fixture, unrounded error must stay below 0.01 sample and rounded error below 0.51 sample. These are numerical regression gates, not an exhaustive proof over every possible floating-point input.

make test-temporal-wasm executes the conductor contract and the dense fixture as actual Wasm under Node, and runs inside make test-browser (CI's Browser Wasm gate) — the temporal core ships to the browser as the same Rust; the native gates build no wasm. The conductor half uses generated wasm-bindgen bindings; the fixture half instantiates directly, because volta-time declares no host imports at all and the gate asserts that (an import means a platform dependency crept in). Node owns the clock, so the Rust side exports plain calls and never asks for an Instant that wasm32-unknown-unknown does not have. It requires the Wasm target and wasm-bindgen-cli 0.2.100; a missing tool fails the gate rather than skipping it. It checks core/engine portability, not a Studio or AudioWorklet session. The fast gate also runs the engine allocation test temporal_conductor_projection_is_allocation_free.

cargo run --profile fast -p volta-time --example temporal_fixture reports preparation and query p50/p95/max for 128 receivers, 10,000 regions, contents at the per-content event ceiling and over one million virtual repeats. Its query windows are real engine blocks — 128 frames at 48 kHz, converted to beats through a Conductor — not invented one-beat viewports. VOLTA_TEMPORAL_QUERIES sets the sample count; make test-temporal-wasm runs the same fixture as Wasm and reports its own numbers. There are no curve readers in it: automation regions are Stage 4, and the fixture does not pretend to cover them. Measurements, including the density at which the scheduling target stops being met, are in Temporal 0b 2026 09 14; earlier evidence is in Temporal Core Spike 2026 09 14 and Temporal Conductor 2026 09 14. Engine delivery and actual UI interaction remain open.