1use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, Ordering};
7
8const ORD: Ordering = Ordering::Relaxed;
11
12#[derive(Debug)]
14pub struct Transport {
15 playing: AtomicBool,
16 recording: AtomicBool,
17 looping: AtomicBool,
18 metronome: AtomicBool,
19 position_ticks: AtomicI64,
21 tempo_centibpm: AtomicU32,
23 loop_start_ticks: AtomicI64,
25 loop_end_ticks: AtomicI64,
27}
28
29#[derive(Debug, Clone, Copy)]
31pub struct TransportSnapshot {
32 pub playing: bool,
33 pub recording: bool,
34 pub looping: bool,
35 pub metronome: bool,
36 pub position_ticks: i64,
37 pub tempo_bpm: f64,
38 pub loop_start_ticks: i64,
39 pub loop_end_ticks: i64,
40}
41
42impl Transport {
43 pub const PPQ: i64 = 960;
45
46 pub fn new(bpm: f64) -> Self {
47 Self {
48 playing: AtomicBool::new(false),
49 recording: AtomicBool::new(false),
50 looping: AtomicBool::new(false),
51 metronome: AtomicBool::new(false),
52 position_ticks: AtomicI64::new(0),
53 tempo_centibpm: AtomicU32::new((bpm * 100.0) as u32),
54 loop_start_ticks: AtomicI64::new(0),
55 loop_end_ticks: AtomicI64::new(Self::PPQ * 4 * 4), }
57 }
58
59 pub fn play(&self) {
62 self.playing.store(true, ORD);
63 }
64
65 pub fn pause(&self) {
66 self.playing.store(false, ORD);
67 }
68
69 pub fn stop(&self) {
70 self.playing.store(false, ORD);
71 self.position_ticks.store(0, ORD);
72 }
73
74 pub fn toggle_record(&self) {
75 self.recording.fetch_xor(true, ORD);
76 }
77
78 pub fn toggle_loop(&self) {
79 self.looping.fetch_xor(true, ORD);
80 }
81
82 pub fn toggle_metronome(&self) {
83 self.metronome.fetch_xor(true, ORD);
84 }
85
86 pub fn is_metronome_on(&self) -> bool {
87 self.metronome.load(ORD)
88 }
89
90 pub fn set_tempo(&self, bpm: f64) {
91 self.tempo_centibpm.store((bpm * 100.0) as u32, ORD);
92 }
93
94 pub fn set_position(&self, ticks: i64) {
95 self.position_ticks.store(ticks, ORD);
96 }
97
98 pub fn set_loop_range(&self, start_ticks: i64, end_ticks: i64) {
99 self.loop_start_ticks.store(start_ticks, ORD);
100 self.loop_end_ticks.store(end_ticks, ORD);
101 }
102
103 pub fn set_loop_bars(&self, start_bar: u32, end_bar: u32) {
106 let ticks_per_bar = Self::PPQ * 4; self.set_loop_range(
108 (start_bar.saturating_sub(1) as i64) * ticks_per_bar,
109 (end_bar as i64) * ticks_per_bar,
110 );
111 }
112
113 pub fn loop_start(&self) -> i64 { self.loop_start_ticks.load(ORD) }
114 pub fn loop_end(&self) -> i64 { self.loop_end_ticks.load(ORD) }
115
116 pub fn start_loop_record(&self) {
119 self.looping.store(true, ORD);
120 self.position_ticks.store(self.loop_start_ticks.load(ORD), ORD);
121 self.recording.store(true, ORD);
122 self.playing.store(true, ORD);
123 }
124
125 pub fn stop_loop_record(&self) {
127 self.recording.store(false, ORD);
128 self.playing.store(false, ORD);
129 }
130
131 pub fn is_playing(&self) -> bool {
134 self.playing.load(ORD)
135 }
136
137 pub fn is_recording(&self) -> bool {
138 self.recording.load(ORD)
139 }
140
141 pub fn is_looping(&self) -> bool {
142 self.looping.load(ORD)
143 }
144
145 pub fn position_ticks(&self) -> i64 {
146 self.position_ticks.load(ORD)
147 }
148
149 pub fn tempo_bpm(&self) -> f64 {
150 self.tempo_centibpm.load(ORD) as f64 / 100.0
151 }
152
153 pub fn advance(&self, num_samples: u32, sample_rate: u32) {
156 if !self.is_playing() {
157 return;
158 }
159
160 let bpm = self.tempo_bpm();
161 let ticks_per_sample = (bpm * Self::PPQ as f64) / (60.0 * sample_rate as f64);
162 let delta = (num_samples as f64 * ticks_per_sample) as i64;
163
164 let mut new_pos = self.position_ticks.load(ORD) + delta;
165
166 if self.is_looping() {
167 let loop_end = self.loop_end_ticks.load(ORD);
168 let loop_start = self.loop_start_ticks.load(ORD);
169 if new_pos >= loop_end && loop_end > loop_start {
170 new_pos = loop_start + (new_pos - loop_end) % (loop_end - loop_start);
171 }
172 }
173
174 self.position_ticks.store(new_pos, ORD);
175 }
176
177 pub fn snapshot(&self) -> TransportSnapshot {
179 TransportSnapshot {
180 playing: self.playing.load(ORD),
181 recording: self.recording.load(ORD),
182 looping: self.looping.load(ORD),
183 metronome: self.metronome.load(ORD),
184 position_ticks: self.position_ticks.load(ORD),
185 tempo_bpm: self.tempo_bpm(),
186 loop_start_ticks: self.loop_start_ticks.load(ORD),
187 loop_end_ticks: self.loop_end_ticks.load(ORD),
188 }
189 }
190}
191
192impl Default for Transport {
193 fn default() -> Self {
194 Self::new(120.0)
195 }
196}
197
198pub fn ticks_to_position_string(ticks: i64, ppq: i64) -> String {
200 let ticks_per_beat = ppq;
201 let ticks_per_bar = ppq * 4; let bar = ticks / ticks_per_bar + 1;
204 let beat = (ticks % ticks_per_bar) / ticks_per_beat + 1;
205 let tick = ticks % ticks_per_beat;
206
207 format!("{bar}.{beat}.{tick:03}")
208}
209
210pub fn ticks_to_samples(ticks: i64, bpm: f64, sample_rate: f64) -> i64 {
212 let seconds = ticks as f64 * 60.0 / (bpm * Transport::PPQ as f64);
213 (seconds * sample_rate) as i64
214}
215
216pub fn samples_to_ticks(samples: i64, bpm: f64, sample_rate: f64) -> i64 {
218 let seconds = samples as f64 / sample_rate;
219 (seconds * bpm * Transport::PPQ as f64 / 60.0) as i64
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn transport_starts_stopped() {
228 let t = Transport::default();
229 assert!(!t.is_playing());
230 assert!(!t.is_recording());
231 assert_eq!(t.position_ticks(), 0);
232 }
233
234 #[test]
235 fn play_pause_stop() {
236 let t = Transport::default();
237 t.play();
238 assert!(t.is_playing());
239 t.pause();
240 assert!(!t.is_playing());
241
242 t.set_position(1000);
244 t.play();
245 t.pause();
246 assert_eq!(t.position_ticks(), 1000);
247
248 t.stop();
249 assert!(!t.is_playing());
250 assert_eq!(t.position_ticks(), 0); }
252
253 #[test]
254 fn tempo_set_and_read() {
255 let t = Transport::new(140.0);
256 assert!((t.tempo_bpm() - 140.0).abs() < 0.01);
257 t.set_tempo(95.5);
258 assert!((t.tempo_bpm() - 95.5).abs() < 0.01);
259 }
260
261 #[test]
262 fn advance_moves_position() {
263 let t = Transport::new(120.0);
264 t.play();
265 t.advance(44100, 44100); let pos = t.position_ticks();
270 assert!(
272 (pos - 1920).abs() <= 1,
273 "Expected ~1920 ticks after 1s at 120bpm, got {pos}"
274 );
275 }
276
277 #[test]
278 fn advance_does_nothing_when_stopped() {
279 let t = Transport::new(120.0);
280 t.advance(44100, 44100);
281 assert_eq!(t.position_ticks(), 0);
282 }
283
284 #[test]
285 fn loop_wraps_position() {
286 let t = Transport::new(120.0);
287 t.set_loop_range(0, 1920); t.toggle_loop();
289 t.play();
290
291 t.advance(44100 * 3, 44100);
293 let pos = t.position_ticks();
294 assert!(
296 pos < 1920,
297 "Position should have wrapped within loop, got {pos}"
298 );
299 }
300
301 #[test]
302 fn position_string_formatting() {
303 assert_eq!(ticks_to_position_string(0, 960), "1.1.000");
304 assert_eq!(ticks_to_position_string(960, 960), "1.2.000");
305 assert_eq!(ticks_to_position_string(3840, 960), "2.1.000");
306 assert_eq!(ticks_to_position_string(4000, 960), "2.1.160");
307 }
308
309 #[test]
310 fn tick_sample_conversion_round_trip() {
311 let bpm = 120.0;
312 let sr = 44100.0;
313 for tick in [0, 480, 960, 1920, 3840, 96000] {
314 let samples = ticks_to_samples(tick, bpm, sr);
315 let back = samples_to_ticks(samples, bpm, sr);
316 assert!(
317 (back - tick).abs() <= 1,
318 "Round trip failed: {tick} → {samples} → {back}"
319 );
320 }
321 }
322
323 #[test]
324 fn snapshot_reflects_current_state() {
325 let t = Transport::new(130.0);
326 t.play();
327 t.toggle_record();
328 t.set_position(500);
329 let snap = t.snapshot();
330 assert!(snap.playing);
331 assert!(snap.recording);
332 assert_eq!(snap.position_ticks, 500);
333 assert!((snap.tempo_bpm - 130.0).abs() < 0.01);
334 }
335
336
337 #[test]
338 fn loop_wraps_at_boundary() {
339 let t = Transport::new(120.0);
340 t.set_loop_range(0, 7680); t.toggle_loop();
342 t.play();
343
344 let mut wrapped = false;
347 for _ in 0..800 {
348 let before = t.position_ticks();
349 t.advance(256, 44100);
350 if t.position_ticks() < before {
351 wrapped = true;
352 break;
353 }
354 }
355 assert!(wrapped, "Loop should have wrapped. pos={}", t.position_ticks());
356 }
357}