Skip to main content

kobo_core/
clock.rs

1//! `SharedClock` - the single playback time source (decision 2).
2//!
3//! Both the Player (worker thread) and the UI (main thread) read the SAME
4//! clock, so highlight and audio physically can't drift. The Player writes
5//! position; the UI's ~15 Hz timer reads [`SharedClock::position_s`] and
6//! highlights the `WordMark` whose `time_s` <= position.
7//!
8//! Cross-thread via a relaxed `AtomicU64` (nanoseconds) - cheap and
9//! single-producer (Player) / single-consumer (UI timer).
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::Arc;
12
13/// Shared playback position in nanoseconds (0 = start of playback).
14#[derive(Clone, Debug)]
15pub struct SharedClock(Arc<AtomicU64>);
16
17impl Default for SharedClock {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23impl SharedClock {
24    pub fn new() -> Self {
25        Self(Arc::new(AtomicU64::new(0)))
26    }
27    /// Player (producer) sets the current position.
28    pub fn set_ns(&self, ns: u64) {
29        self.0.store(ns, Ordering::Relaxed);
30    }
31    /// UI timer (consumer) reads the current position in seconds.
32    pub fn position_s(&self) -> f64 {
33        self.0.load(Ordering::Relaxed) as f64 / 1e9
34    }
35}
36
37/// Pick the index of the last `WordMark` whose `time_s` is <= `position_s`
38/// (the currently-highlighted word). Returns None before the first word.
39pub fn highlight_index(word_times: &[f64], position_s: f64) -> Option<usize> {
40    // word_times assumed ascending; find the last index with time_s <= position.
41    let mut found = None;
42    for (i, &t) in word_times.iter().enumerate() {
43        if t <= position_s {
44            found = Some(i);
45        } else {
46            break;
47        }
48    }
49    found
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn highlight_advances_with_clock() {
58        let times = [0.0, 0.5, 1.0, 1.5, 2.0]; // word start times
59        assert_eq!(highlight_index(&times, 0.0), Some(0));
60        assert_eq!(highlight_index(&times, 0.4), Some(0));
61        assert_eq!(highlight_index(&times, 0.5), Some(1));
62        assert_eq!(highlight_index(&times, 1.24), Some(2));
63        assert_eq!(highlight_index(&times, 2.0), Some(4));
64        assert_eq!(highlight_index(&times, 5.0), Some(4)); // past end -> last
65    }
66
67    #[test]
68    fn clock_is_shared_across_clone() {
69        let player_clock = SharedClock::new();
70        let ui_clock = player_clock.clone(); // shared (Arc)
71        assert_eq!(ui_clock.position_s(), 0.0);
72        player_clock.set_ns(1_500_000_000); // 1.5s
73        assert_eq!(ui_clock.position_s(), 1.5); // UI sees the Player's write
74    }
75}