use crate::rational::Rational;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TimeBase(pub Rational);
impl TimeBase {
pub const fn new(num: i64, den: i64) -> Self {
Self(Rational::new(num, den))
}
pub const fn from_rate(rate: u32) -> Self {
Self(Rational::new(1, rate as i64))
}
pub const fn num(&self) -> i64 {
self.0.num
}
pub const fn den(&self) -> i64 {
self.0.den
}
pub fn as_rational(&self) -> Rational {
self.0
}
pub const fn is_valid(&self) -> bool {
self.0.num != 0 && self.0.den != 0
}
pub fn seconds_of(&self, ticks: i64) -> f64 {
ticks as f64 * self.0.as_f64()
}
pub fn ticks_of(&self, seconds: f64) -> i64 {
if !self.is_valid() || !seconds.is_finite() {
return 0;
}
let scaled = seconds * (self.0.den as f64) / (self.0.num as f64);
if !scaled.is_finite() {
return 0;
}
let rounded = if scaled >= 0.0 {
(scaled + 0.5).floor()
} else {
(scaled - 0.5).ceil()
};
if rounded >= i64::MAX as f64 {
i64::MAX
} else if rounded <= i64::MIN as f64 {
i64::MIN
} else {
rounded as i64
}
}
pub fn rescale(&self, ts: i64, target: TimeBase) -> i64 {
rescale(ts, self.0, target.0)
}
pub fn rescale_rnd(&self, ts: i64, target: TimeBase, rounding: Rounding) -> i64 {
rescale_rnd(ts, self.0, target.0, rounding)
}
pub fn rescale_checked(&self, ts: i64, target: TimeBase) -> Option<i64> {
rescale_checked(ts, self.0, target.0)
}
}
impl TimeBase {
pub const SECONDS: TimeBase = TimeBase::new(1, 1);
pub const MILLIS: TimeBase = TimeBase::new(1, 1_000);
pub const MICROS: TimeBase = TimeBase::new(1, 1_000_000);
pub const NANOS: TimeBase = TimeBase::new(1, 1_000_000_000);
pub const MPEG_TS: TimeBase = TimeBase::new(1, 90_000);
pub const AUDIO_48K: TimeBase = TimeBase::new(1, 48_000);
pub const AUDIO_44K1: TimeBase = TimeBase::new(1, 44_100);
pub const AUDIO_8K: TimeBase = TimeBase::new(1, 8_000);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Timestamp {
pub value: i64,
pub base: TimeBase,
}
impl Timestamp {
pub const fn new(value: i64, base: TimeBase) -> Self {
Self { value, base }
}
pub fn from_seconds(seconds: f64, base: TimeBase) -> Self {
Self::new(base.ticks_of(seconds), base)
}
pub fn seconds(&self) -> f64 {
self.base.seconds_of(self.value)
}
pub fn rescale(&self, target: TimeBase) -> Self {
Self {
value: self.base.rescale(self.value, target),
base: target,
}
}
pub fn rescale_rnd(&self, target: TimeBase, rounding: Rounding) -> Self {
Self {
value: self.base.rescale_rnd(self.value, target, rounding),
base: target,
}
}
pub fn checked_rescale(&self, target: TimeBase) -> Option<Self> {
self.base
.rescale_checked(self.value, target)
.map(|value| Self {
value,
base: target,
})
}
pub fn checked_add_ticks(&self, ticks: i64) -> Option<Self> {
self.value.checked_add(ticks).map(|v| Self {
value: v,
base: self.base,
})
}
pub fn checked_sub_ticks(&self, ticks: i64) -> Option<Self> {
self.value.checked_sub(ticks).map(|v| Self {
value: v,
base: self.base,
})
}
pub fn checked_diff(&self, other: Timestamp) -> Option<i64> {
let other_in_self_base = other.rescale(self.base).value;
self.value.checked_sub(other_in_self_base)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Rounding {
#[default]
NearestAway,
Floor,
Ceil,
TowardZero,
}
fn div_round_abs(p: u128, d: u128, neg: bool, rounding: Rounding) -> u128 {
let q = p / d;
let r = p % d;
let bump = match rounding {
Rounding::NearestAway => r * 2 >= d,
Rounding::Floor => neg && r != 0,
Rounding::Ceil => !neg && r != 0,
Rounding::TowardZero => false,
};
if bump {
q.saturating_add(1)
} else {
q
}
}
fn sat_narrow(neg: bool, q_abs: u128) -> i64 {
if neg {
if q_abs >= 1u128 << 63 {
i64::MIN
} else {
-(q_abs as i64)
}
} else if q_abs > i64::MAX as u128 {
i64::MAX
} else {
q_abs as i64
}
}
fn checked_narrow(neg: bool, q_abs: u128) -> Option<i64> {
if neg {
if q_abs > 1u128 << 63 {
None
} else {
Some((q_abs as i128).wrapping_neg() as i64)
}
} else if q_abs > i64::MAX as u128 {
None
} else {
Some(q_abs as i64)
}
}
fn rescale_factor(from: Rational, to: Rational) -> Option<(i128, u128)> {
let mut num = from.num as i128 * to.den as i128;
let den = from.den as i128 * to.num as i128;
if den == 0 {
return None;
}
if den < 0 {
num = -num;
}
Some((num, den.unsigned_abs()))
}
pub fn rescale(value: i64, from: Rational, to: Rational) -> i64 {
rescale_rnd(value, from, to, Rounding::NearestAway)
}
pub fn rescale_rnd(value: i64, from: Rational, to: Rational, rounding: Rounding) -> i64 {
let Some((num, den)) = rescale_factor(from, to) else {
return 0;
};
let neg = (value < 0) != (num < 0);
let Some(p) = (value.unsigned_abs() as u128).checked_mul(num.unsigned_abs()) else {
return if neg { i64::MIN } else { i64::MAX };
};
sat_narrow(neg && p != 0, div_round_abs(p, den, neg, rounding))
}
pub fn rescale_checked(value: i64, from: Rational, to: Rational) -> Option<i64> {
let (num, den) = rescale_factor(from, to)?;
let neg = (value < 0) != (num < 0);
let p = (value.unsigned_abs() as u128).checked_mul(num.unsigned_abs())?;
checked_narrow(
neg && p != 0,
div_round_abs(p, den, neg, Rounding::NearestAway),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rescale_samples_to_pts() {
assert_eq!(
rescale(48000, Rational::new(1, 48000), Rational::new(1, 1000)),
1000
);
}
#[test]
fn timestamp_seconds() {
let ts = Timestamp::new(48000, TimeBase::new(1, 48000));
assert!((ts.seconds() - 1.0).abs() < 1e-9);
}
#[test]
fn rescale_rounds_half_away_from_zero() {
assert_eq!(rescale(1, Rational::new(1, 2), Rational::new(1, 1)), 1);
assert_eq!(rescale(-1, Rational::new(1, 2), Rational::new(1, 1)), -1);
assert_eq!(rescale(3, Rational::new(1, 2), Rational::new(1, 1)), 2);
assert_eq!(rescale(-3, Rational::new(1, 2), Rational::new(1, 1)), -2);
}
#[test]
fn rescale_saturates_instead_of_wrapping() {
assert_eq!(
rescale(i64::MAX, Rational::new(1, 1), Rational::new(1, 1000)),
i64::MAX
);
assert_eq!(
rescale(i64::MIN, Rational::new(1, 1), Rational::new(1, 1000)),
i64::MIN
);
assert_eq!(
rescale(i64::MAX, Rational::new(-1, 1), Rational::new(1, 1000)),
i64::MIN
);
assert_eq!(
rescale(
i64::MAX,
Rational::new(i64::MAX, 1),
Rational::new(1, i64::MAX)
),
i64::MAX
);
assert_eq!(rescale(5, Rational::new(1, 0), Rational::new(1, 1)), 0);
assert_eq!(rescale(5, Rational::new(1, 1), Rational::new(0, 1)), 0);
}
#[test]
fn rescale_negative_denominator_ties_away_from_zero() {
assert_eq!(rescale(3, Rational::new(1, 1), Rational::new(-2, 1)), -2);
assert_eq!(rescale(-3, Rational::new(1, 1), Rational::new(-2, 1)), 2);
assert_eq!(rescale(4, Rational::new(1, -2), Rational::new(1, 1)), -2);
}
#[test]
fn rescale_checked_reports_failure() {
assert_eq!(
rescale_checked(48000, Rational::new(1, 48000), Rational::new(1, 1000)),
Some(1000)
);
assert_eq!(
rescale_checked(i64::MAX, Rational::new(1, 1), Rational::new(1, 1000)),
None
);
assert_eq!(
rescale_checked(5, Rational::new(1, 0), Rational::new(1, 1)),
None
);
assert_eq!(
rescale_checked(5, Rational::new(1, 1), Rational::new(0, 1)),
None
);
assert_eq!(
rescale_checked(i64::MIN, Rational::new(1, 1), Rational::new(1, 1)),
Some(i64::MIN)
);
assert_eq!(
rescale_checked(i64::MIN, Rational::new(2, 1), Rational::new(1, 1)),
None
);
}
#[test]
fn rescale_rnd_modes() {
let from = Rational::new(1, 2);
let to = Rational::new(1, 1);
assert_eq!(rescale_rnd(5, from, to, Rounding::NearestAway), 3);
assert_eq!(rescale_rnd(5, from, to, Rounding::Floor), 2);
assert_eq!(rescale_rnd(5, from, to, Rounding::Ceil), 3);
assert_eq!(rescale_rnd(5, from, to, Rounding::TowardZero), 2);
assert_eq!(rescale_rnd(-5, from, to, Rounding::NearestAway), -3);
assert_eq!(rescale_rnd(-5, from, to, Rounding::Floor), -3);
assert_eq!(rescale_rnd(-5, from, to, Rounding::Ceil), -2);
assert_eq!(rescale_rnd(-5, from, to, Rounding::TowardZero), -2);
for mode in [
Rounding::NearestAway,
Rounding::Floor,
Rounding::Ceil,
Rounding::TowardZero,
] {
assert_eq!(rescale_rnd(4, from, to, mode), 2);
}
assert_eq!(
rescale_rnd(5, from, to, Rounding::default()),
rescale(5, from, to)
);
}
#[test]
fn timestamp_rescale_rnd_and_checked() {
let ts = Timestamp::new(1, TimeBase::new(1, 3));
assert_eq!(ts.rescale_rnd(TimeBase::MILLIS, Rounding::Floor).value, 333);
assert_eq!(ts.rescale_rnd(TimeBase::MILLIS, Rounding::Ceil).value, 334);
assert_eq!(
ts.rescale_rnd(TimeBase::MILLIS, Rounding::Ceil).base,
TimeBase::MILLIS
);
let ok = Timestamp::new(48_000, TimeBase::AUDIO_48K)
.checked_rescale(TimeBase::MILLIS)
.unwrap();
assert_eq!(ok.value, 1000);
assert_eq!(ok.base, TimeBase::MILLIS);
let edge = Timestamp::new(i64::MAX, TimeBase::SECONDS);
assert!(edge.checked_rescale(TimeBase::MILLIS).is_none());
assert_eq!(edge.rescale(TimeBase::MILLIS).value, i64::MAX);
}
#[test]
fn from_rate_matches_long_form() {
assert_eq!(TimeBase::from_rate(48_000), TimeBase::new(1, 48_000));
assert_eq!(TimeBase::from_rate(90_000), TimeBase::new(1, 90_000));
assert_eq!(TimeBase::from_rate(1), TimeBase::new(1, 1));
}
#[test]
fn num_den_accessors() {
let tb = TimeBase::new(1, 90_000);
assert_eq!(tb.num(), 1);
assert_eq!(tb.den(), 90_000);
const NUM: i64 = TimeBase::AUDIO_48K.num();
const DEN: i64 = TimeBase::AUDIO_48K.den();
assert_eq!(NUM, 1);
assert_eq!(DEN, 48_000);
}
#[test]
fn is_valid_rejects_zero_terms() {
assert!(TimeBase::new(1, 1000).is_valid());
assert!(!TimeBase::new(1, 0).is_valid());
assert!(!TimeBase::new(0, 1).is_valid());
}
#[test]
fn ticks_of_is_inverse_of_seconds_of() {
assert_eq!(TimeBase::AUDIO_48K.ticks_of(1.0), 48_000);
assert_eq!(TimeBase::MPEG_TS.ticks_of(1.0), 90_000);
assert_eq!(TimeBase::MILLIS.ticks_of(0.5), 500);
let tb = TimeBase::AUDIO_44K1;
assert_eq!(tb.ticks_of(tb.seconds_of(44_100)), 44_100);
}
#[test]
fn ticks_of_rounds_half_away_from_zero() {
assert_eq!(TimeBase::SECONDS.ticks_of(0.5), 1);
assert_eq!(TimeBase::SECONDS.ticks_of(-0.5), -1);
assert_eq!(TimeBase::SECONDS.ticks_of(1.5), 2);
assert_eq!(TimeBase::SECONDS.ticks_of(-1.5), -2);
}
#[test]
fn ticks_of_invalid_inputs() {
assert_eq!(TimeBase::new(1, 0).ticks_of(1.0), 0);
assert_eq!(TimeBase::new(0, 1).ticks_of(1.0), 0);
assert_eq!(TimeBase::MILLIS.ticks_of(f64::NAN), 0);
assert_eq!(TimeBase::MILLIS.ticks_of(f64::INFINITY), 0);
assert_eq!(TimeBase::MILLIS.ticks_of(f64::NEG_INFINITY), 0);
}
#[test]
fn common_constants_match_long_form() {
assert_eq!(TimeBase::SECONDS, TimeBase::new(1, 1));
assert_eq!(TimeBase::MILLIS, TimeBase::new(1, 1_000));
assert_eq!(TimeBase::MICROS, TimeBase::new(1, 1_000_000));
assert_eq!(TimeBase::NANOS, TimeBase::new(1, 1_000_000_000));
assert_eq!(TimeBase::MPEG_TS, TimeBase::new(1, 90_000));
assert_eq!(TimeBase::AUDIO_48K, TimeBase::new(1, 48_000));
assert_eq!(TimeBase::AUDIO_44K1, TimeBase::new(1, 44_100));
assert_eq!(TimeBase::AUDIO_8K, TimeBase::new(1, 8_000));
}
#[test]
fn timestamp_from_seconds() {
let ts = Timestamp::from_seconds(1.0, TimeBase::AUDIO_48K);
assert_eq!(ts.value, 48_000);
assert_eq!(ts.base, TimeBase::AUDIO_48K);
assert!((ts.seconds() - 1.0).abs() < 1e-9);
}
#[test]
fn checked_add_sub_ticks_round_trip() {
let ts = Timestamp::new(100, TimeBase::MILLIS);
assert_eq!(ts.checked_add_ticks(50).unwrap().value, 150);
assert_eq!(ts.checked_sub_ticks(50).unwrap().value, 50);
assert_eq!(ts.checked_add_ticks(50).unwrap().base, TimeBase::MILLIS);
}
#[test]
fn checked_add_ticks_detects_overflow() {
let ts = Timestamp::new(i64::MAX - 5, TimeBase::SECONDS);
assert!(ts.checked_add_ticks(10).is_none());
let near_max = Timestamp::new(i64::MAX - 1, TimeBase::SECONDS);
assert_eq!(near_max.checked_add_ticks(1).unwrap().value, i64::MAX);
}
#[test]
fn checked_sub_ticks_detects_overflow() {
let ts = Timestamp::new(i64::MIN + 5, TimeBase::SECONDS);
assert!(ts.checked_sub_ticks(10).is_none());
}
#[test]
fn checked_diff_rescales_other_onto_self_base() {
let a = Timestamp::new(48_000, TimeBase::AUDIO_48K); let b = Timestamp::new(500, TimeBase::MILLIS); assert_eq!(a.checked_diff(b), Some(24_000));
}
#[test]
fn checked_diff_same_base() {
let a = Timestamp::new(1000, TimeBase::MILLIS);
let b = Timestamp::new(250, TimeBase::MILLIS);
assert_eq!(a.checked_diff(b), Some(750));
assert_eq!(b.checked_diff(a), Some(-750));
}
}