1use crate::dsl::TempoPoint;
9use crate::units::{Beat, MeterPoint};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum TransportState {
14 Stopped,
16 Playing,
18 Paused,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct Advance {
25 pub wrapped: bool,
27 pub finished: bool,
30}
31
32#[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 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 pub fn sample_rate(&self) -> u32 {
69 self.sample_rate
70 }
71
72 pub fn state(&self) -> TransportState {
74 self.state
75 }
76
77 pub fn is_playing(&self) -> bool {
79 self.state == TransportState::Playing
80 }
81
82 pub fn play(&mut self) {
84 self.state = TransportState::Playing;
85 }
86
87 pub fn pause(&mut self) {
89 self.state = TransportState::Paused;
90 }
91
92 pub fn stop(&mut self) {
94 self.state = TransportState::Stopped;
95 self.position = 0;
96 }
97
98 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 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 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 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 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 pub fn position_frames(&self) -> u64 {
136 self.position
137 }
138
139 pub fn position_beats(&self) -> f64 {
141 self.beat_at_frame(self.position)
142 }
143
144 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 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 pub fn length_frames(&self) -> u64 {
165 self.length_frames
166 }
167
168 pub fn seek_frame(&mut self, frame: u64) {
170 self.position = frame.min(self.length_frames);
171 }
172
173 pub fn seek_beat(&mut self, beat: f64) {
175 self.seek_frame(self.frame_at_beat(beat));
176 }
177
178 pub fn seek_bar(&mut self, bar: u32) {
180 self.seek_frame(self.frame_at_bar(bar));
181 }
182
183 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 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 pub fn clear_loop(&mut self) {
203 self.loop_range = None;
204 }
205
206 pub fn loop_range(&self) -> Option<(u64, u64)> {
208 self.loop_range
209 }
210
211 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 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 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 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}