pocketstation 1.0.0

Source-aware desktop audio Session SDK
const MAX_CORRECTION_NS: i64 = 10_000_000;

#[derive(Debug, Clone, Copy)]
pub struct ClockCorrectionController {
    proportional_gain: f64,
    integral_gain: f64,
    integral_error_ns: f64,
    last_offset_ns: i64,
    last_correction_ns: i64,
}

impl ClockCorrectionController {
    pub fn new(proportional_gain: f64, integral_gain: f64) -> Self {
        Self {
            proportional_gain,
            integral_gain,
            integral_error_ns: 0.0,
            last_offset_ns: 0,
            last_correction_ns: 0,
        }
    }

    pub fn tick(&mut self, measured_offset_ns: i64) -> i64 {
        let error_ns = measured_offset_ns as f64;
        self.integral_error_ns += error_ns;
        let correction_ns =
            self.proportional_gain * error_ns + self.integral_gain * self.integral_error_ns;
        self.last_offset_ns = measured_offset_ns;
        self.last_correction_ns = correction_ns
            .round()
            .clamp(-MAX_CORRECTION_NS as f64, MAX_CORRECTION_NS as f64)
            as i64;
        self.last_correction_ns
    }

    pub fn last_offset_ns(&self) -> i64 {
        self.last_offset_ns
    }
    pub fn last_correction_ns(&self) -> i64 {
        self.last_correction_ns
    }
    pub fn integral_error_ns(&self) -> f64 {
        self.integral_error_ns
    }

    pub fn integral_ns(&self) -> f64 {
        self.integral_error_ns
    }
}

impl Default for ClockCorrectionController {
    fn default() -> Self {
        Self::new(0.1, 0.001)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn given_zero_offset_when_corrected_then_correction_is_zero() {
        let mut controller = ClockCorrectionController::default();
        assert_eq!(controller.tick(0), 0);
        assert_eq!(controller.last_offset_ns(), 0);
    }

    #[test]
    fn given_positive_offset_when_corrected_then_correction_is_positive() {
        let mut controller = ClockCorrectionController::default();
        assert!(controller.tick(1_000_000) > 0);
    }

    #[test]
    fn given_negative_offset_when_corrected_then_correction_is_negative() {
        let mut controller = ClockCorrectionController::default();
        assert!(controller.tick(-1_000_000) < 0);
    }

    #[test]
    fn given_large_offset_when_corrected_then_correction_is_clamped() {
        let mut controller = ClockCorrectionController::default();
        assert!(controller.tick(100_000_000).abs() <= MAX_CORRECTION_NS);
    }

    #[test]
    fn given_repeated_offset_when_corrected_then_integral_accumulates() {
        let mut controller = ClockCorrectionController::new(0.0, 1.0);
        controller.tick(1_000);
        controller.tick(1_000);
        assert!(controller.integral_error_ns() > 1_000.0);
    }
}