Skip to main content

dspk_core/time/
rate.rs

1use num_rational::Ratio;
2use serde::{Deserialize, Serialize};
3use std::time::Duration;
4use crate::error::{DspError, DspResult};
5
6/// `value` as the exact fraction of its shortest decimal form (`30000.12` → `750003/25`); a
7/// continued-fraction approximation when that does not fit in `u64`. `value` must be finite and
8/// non-negative.
9pub(crate) fn decimal_ratio(value: f64) -> Ratio<u64> {
10    let text = format!("{value}");
11    let (int, frac) = text.split_once('.').unwrap_or((&text, ""));
12    let digits = format!("{int}{frac}");
13    match (digits.parse::<u64>(), 10u64.checked_pow(frac.len() as u32)) {
14        (Ok(numer), Some(denom)) => Ratio::new(numer, denom),
15        _ => Ratio::<i64>::approximate_float(value)
16            .map(|r| Ratio::new(*r.numer() as u64, *r.denom() as u64))
17            .unwrap_or_else(|| Ratio::from_integer(value.round() as u64)),
18    }
19}
20
21/// Sampling rate, stored as an exact fraction of hertz for drift-free sample ↔ time conversion.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub struct SampleRate {
24    hz: Ratio<u64>,
25}
26
27impl SampleRate {
28    /// The rate as the exact fraction of the decimal value it is given as (`30000.12` →
29    /// `750003/25`, `24414.0625` → `390625/16`).
30    pub fn new(rate_hz: f64) -> DspResult<Self> {
31        if rate_hz <= 0.0 || rate_hz.is_nan() || rate_hz.is_infinite() {
32            return Err(DspError::InvalidSampleRate { rate_hz });
33        }
34        let hz = decimal_ratio(rate_hz);
35        Self::from_ratio(*hz.numer(), *hz.denom())
36    }
37
38    /// The rate `numer / denom` Hz, exactly.
39    pub fn from_ratio(numer: u64, denom: u64) -> DspResult<Self> {
40        if numer == 0 || denom == 0 {
41            return Err(DspError::InvalidSampleRate { rate_hz: if denom == 0 { f64::NAN } else { 0.0 } });
42        }
43        Ok(Self { hz: Ratio::new(numer, denom) })
44    }
45
46    pub fn rate_hz(&self) -> f64 {
47        *self.hz.numer() as f64 / *self.hz.denom() as f64
48    }
49
50    /// The rate as an exact fraction of hertz.
51    pub fn as_ratio(&self) -> Ratio<u64> {
52        self.hz
53    }
54
55    pub fn nyquist_hz(&self) -> f64 {
56        self.rate_hz() / 2.0
57    }
58
59    pub fn sample_period_seconds(&self) -> f64 {
60        1.0 / self.rate_hz()
61    }
62
63    pub fn duration_for_samples(&self, num_samples: usize) -> Duration {
64        Duration::from_secs_f64(num_samples as f64 / self.rate_hz())
65    }
66
67    pub fn samples_for_duration(&self, duration: Duration) -> usize {
68        (duration.as_secs_f64() * self.rate_hz()).round() as usize
69    }
70}