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;
const NOISE_ATTACK: f32 = 0.001;
const RELEASE: f32 = 0.02;
pub(super) fn xorshift(state: &mut u32) -> u32 {
*state ^= *state << 13;
*state ^= *state >> 17;
*state ^= *state << 5;
*state
}
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 {
(self.bits() >> 8) as f32 / (1u32 << 24) as f32 * 2.0 - 1.0
}
fn vary(&mut self, spread: f32) -> f32 {
1.0 + self.sample() * spread
}
}
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
}
}
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
}
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
})
}
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()
})
}
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
})
}
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
})
}
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),
]
}
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"
);
}
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));
}
}