helena 0.1.0

Core types and component interfaces for helena, a latent data-to-waveform generation platform.
Documentation
//! Time-domain audio — the final output of the pipeline (PRD §8, space `Y`).
//!
//! [`Pcm`] is the domain type for interleaved `f32` audio. Its invariants —
//! nonzero rate, a closed [`ChannelLayout`], whole interleaved frames, and
//! *finite samples* — hold from construction on, so downstream consumers
//! never re-scan for NaNs or special-case a zero rate (the two most repeated
//! runtime checks in the previous architecture).

use serde::{Deserialize, Serialize};

use crate::error::{Error, Result};
use crate::time::{SampleRate, Samples};

/// Channel layout of interleaved PCM (PRD FR-014 scopes helena to mono and
/// stereo; a wider layout is a deliberate future decision, not a `u16`).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChannelLayout {
    /// One channel.
    Mono,
    /// Two interleaved channels, left first.
    Stereo,
}

impl ChannelLayout {
    /// Number of interleaved channels.
    pub const fn channels(self) -> usize {
        match self {
            ChannelLayout::Mono => 1,
            ChannelLayout::Stereo => 2,
        }
    }

    /// Layout for a channel count, if helena supports it.
    pub fn from_channels(channels: usize) -> Result<Self> {
        match channels {
            1 => Ok(ChannelLayout::Mono),
            2 => Ok(ChannelLayout::Stereo),
            other => Err(Error::validation(format!(
                "unsupported channel count {other}: helena audio is mono or stereo (FR-014)"
            ))),
        }
    }
}

impl std::fmt::Display for ChannelLayout {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ChannelLayout::Mono => write!(f, "mono"),
            ChannelLayout::Stereo => write!(f, "stereo"),
        }
    }
}

/// Interleaved `f32` PCM audio, samples nominally in `[-1.0, 1.0]`.
///
/// Invariants, enforced by every constructor and re-checked on
/// deserialization: a nonzero [`SampleRate`], a whole number of
/// [`layout`](Self::layout)-channel frames, and finite samples. The fields
/// are private and there is no unchecked constructor, so
/// [`frames`](Self::frames) is exact and consumers may assume finiteness
/// rather than re-proving it. Amplitude range is *nominal* — clipping is a
/// measured quality signal ([`crate::eval`]), not a construction failure.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "PcmRepr")]
pub struct Pcm {
    rate: SampleRate,
    layout: ChannelLayout,
    samples: Vec<f32>,
}

impl Pcm {
    /// Wrap interleaved `samples` at `rate` with the given `layout`.
    ///
    /// Returns [`Error::Validation`] if `samples.len()` is not a whole number
    /// of frames, or [`Error::NonFinite`] if any sample is NaN or infinite.
    pub fn new(rate: SampleRate, layout: ChannelLayout, samples: Vec<f32>) -> Result<Self> {
        if !samples.len().is_multiple_of(layout.channels()) {
            return Err(Error::validation(format!(
                "interleaved sample count {} is not a whole number of {layout} frames",
                samples.len()
            )));
        }
        if !samples.iter().all(|s| s.is_finite()) {
            return Err(Error::NonFinite {
                context: "pcm samples",
            });
        }
        Ok(Self {
            rate,
            layout,
            samples,
        })
    }

    /// A silent clip of `frames` frames.
    ///
    /// Returns [`Error::Validation`] if `frames × channels` overflows `usize`
    /// (an unchecked wrap would allocate a clip whose frame count silently
    /// differs from the one requested).
    pub fn silence(rate: SampleRate, layout: ChannelLayout, frames: usize) -> Result<Self> {
        let len = frames.checked_mul(layout.channels()).ok_or_else(|| {
            Error::validation(format!(
                "silent clip of {frames} frames over {layout} overflows usize"
            ))
        })?;
        Ok(Self {
            rate,
            layout,
            samples: vec![0.0; len],
        })
    }

    /// Samples per second. Never zero.
    pub fn rate(&self) -> SampleRate {
        self.rate
    }

    /// The channel layout.
    pub fn layout(&self) -> ChannelLayout {
        self.layout
    }

    /// Number of interleaved channels (1 or 2).
    pub fn channels(&self) -> usize {
        self.layout.channels()
    }

    /// The interleaved samples, length `frames × channels`, all finite.
    pub fn samples(&self) -> &[f32] {
        &self.samples
    }

    /// Consume into the interleaved samples.
    pub fn into_samples(self) -> Vec<f32> {
        self.samples
    }

    /// Number of frames (samples per channel). Exact by the invariant.
    pub fn frames(&self) -> usize {
        self.samples.len() / self.layout.channels()
    }

    /// Frame count on the PCM clock.
    pub fn len_samples(&self) -> Samples {
        Samples(self.frames() as u64)
    }

    /// Duration in seconds. Total: the rate is nonzero by construction.
    pub fn duration_seconds(&self) -> f64 {
        self.frames() as f64 / f64::from(self.rate.get())
    }
}

/// Deserialization shim: the wire form, routed back through [`Pcm::new`] so a
/// hand-written record cannot deserialize into a clip with a partial trailing
/// frame or non-finite samples.
#[derive(Deserialize)]
struct PcmRepr {
    rate: SampleRate,
    layout: ChannelLayout,
    samples: Vec<f32>,
}

impl TryFrom<PcmRepr> for Pcm {
    type Error = Error;

    fn try_from(repr: PcmRepr) -> Result<Self> {
        Pcm::new(repr.rate, repr.layout, repr.samples)
    }
}

/// One streaming emission: the audio produced by a single
/// [`StreamSession::push`](crate::stream::StreamSession::push) (or the drain
/// tail from `finish`). A distinct type so a block is never confused with a
/// whole clip, and so concatenation — the streaming law's join — validates
/// clock agreement in exactly one place.
#[derive(Clone, Debug, PartialEq)]
pub struct PcmBlock(Pcm);

impl PcmBlock {
    /// Wrap a block of PCM.
    pub fn new(pcm: Pcm) -> Self {
        Self(pcm)
    }

    /// Borrow the block's audio.
    pub fn pcm(&self) -> &Pcm {
        &self.0
    }

    /// Consume into the block's audio.
    pub fn into_pcm(self) -> Pcm {
        self.0
    }

    /// Concatenate blocks into one clip, demanding rate and layout agreement.
    ///
    /// Returns [`Error::Validation`] on an empty block list and
    /// [`Error::ClockMismatch`]-class validation on disagreement. This is the
    /// join the streaming law compares against the offline decode.
    pub fn concat(blocks: impl IntoIterator<Item = PcmBlock>) -> Result<Pcm> {
        let mut blocks = blocks.into_iter();
        let Some(first) = blocks.next() else {
            return Err(Error::validation("cannot concatenate zero pcm blocks"));
        };
        let (rate, layout) = (first.0.rate, first.0.layout);
        let mut samples = first.0.samples;
        for block in blocks {
            if block.0.rate != rate || block.0.layout != layout {
                return Err(Error::validation(format!(
                    "pcm block clock/layout mismatch: {} {} vs {} {}",
                    rate, layout, block.0.rate, block.0.layout
                )));
            }
            samples.extend_from_slice(&block.0.samples);
        }
        // Re-wrapping is infallible in practice (inputs were valid), but the
        // constructor stays the single gate.
        Pcm::new(rate, layout, samples)
    }
}

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

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

    #[test]
    fn frames_and_duration() {
        let p = Pcm::silence(rate(8_000), ChannelLayout::Stereo, 4_000).unwrap();
        assert_eq!(p.samples().len(), 8_000);
        assert_eq!(p.frames(), 4_000);
        assert!((p.duration_seconds() - 0.5).abs() < 1e-12);
    }

    #[test]
    fn new_rejects_partial_frames_and_non_finite() {
        assert!(Pcm::new(rate(8_000), ChannelLayout::Stereo, vec![0.0; 3]).is_err());
        assert!(matches!(
            Pcm::new(rate(8_000), ChannelLayout::Mono, vec![0.0, f32::NAN]),
            Err(Error::NonFinite { .. })
        ));
        assert!(Pcm::new(rate(8_000), ChannelLayout::Stereo, vec![0.0; 4]).is_ok());
        assert!(Pcm::silence(rate(8_000), ChannelLayout::Stereo, usize::MAX).is_err());
    }

    #[test]
    fn layout_is_closed_and_checked() {
        assert_eq!(
            ChannelLayout::from_channels(1).unwrap(),
            ChannelLayout::Mono
        );
        assert_eq!(
            ChannelLayout::from_channels(2).unwrap(),
            ChannelLayout::Stereo
        );
        assert!(ChannelLayout::from_channels(0).is_err());
        assert!(ChannelLayout::from_channels(6).is_err());
    }

    #[test]
    fn deserialize_enforces_the_invariant() {
        let good = r#"{"rate":8000,"layout":"stereo","samples":[0.0,1.0,0.5,0.25]}"#;
        let p: Pcm = serde_json::from_str(good).unwrap();
        assert_eq!(p.frames(), 2);

        let partial = r#"{"rate":8000,"layout":"stereo","samples":[0.0,1.0,0.5]}"#;
        assert!(serde_json::from_str::<Pcm>(partial).is_err());
        let zero_rate = r#"{"rate":0,"layout":"mono","samples":[0.0]}"#;
        assert!(serde_json::from_str::<Pcm>(zero_rate).is_err());
    }

    #[test]
    fn block_concat_joins_and_checks() {
        let a = PcmBlock::new(Pcm::new(rate(8_000), ChannelLayout::Mono, vec![1.0, 2.0]).unwrap());
        let b = PcmBlock::new(Pcm::new(rate(8_000), ChannelLayout::Mono, vec![3.0]).unwrap());
        let joined = PcmBlock::concat([a.clone(), b]).unwrap();
        assert_eq!(joined.samples(), &[1.0, 2.0, 3.0]);

        let other_rate =
            PcmBlock::new(Pcm::new(rate(16_000), ChannelLayout::Mono, vec![0.0]).unwrap());
        assert!(PcmBlock::concat([a, other_rate]).is_err());
        assert!(PcmBlock::concat(std::iter::empty::<PcmBlock>()).is_err());
    }
}