Skip to main content

lsl_core/
postproc.rs

1//! Timestamp post-processing filters.
2//!
3//! Implements the three standard LSL timestamp corrections:
4//! - **Clocksync**: adds the estimated clock offset (time_correction)
5//! - **Dejitter**: smooths timestamps using an exponential moving average
6//! - **Monotonize**: ensures timestamps are strictly increasing
7
8use crate::types::*;
9
10/// Timestamp post-processing pipeline.
11pub struct TimestampPostProcessor {
12    flags: u32,
13    clock_offset: f64,
14    // Dejitter state
15    _smoothing_halftime: f64,
16    srate: f64,
17    samples_seen: u64,
18    expected_next: f64,
19    alpha: f64,
20    // Monotonize state
21    last_output: f64,
22}
23
24impl TimestampPostProcessor {
25    pub fn new(flags: u32, srate: f64, smoothing_halftime: f32) -> Self {
26        let alpha = if srate > 0.0 && smoothing_halftime > 0.0 {
27            // Exponential smoothing: alpha = 1 - exp(-1 / (halftime * srate))
28            1.0 - (-1.0 / (smoothing_halftime as f64 * srate)).exp()
29        } else {
30            1.0
31        };
32
33        TimestampPostProcessor {
34            flags,
35            clock_offset: 0.0,
36            _smoothing_halftime: smoothing_halftime as f64,
37            srate,
38            samples_seen: 0,
39            expected_next: 0.0,
40            alpha,
41            last_output: 0.0,
42        }
43    }
44
45    /// Update the clock offset (called periodically from time_correction probes).
46    pub fn set_clock_offset(&mut self, offset: f64) {
47        self.clock_offset = offset;
48    }
49
50    /// Process a single timestamp through the enabled filters.
51    pub fn process(&mut self, ts: f64) -> f64 {
52        let mut t = ts;
53
54        // 1. Clock sync: add remote-to-local offset
55        if self.flags & PROC_CLOCKSYNC != 0 {
56            t += self.clock_offset;
57        }
58
59        // 2. Dejitter: exponential smoothing against expected timestamps
60        if self.flags & PROC_DEJITTER != 0 && self.srate > 0.0 {
61            if self.samples_seen == 0 {
62                // First sample: initialize
63                self.expected_next = t;
64            } else {
65                // Expected timestamp based on nominal rate
66                self.expected_next += 1.0 / self.srate;
67                // Blend observed with expected
68                let error = t - self.expected_next;
69                self.expected_next += self.alpha * error;
70            }
71            self.samples_seen += 1;
72            t = self.expected_next;
73        }
74
75        // 3. Monotonize: ensure strictly increasing
76        if self.flags & PROC_MONOTONIZE != 0 && t <= self.last_output {
77            t = self.last_output + 1e-12; // tiny epsilon
78        }
79
80        self.last_output = t;
81        t
82    }
83
84    /// Reset state (e.g. after clock reset).
85    pub fn reset(&mut self) {
86        self.samples_seen = 0;
87        self.expected_next = 0.0;
88        self.last_output = 0.0;
89    }
90}