helena 0.1.0

Core types and component interfaces for helena, a latent data-to-waveform generation platform.
Documentation
//! Clocks and counts: the time vocabulary every framed value carries.
//!
//! helena runs on two clocks. Time-domain audio ticks in *samples* at a
//! [`SampleRate`]; framed latents tick in *frames* at that rate divided by a
//! stride. [`TimeBase`] keeps the latter exact as the `(rate, stride)` pair
//! rather than a lossy frames-per-second float, so frames↔samples arithmetic
//! is integer-exact and "which clock am I in" is answered by the type:
//! [`Samples`] and [`Frames`] are distinct counts that do not mix.
//!
//! Joining two clocked values (a codec and a generator, a loss and its
//! target) demands [`TimeBase`] equality once, at construction of the joint
//! object; thereafter the type carries the agreement. Equality is
//! representational — `88.2 kHz / 1024` is *not* the same clock as
//! `44.1 kHz / 512` even though both tick at 86.13 Hz — because a joint
//! object must share literal sample arithmetic, not just a rate.

use std::num::NonZeroU32;

use serde::{Deserialize, Serialize};

use crate::error::Error;

/// Samples per second. Zero is unrepresentable: the old `sample_rate == 0`
/// sentinel (and its downstream special cases) cannot be constructed.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(into = "u32", try_from = "u32")]
pub struct SampleRate(NonZeroU32);

impl SampleRate {
    /// Wrap a nonzero rate. Infallible: the argument already proves the
    /// invariant.
    pub const fn new(rate: NonZeroU32) -> Self {
        Self(rate)
    }

    /// The rate in hertz.
    pub const fn get(self) -> u32 {
        self.0.get()
    }
}

impl TryFrom<u32> for SampleRate {
    type Error = Error;

    fn try_from(rate: u32) -> Result<Self, Error> {
        NonZeroU32::new(rate)
            .map(Self)
            .ok_or_else(|| Error::validation("sample rate must be nonzero"))
    }
}

impl From<SampleRate> for u32 {
    fn from(rate: SampleRate) -> u32 {
        rate.get()
    }
}

impl std::fmt::Display for SampleRate {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} Hz", self.get())
    }
}

/// A count on the PCM clock (samples per channel, i.e. audio frames of the
/// waveform). Distinct from [`Frames`] so the two clocks cannot be confused.
#[derive(
    Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(transparent)]
pub struct Samples(pub u64);

impl Samples {
    /// The raw count.
    pub const fn get(self) -> u64 {
        self.0
    }
}

impl std::fmt::Display for Samples {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} samples", self.0)
    }
}

/// A count on the latent clock (framed-sequence steps).
#[derive(
    Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(transparent)]
pub struct Frames(pub u64);

impl Frames {
    /// The raw count.
    pub const fn get(self) -> u64 {
        self.0
    }
}

impl std::fmt::Display for Frames {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} frames", self.0)
    }
}

/// The clock of a framed sequence: one frame per `stride` samples at `rate`.
///
/// Kept exact as the pair rather than a frames-per-second float, so
/// [`samples`](Self::samples) is integer arithmetic and two values that must
/// share sample arithmetic can be checked for *representational* equality
/// (see the module doc for why representational, not numeric).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TimeBase {
    rate: SampleRate,
    stride: NonZeroU32,
}

impl TimeBase {
    /// A latent clock ticking once per `stride` samples at `rate`.
    pub const fn new(rate: SampleRate, stride: NonZeroU32) -> Self {
        Self { rate, stride }
    }

    /// The PCM clock itself: one frame per sample.
    pub const fn of_samples(rate: SampleRate) -> Self {
        Self {
            rate,
            stride: NonZeroU32::MIN,
        }
    }

    /// The underlying sample rate.
    pub const fn sample_rate(self) -> SampleRate {
        self.rate
    }

    /// Samples per frame.
    pub const fn stride(self) -> NonZeroU32 {
        self.stride
    }

    /// Frames per second, for display and budget arithmetic. The exact value
    /// lives in the pair; this projection is lossy and never drives layout.
    pub fn frames_per_second(self) -> f64 {
        f64::from(self.rate.get()) / f64::from(self.stride.get())
    }

    /// Exact sample count covered by `frames`, or `None` on `u64` overflow.
    pub fn samples(self, frames: Frames) -> Option<Samples> {
        frames
            .get()
            .checked_mul(u64::from(self.stride.get()))
            .map(Samples)
    }

    /// Frame count that exactly covers `samples`, or `None` if `samples` is
    /// not a whole number of frames. Partial frames are a caller decision
    /// (pad or reject), never a silent truncation.
    pub fn frames_exact(self, samples: Samples) -> Option<Frames> {
        let stride = u64::from(self.stride.get());
        samples
            .get()
            .is_multiple_of(stride)
            .then(|| Frames(samples.get() / stride))
    }

    /// Duration of `frames` in seconds.
    pub fn seconds(self, frames: Frames) -> f64 {
        // Overflow-safe: promote to f64 before multiplying.
        frames.get() as f64 * f64::from(self.stride.get()) / f64::from(self.rate.get())
    }
}

impl std::fmt::Display for TimeBase {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} Hz / {}", self.rate.get(), self.stride.get())
    }
}

/// Demand that two clocks agree, once, at a construction joint.
///
/// The single shared gate every joint constructor routes through, so the
/// [`Error::ClockMismatch`] report is uniform.
pub fn require_same_clock(left: TimeBase, right: TimeBase) -> Result<TimeBase, Error> {
    if left == right {
        Ok(left)
    } else {
        Err(Error::ClockMismatch { left, right })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn rate(hz: u32) -> SampleRate {
        SampleRate::try_from(hz).unwrap()
    }

    fn stride(n: u32) -> NonZeroU32 {
        NonZeroU32::new(n).unwrap()
    }

    #[test]
    fn zero_sample_rate_is_unrepresentable() {
        assert!(SampleRate::try_from(0).is_err());
        assert!(serde_json::from_str::<SampleRate>("0").is_err());
        assert_eq!(
            serde_json::from_str::<SampleRate>("44100").unwrap().get(),
            44_100
        );
    }

    #[test]
    fn frames_to_samples_is_exact() {
        let tb = TimeBase::new(rate(44_100), stride(512));
        assert_eq!(tb.samples(Frames(3)), Some(Samples(1536)));
        assert_eq!(tb.frames_exact(Samples(1536)), Some(Frames(3)));
        // A partial frame is a caller decision, not a truncation.
        assert_eq!(tb.frames_exact(Samples(1537)), None);
    }

    #[test]
    fn samples_overflow_is_none_not_wrap() {
        let tb = TimeBase::new(rate(48_000), stride(u32::MAX));
        assert_eq!(tb.samples(Frames(u64::MAX)), None);
    }

    #[test]
    fn equality_is_representational() {
        let a = TimeBase::new(rate(44_100), stride(512));
        let b = TimeBase::new(rate(88_200), stride(1024));
        // Same frames-per-second, different sample arithmetic: not one clock.
        assert!((a.frames_per_second() - b.frames_per_second()).abs() < 1e-9);
        assert_ne!(a, b);
        assert!(require_same_clock(a, b).is_err());
        assert_eq!(require_same_clock(a, a).unwrap(), a);
    }

    #[test]
    fn seconds_and_display() {
        let tb = TimeBase::new(rate(48_000), stride(480));
        assert!((tb.seconds(Frames(100)) - 1.0).abs() < 1e-12);
        assert_eq!(tb.to_string(), "48000 Hz / 480");
        assert_eq!(TimeBase::of_samples(rate(48_000)).stride().get(), 1);
    }

    #[test]
    fn serde_round_trips() {
        let tb = TimeBase::new(rate(44_100), stride(512));
        let json = serde_json::to_string(&tb).unwrap();
        assert_eq!(serde_json::from_str::<TimeBase>(&json).unwrap(), tb);
    }
}