mirage-engine 0.1.1

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
use core::time::Duration;
use std::sync::Arc;

/// A sound's samples, or the loaded bytes they are decoded from.
///
/// Build one by hand for computed audio; a loaded one comes from
/// [`Assets::sound`](crate::Assets::sound).
#[derive(Clone, Debug)]
pub struct SoundData {
    source: Source,
}

impl SoundData {
    /// A sound of `rate` frames a second, one sample per frame.
    ///
    /// The rate must be more than zero; checked only in debug builds.
    pub fn mono(rate: u32, samples: Vec<f32>) -> Self {
        Self::from_samples(SampleRate::new(rate), Channels::Mono, samples)
    }

    /// A sound of `rate` frames a second, `samples` interleaved left then
    /// right.
    ///
    /// The rate must be more than zero and the length even; checked only in
    /// debug builds.
    pub fn stereo(rate: u32, samples: Vec<f32>) -> Self {
        debug_assert_eq!(
            samples.len() % 2,
            0,
            "a stereo sound needs a left and a right sample per frame"
        );

        Self::from_samples(SampleRate::new(rate), Channels::Stereo, samples)
    }

    /// How long the sound plays for, at its own pitch.
    pub fn duration(&self) -> Duration {
        self.rate().duration_of(self.frames())
    }

    /// Decodes the sound while it plays, in place of keeping every sample in
    /// memory.
    ///
    /// For music and other long loaded clips. A sound built by hand is
    /// already in memory, so this returns it as it is, with a debug log.
    #[must_use]
    pub fn streamed(self) -> Self {
        let source = match self.source {
            Source::Resident(clip) | Source::Streamed(clip) => Source::Streamed(clip),
            Source::Samples(samples) => {
                log::debug!("a sound built out of samples is already in memory, not streamed");
                Source::Samples(samples)
            }
        };

        Self { source }
    }

    /// A sound with nothing to play — what a name that never resolved
    /// becomes.
    pub(crate) fn empty() -> Self {
        Self::from_samples(SampleRate::new(1), Channels::Mono, Vec::new())
    }

    /// A loaded clip, decoded before it plays until [`SoundData::streamed`]
    /// sets it streamed.
    pub(crate) fn loaded(clip: Arc<Encoded>) -> Self {
        Self {
            source: Source::Resident(clip),
        }
    }

    pub(crate) fn source(&self) -> &Source {
        &self.source
    }

    pub(crate) fn rate(&self) -> SampleRate {
        match &self.source {
            Source::Samples(samples) => samples.rate,
            Source::Resident(clip) | Source::Streamed(clip) => clip.rate(),
        }
    }

    pub(crate) fn channels(&self) -> Channels {
        match &self.source {
            Source::Samples(samples) => samples.channels,
            Source::Resident(clip) | Source::Streamed(clip) => clip.channels(),
        }
    }

    fn frames(&self) -> u64 {
        match &self.source {
            Source::Samples(samples) => {
                samples.values.len() as u64 / samples.channels.count() as u64
            }
            Source::Resident(clip) | Source::Streamed(clip) => clip.frames(),
        }
    }

    fn from_samples(rate: SampleRate, channels: Channels, samples: Vec<f32>) -> Self {
        Self {
            source: Source::Samples(Samples {
                rate,
                channels,
                values: samples.into(),
            }),
        }
    }
}

/// Source of a sound's samples, and whether a loaded one is decoded before
/// it plays or while it plays.
#[derive(Clone, Debug)]
pub(crate) enum Source {
    Samples(Samples),
    Resident(Arc<Encoded>),
    Streamed(Arc<Encoded>),
}

/// Samples in memory, interleaved, one value per channel per frame.
#[derive(Clone, Debug)]
pub(crate) struct Samples {
    pub(crate) rate: SampleRate,
    pub(crate) channels: Channels,
    pub(crate) values: Arc<[f32]>,
}

/// How many samples one frame of a sound holds: one, or two for left and
/// right.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum Channels {
    Mono,
    Stereo,
}

impl Channels {
    pub(crate) fn count(self) -> usize {
        match self {
            Self::Mono => 1,
            Self::Stereo => 2,
        }
    }

    /// The channels a source with `count` of them decodes to: `1` stays
    /// mono, anything else becomes stereo by keeping only the first two
    /// channels and dropping the rest.
    pub(crate) fn of(count: u8) -> Self {
        match count {
            1 => Self::Mono,
            _ => Self::Stereo,
        }
    }
}

/// A sound the engine can play: samples already in memory, or an encoded
/// clip decoded a packet at a time as it plays.
#[derive(Debug)]
pub(crate) struct Clip {
    pub(crate) rate: SampleRate,
    pub(crate) channels: Channels,
    pub(crate) frames: u64,
    pub(crate) body: Body,
}

/// Source of a clip's samples while it plays.
#[derive(Debug)]
pub(crate) enum Body {
    Samples(Arc<[f32]>),
    Encoded(Arc<Encoded>),
}

/// A loaded clip's bytes exactly as its source held them, plus the rate,
/// channels, and frame count that decoding it once at startup reported.
#[derive(Debug)]
pub(crate) struct Encoded {
    bytes: Arc<[u8]>,
    rate: SampleRate,
    channels: Channels,
    frames: u64,
    /// A frame position about every second, ascending, that a seek starts
    /// from.
    seeks: Vec<ClipFrame>,
}

impl Encoded {
    pub(crate) fn new(
        bytes: Arc<[u8]>,
        rate: SampleRate,
        channels: Channels,
        frames: u64,
        seeks: Vec<ClipFrame>,
    ) -> Self {
        Self {
            bytes,
            rate,
            channels,
            frames,
            seeks,
        }
    }

    pub(crate) fn bytes(&self) -> &Arc<[u8]> {
        &self.bytes
    }

    pub(crate) fn rate(&self) -> SampleRate {
        self.rate
    }

    pub(crate) fn channels(&self) -> Channels {
        self.channels
    }

    pub(crate) fn frames(&self) -> u64 {
        self.frames
    }

    /// The last indexed position at or before `frame`, which a seek to it
    /// starts from.
    pub(crate) fn seek_before(&self, frame: ClipFrame) -> ClipFrame {
        self.seeks
            .partition_point(|&indexed| indexed <= frame)
            .checked_sub(1)
            .map_or(ClipFrame::ZERO, |at| self.seeks[at])
    }
}

/// Frames of audio a second; more than zero.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct SampleRate(u32);

impl SampleRate {
    /// `rate` frames a second; the rate must be more than zero, checked only
    /// in debug builds, and held to at least one otherwise.
    pub(crate) const fn new(rate: u32) -> Self {
        debug_assert!(
            rate > 0,
            "a sound needs a rate of more than zero frames a second"
        );

        Self(if rate > 0 { rate } else { 1 })
    }

    /// How long `frames` take to play at this rate.
    pub(crate) fn duration_of(self, frames: u64) -> Duration {
        Duration::from_secs_f64(frames as f64 / f64::from(self.0))
    }

    /// The frame count at `rate` that spans the same duration as `frames` of
    /// this rate.
    pub(crate) fn frames_at(self, frames: u64, rate: SampleRate) -> u64 {
        (frames * u64::from(rate)).div_ceil(u64::from(self))
    }

    /// The clip frame nearest to `span` at this rate: `span` is a
    /// nanosecond count, so it rounds to that frame, not the frame before
    /// it.
    pub(crate) fn frame_at(self, span: Duration) -> ClipFrame {
        ClipFrame::new((span.as_secs_f64() * f64::from(self.0)).round() as u64)
    }
}

/// A position on a clip's own timeline, in frames.
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) struct ClipFrame(u64);

impl ClipFrame {
    pub(crate) const ZERO: Self = Self(0);

    pub(crate) const fn new(frame: u64) -> Self {
        Self(frame)
    }

    pub(crate) const fn get(self) -> u64 {
        self.0
    }

    /// Frames from `other` up to `self`; zero when `other` is past `self`.
    pub(crate) fn saturating_sub(self, other: Self) -> u64 {
        self.0.saturating_sub(other.0)
    }

    /// `self` less `count` frames, held at zero rather than wrapping under
    /// it.
    pub(crate) fn back(self, count: u64) -> Self {
        Self(self.0.saturating_sub(count))
    }
}

impl core::ops::Add for ClipFrame {
    type Output = Self;

    fn add(self, other: Self) -> Self {
        Self(self.0 + other.0)
    }
}

impl core::ops::Sub for ClipFrame {
    type Output = Self;

    fn sub(self, other: Self) -> Self {
        Self(self.0 - other.0)
    }
}

impl core::ops::Rem for ClipFrame {
    type Output = Self;

    fn rem(self, other: Self) -> Self {
        Self(self.0 % other.0)
    }
}

impl core::ops::Add<u64> for ClipFrame {
    type Output = Self;

    fn add(self, count: u64) -> Self {
        Self(self.0 + count)
    }
}

impl core::ops::AddAssign<u64> for ClipFrame {
    fn add_assign(&mut self, count: u64) {
        self.0 += count;
    }
}

impl From<SampleRate> for u64 {
    fn from(rate: SampleRate) -> Self {
        Self::from(rate.0)
    }
}

impl From<SampleRate> for f64 {
    fn from(rate: SampleRate) -> Self {
        f64::from(rate.0)
    }
}

impl From<SampleRate> for f32 {
    fn from(rate: SampleRate) -> Self {
        rate.0 as f32
    }
}

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

    #[test]
    fn a_sound_lasts_as_long_as_its_frames_take_at_its_rate() {
        let mono = SoundData::mono(8_000, vec![0.0; 4_000]);
        let stereo = SoundData::stereo(8_000, vec![0.0; 4_000]);

        assert_eq!(mono.duration(), Duration::from_millis(500));
        assert_eq!(stereo.duration(), Duration::from_millis(250));
        assert_eq!(SoundData::empty().duration(), Duration::ZERO);
    }

    #[test]
    fn a_seek_starts_from_the_last_indexed_position_before_it() {
        let clip = Encoded::new(
            Arc::from(&b""[..]),
            SampleRate::new(44_100),
            Channels::Mono,
            132_300,
            vec![
                ClipFrame::new(0),
                ClipFrame::new(44_100),
                ClipFrame::new(88_200),
            ],
        );

        assert_eq!(clip.seek_before(ClipFrame::new(0)).get(), 0);
        assert_eq!(clip.seek_before(ClipFrame::new(44_099)).get(), 0);
        assert_eq!(clip.seek_before(ClipFrame::new(44_100)).get(), 44_100);
        assert_eq!(clip.seek_before(ClipFrame::new(120_000)).get(), 88_200);
    }
}