1use std::sync::Arc;
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::time::{Duration, Instant};
10
11enum ClockState {
24 Stopped,
25 Running { started_at: Instant, base: Duration },
26 Paused { frozen_at: Duration },
27}
28
29pub struct PlaybackClock {
54 state: ClockState,
55 rate: f64,
57 seek_offset: Duration,
60}
61
62impl PlaybackClock {
63 #[must_use]
65 pub fn new() -> Self {
66 Self {
67 state: ClockState::Stopped,
68 rate: 1.0,
69 seek_offset: Duration::ZERO,
70 }
71 }
72
73 pub fn start(&mut self) {
81 let base = match &self.state {
82 ClockState::Running { .. } => return,
83 ClockState::Stopped => self.seek_offset,
84 ClockState::Paused { frozen_at } => *frozen_at,
85 };
86 self.state = ClockState::Running {
87 started_at: Instant::now(),
88 base,
89 };
90 }
91
92 pub fn stop(&mut self) {
97 self.state = ClockState::Stopped;
98 self.seek_offset = Duration::ZERO;
99 }
100
101 pub fn pause(&mut self) {
106 if let ClockState::Running { started_at, base } = &self.state {
107 let elapsed = started_at.elapsed().mul_f64(self.rate);
108 self.state = ClockState::Paused {
109 frozen_at: *base + elapsed,
110 };
111 }
112 }
113
114 pub fn resume(&mut self) {
116 if let ClockState::Paused { frozen_at } = self.state {
117 self.state = ClockState::Running {
118 started_at: Instant::now(),
119 base: frozen_at,
120 };
121 }
122 }
123
124 #[must_use]
129 pub fn current_time(&self) -> Duration {
130 match &self.state {
131 ClockState::Stopped => Duration::ZERO,
132 ClockState::Paused { frozen_at } => *frozen_at,
133 ClockState::Running { started_at, base } => {
134 *base + started_at.elapsed().mul_f64(self.rate)
135 }
136 }
137 }
138
139 #[must_use]
145 pub fn current_pts(&self) -> Duration {
146 match &self.state {
147 ClockState::Stopped => self.seek_offset,
148 _ => self.current_time(),
149 }
150 }
151
152 #[must_use]
154 pub fn is_running(&self) -> bool {
155 matches!(self.state, ClockState::Running { .. })
156 }
157
158 pub fn set_rate(&mut self, rate: f64) {
163 if rate <= 0.0 {
164 return;
165 }
166 if let ClockState::Running { started_at, base } = &mut self.state {
167 let elapsed = started_at.elapsed().mul_f64(self.rate);
169 *base += elapsed;
170 *started_at = Instant::now();
171 }
172 self.rate = rate;
173 }
174
175 #[must_use]
177 pub fn rate(&self) -> f64 {
178 self.rate
179 }
180
181 pub fn set_position(&mut self, pts: Duration) {
191 self.seek_offset = pts;
193 if matches!(self.state, ClockState::Running { .. }) {
194 self.state = ClockState::Running {
196 started_at: Instant::now(),
197 base: pts,
198 };
199 } else if matches!(self.state, ClockState::Paused { .. }) {
200 self.state = ClockState::Paused { frozen_at: pts };
201 }
202 }
204}
205
206impl Default for PlaybackClock {
207 fn default() -> Self {
208 Self::new()
209 }
210}
211
212pub(crate) enum MasterClock {
219 Audio {
220 samples_consumed: Arc<AtomicU64>,
221 sample_rate: u32,
222 },
223 System {
224 started_at: Instant,
225 base_pts: Duration,
226 },
227}
228
229impl MasterClock {
230 #[allow(clippy::cast_precision_loss)]
232 pub(crate) fn current_pts(&self) -> Duration {
233 match self {
234 Self::Audio {
235 samples_consumed,
236 sample_rate,
237 } => {
238 let s = samples_consumed.load(Ordering::Relaxed);
239 Duration::from_secs_f64(s as f64 / f64::from(*sample_rate))
240 }
241 Self::System {
242 started_at,
243 base_pts,
244 } => *base_pts + started_at.elapsed(),
245 }
246 }
247
248 pub(crate) fn should_sync(&self) -> bool {
255 match self {
256 Self::System { .. } => true,
257 Self::Audio {
258 samples_consumed, ..
259 } => samples_consumed.load(Ordering::Relaxed) > 0,
260 }
261 }
262
263 pub(crate) fn reset(&mut self, base: Duration) {
267 if let Self::System {
268 started_at,
269 base_pts,
270 } = self
271 {
272 *started_at = Instant::now();
273 *base_pts = base;
274 }
275 }
276}
277
278#[cfg(test)]
281mod tests {
282 use super::*;
283 use std::thread;
284
285 #[test]
286 fn clock_stopped_should_return_zero() {
287 let clock = PlaybackClock::new();
289 assert_eq!(clock.current_time(), Duration::ZERO);
290
291 let mut clock = PlaybackClock::new();
293 clock.start();
294 thread::sleep(Duration::from_millis(5));
295 clock.stop();
296 assert_eq!(
297 clock.current_time(),
298 Duration::ZERO,
299 "current_time() must be ZERO after stop()"
300 );
301 }
302
303 #[test]
304 fn clock_paused_should_freeze_at_pause_time() {
305 let mut clock = PlaybackClock::new();
306 clock.start();
307 thread::sleep(Duration::from_millis(10));
308 clock.pause();
309
310 let t1 = clock.current_time();
311 thread::sleep(Duration::from_millis(10));
312 let t2 = clock.current_time();
313
314 assert_eq!(t1, t2, "current_time() must not advance while paused");
315 assert!(
316 !clock.is_running(),
317 "clock must not report running while paused"
318 );
319 }
320
321 #[test]
322 fn clock_resumed_should_continue_from_pause() {
323 let mut clock = PlaybackClock::new();
324 clock.start();
325 thread::sleep(Duration::from_millis(10));
326 clock.pause();
327 let t_paused = clock.current_time();
328
329 thread::sleep(Duration::from_millis(10));
331 assert_eq!(clock.current_time(), t_paused);
332
333 clock.resume();
334 assert!(clock.is_running());
335 thread::sleep(Duration::from_millis(10));
336
337 let t_after = clock.current_time();
338 assert!(
339 t_after > t_paused,
340 "current_time() must advance after resume(); paused={t_paused:?} after={t_after:?}"
341 );
342 }
343
344 #[test]
345 fn clock_start_should_be_noop_when_already_running() {
346 let mut clock = PlaybackClock::new();
347 clock.start();
348 thread::sleep(Duration::from_millis(10));
349 let t_before = clock.current_time();
350
351 clock.start();
353 let t_after = clock.current_time();
354
355 assert!(
356 t_after >= t_before,
357 "second start() must not reset the clock; before={t_before:?} after={t_after:?}"
358 );
359 }
360
361 #[test]
362 fn clock_resume_should_be_noop_when_not_paused() {
363 let mut clock = PlaybackClock::new();
365 clock.resume();
366 assert!(!clock.is_running());
367 assert_eq!(clock.current_time(), Duration::ZERO);
368
369 clock.start();
371 thread::sleep(Duration::from_millis(5));
372 let t = clock.current_time();
373 clock.resume(); assert!(clock.is_running());
375 assert!(clock.current_time() >= t);
376 }
377
378 #[test]
379 fn clock_default_should_equal_new() {
380 let a = PlaybackClock::new();
381 let b = PlaybackClock::default();
382 assert_eq!(a.current_time(), b.current_time());
383 assert_eq!(a.is_running(), b.is_running());
384 }
385
386 #[test]
387 fn set_rate_should_reject_non_positive_values() {
388 let mut clock = PlaybackClock::new();
389
390 clock.set_rate(0.0);
391 assert!(
392 (clock.rate() - 1.0).abs() < f64::EPSILON,
393 "rate must remain 1.0 after set_rate(0.0)"
394 );
395
396 clock.set_rate(-1.0);
397 assert!(
398 (clock.rate() - 1.0).abs() < f64::EPSILON,
399 "rate must remain 1.0 after set_rate(-1.0)"
400 );
401 }
402
403 #[test]
404 fn set_rate_should_update_rate_when_stopped_or_paused() {
405 let mut clock = PlaybackClock::new();
407 clock.set_rate(0.5);
408 assert!((clock.rate() - 0.5).abs() < f64::EPSILON);
409
410 let mut clock = PlaybackClock::new();
412 clock.start();
413 clock.pause();
414 clock.set_rate(2.0);
415 assert!((clock.rate() - 2.0).abs() < f64::EPSILON);
416 assert!(
417 !clock.is_running(),
418 "clock must remain paused after set_rate"
419 );
420 }
421
422 #[test]
423 fn set_rate_running_should_not_jump_current_time() {
424 let mut clock = PlaybackClock::new();
425 clock.start();
426 thread::sleep(Duration::from_millis(10));
427 let before = clock.current_time();
428 clock.set_rate(2.0);
429 let after = clock.current_time();
430
431 assert!(
434 after >= before,
435 "current_time() must not go backward on set_rate; before={before:?} after={after:?}"
436 );
437 assert!(
438 after - before < Duration::from_millis(20),
439 "current_time() must not jump forward on set_rate; before={before:?} after={after:?}"
440 );
441 assert!((clock.rate() - 2.0).abs() < f64::EPSILON);
442 }
443
444 #[test]
445 #[ignore = "performance thresholds are environment-dependent; run explicitly with -- --include-ignored"]
446 fn rate_two_x_should_advance_at_double_speed() {
447 let mut clock = PlaybackClock::new();
448 clock.set_rate(2.0);
449 clock.start();
450 thread::sleep(Duration::from_millis(50));
451 let elapsed = clock.current_time();
452
453 assert!(
455 elapsed >= Duration::from_millis(80),
456 "2× rate: expected ≥80 ms after 50 ms wall time, got {elapsed:?}"
457 );
458 }
459
460 #[test]
461 fn set_position_should_shift_pts_by_seek_offset() {
462 let seek_target = Duration::from_secs(30);
463
464 let mut clock = PlaybackClock::new();
466 clock.set_position(seek_target);
467 assert_eq!(
468 clock.current_pts(),
469 seek_target,
470 "current_pts() must reflect seek_offset when stopped"
471 );
472
473 clock.start();
475 let pts = clock.current_pts();
476 assert!(
477 pts >= seek_target,
478 "current_pts() must be ≥ seek target after start(); target={seek_target:?} pts={pts:?}"
479 );
480 assert!(
481 clock.is_running(),
482 "clock must be running after set_position + start()"
483 );
484 }
485
486 #[test]
487 fn set_position_while_paused_should_update_frozen_time() {
488 let mut clock = PlaybackClock::new();
489 clock.start();
490 thread::sleep(Duration::from_millis(5));
491 clock.pause();
492
493 let seek_target = Duration::from_secs(10);
494 clock.set_position(seek_target);
495
496 let pts = clock.current_pts();
497 assert_eq!(
498 pts, seek_target,
499 "frozen time must update to seek target; expected={seek_target:?} got={pts:?}"
500 );
501 assert!(
502 !clock.is_running(),
503 "clock must remain paused after set_position"
504 );
505
506 clock.resume();
508 thread::sleep(Duration::from_millis(5));
509 let pts_after = clock.current_pts();
510 assert!(
511 pts_after > seek_target,
512 "current_pts() must advance past seek target after resume(); target={seek_target:?} after={pts_after:?}"
513 );
514 }
515
516 #[test]
517 fn set_position_while_running_should_continue_from_new_position() {
518 let mut clock = PlaybackClock::new();
519 clock.start();
520 thread::sleep(Duration::from_millis(5));
521
522 let seek_target = Duration::from_secs(60);
523 clock.set_position(seek_target);
524
525 let pts = clock.current_pts();
526 assert!(
527 pts >= seek_target,
528 "current_pts() must be ≥ seek target immediately after set_position while running; \
529 target={seek_target:?} pts={pts:?}"
530 );
531 assert!(
532 clock.is_running(),
533 "clock must remain running after set_position"
534 );
535 }
536
537 #[test]
538 fn stop_should_clear_seek_offset() {
539 let mut clock = PlaybackClock::new();
540 clock.set_position(Duration::from_secs(30));
541 clock.stop();
542
543 assert_eq!(
544 clock.current_pts(),
545 Duration::ZERO,
546 "stop() must reset seek_offset to ZERO"
547 );
548 }
549
550 #[test]
553 fn master_clock_system_should_advance_from_base_pts() {
554 let clock = MasterClock::System {
555 started_at: Instant::now(),
556 base_pts: Duration::from_secs(5),
557 };
558 let pts = clock.current_pts();
559 assert!(
560 pts >= Duration::from_secs(5),
561 "pts must be >= base_pts; got {pts:?}"
562 );
563 assert!(
564 pts < Duration::from_secs(6),
565 "pts must not advance 1 s in a unit test; got {pts:?}"
566 );
567 assert!(clock.should_sync(), "System clock must always sync");
568 }
569
570 #[test]
571 fn master_clock_system_reset_should_update_base_and_time_reference() {
572 let mut clock = MasterClock::System {
573 started_at: Instant::now() - Duration::from_secs(10),
574 base_pts: Duration::ZERO,
575 };
576 assert!(
577 clock.current_pts() >= Duration::from_secs(9),
578 "clock should show ~10 s before reset"
579 );
580 clock.reset(Duration::from_secs(5));
581 let pts = clock.current_pts();
582 assert!(
583 pts >= Duration::from_secs(5),
584 "pts must be >= new base after reset; got {pts:?}"
585 );
586 assert!(
587 pts < Duration::from_secs(6),
588 "pts must not advance 1 s in a unit test after reset; got {pts:?}"
589 );
590 }
591
592 #[test]
593 fn master_clock_audio_should_not_sync_before_first_sample() {
594 let clock = MasterClock::Audio {
595 samples_consumed: Arc::new(AtomicU64::new(0)),
596 sample_rate: 48_000,
597 };
598 assert!(
599 !clock.should_sync(),
600 "audio clock must not sync before any samples are consumed"
601 );
602 assert_eq!(
603 clock.current_pts(),
604 Duration::ZERO,
605 "audio clock PTS must be zero before any samples"
606 );
607 }
608
609 #[test]
610 fn master_clock_audio_should_sync_and_report_pts_after_samples_consumed() {
611 let consumed = Arc::new(AtomicU64::new(48_000));
612 let clock = MasterClock::Audio {
613 samples_consumed: Arc::clone(&consumed),
614 sample_rate: 48_000,
615 };
616 assert!(
617 clock.should_sync(),
618 "audio clock must sync when samples > 0"
619 );
620 assert_eq!(
621 clock.current_pts(),
622 Duration::from_secs(1),
623 "48000 samples at 48000 Hz must equal 1 second"
624 );
625 }
626}