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}