Skip to content

Tempo map

Model. Project.tempo_map: Vec<TempoEvent { pos, bpm, ramp }> (positions in samples) and Project.signatures: Vec<SigEvent { bar, num, den }> (1-based bars). Empty lists mean the constant tempo / sig_num / sig_den, which are kept equal to the values at position 0, so old projects behave and save exactly as before; the session stores a one-event map as empty.

crates/engine/src/tempo.rs builds a TempoMap from the project: beats_at (quarter notes at a sample), sample_at (the inverse), bpm_at, sig_at_bar / sig_at, bar_at, bar_start, bar_beat_at, next/previous beat and bar, grid snapping counted from the bar line (snap_q), and bars() / grid() iterators. Linear ramps use the closed form of ∫ bpm/60 dt, and its inverse a numerically stable quadratic. src/tempo.ts mirrors it and is cached per project snapshot; the src/time.ts helpers (toBbt, snapPos, bar stepping) go through it, and samplesPerBeat / samplesPerBar remain only as a zoom scale at position 0.

Engine. Cmd::Tempo(Arc<TempoMap>) carries the map; the old map returns as Garbage::Tempo. sync_globals() sends it, so undo/restore(), project load, sample-rate rebuilds and offline renders all have it. The click, its subdivisions and accents, the count-in (computed at the punch’s tempo and signature) and TransportInfo for plug-ins (tempo at the block start, position and bar start in quarters, signature, with the PDC offset) are all read from the map.

Regions stay in samples, so tempo edits never move audio. move_midi_with_tempo(old, new) (and the moveMidi flag on set_tempo_map / set_tempo_at) rescales MIDI regions, notes and CC to keep their bar/beat positions. Commands: set_tempo_map, set_signatures, set_tempo_at, set_signature_at, move_midi_with_tempo; set_tempo / set_signature edit the base values. MIDI files (smf.rs) read and write tempo (0x51) and time signature (0x58) events, converting ticks through the file’s own map. The UI is src/components/GlobalTracks.tsx (Tempo and Signature lanes) and the LCD in ControlBar.tsx.