use rodio::buffer::SamplesBuffer;
use rodio::{DeviceSinkBuilder, MixerDeviceSink};
use crate::app::SoundEvent;
const RATE: u32 = 44_100;
const MASTER: f32 = 0.38;
const SPEED_FULL: f32 = 90.0;
pub struct Foley {
sink: MixerDeviceSink,
}
impl Foley {
pub fn new() -> Option<Foley> {
let mut sink = DeviceSinkBuilder::from_default_device()
.ok()?
.open_stream()
.ok()?;
sink.log_on_drop(false);
Some(Foley { sink })
}
pub fn play(&self, ev: SoundEvent) {
let samples = synth(ev);
if !samples.is_empty() {
let mono = std::num::NonZero::<u16>::MIN; let rate = std::num::NonZero::new(RATE).expect("RATE is non-zero");
self.sink
.mixer()
.add(SamplesBuffer::new(mono, rate, samples));
}
}
}
fn pitch(sides: u32) -> f32 {
1500.0 / (sides.max(2) as f32).powf(0.45)
}
fn loudness(speed: f32) -> f32 {
(speed / SPEED_FULL).clamp(0.12, 1.0)
}
pub fn synth(ev: SoundEvent) -> Vec<f32> {
match ev {
SoundEvent::Impact { sides, speed } => knock(pitch(sides), loudness(speed), 0.05),
SoundEvent::Knock { sides, speed } => knock(pitch(sides) * 1.35, loudness(speed), 0.04),
SoundEvent::Settle { sides } => knock(pitch(sides) * 0.8, 0.28, 0.06),
SoundEvent::Rattle { power } => rattle(0.25 + 0.55 * power),
SoundEvent::Throw { power } => whoosh(0.4 + 0.6 * power),
SoundEvent::Crit => chime(&[(1318.5, 1.0), (1975.5, 0.5)], 0.5, 0.7),
SoundEvent::Fumble => thud(),
SoundEvent::Success => melody(&[(660.0, 0.09), (880.0, 0.12)], 0.5),
SoundEvent::Failure => melody(&[(233.1, 0.11), (185.0, 0.16)], 0.45),
}
}
struct Noise(u32);
impl Noise {
fn next(&mut self) -> f32 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
self.0 = x;
(x as f32 / u32::MAX as f32) * 2.0 - 1.0
}
}
fn seconds(dur: f32) -> usize {
(dur * RATE as f32) as usize
}
fn knock(freq: f32, gain: f32, dur: f32) -> Vec<f32> {
let n = seconds(dur);
let mut noise = Noise(0x9e3779b9);
(0..n)
.map(|i| {
let t = i as f32 / RATE as f32;
let tone = (std::f32::consts::TAU * freq * t).sin() * 0.75;
let click = noise.next() * (-t * 900.0).exp() * 0.5;
(tone + click) * (-t * 55.0).exp() * gain * MASTER
})
.collect()
}
fn rattle(gain: f32) -> Vec<f32> {
let n = seconds(0.05);
let mut noise = Noise(0x2545f491);
(0..n)
.map(|i| {
let t = i as f32 / RATE as f32;
let a = (-t * 500.0).exp();
let b = if t > 0.018 {
(-(t - 0.018) * 500.0).exp()
} else {
0.0
};
let body = (std::f32::consts::TAU * 2600.0 * t).sin() * 0.3 + noise.next() * 0.7;
body * (a + b * 0.8) * gain * MASTER * 0.6
})
.collect()
}
fn whoosh(gain: f32) -> Vec<f32> {
let n = seconds(0.14);
let mut noise = Noise(0x1f123bb5);
let mut low = 0.0f32; (0..n)
.map(|i| {
let t = i as f32 / RATE as f32;
low += (noise.next() - low) * 0.12;
let env = (t * 30.0).min(1.0) * (-t * 26.0).exp();
low * env * gain * MASTER * 1.6
})
.collect()
}
fn chime(partials: &[(f32, f32)], dur: f32, gain: f32) -> Vec<f32> {
let n = seconds(dur);
(0..n)
.map(|i| {
let t = i as f32 / RATE as f32;
let mut s = 0.0;
for &(freq, amp) in partials {
s += (std::f32::consts::TAU * freq * t).sin() * amp;
}
s * (-t * 7.0).exp() * gain * MASTER * 0.5
})
.collect()
}
fn thud() -> Vec<f32> {
let n = seconds(0.2);
(0..n)
.map(|i| {
let t = i as f32 / RATE as f32;
let freq = 95.0 - 30.0 * t;
(std::f32::consts::TAU * freq * t).sin() * (-t * 22.0).exp() * MASTER * 0.9
})
.collect()
}
fn melody(notes: &[(f32, f32)], gain: f32) -> Vec<f32> {
let mut out = Vec::new();
for &(freq, dur) in notes {
let n = seconds(dur);
out.extend((0..n).map(|i| {
let t = i as f32 / RATE as f32;
(std::f32::consts::TAU * freq * t).sin() * (-t * 18.0).exp() * gain * MASTER
}));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_event_synthesizes_in_range() {
let events = [
SoundEvent::Impact {
sides: 6,
speed: 40.0,
},
SoundEvent::Knock {
sides: 20,
speed: 25.0,
},
SoundEvent::Settle { sides: 12 },
SoundEvent::Rattle { power: 0.8 },
SoundEvent::Throw { power: 1.0 },
SoundEvent::Crit,
SoundEvent::Fumble,
SoundEvent::Success,
SoundEvent::Failure,
];
for ev in events {
let s = synth(ev);
assert!(!s.is_empty(), "{ev:?} rendered nothing");
assert!(
s.iter().all(|v| v.abs() <= 1.0),
"{ev:?} clips: max {}",
s.iter().fold(0.0f32, |m, v| m.max(v.abs()))
);
}
}
#[test]
fn small_dice_click_higher_than_big_dice() {
let crossings = |s: &[f32]| {
s.windows(2)
.filter(|w| w[0].signum() != w[1].signum())
.count()
};
let d4 = synth(SoundEvent::Impact {
sides: 4,
speed: 50.0,
});
let d100 = synth(SoundEvent::Impact {
sides: 100,
speed: 50.0,
});
assert!(
crossings(&d4) > crossings(&d100) * 2,
"d4 {} vs d100 {} crossings",
crossings(&d4),
crossings(&d100)
);
}
#[test]
#[ignore]
fn audible_smoke_test() {
let foley = Foley::new().expect("no audio output device");
let script = [
SoundEvent::Rattle { power: 0.4 },
SoundEvent::Rattle { power: 0.9 },
SoundEvent::Throw { power: 1.0 },
SoundEvent::Impact {
sides: 20,
speed: 70.0,
},
SoundEvent::Knock {
sides: 20,
speed: 40.0,
},
SoundEvent::Impact {
sides: 6,
speed: 30.0,
},
SoundEvent::Settle { sides: 20 },
SoundEvent::Crit,
SoundEvent::Success,
SoundEvent::Fumble,
SoundEvent::Failure,
];
for ev in script {
eprintln!(" ♪ {ev:?}");
foley.play(ev);
std::thread::sleep(std::time::Duration::from_millis(350));
}
std::thread::sleep(std::time::Duration::from_millis(600));
}
#[test]
fn loudness_follows_impact_speed() {
let peak = |s: Vec<f32>| s.iter().fold(0.0f32, |m, v| m.max(v.abs()));
let soft = peak(synth(SoundEvent::Impact {
sides: 6,
speed: 12.0,
}));
let hard = peak(synth(SoundEvent::Impact {
sides: 6,
speed: 80.0,
}));
assert!(
hard > soft * 2.0,
"hard {hard} not clearly louder than soft {soft}"
);
}
}