mirage-engine 0.1.1

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
//! Turns a clip's frames into the frames a mix takes, at the rate it runs at.

use std::collections::VecDeque;

use crate::sound::data::{Channels, ClipFrame, SampleRate};

/// Input frames the filter reads each side of a read.
const HALF: usize = 32;

/// Weights one output frame reads.
const TAPS: usize = 2 * HALF;

/// Fractions of one input frame the table holds a filter for.
const PHASES: usize = 256;

/// Part of the smaller of the two rates the filter passes; the rest is the
/// room its edge takes.
const PASSED: f64 = 0.95;

/// The window the filter's shape is cut from; it sets how far down the band
/// the filter stops falls.
const BLACKMAN_HARRIS: [f64; 4] = [0.35875, 0.48829, 0.14128, 0.01168];

/// One clip's frames at the rate a mix runs at.
///
/// Frames go in at the clip's own rate and come out at the mix's. Feed it the
/// clip in order, take what it has made, and state where the clip ends; a
/// stream feeds it one packet at a time, keeping the same resampler from one
/// to the next.
pub(crate) struct Resampler {
    channels: usize,
    /// Input frames one output frame advances by, as `from` over `to`.
    from: u64,
    to: u64,
    /// Nothing where the rates match and frames pass through as they are.
    taps: Option<Taps>,
    /// The output frame that comes next.
    at: u64,
    /// Input frame the held samples start at.
    first: u64,
    /// Input samples the filter still reads, interleaved.
    held: VecDeque<f32>,
    /// Output samples made and not taken, interleaved.
    ready: VecDeque<f32>,
}

impl Resampler {
    /// Frames of `from` as frames of `to`, each `channels` wide.
    pub(crate) fn new(from: SampleRate, to: SampleRate, channels: Channels) -> Self {
        let (from, to) = (u64::from(from), u64::from(to));

        Self {
            channels: channels.count(),
            from,
            to,
            taps: (from != to).then(|| Taps::new(from, to)),
            at: 0,
            first: 0,
            held: VecDeque::new(),
            ready: VecDeque::new(),
        }
    }

    /// All of `input`, which holds one clip end to end, at `to`.
    pub(crate) fn whole(
        input: &[f32],
        from: SampleRate,
        to: SampleRate,
        channels: Channels,
    ) -> Vec<f32> {
        let mut resampler = Self::new(from, to, channels);
        let frames = from.frames_at(input.len() as u64 / channels.count() as u64, to);
        let mut made = Vec::with_capacity(frames as usize * channels.count());
        resampler.feed(input);
        resampler.end();
        resampler.take(frames as usize, &mut made);

        made
    }

    /// Reads the clip again from the frame this returns, at the clip's own
    /// rate, so that output frame `at` comes out next; everything held is
    /// dropped.
    pub(crate) fn restart(&mut self, at: ClipFrame) -> ClipFrame {
        self.held.clear();
        self.ready.clear();
        self.at = at.get();
        self.first = self.reads(self.at).0.saturating_sub(self.lead());

        ClipFrame::new(self.first)
    }

    /// Takes the clip's next samples at its own rate, interleaved.
    pub(crate) fn feed(&mut self, input: &[f32]) {
        if self.taps.is_none() {
            self.ready.extend(input);
            return;
        }

        self.held.extend(input);
        self.produce(false);
    }

    /// States that the clip is over: the frames left over read silence past
    /// its end.
    pub(crate) fn end(&mut self) {
        self.produce(true);
    }

    /// Moves up to `frames` made frames into `into`, and reports how many.
    pub(crate) fn take(&mut self, frames: usize, into: &mut impl Extend<f32>) -> usize {
        let taking = frames.min(self.ready.len() / self.channels);
        into.extend(self.ready.drain(..taking * self.channels));

        taking
    }

    /// Makes every output frame the held input reaches, reading silence past
    /// the end of the clip once `ended`.
    fn produce(&mut self, ended: bool) {
        let Some(taps) = &self.taps else {
            return;
        };

        loop {
            let (frame, over) = self.reads(self.at);
            let held = self.first + (self.held.len() / self.channels) as u64;
            let reach = match ended {
                true => frame,
                false => frame + HALF as u64,
            };
            if reach >= held {
                return;
            }

            let read = frame as i64 - HALF as i64 + 1;
            for channel in 0..self.channels {
                let made = taps
                    .weights(over)
                    .enumerate()
                    .map(|(tap, weight)| weight * self.sample(read + tap as i64, channel))
                    .sum();
                self.ready.push_back(made);
            }

            self.at += 1;
            let dropped = (read.max(0) as u64).saturating_sub(self.first);
            self.held.drain(..dropped as usize * self.channels);
            self.first += dropped;
        }
    }

    /// The input frame that output frame `at` reads from, and how far past it
    /// the read lies, in `0..1`.
    fn reads(&self, at: u64) -> (u64, f64) {
        let read = at * self.from;

        (read / self.to, (read % self.to) as f64 / self.to as f64)
    }

    /// Input frames the filter reads before the frame a read lands on.
    fn lead(&self) -> u64 {
        match self.taps {
            Some(_) => HALF as u64 - 1,
            None => 0,
        }
    }

    /// One held input sample, and silence outside what is held.
    fn sample(&self, frame: i64, channel: usize) -> f32 {
        let Ok(held) = usize::try_from(frame - self.first as i64) else {
            return 0.0;
        };

        self.held
            .get(held * self.channels + channel)
            .copied()
            .unwrap_or_default()
    }
}

/// One windowed `sinc` per fraction of an input frame, and one more at the
/// whole frame, so that every read lies between two of them.
struct Taps(Vec<f32>);

impl Taps {
    /// The filter that takes `from` into `to`, cut off below half of
    /// whichever rate is smaller.
    fn new(from: u64, to: u64) -> Self {
        let band = PASSED * to.min(from) as f64 / (2.0 * from as f64);
        let mut rows = Vec::with_capacity((PHASES + 1) * TAPS);
        for phase in 0..=PHASES {
            let over = phase as f64 / PHASES as f64;
            let row: Vec<f64> = (0..TAPS)
                .map(|tap| {
                    let at = (tap + 1) as f64 - HALF as f64 - over;
                    2.0 * band * sinc(2.0 * band * at) * window(at)
                })
                .collect();
            let whole: f64 = row.iter().sum();
            rows.extend(row.iter().map(|weight| (weight / whole) as f32));
        }

        Self(rows)
    }

    /// The weights a read takes `over` of the way past its input frame,
    /// interpolated between the two rows on either side of it; `over` lies in
    /// `0..1`.
    fn weights(&self, over: f64) -> impl Iterator<Item = f32> {
        let place = over * PHASES as f64;
        let row = place as usize;
        let between = (place - row as f64) as f32;
        let (near, next) = (row * TAPS, (row + 1) * TAPS);

        (0..TAPS).map(move |tap| {
            let (near, next) = (self.0[near + tap], self.0[next + tap]);
            near + (next - near) * between
        })
    }
}

/// The curve the filter is cut from, `at` frames from its middle: one at the
/// middle, and zero at every whole frame from it.
fn sinc(at: f64) -> f64 {
    let turn = core::f64::consts::PI * at;
    match at == 0.0 {
        true => 1.0,
        false => turn.sin() / turn,
    }
}

/// How much of the filter is left at `at` frames from its middle, which is
/// nothing at `HALF` frames and past them.
fn window(at: f64) -> f64 {
    let over = (at + HALF as f64) / TAPS as f64;
    let turn = core::f64::consts::TAU * over;

    BLACKMAN_HARRIS[0] - BLACKMAN_HARRIS[1] * turn.cos() + BLACKMAN_HARRIS[2] * (2.0 * turn).cos()
        - BLACKMAN_HARRIS[3] * (3.0 * turn).cos()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::sound::mixer::tests::tone;

    const CLIP: SampleRate = SampleRate::new(48_000);
    const DEVICE: SampleRate = SampleRate::new(44_100);

    #[test]
    fn a_clip_keeps_its_length_at_the_rate_the_mix_runs_at() {
        let made = Resampler::whole(&tone(CLIP, 1_000.0, 48_000), CLIP, DEVICE, Channels::Mono);

        assert_eq!(made.len(), 44_100, "a second of it either way");
        assert_eq!(
            Resampler::whole(&[0.25; 64], CLIP, CLIP, Channels::Stereo),
            [0.25; 64],
            "and one already at the mix's rate passes through as it is"
        );
    }

    #[test]
    fn frames_read_from_the_middle_are_the_ones_read_from_the_start() {
        let clip = tone(CLIP, 1_000.0, 48_000);
        let whole = Resampler::whole(&clip, CLIP, DEVICE, Channels::Mono);

        let mut resampler = Resampler::new(CLIP, DEVICE, Channels::Mono);
        let from = resampler.restart(ClipFrame::new(20_000)).get() as usize;
        resampler.feed(&clip[from..]);
        let mut made = Vec::new();
        resampler.take(1_000, &mut made);

        assert_eq!(made, whole[20_000..21_000]);
    }

    #[test]
    fn a_resampled_tone_keeps_the_loudness_it_was_recorded_at() {
        let made = Resampler::whole(&tone(CLIP, 1_000.0, 48_000), CLIP, DEVICE, Channels::Mono);
        let loudest = made[1_000..43_000]
            .iter()
            .fold(0.0f32, |loudest, &sample| loudest.max(sample.abs()));

        assert!(
            (loudest - 0.5).abs() < 0.001,
            "half of full scale: {loudest}"
        );
    }
}