Skip to main content

tono_core/runtime/
transport.rs

1//! transport — a sample-accurate musical clock for a compiled Program
2//! (ADR 0005): position in frames with exact conversions to beats and bars
3//! through the program's tempo and meter maps. The transport owns no audio;
4//! it answers "where am I" and "what frame is that", deterministically, so
5//! scheduling never needs Python, a game loop, or an OS timer to wake on a
6//! musical boundary.
7
8use crate::dsl::TempoPoint;
9use crate::units::{Beat, MeterPoint};
10
11/// Transport playback state.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum TransportState {
14    /// At frame 0 (or the loop start), not advancing.
15    Stopped,
16    /// Advancing with the render.
17    Playing,
18    /// Holding position, not advancing.
19    Paused,
20}
21
22/// What one [`Transport::advance`] did.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct Advance {
25    /// The playhead wrapped at the loop end this call.
26    pub wrapped: bool,
27    /// The playhead reached the end with no loop (the transport is now
28    /// Stopped at the last frame).
29    pub finished: bool,
30}
31
32/// A sample-accurate musical clock. All arithmetic before the single
33/// frame-boundary crossings is the exact rational/f64 segment walk shared
34/// with the compiler (ADR 0002), so the transport and the offline render
35/// can never disagree about where a beat lands.
36#[derive(Debug, Clone)]
37pub struct Transport {
38    sample_rate: u32,
39    bpm: f32,
40    beats_per_bar: u32,
41    tempo_map: Vec<TempoPoint>,
42    meter_map: Vec<MeterPoint>,
43    pickup: Option<Beat>,
44    length_frames: u64,
45    state: TransportState,
46    position: u64,
47    loop_range: Option<(u64, u64)>,
48}
49
50impl Transport {
51    /// A transport for a compiled program, stopped at frame 0.
52    pub fn for_program(meta: &crate::program::ProgramMeta) -> Self {
53        Transport {
54            sample_rate: meta.sample_rate,
55            bpm: meta.tempo_bpm,
56            beats_per_bar: meta.beats_per_bar,
57            tempo_map: meta.tempo_map.clone(),
58            meter_map: meta.meter_map.clone(),
59            pickup: meta.pickup,
60            length_frames: meta.duration_frames,
61            state: TransportState::Stopped,
62            position: 0,
63            loop_range: None,
64        }
65    }
66
67    /// The clock's sample rate.
68    pub fn sample_rate(&self) -> u32 {
69        self.sample_rate
70    }
71
72    /// The current state.
73    pub fn state(&self) -> TransportState {
74        self.state
75    }
76
77    /// Whether the transport is advancing.
78    pub fn is_playing(&self) -> bool {
79        self.state == TransportState::Playing
80    }
81
82    /// Start or resume advancing.
83    pub fn play(&mut self) {
84        self.state = TransportState::Playing;
85    }
86
87    /// Hold position.
88    pub fn pause(&mut self) {
89        self.state = TransportState::Paused;
90    }
91
92    /// Stop and rewind to frame 0.
93    pub fn stop(&mut self) {
94        self.state = TransportState::Stopped;
95        self.position = 0;
96    }
97
98    /// Seconds elapsed at `beat` (constant tempo, or the map's segment walk).
99    fn seconds_at_beat(&self, beat: f64) -> f64 {
100        if self.tempo_map.is_empty() {
101            beat * 60.0 / self.bpm.max(1.0) as f64
102        } else {
103            crate::dsl::tempo_map_seconds_at(&self.tempo_map, beat)
104        }
105    }
106
107    /// Beats elapsed at `seconds` (the inverse walk).
108    fn beats_at_seconds(&self, seconds: f64) -> f64 {
109        if self.tempo_map.is_empty() {
110            seconds * self.bpm.max(1.0) as f64 / 60.0
111        } else {
112            crate::dsl::tempo_map_beat_at_seconds(&self.tempo_map, seconds)
113        }
114    }
115
116    /// The frame a beat lands on (rounds halves away from zero, ADR 0002).
117    pub fn frame_at_beat(&self, beat: f64) -> u64 {
118        (self.seconds_at_beat(beat) * self.sample_rate as f64)
119            .round()
120            .max(0.0) as u64
121    }
122
123    /// The beat at a frame (the inverse conversion).
124    pub fn beat_at_frame(&self, frame: u64) -> f64 {
125        self.beats_at_seconds(frame as f64 / self.sample_rate as f64)
126    }
127
128    /// The frame a bar starts on (through the meter map and pickup).
129    pub fn frame_at_bar(&self, bar: u32) -> u64 {
130        let beat = crate::units::beat_at_bar(&self.meter_map, self.beats_per_bar, self.pickup, bar);
131        self.frame_at_beat(beat.to_f64())
132    }
133
134    /// The position in frames.
135    pub fn position_frames(&self) -> u64 {
136        self.position
137    }
138
139    /// The position in beats.
140    pub fn position_beats(&self) -> f64 {
141        self.beat_at_frame(self.position)
142    }
143
144    /// The position in bars (bar index plus intra-bar fraction).
145    pub fn position_bars(&self) -> f64 {
146        let beat = Beat::new((self.position_beats() * 1e9).round() as i64, 1_000_000_000);
147        let bars =
148            crate::units::bar_count_at_beat(&self.meter_map, self.beats_per_bar, self.pickup, beat);
149        // bar_count gives bars elapsed with ceil semantics; the current bar
150        // index is one less when the position sits inside it.
151        let idx = bars.saturating_sub(1);
152        let start =
153            crate::units::beat_at_bar(&self.meter_map, self.beats_per_bar, self.pickup, idx);
154        let len = crate::units::bar_len(&self.meter_map, self.beats_per_bar, self.pickup, idx);
155        let within = if len > Beat::zero() {
156            (beat.to_f64() - start.to_f64()) / len.to_f64()
157        } else {
158            0.0
159        };
160        idx as f64 + within.clamp(0.0, 1.0)
161    }
162
163    /// The program length in frames.
164    pub fn length_frames(&self) -> u64 {
165        self.length_frames
166    }
167
168    /// Seek to a frame (clamped to the program length).
169    pub fn seek_frame(&mut self, frame: u64) {
170        self.position = frame.min(self.length_frames);
171    }
172
173    /// Seek to a beat position.
174    pub fn seek_beat(&mut self, beat: f64) {
175        self.seek_frame(self.frame_at_beat(beat));
176    }
177
178    /// Seek to a bar (through the meter map and pickup).
179    pub fn seek_bar(&mut self, bar: u32) {
180        self.seek_frame(self.frame_at_bar(bar));
181    }
182
183    /// Loop a frame range [start, end). At `end` the playhead wraps to
184    /// `start`; seeking outside the range is allowed (the loop engages when
185    /// the playhead reaches `end`). Invalid ranges (empty, past the end) are
186    /// rejected with `false` and leave the old range.
187    pub fn set_loop_frames(&mut self, start: u64, end: u64) -> bool {
188        if start >= end || end > self.length_frames {
189            return false;
190        }
191        self.loop_range = Some((start, end));
192        true
193    }
194
195    /// Loop a bar range (through the meter map), or report an invalid range.
196    pub fn set_loop_bars(&mut self, start_bar: u32, end_bar: u32) -> bool {
197        let (start, end) = (self.frame_at_bar(start_bar), self.frame_at_bar(end_bar));
198        self.set_loop_frames(start, end)
199    }
200
201    /// Clear the loop range.
202    pub fn clear_loop(&mut self) {
203        self.loop_range = None;
204    }
205
206    /// The loop range in frames, if set.
207    pub fn loop_range(&self) -> Option<(u64, u64)> {
208        self.loop_range
209    }
210
211    /// Advance the playhead by `frames` (no-op unless Playing). At the loop
212    /// end the playhead wraps; at the program end with no loop it stops.
213    pub fn advance(&mut self, frames: u64) -> Advance {
214        if self.state != TransportState::Playing {
215            return Advance {
216                wrapped: false,
217                finished: false,
218            };
219        }
220        let mut pos = self.position.saturating_add(frames);
221        let mut wrapped = false;
222        if let Some((start, end)) = self.loop_range
223            && pos >= end
224        {
225            let span = end - start;
226            pos = start + (pos - start) % span;
227            wrapped = true;
228        }
229        if !wrapped && pos >= self.length_frames {
230            self.position = self.length_frames;
231            self.state = TransportState::Stopped;
232            return Advance {
233                wrapped: false,
234                finished: true,
235            };
236        }
237        self.position = pos;
238        Advance {
239            wrapped,
240            finished: false,
241        }
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    fn mapped_transport() -> Transport {
250        Transport {
251            sample_rate: 48_000,
252            bpm: 120.0,
253            beats_per_bar: 4,
254            tempo_map: vec![
255                TempoPoint {
256                    at: Beat::zero(),
257                    bpm: 120.0,
258                },
259                TempoPoint {
260                    at: Beat::from_int(4),
261                    bpm: 240.0,
262                },
263            ],
264            meter_map: vec![],
265            pickup: None,
266            length_frames: 192_000,
267            state: TransportState::Stopped,
268            position: 0,
269            loop_range: None,
270        }
271    }
272
273    #[test]
274    fn frames_and_beats_cross_exactly() {
275        let t = mapped_transport();
276        assert_eq!(t.frame_at_beat(4.0), 96_000, "4 beats at 120 BPM = 2 s");
277        assert_eq!(t.frame_at_beat(8.0), 144_000, "2 s at 120 + 1 s at 240");
278        assert_eq!(t.beat_at_frame(96_000), 4.0);
279        assert_eq!(t.beat_at_frame(144_000), 8.0);
280        // The constant-tempo transport.
281        let mut plain = mapped_transport();
282        plain.tempo_map.clear();
283        assert_eq!(plain.frame_at_beat(8.0), 192_000);
284        assert_eq!(plain.beat_at_frame(192_000), 8.0);
285    }
286
287    #[test]
288    fn bars_convert_through_meter_and_pickup() {
289        let mut t = mapped_transport();
290        t.tempo_map.clear();
291        t.meter_map = vec![MeterPoint {
292            bar: 0,
293            numerator: 6,
294            denominator: 8,
295        }];
296        assert_eq!(t.frame_at_bar(1), 72_000, "bar 1 of 6/8 = 3 beats = 1.5 s");
297        t.pickup = Some(Beat::from_int(1));
298        assert_eq!(t.frame_at_bar(1), 24_000, "bar 1 after a one-beat pickup");
299        // Position in bars with a fraction: 3 beats is 2/3 through bar 1
300        // (bar 0 = the 1-beat pickup, bar 1 = 3 beats).
301        t.seek_frame(72_000);
302        let bars = t.position_bars();
303        assert!((bars - 1.6666666666666667).abs() < 1e-9, "{bars}");
304    }
305
306    #[test]
307    fn play_pause_stop_seek() {
308        let mut t = mapped_transport();
309        assert_eq!(t.state(), TransportState::Stopped);
310        assert!(!t.advance(100).finished);
311        assert_eq!(t.position_frames(), 0, "stopped transport doesn't advance");
312        t.play();
313        let a = t.advance(48_000);
314        assert!(!a.finished && !a.wrapped);
315        assert_eq!(t.position_frames(), 48_000);
316        t.pause();
317        assert!(!t.advance(48_000).finished);
318        assert_eq!(t.position_frames(), 48_000, "paused holds");
319        t.seek_bar(1);
320        assert_eq!(t.position_frames(), 96_000);
321        t.stop();
322        assert_eq!(t.position_frames(), 0);
323        assert_eq!(t.state(), TransportState::Stopped);
324    }
325
326    #[test]
327    fn loop_wraps_and_end_finishes() {
328        let mut t = mapped_transport();
329        assert!(t.set_loop_bars(1, 2));
330        assert!(!t.set_loop_frames(96_000, 48_000), "empty range rejected");
331        assert!(!t.set_loop_frames(0, 999_999), "past the end rejected");
332        t.play();
333        let a = t.advance(200_000);
334        assert!(a.wrapped);
335        assert_eq!(t.position_frames(), 96_000 + 8_000, "wrapped into the loop");
336        // No loop: stops at the program end.
337        t.clear_loop();
338        t.seek_frame(190_000);
339        let a = t.advance(48_000);
340        assert!(a.finished);
341        assert_eq!(t.position_frames(), 192_000);
342        assert_eq!(t.state(), TransportState::Stopped);
343    }
344}