codecraft 0.1.2

A minimalist 3D game engine built on parts of Bevy (ECS, color) with wgpu and winit: OpenPBR materials, clustered lighting, a yakui-drawn UI, audio and gamepad haptics; its binary maps any folder, and the symbols of its Rust files, as a 3D wall of boxes
Documentation
//! Sounds made from arithmetic rather than recordings; pure functions of sample rate and seed.
use std::f32::consts::TAU;

const GUNSHOT_SECONDS: f32 = 0.25;
const EXPLOSION_SECONDS: f32 = 1.5;
const IMPACT_SECONDS: f32 = 0.12;
const CLANK_SECONDS: f32 = 0.12;
const SHIELD_SECONDS: f32 = 0.7;

/// Attack on a noise burst; a hard step from silence is a DC edge some outputs turn into a pop.
const NOISE_ATTACK: f32 = 0.001;

/// Final fade on every clip, so the mixer never drops a buffer mid-value and clicks.
const RELEASE: f32 = 0.02;

/// One step of a 32-bit xorshift; a state of zero stays zero forever.
pub(super) fn xorshift(state: &mut u32) -> u32 {
    *state ^= *state << 13;
    *state ^= *state >> 17;
    *state ^= *state << 5;
    *state
}

/// White noise from a xorshift, in -1..1.
pub(super) struct Noise(u32);

impl Noise {
    pub(super) fn new(seed: u32) -> Self {
        Self(seed.max(1))
    }

    fn bits(&mut self) -> u32 {
        xorshift(&mut self.0)
    }

    pub(super) fn sample(&mut self) -> f32 {
        // Top 24 bits: as many as an f32 holds exactly.
        (self.bits() >> 8) as f32 / (1u32 << 24) as f32 * 2.0 - 1.0
    }

    /// A factor within `spread` of one.
    fn vary(&mut self, spread: f32) -> f32 {
        1.0 + self.sample() * spread
    }
}

/// A one-pole low-pass filter.
pub(super) struct OnePole {
    rate: f32,
    value: f32,
    coefficient: f32,
}

impl OnePole {
    pub(super) fn new(rate: f32, cutoff: f32) -> Self {
        let mut filter = Self {
            rate,
            value: 0.0,
            coefficient: 0.0,
        };
        filter.set_cutoff(cutoff);
        filter
    }

    pub(super) fn set_cutoff(&mut self, cutoff: f32) {
        self.coefficient = 1.0 - (-TAU * cutoff / self.rate).exp();
    }

    pub(super) fn step(&mut self, input: f32) -> f32 {
        self.value += (input - self.value) * self.coefficient;
        self.value
    }
}

/// Scales a clip so its loudest sample is exactly one, so [`super::Effect::gain`] means the same for every clip.
fn normalise(samples: &mut [f32]) {
    let peak = samples.iter().fold(0.0f32, |peak, s| peak.max(s.abs()));
    if peak > 0.0 {
        for sample in samples {
            *sample /= peak;
        }
    }
}

fn attack(t: f32, seconds: f32) -> f32 {
    (t / seconds).min(1.0)
}

fn release(t: f32, total: f32, seconds: f32) -> f32 {
    ((total - t) / seconds).clamp(0.0, 1.0)
}

fn render(rate: u32, seconds: f32, mut sample: impl FnMut(f32) -> f32) -> Vec<f32> {
    let count = (rate as f32 * seconds).round() as usize;
    let mut samples: Vec<f32> = (0..count)
        .map(|i| {
            let t = i as f32 / rate as f32;
            sample(t) * release(t, seconds, RELEASE)
        })
        .collect();
    normalise(&mut samples);
    samples
}

/// A tank gun: a dulling crack of noise over a thump sagging from 90 Hz to 40.
pub(super) fn gunshot(rate: u32, seed: u32) -> Vec<f32> {
    let mut noise = Noise::new(seed);
    let brightness = noise.vary(0.15);
    let weight = noise.vary(0.1);
    let mut crack = OnePole::new(rate as f32, 8000.0);
    let mut thump_phase = 0.0f32;

    render(rate, GUNSHOT_SECONDS, |t| {
        let decay = (-30.0 * t).exp();
        crack.set_cutoff(300.0 + 9000.0 * brightness * decay);
        let crack = crack.step(noise.sample()) * decay * attack(t, NOISE_ATTACK);

        let thump = (TAU * thump_phase).sin() * (-15.0 * t).exp();
        let frequency = (40.0 + 50.0 * (-20.0 * t).exp()) * weight;
        thump_phase += frequency / rate as f32;

        crack + 0.8 * thump
    })
}

/// An explosion: falling low-passed noise under a 35 Hz sub, soft-clipped.
pub(super) fn explosion(rate: u32, seed: u32) -> Vec<f32> {
    let mut noise = Noise::new(seed);
    let depth = noise.vary(0.2);
    let mut rumble = OnePole::new(rate as f32, 250.0);
    let mut sub_phase = 0.0f32;

    render(rate, EXPLOSION_SECONDS, |t| {
        let decay = (-3.0 * t).exp();
        rumble.set_cutoff(40.0 + 250.0 * depth * (-1.5 * t).exp());
        let rumble = rumble.step(noise.sample()) * decay;

        let sub = (TAU * sub_phase).sin() * decay;
        sub_phase += 35.0 * depth / rate as f32;

        ((6.0 * rumble + 0.8 * sub) * attack(t, 0.01)).tanh()
    })
}

/// A shell hitting the ground: a tick of noise and a brief 500 Hz ring.
pub(super) fn impact(rate: u32, seed: u32) -> Vec<f32> {
    let mut noise = Noise::new(seed);
    let tone = noise.vary(0.1);
    let mut tick = OnePole::new(rate as f32, 3000.0);
    let mut ping_phase = 0.0f32;

    render(rate, IMPACT_SECONDS, |t| {
        let tick = tick.step(noise.sample()) * (-80.0 * t).exp() * attack(t, NOISE_ATTACK);

        let ping = (TAU * ping_phase).sin() * (-35.0 * t).exp();
        ping_phase += 500.0 * tone / rate as f32;

        tick + 0.6 * ping
    })
}

/// Metal on metal: two inharmonic sines (a plate, not a bell) and a tick for the blow.
pub(super) fn clank(rate: u32, seed: u32) -> Vec<f32> {
    let mut noise = Noise::new(seed);
    let tone = noise.vary(0.08);
    let mut tick = OnePole::new(rate as f32, 4000.0);
    let mut low_phase = 0.0f32;
    let mut high_phase = 0.0f32;

    render(rate, CLANK_SECONDS, |t| {
        let tick = tick.step(noise.sample()) * (-150.0 * t).exp() * attack(t, NOISE_ATTACK);

        let low = (TAU * low_phase).sin() * (-40.0 * t).exp();
        let high = (TAU * high_phase).sin() * (-50.0 * t).exp();
        low_phase += 1200.0 * tone / rate as f32;
        high_phase += 1900.0 * tone / rate as f32;

        0.5 * tick + 0.6 * low + 0.4 * high
    })
}

/// A shield going up: a sine sweeping exponentially from 180 Hz to 50 over 0.4 s, over a swell of low-passed noise.
pub(super) fn shield(rate: u32, seed: u32) -> Vec<f32> {
    let mut noise = Noise::new(seed);
    let depth = noise.vary(0.08);
    let mut swell = OnePole::new(rate as f32, 350.0);
    let mut phase = 0.0f32;

    render(rate, SHIELD_SECONDS, |t| {
        let along = (t / 0.4).min(1.0);
        let frequency = 180.0 * (50.0f32 / 180.0).powf(along) * depth;
        phase += frequency / rate as f32;
        let tone = (TAU * phase).sin() * attack(t, 0.04) * (-6.0 * t).exp();

        let swell = swell.step(noise.sample()) * attack(t, 0.08) * (-7.0 * t).exp();
        tone + 1.5 * swell
    })
}

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

    fn clips(rate: u32) -> [(&'static str, Vec<f32>, f32); 5] {
        [
            ("gunshot", gunshot(rate, 7), GUNSHOT_SECONDS),
            ("explosion", explosion(rate, 7), EXPLOSION_SECONDS),
            ("impact", impact(rate, 7), IMPACT_SECONDS),
            ("clank", clank(rate, 7), CLANK_SECONDS),
            ("shield", shield(rate, 7), SHIELD_SECONDS),
        ]
    }

    /// The clips that build rather than crack.
    fn swells(name: &str) -> bool {
        matches!(name, "explosion" | "shield")
    }

    fn peak(samples: &[f32]) -> f32 {
        samples.iter().fold(0.0f32, |peak, s| peak.max(s.abs()))
    }

    #[test]
    fn every_clip_is_as_long_as_it_says_at_any_device_rate() {
        for rate in [44100, 48000] {
            for (name, samples, seconds) in clips(rate) {
                assert_eq!(
                    samples.len(),
                    (rate as f32 * seconds).round() as usize,
                    "{name} at {rate} Hz",
                );
            }
        }
    }

    #[test]
    fn every_clip_peaks_at_exactly_one() {
        for (name, samples, _) in clips(48000) {
            let peak = peak(&samples);
            assert!((peak - 1.0).abs() < 1e-5, "{name} peaks at {peak}");
        }
    }

    #[test]
    fn every_clip_starts_from_near_silence() {
        for (name, samples, _) in clips(48000) {
            assert!(samples[0].abs() < 1e-3, "{name} opens at {}", samples[0]);
        }
    }

    #[test]
    fn the_transients_hit_within_a_millisecond_and_the_explosion_swells() {
        for (name, samples, _) in clips(48000) {
            let first_millisecond = peak(&samples[..48]);
            if swells(name) {
                assert!(
                    first_millisecond < 0.3,
                    "the {name} pops: {first_millisecond}"
                );
            } else {
                assert!(
                    first_millisecond > 0.3,
                    "the {name} went soft: {first_millisecond}"
                );
            }
        }
    }

    #[test]
    fn every_clip_has_died_away_by_its_last_twentieth() {
        for (name, samples, _) in clips(48000) {
            let tail = &samples[samples.len() * 19 / 20..];
            let tail = peak(tail);
            assert!(tail < 0.05, "{name} ends at {tail}");
            let last = samples.last().unwrap().abs();
            assert!(last < 1e-4, "{name} does not reach nothing: {last}");
        }
    }

    #[test]
    fn the_seed_makes_one_play_differ_from_the_next() {
        assert_ne!(gunshot(48000, 1), gunshot(48000, 2));
        assert_ne!(explosion(48000, 1), explosion(48000, 2));
        assert_ne!(impact(48000, 1), impact(48000, 2));
        assert_ne!(clank(48000, 1), clank(48000, 2));
        assert_ne!(shield(48000, 1), shield(48000, 2));
        assert_eq!(
            clank(48000, 5),
            clank(48000, 5),
            "and the same seed repeats"
        );
    }

    /// Loudest pitch in a stretch of a clip, to the nearest 5 Hz, by Fourier sum (robust to the noise over the tone).
    fn loudest_hz(samples: &[f32], rate: u32, from: f32, to: f32) -> f32 {
        let window = &samples[(from * rate as f32) as usize..(to * rate as f32) as usize];
        (30..=300)
            .step_by(5)
            .map(|hz| {
                let (mut re, mut im) = (0.0f32, 0.0f32);
                for (i, sample) in window.iter().enumerate() {
                    let phase = TAU * hz as f32 * i as f32 / rate as f32;
                    re += sample * phase.cos();
                    im += sample * phase.sin();
                }
                (hz as f32, re * re + im * im)
            })
            .max_by(|a, b| a.1.total_cmp(&b.1))
            .map(|(hz, _)| hz)
            .unwrap()
    }

    #[test]
    fn the_shield_sweeps_down_from_around_180_hz_to_around_50() {
        let rate = 48000;
        let samples = shield(rate, 7);
        let (high, low) = (
            loudest_hz(&samples, rate, 0.02, 0.08),
            loudest_hz(&samples, rate, 0.35, 0.5),
        );
        assert!(
            (120.0..=200.0).contains(&high),
            "near the top of the sweep to begin with: {high} Hz"
        );
        assert!(
            (40.0..=70.0).contains(&low),
            "and at the bottom once the sweep has run: {low} Hz"
        );
        let loudest = samples
            .iter()
            .enumerate()
            .max_by(|a, b| a.1.abs().total_cmp(&b.1.abs()))
            .map(|(i, _)| i as f32 / rate as f32)
            .unwrap();
        assert!(loudest < 0.2, "loudest early, as a whoomp is: {loudest} s");
    }

    #[test]
    fn noise_stays_within_a_sample_and_covers_both_signs() {
        let mut noise = Noise::new(42);
        let (mut low, mut high) = (0.0f32, 0.0f32);
        for _ in 0..10_000 {
            let sample = noise.sample();
            assert!((-1.0..=1.0).contains(&sample), "{sample}");
            low = low.min(sample);
            high = high.max(sample);
        }
        assert!(low < -0.9 && high > 0.9, "{low}..{high}");
    }

    #[test]
    fn a_zero_seed_still_makes_noise() {
        let mut noise = Noise::new(0);
        assert!((0..100).any(|_| noise.sample() != -1.0));
    }
}