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.