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
impl DriftClock
Sourcepub const DEFAULT_WINDOW: usize = 64
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.
Sourcepub const DEFAULT_OUTLIER_GATE_NS: u64 = 10_000_000
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).
Sourcepub const RECENCY_DECAY: f64 = 0.95
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.
Sourcepub const MIN_SAMPLES_TO_GATE: usize = 8
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.
Sourcepub const MAX_CONSECUTIVE_REJECTS: u32 = 8
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.
Sourcepub fn new(reference: Arc<dyn PipelineClock + Send + Sync>) -> Self
pub fn new(reference: Arc<dyn PipelineClock + Send + Sync>) -> Self
A drift clock over reference with the DEFAULT_WINDOW
and DEFAULT_OUTLIER_GATE_NS.
Sourcepub fn with_window(
reference: Arc<dyn PipelineClock + Send + Sync>,
window: usize,
) -> Self
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).
Sourcepub fn with_gate(
reference: Arc<dyn PipelineClock + Send + Sync>,
outlier_gate_ns: u64,
) -> Self
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.
Sourcepub fn reference_now(&self) -> u64
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.
Sourcepub fn observe(&self, local_ns: u64, master_ns: u64) -> DriftObservation
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.
Sourcepub fn slope(&self) -> f64
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.
Sourcepub fn observations(&self) -> usize
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.
Sourcepub fn project_ns(&self, local_ns: u64) -> u64
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.
impl Debug for DriftClock
runtime only.Source§impl PipelineClock for DriftClock
Available on crate feature runtime only.
impl PipelineClock for DriftClock
runtime only.fn now_ns(&self) -> u64
Source§fn as_ticker(&self) -> Option<&dyn DynAsyncClock>
fn as_ticker(&self) -> Option<&dyn DynAsyncClock>
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 moreSource§fn healthy(&self) -> bool
fn healthy(&self) -> bool
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.