Skip to main content

DriftClock

Struct DriftClock 

Source
pub struct DriftClock { /* private fields */ }
Expand description

A disciplined clock that slaves a smooth pipeline timeline to a real hardware playout rate (M590 A/V sync, phase 2).

The problem it solves: every clock the pipeline elects today is just monotonic_ns(), so there is a single wall-clock timeline and true A/V synchronisation cannot exist. An audio sink actually plays samples at its DAC’s rate, which drifts from wall time by tens to hundreds of ppm. To make audio the master (GStreamer’s model), something has to turn the sink’s coarse, jittery “how many samples have really played” readings into a continuous clock the video sink can pace to. That is this type.

It is fed observations (local_ns, master_ns): local_ns sampled from a reference monotonic clock (via reference_now) and master_ns the true playout position (for audio, (frames_written - snd_pcm_delay()) * 1e9 / rate). Over a sliding window it fits master ≈ slope * local + offset by least squares, and now_ns projects the current reference time through that fit. The regression both estimates the playout rate (slope, ~1.0 plus the drift) and smooths the per-observation jitter, so the timeline the video sink reads is continuous even though the underlying snd_pcm_delay readings step.

The fit is exponentially weighted: the newest sample carries weight 1 and each older one RECENCY_DECAY times its successor, so half of a rate step is taken up 24 samples after it rather than 32. The window is however many observations have arrived, up to its capacity, so the slope is usable from the second one.

A sample landing further than the outlier gate from the current fit is dropped rather than folded in: an underrun recovery or a stale snd_pcm_delay() reading would otherwise bend the fit for a whole window. MAX_CONSECUTIVE_REJECTS rejections in a row mean the timeline genuinely moved (a device re-open), so the window is cleared and the fit restarts from the new samples.

Single-writer by contract: one worker (the audio sink) calls observe; any number of sinks call now_ns. Both are serialised by an internal spin lock, so it is Send + Sync and shares as an Arc<dyn PipelineClock> through clock election. Before the first observation it passes the reference clock through unchanged, so it is usable immediately.

Implementations§

Source§

impl DriftClock

Source

pub const DEFAULT_WINDOW: usize = 64

Default observation window. At a ~10 Hz discipline cadence this is a few seconds of history, long enough to average out snd_pcm_delay jitter without lagging a real rate change.

Source

pub const DEFAULT_OUTLIER_GATE_NS: u64 = 10_000_000

Default outlier gate. An audio sink’s snd_pcm_delay() jitters by a few milliseconds in normal running, so 10 ms only catches a real glitch (an underrun recovery, a stale delay reading).

Source

pub const RECENCY_DECAY: f64 = 0.95

Weight of each sample relative to the one after it, newest first. Chosen against the alternative of no weighting at all: over a full 64-sample window a rate step is taken up twice as fast (0.29 of it after 16 more samples, against 0.15 unweighted) for 37% more slope noise. 0.9 doubles the speed again but costs 2.6x the noise.

Source

pub const MIN_SAMPLES_TO_GATE: usize = 8

Samples the window must hold before the outlier gate applies. Below this the slope is still noisy enough that a good sample could be scored as an outlier against it.

Source

pub const MAX_CONSECUTIVE_REJECTS: u32 = 8

Rejections in a row that mean the timeline moved rather than one sample glitching, so the window restarts.

Source

pub fn new(reference: Arc<dyn PipelineClock + Send + Sync>) -> Self

A drift clock over reference with the DEFAULT_WINDOW and DEFAULT_OUTLIER_GATE_NS.

Source

pub fn with_window( reference: Arc<dyn PipelineClock + Send + Sync>, window: usize, ) -> Self

A drift clock keeping the last window observations (clamped to at least 2, since a slope needs two points).

Source

pub fn with_gate( reference: Arc<dyn PipelineClock + Send + Sync>, outlier_gate_ns: u64, ) -> Self

A drift clock whose outlier gate is outlier_gate_ns rather than the default. A servo over a noisier medium sets its own width: the PTP servo runs at 20 ms, since a queued packet is a much bigger excursion than a DAC’s delay jitter.

Source

pub fn reference_now(&self) -> u64

Sample the reference clock. The disciplining worker must read its local_ns from here so the fit’s domain matches what now_ns projects.

Source

pub fn observe(&self, local_ns: u64, master_ns: u64) -> DriftObservation

Record one (local_ns, master_ns) observation and refit. local_ns must come from reference_now; master_ns is the true playout position. Call this from a single worker.

A sample past the outlier gate is dropped and the fit left alone. A run of them restarts the window. See DriftObservation.

Source

pub fn slope(&self) -> f64

The current playout-rate estimate: d(master)/d(local). 1.0 means no drift; 1.001 means the master runs 0.1% fast relative to the reference. 1.0 before enough samples exist to estimate it.

Source

pub fn observations(&self) -> usize

Number of observations currently in the window. >= 2 means a real two-point (or better) rate estimate is in effect rather than the pass-through / single-point fallback; useful to confirm a live device has actually disciplined the clock.

Source

pub fn project_ns(&self, local_ns: u64) -> u64

Project an arbitrary reference time through the current fit, giving the estimated master time at that reference instant. now_ns is this applied to reference.now_ns(). Used by a servo (eg PTP) to score a fresh observation against the fit before folding it in. Identity before the first observation; a negative projection saturates to 0.

Trait Implementations§

Source§

impl Debug for DriftClock

Available on crate feature runtime only.
Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl PipelineClock for DriftClock

Available on crate feature runtime only.
Source§

fn now_ns(&self) -> u64

Source§

fn as_ticker(&self) -> Option<&dyn DynAsyncClock>

This clock as a deadline sleeper, when it is one. The cooperative runner reads it to derive a fan-in element’s tick timer (PipelinePacket::Tick, M880) from the pipeline clock itself, so a parse_launch line running against any sleepable clock ticks without a separate entry point. Read more
Source§

fn shared_ticker(&self) -> Option<Arc<dyn DynAsyncClock + Send + Sync>>

The same timer as as_ticker, as an owned shared handle. The thread-per-arm runner builds each arm’s future on its own OS thread, which a borrow cannot cross, so that is where it reads the tick from instead. Read more
Source§

fn healthy(&self) -> bool

Whether this clock still holds the reference it disciplines to. A clock that tells time from a source it cannot lose (a monotonic counter, a DAC) is always healthy, which is the default; a disciplined clock (PtpClock) reports false once its master is gone. The runner polls the elected clock and, on a loss, posts BusMessage::ClockLost and re-elects over the candidates that are still healthy.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> ElementBound for T
where T: Send,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.