Skip to main content

flexaudio_core/
clock.rs

1//! デバイス PTS / 単調クロックを共通の `i64` ナノ秒へ正規化するヘルパ。
2//!
3//! ループバック系はデバイスクロック由来の PTS を持ち、マイク(cpal)系は
4//! 相対的・不透明なタイムスタンプしか持たない。コアは両者を「open 時に
5//! 取得した原点オフセット」で共通の単調クロックへ写像する。クロスストリーム
6//! 厳密同期はベストエフォート。
7
8use std::time::Instant;
9
10/// プロセス単調クロックの現在値をナノ秒で返す。
11///
12/// [`Instant`] 基準の単調値。絶対的な壁時計時刻ではなく、
13/// 同一プロセス内での差分・順序付けにのみ意味がある。
14pub fn monotonic_now_ns() -> i64 {
15    monotonic_base().elapsed().as_nanos() as i64
16}
17
18/// プロセス起動時に一度だけ確定する単調クロックの原点。
19fn monotonic_base() -> Instant {
20    use std::sync::OnceLock;
21    static BASE: OnceLock<Instant> = OnceLock::new();
22    *BASE.get_or_init(Instant::now)
23}
24
25/// デバイス PTS を共通単調クロックへ正規化する。
26///
27/// 初回サンプルで「デバイス PTS の原点」と「単調クロックの原点」を記録し、
28/// 以降は `device_pts - device_origin + monotonic_origin` を返すことで、
29/// デバイスクロックの歩度を保ちつつ単調クロック軸へ平行移動する。
30///
31/// マイクのようにデバイス PTS を持たない経路では、各サンプルの到着時刻
32/// ([`monotonic_now_ns`])を device_pts として渡せばよい。
33#[derive(Debug, Clone)]
34pub struct ClockNormalizer {
35    /// 初回サンプルで記録するデバイス PTS の原点(ns)。
36    device_origin_ns: Option<i64>,
37    /// 初回サンプルで記録する単調クロックの原点(ns)。
38    monotonic_origin_ns: i64,
39}
40
41impl ClockNormalizer {
42    /// 新しい正規化器を作る。原点はまだ未確定(最初の [`normalize`](Self::normalize) で確定)。
43    pub fn new() -> Self {
44        Self {
45            device_origin_ns: None,
46            monotonic_origin_ns: 0,
47        }
48    }
49
50    /// まだ原点が確定していない(最初のサンプル未到着)か。
51    pub fn is_unset(&self) -> bool {
52        self.device_origin_ns.is_none()
53    }
54
55    /// デバイス PTS(ns)を正規化済み単調 PTS(ns)へ写像する。
56    ///
57    /// 初回呼び出しで原点を確定し、その時点の [`monotonic_now_ns`] を
58    /// 単調原点として採用する。以降はデバイスクロックの差分を保つ。
59    pub fn normalize(&mut self, device_pts_ns: i64) -> i64 {
60        match self.device_origin_ns {
61            Some(origin) => self.monotonic_origin_ns + device_pts_ns.wrapping_sub(origin),
62            None => {
63                self.device_origin_ns = Some(device_pts_ns);
64                self.monotonic_origin_ns = monotonic_now_ns();
65                self.monotonic_origin_ns
66            }
67        }
68    }
69}
70
71impl Default for ClockNormalizer {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn monotonic_is_non_decreasing() {
83        let a = monotonic_now_ns();
84        let b = monotonic_now_ns();
85        assert!(b >= a);
86    }
87
88    #[test]
89    fn first_sample_sets_origin_and_preserves_deltas() {
90        let mut n = ClockNormalizer::new();
91        assert!(n.is_unset());
92        // 初回は device 原点を確定し、単調原点を返す。
93        let t0 = n.normalize(1_000_000); // device pts 1ms
94        assert!(!n.is_unset());
95        // 以降はデバイス差分を保つ: +5ms → 正規化値も +5ms。
96        let t1 = n.normalize(6_000_000);
97        assert_eq!(t1 - t0, 5_000_000);
98        // 後退してもデルタが負として保たれる(呼び出し側がギャップ検知に使う)。
99        let t2 = n.normalize(4_000_000);
100        assert_eq!(t2 - t0, 3_000_000);
101    }
102
103    /// `normalize` の差分は `wrapping_sub` 由来。device_pts が `i64::MAX` 近辺で
104    /// オーバーフローしても panic せず、ラップした差分が単調原点に加算される。
105    /// 原点 `i64::MAX` から +1(ラップして `i64::MIN`)→ デルタは `wrapping_sub` で
106    /// `i64::MIN - i64::MAX = +1`(ラップ)になることを確認する。
107    #[test]
108    fn normalize_wrapping_sub_handles_i64_boundary() {
109        let mut n = ClockNormalizer::new();
110        // 原点を i64::MAX に固定(単調原点はその時の monotonic_now_ns)。
111        let base = n.normalize(i64::MAX);
112        // device_pts を i64::MIN へ(壁時計の桁あふれを模す)。
113        // wrapping_sub(i64::MAX) は +1 にラップする(i64::MIN - i64::MAX = 1 mod 2^64)。
114        let next = n.normalize(i64::MIN);
115        assert_eq!(
116            next.wrapping_sub(base),
117            1,
118            "wrapping_sub 境界: MIN - MAX はラップして +1 のはず"
119        );
120    }
121
122    /// 原点未確定の正規化器を作るたびに、初回 normalize が `is_unset` を false にする。
123    /// 大きな負の device_pts でも初回はその時点の monotonic 原点を返す(差分計算しない)。
124    #[test]
125    fn first_normalize_ignores_device_value_for_origin() {
126        let mut n = ClockNormalizer::new();
127        // 初回は device 値に依らず monotonic_now_ns を返す(原点確定のみ)。
128        let before = monotonic_now_ns();
129        let t0 = n.normalize(i64::MIN);
130        let after = monotonic_now_ns();
131        assert!(
132            t0 >= before && t0 <= after,
133            "初回は monotonic 原点を返すはず: {before} <= {t0} <= {after}"
134        );
135        assert!(!n.is_unset());
136    }
137
138    /// `Default` は `new` と同じ未確定状態を作る。
139    #[test]
140    fn default_is_unset_like_new() {
141        let n = ClockNormalizer::default();
142        assert!(n.is_unset());
143    }
144}