#![forbid(unsafe_code)]
#![deny(missing_docs)]
pub const DEFAULT_SMOOTHING_HALFTIME: f64 = 90.0;
pub const SAMPLES_BETWEEN_CLOCKSYNCS: u8 = 50;
pub const MIN_CLOCKSYNC_INTERVAL: f64 = 0.5;
pub mod flags {
pub const NONE: u32 = 0;
pub const CLOCKSYNC: u32 = 1;
pub const DEJITTER: u32 = 2;
pub const MONOTONIZE: u32 = 4;
pub const THREADSAFE: u32 = 8;
pub const ALL: u32 = 1 | 2 | 4 | 8;
}
#[derive(Debug, Clone, PartialEq)]
pub struct Dejitterer {
pub t0: u32,
pub samples_since_t0: u32,
pub w0: f64,
pub w1: f64,
pub p00: f64,
pub p11: f64,
pub p01: f64,
pub lam: f64,
}
impl Default for Dejitterer {
fn default() -> Self {
Dejitterer {
t0: 0,
samples_since_t0: 0,
w0: 0.0,
w1: 0.0,
p00: 1e10,
p11: 1e10,
p01: 0.0,
lam: 0.0,
}
}
}
impl Dejitterer {
pub fn new(t0: f64, srate: f64, halftime: f64) -> Self {
let mut d = Dejitterer {
t0: t0 as u32,
..Default::default()
};
if srate > 0.0 {
d.w1 = 1. / srate;
d.lam = 2f64.powf(-1. / (srate * halftime));
}
d
}
pub fn is_initialized(&self) -> bool {
self.t0 != 0
}
pub fn smoothing_applicable(&self) -> bool {
self.lam > 0.0
}
pub fn dejitter(&mut self, t: f64) -> f64 {
if !self.smoothing_applicable() {
return t;
}
let t = t - self.t0 as f64;
let u1 = self.samples_since_t0 as f64;
self.samples_since_t0 = self.samples_since_t0.wrapping_add(1);
let pi0 = self.p00 + u1 * self.p01;
let pi1 = self.p01 + u1 * self.p11;
let al = t - (self.w0 + u1 * self.w1);
let g_inv = 1. / (self.lam + pi0 + pi1 * u1);
let il_ = 1. / self.lam;
self.p00 = il_ * (self.p00 - pi0 * pi0 * g_inv);
self.p01 = il_ * (self.p01 - pi0 * pi1 * g_inv);
self.p11 = il_ * (self.p11 - pi1 * pi1 * g_inv);
self.w0 += al * (self.p00 + self.p01 * u1);
self.w1 += al * (self.p01 + self.p11 * u1);
self.w0 + u1 * self.w1 + self.t0 as f64
}
pub fn skip_samples(&mut self, skipped: u32) {
self.samples_since_t0 = self.samples_since_t0.wrapping_add(skipped);
}
}
pub trait OffsetSource {
fn correction(&mut self) -> f64;
fn srate(&mut self) -> f64;
fn was_reset(&mut self) -> bool;
fn clock(&mut self) -> f64;
}
pub struct PostProcessor {
options: u32,
halftime: f64,
dejitter: Dejitterer,
samples_since_last_clocksync: u8,
next_query_time: f64,
last_offset: f64,
last_value: f64,
}
impl PostProcessor {
pub fn new() -> Self {
PostProcessor {
options: flags::NONE,
halftime: DEFAULT_SMOOTHING_HALFTIME,
dejitter: Dejitterer::default(),
samples_since_last_clocksync: SAMPLES_BETWEEN_CLOCKSYNCS,
next_query_time: 0.0,
last_offset: 0.0,
last_value: f64::MIN,
}
}
pub fn set_halftime(&mut self, halftime: f64) {
self.halftime = halftime;
}
pub fn options(&self) -> u32 {
self.options
}
pub fn dejitterer(&self) -> &Dejitterer {
&self.dejitter
}
pub fn set_options(&mut self, options: u32) {
let changed = self.options ^ options;
if changed & flags::DEJITTER != 0 {
self.dejitter = Dejitterer::default();
}
if changed & flags::MONOTONIZE != 0 {
self.last_value = f64::MIN;
}
self.options = options;
}
pub fn process(&mut self, value: f64, src: &mut impl OffsetSource) -> f64 {
let mut value = value;
if self.options & flags::CLOCKSYNC != 0 {
self.samples_since_last_clocksync = self.samples_since_last_clocksync.saturating_add(1);
if self.samples_since_last_clocksync > SAMPLES_BETWEEN_CLOCKSYNCS
&& src.clock() > self.next_query_time
{
self.last_offset = src.correction();
self.samples_since_last_clocksync = 0;
if src.was_reset() {
self.last_offset = src.correction();
self.last_value = f64::MIN;
self.dejitter = Dejitterer::default();
}
self.next_query_time = src.clock() + MIN_CLOCKSYNC_INTERVAL;
}
value += self.last_offset;
}
if self.options & flags::DEJITTER != 0 {
if !self.dejitter.is_initialized() {
let srate = src.srate();
self.dejitter = Dejitterer::new(value, srate, self.halftime);
}
value = self.dejitter.dejitter(value);
}
if self.options & flags::MONOTONIZE != 0 {
if value < self.last_value {
value = self.last_value;
} else {
self.last_value = value;
}
}
value
}
pub fn skip_samples(&mut self, skipped: u32) {
if self.options & flags::DEJITTER != 0 && self.dejitter.smoothing_applicable() {
self.dejitter.skip_samples(skipped);
}
}
}
impl Default for PostProcessor {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct FixedSource {
pub offset: f64,
pub srate: f64,
pub reset: bool,
pub now: f64,
}
impl OffsetSource for FixedSource {
fn correction(&mut self) -> f64 {
self.offset
}
fn srate(&mut self) -> f64 {
self.srate
}
fn was_reset(&mut self) -> bool {
self.reset
}
fn clock(&mut self) -> f64 {
self.now
}
}
#[cfg(test)]
mod tests {
use super::*;
fn src(srate: f64) -> FixedSource {
FixedSource {
offset: 0.0,
srate,
reset: false,
now: 1e9,
}
}
#[test]
fn no_stage_changes_a_timestamp() {
let mut p = PostProcessor::new();
let mut s = src(1.0);
for t in [2.0, 3.1, 3.0, 5.0, 5.9, 7.1] {
assert_eq!(p.process(t, &mut s), t);
}
}
#[test]
fn clock_sync_adds_the_offset() {
let mut p = PostProcessor::new();
p.set_options(flags::CLOCKSYNC);
let mut s = src(1.0);
s.offset = -50.0;
for t in [2.0, 3.1, 3.0, 5.0, 5.9, 7.1] {
assert!((p.process(t, &mut s) - (t - 50.0)).abs() < 1e-12);
}
}
#[test]
fn the_clamp_never_lets_a_timestamp_go_backward() {
let mut p = PostProcessor::new();
p.set_options(flags::MONOTONIZE);
let mut s = src(1.0);
let input = [2.0, 3.1, 3.0, 5.0, 5.9, 7.1];
let want = [2.0, 3.1, 3.1, 5.0, 5.9, 7.1];
for (i, t) in input.iter().enumerate() {
assert!(
(p.process(*t, &mut s) - want[i]).abs() < 1e-12,
"sample {i}"
);
}
}
#[test]
fn a_stage_change_clears_that_state() {
let mut p = PostProcessor::new();
p.set_options(flags::MONOTONIZE);
let mut s = src(1.0);
p.process(100.0, &mut s);
p.set_options(flags::NONE);
p.set_options(flags::MONOTONIZE);
assert_eq!(p.process(2.0, &mut s), 2.0);
}
#[test]
fn a_rate_of_zero_leaves_the_filter_off() {
let d = Dejitterer::new(1000.0, 0.0, 90.0);
assert!(!d.smoothing_applicable());
let mut d = d;
assert_eq!(d.dejitter(1234.5), 1234.5);
}
#[test]
fn the_baseline_truncates_to_a_whole_number() {
let d = Dejitterer::new(1000.75, 100.0, 90.0);
assert_eq!(d.t0, 1000);
assert!(d.is_initialized());
}
#[test]
fn a_first_timestamp_below_one_reads_as_uninitialized() {
let d = Dejitterer::new(0.5, 100.0, 90.0);
assert_eq!(d.t0, 0);
assert!(!d.is_initialized());
}
#[test]
fn the_filter_converges_on_a_clean_ramp() {
let srate = 100.0;
let t0 = 5000.0;
let mut d = Dejitterer::new(t0, srate, 90.0);
d.dejitter(t0);
for i in 0..2000 {
let t = t0 + i as f64 / srate;
d.dejitter(t);
}
assert!((d.w1 - 1.0 / srate).abs() < 1e-6, "slope {}", d.w1);
assert!(d.w0.abs() < 0.1, "intercept {}", d.w0);
}
#[test]
fn the_filter_removes_a_constant_latency_from_the_slope() {
let srate = 100.0;
let t0 = 5000.0;
let latency = 0.05;
let mut d = Dejitterer::new(t0, srate, 90.0);
d.dejitter(t0);
for i in 0..5000 {
d.dejitter(t0 + i as f64 / srate + latency);
}
assert!((d.w1 - 1.0 / srate).abs() < 1e-6);
assert!((d.w0 - latency).abs() < 0.1, "intercept {}", d.w0);
}
#[test]
fn skipped_samples_move_the_counter() {
let mut d = Dejitterer::new(1000.0, 100.0, 90.0);
d.dejitter(1000.0);
let before = d.samples_since_t0;
d.skip_samples(7);
assert_eq!(d.samples_since_t0, before + 7);
}
#[test]
fn clock_sync_refreshes_at_most_twice_per_second() {
let mut p = PostProcessor::new();
p.set_options(flags::CLOCKSYNC);
let mut s = src(1.0);
s.offset = 1.0;
s.now = 100.0;
p.process(0.0, &mut s);
s.offset = 99.0;
for _ in 0..50 {
let got = p.process(0.0, &mut s);
assert!((got - 1.0).abs() < 1e-12, "the offset changed too early");
}
assert!((p.process(0.0, &mut s) - 1.0).abs() < 1e-12);
s.now = 101.0;
assert!((p.process(0.0, &mut s) - 99.0).abs() < 1e-12);
}
#[test]
fn a_reset_gives_the_filter_a_new_baseline() {
let mut p = PostProcessor::new();
p.set_options(flags::CLOCKSYNC | flags::DEJITTER);
let mut s = src(100.0);
s.now = 100.0;
p.process(5000.0, &mut s);
assert_eq!(p.dejitterer().t0, 5000);
for i in 1..60 {
p.process(5000.0 + i as f64 / 100.0, &mut s);
}
assert_eq!(p.dejitterer().t0, 5000, "no query fired yet");
s.reset = true;
s.now = 101.0;
p.process(9000.0, &mut s);
assert_eq!(p.dejitterer().t0, 9000, "the reset gives a new baseline");
}
#[test]
fn the_clamp_state_clears_on_a_reset() {
let mut p = PostProcessor::new();
p.set_options(flags::CLOCKSYNC | flags::MONOTONIZE);
let mut s = src(100.0);
s.now = 100.0;
p.process(500.0, &mut s);
for _ in 0..55 {
p.process(500.0, &mut s);
}
assert!((p.process(10.0, &mut s) - 500.0).abs() < 1e-12);
s.reset = true;
s.now = 101.0;
assert!((p.process(10.0, &mut s) - 10.0).abs() < 1e-12);
}
}