use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
const STALL_PERIODS: u32 = 4;
const STALL_FLOOR: Duration = Duration::from_millis(50);
#[derive(Clone, Debug)]
pub struct PlayClock {
state: Arc<State>,
}
#[derive(Debug)]
struct State {
epoch: Instant,
sample_rate: AtomicU32,
frames_before: AtomicU64,
period: AtomicU64,
called_at: AtomicU64,
latency: AtomicU64,
open: AtomicBool,
furthest: AtomicU64,
}
impl PlayClock {
pub(crate) fn new(sample_rate: u32) -> Self {
PlayClock {
state: Arc::new(State {
epoch: Instant::now(),
sample_rate: AtomicU32::new(sample_rate.max(1)),
frames_before: AtomicU64::new(0),
period: AtomicU64::new(0),
called_at: AtomicU64::new(0),
latency: AtomicU64::new(0),
open: AtomicBool::new(false),
furthest: AtomicU64::new(0),
}),
}
}
pub fn position(&self) -> Duration {
let state = &*self.state;
let called_at = state.called_at.load(Ordering::Acquire);
if called_at == 0 {
return Duration::ZERO;
}
let rate = u64::from(state.sample_rate.load(Ordering::Relaxed));
let frames_before = state.frames_before.load(Ordering::Relaxed);
let period = state.period.load(Ordering::Relaxed);
let latency = state.latency.load(Ordering::Relaxed);
let since_callback = (state.epoch.elapsed().as_nanos() as u64).saturating_sub(called_at - 1);
let period_nanos = frames_to_nanos(period, rate);
let heard = frames_to_nanos(frames_before, rate) + since_callback.min(period_nanos);
let heard = heard.saturating_sub(latency);
let furthest = state.furthest.fetch_max(heard, Ordering::AcqRel).max(heard);
Duration::from_nanos(furthest)
}
pub fn frames(&self) -> u64 {
let state = &*self.state;
state.frames_before.load(Ordering::Relaxed) + state.period.load(Ordering::Relaxed)
}
pub fn latency(&self) -> Duration {
Duration::from_nanos(self.state.latency.load(Ordering::Relaxed))
}
pub fn sample_rate(&self) -> u32 {
self.state.sample_rate.load(Ordering::Relaxed)
}
pub fn is_running(&self) -> bool {
let state = &*self.state;
if !state.open.load(Ordering::Acquire) {
return false;
}
let called_at = state.called_at.load(Ordering::Acquire);
if called_at == 0 {
return false;
}
let rate = u64::from(state.sample_rate.load(Ordering::Relaxed));
let period = frames_to_nanos(state.period.load(Ordering::Relaxed), rate);
let allowance = Duration::from_nanos(period.saturating_mul(u64::from(STALL_PERIODS)))
.max(STALL_FLOOR);
let since = (state.epoch.elapsed().as_nanos() as u64).saturating_sub(called_at - 1);
Duration::from_nanos(since) <= allowance
}
pub(crate) fn retune(&self, sample_rate: u32) {
self.state.sample_rate.store(sample_rate.max(1), Ordering::Relaxed);
}
pub(crate) fn opened(&self) {
self.state.open.store(true, Ordering::Release);
}
pub(crate) fn closed(&self) {
self.state.open.store(false, Ordering::Release);
}
pub(crate) fn advance(&self, frames: u64, latency: Duration) {
let state = &*self.state;
let previous = state.period.swap(frames, Ordering::Relaxed);
state.frames_before.fetch_add(previous, Ordering::Relaxed);
state
.latency
.store(latency.as_nanos().min(u128::from(u64::MAX)) as u64, Ordering::Relaxed);
let now = state.epoch.elapsed().as_nanos() as u64;
state.called_at.store(now.saturating_add(1), Ordering::Release);
}
}
fn frames_to_nanos(frames: u64, rate: u64) -> u64 {
if rate == 0 {
return 0;
}
(u128::from(frames) * 1_000_000_000 / u128::from(rate)).min(u128::from(u64::MAX)) as u64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_clock_that_has_never_been_called_is_at_zero_and_stopped() {
let clock = PlayClock::new(48_000);
assert_eq!(clock.position(), Duration::ZERO);
assert_eq!(clock.frames(), 0);
assert!(!clock.is_running());
clock.opened();
assert!(!clock.is_running());
}
#[test]
fn the_position_counts_what_was_handed_over_less_the_latency() {
let clock = PlayClock::new(48_000);
clock.opened();
clock.advance(48_000, Duration::from_millis(100));
clock.advance(48_000, Duration::from_millis(100));
let position = clock.position();
assert_eq!(clock.frames(), 96_000);
assert!(position >= Duration::from_millis(900), "{position:?}");
assert!(position < Duration::from_millis(1_000), "{position:?}");
assert!(clock.is_running());
}
#[test]
fn between_callbacks_it_moves_on_but_never_past_one_buffer() {
let clock = PlayClock::new(1_000);
clock.opened();
clock.advance(10, Duration::ZERO);
let first = clock.position();
std::thread::sleep(Duration::from_millis(30));
let later = clock.position();
assert!(later > first, "{first:?} → {later:?}");
assert!(later <= Duration::from_millis(10), "ran past its buffer: {later:?}");
assert!(clock.is_running(), "50 ms floor, not four 10 ms periods");
std::thread::sleep(Duration::from_millis(60));
assert!(!clock.is_running(), "callbacks stopped");
}
#[test]
fn it_never_runs_backwards() {
let clock = PlayClock::new(1_000);
clock.opened();
clock.advance(100, Duration::ZERO);
std::thread::sleep(Duration::from_millis(20));
let before = clock.position();
clock.advance(1, Duration::from_millis(500));
assert!(clock.position() >= before);
}
#[test]
fn closing_stops_it_whatever_the_callbacks_did_last() {
let clock = PlayClock::new(48_000);
clock.opened();
clock.advance(480, Duration::ZERO);
assert!(clock.is_running());
clock.closed();
assert!(!clock.is_running());
}
#[test]
fn years_of_frames_do_not_overflow() {
let frames = 192_000_u64 * 60 * 60 * 24 * 365 * 10;
assert_eq!(
frames_to_nanos(frames, 192_000),
10 * 365 * 24 * 60 * 60 * 1_000_000_000
);
assert_eq!(frames_to_nanos(1, 0), 0);
}
}