use std::collections::HashMap;
use std::f32::consts::TAU;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::time::Duration;
use rodio::Source;
use rodio::buffer::SamplesBuffer;
pub mod pads;
mod synth;
pub use pads::{PadKey, PadSpeaker};
struct Clip {
channels: rodio::ChannelCount,
sample_rate: rodio::SampleRate,
samples: Vec<f32>,
}
#[derive(Default)]
struct Set {
clips: Vec<Clip>,
last: Option<usize>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Effect {
Gunshot,
Explosion,
Impact,
Clank,
Shield,
}
impl Effect {
const ALL: [Self; 5] = [
Self::Gunshot,
Self::Explosion,
Self::Impact,
Self::Clank,
Self::Shield,
];
fn render(self, rate: u32, seed: u32) -> Vec<f32> {
match self {
Self::Gunshot => synth::gunshot(rate, seed),
Self::Explosion => synth::explosion(rate, seed),
Self::Impact => synth::impact(rate, seed),
Self::Clank => synth::clank(rate, seed),
Self::Shield => synth::shield(rate, seed),
}
}
fn gain(self) -> f32 {
match self {
Self::Gunshot => 0.6,
Self::Explosion => 0.6,
Self::Impact => 0.4,
Self::Clank => 0.5,
Self::Shield => 0.55,
}
}
}
const EFFECT_TAKES: usize = 4;
fn render_effects(sample_rate: rodio::SampleRate, seed: &mut u32) -> [Set; Effect::ALL.len()] {
Effect::ALL.map(|effect| Set {
clips: (0..EFFECT_TAKES)
.map(|_| Clip {
channels: rodio::ChannelCount::MIN,
sample_rate,
samples: effect.render(sample_rate.get(), synth::xorshift(seed)),
})
.collect(),
last: None,
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Voice {
Engine,
}
const ENGINE_BASE_HZ: f32 = 40.0;
const ENGINE_CEILING: f32 = 0.35;
const SMOOTHING_SECONDS: f32 = 0.04;
const SILENCE: f32 = 1e-3;
struct Controls {
pitch: AtomicU32,
gain: AtomicU32,
alive: AtomicBool,
}
impl Controls {
fn pitch(&self) -> f32 {
f32::from_bits(self.pitch.load(Ordering::Relaxed))
}
fn gain(&self) -> f32 {
f32::from_bits(self.gain.load(Ordering::Relaxed))
}
}
#[must_use = "a loop stops when its last handle is dropped"]
#[derive(Clone)]
pub struct LoopHandle(Arc<Controls>);
impl std::fmt::Debug for LoopHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LoopHandle")
.field("pitch", &self.0.pitch())
.field("gain", &self.0.gain())
.field("alive", &self.0.alive.load(Ordering::Relaxed))
.finish()
}
}
impl LoopHandle {
fn new() -> Self {
Self(Arc::new(Controls {
pitch: AtomicU32::new(1.0f32.to_bits()),
gain: AtomicU32::new(1.0f32.to_bits()),
alive: AtomicBool::new(true),
}))
}
pub fn set(&self, pitch: f32, gain: f32) {
let pitch = if pitch.is_finite() {
pitch.clamp(0.25, 4.0)
} else {
1.0
};
let gain = if gain.is_finite() {
gain.clamp(0.0, 1.0)
} else {
0.0
};
self.0.pitch.store(pitch.to_bits(), Ordering::Relaxed);
self.0.gain.store(gain.to_bits(), Ordering::Relaxed);
}
pub fn stop(&self) {
self.0.alive.store(false, Ordering::Relaxed);
}
}
struct Smoothed {
value: f32,
coefficient: f32,
}
impl Smoothed {
fn new(value: f32, rate: f32, seconds: f32) -> Self {
Self {
value,
coefficient: 1.0 - (-1.0 / (rate * seconds)).exp(),
}
}
fn step(&mut self, target: f32) -> f32 {
self.value += (target - self.value) * self.coefficient;
self.value
}
}
struct Loop {
voice: Voice,
controls: Arc<Controls>,
master: Arc<AtomicU32>,
sample_rate: rodio::SampleRate,
phase: f32,
stroke: bool,
pitch: Smoothed,
gain: Smoothed,
tone: synth::OnePole,
rumble: synth::OnePole,
noise: synth::Noise,
}
impl Loop {
fn new(
voice: Voice,
controls: Arc<Controls>,
master: Arc<AtomicU32>,
sample_rate: rodio::SampleRate,
seed: u32,
) -> Self {
let rate = sample_rate.get() as f32;
Self {
voice,
controls,
master,
sample_rate,
phase: 0.0,
stroke: false,
pitch: Smoothed::new(1.0, rate, SMOOTHING_SECONDS),
gain: Smoothed::new(0.0, rate, SMOOTHING_SECONDS),
tone: synth::OnePole::new(rate, 600.0),
rumble: synth::OnePole::new(rate, 200.0),
noise: synth::Noise::new(seed),
}
}
fn engine(&mut self) -> f32 {
let saw = 2.0 * self.phase - 1.0;
let second = (2.0 * TAU * self.phase).sin();
let rumble = self.rumble.step(self.noise.sample());
let stroke = if self.stroke { 1.0 } else { 0.7 };
self.tone
.step((0.45 * saw + 0.35 * second) * stroke + 0.2 * rumble)
}
}
impl Iterator for Loop {
type Item = f32;
fn next(&mut self) -> Option<f32> {
let held = Arc::strong_count(&self.controls) > 1;
let alive = held && self.controls.alive.load(Ordering::Relaxed);
let master = f32::from_bits(self.master.load(Ordering::Relaxed));
let target = if alive {
self.controls.gain() * master
} else {
0.0
};
let gain = self.gain.step(target);
if !alive && gain < SILENCE {
return None;
}
let pitch = self.pitch.step(self.controls.pitch());
let base = match self.voice {
Voice::Engine => ENGINE_BASE_HZ,
};
self.phase += base * pitch / self.sample_rate.get() as f32;
if self.phase >= 1.0 {
self.phase -= 1.0;
self.stroke = !self.stroke;
}
let sample = match self.voice {
Voice::Engine => self.engine() * ENGINE_CEILING,
};
Some(sample * gain)
}
}
impl Source for Loop {
fn current_span_len(&self) -> Option<usize> {
None
}
fn channels(&self) -> rodio::ChannelCount {
rodio::ChannelCount::MIN
}
fn sample_rate(&self) -> rodio::SampleRate {
self.sample_rate
}
fn total_duration(&self) -> Option<Duration> {
None
}
}
pub struct Audio {
_device: rodio::MixerDeviceSink,
mixer: rodio::mixer::Mixer,
sample_rate: rodio::SampleRate,
sets: HashMap<String, Set>,
effects: [Set; Effect::ALL.len()],
muted: bool,
volume: f32,
master: Arc<AtomicU32>,
seed: u32,
pads: Vec<PadSpeaker>,
}
impl Audio {
pub fn new() -> Option<Self> {
let mut device = match rodio::DeviceSinkBuilder::open_default_sink() {
Ok(device) => device,
Err(error) => {
log::warn!("no audio output ({error}); the game will be silent");
return None;
}
};
device.log_on_drop(false);
let mixer = device.mixer().clone();
let sample_rate = device.config().sample_rate();
let mut seed = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|since| since.subsec_nanos())
.unwrap_or(0x2545_F491)
| 1;
let effects = render_effects(sample_rate, &mut seed);
let pads = pads::open_pad_speakers();
Some(Self {
_device: device,
mixer,
sample_rate,
sets: HashMap::new(),
effects,
muted: false,
volume: 1.0,
master: Arc::new(AtomicU32::new(1.0f32.to_bits())),
seed,
pads,
})
}
pub fn pad_keys(&self) -> Vec<PadKey> {
self.pads.iter().map(|pad| pad.key).collect()
}
fn pad(&self, key: &PadKey) -> Option<&PadSpeaker> {
self.pads.iter().find(|pad| pad.key == *key)
}
pub fn load_set(&mut self, name: impl Into<String>, clips: &[&'static [u8]]) {
let name = name.into();
let mut set = Set::default();
for bytes in clips {
match decode(bytes) {
Ok(clip) => set.clips.push(clip),
Err(error) => log::error!("could not decode a {name} clip: {error}"),
}
}
log::debug!("loaded {} {name} clips", set.clips.len());
self.sets.insert(name, set);
}
pub fn play(&mut self, name: &str) {
if self.silent() {
return;
}
let roll = self.roll();
let volume = self.volume;
let Some(set) = self.sets.get_mut(name) else {
log::warn!("no sound set called {name:?}");
return;
};
play_from(&self.mixer, set, roll, volume);
}
pub fn play_effect(&mut self, effect: Effect) {
if self.silent() {
return;
}
let roll = self.roll();
let gain = self.volume * effect.gain();
play_from(&self.mixer, &mut self.effects[effect as usize], roll, gain);
}
pub fn play_effect_on(&mut self, key: &PadKey, effect: Effect, speaker: f32, haptic: f32) {
if self.silent() {
return;
}
let roll = self.roll();
let gain = self.volume * effect.gain();
let Some(pad) = self.pads.iter().find(|pad| pad.key == *key) else {
log::debug!("no pad speaker for {key:?}");
return;
};
if let Some(clip) = take(&mut self.effects[effect as usize], roll) {
pad.play(clip, speaker * gain, haptic * gain);
}
}
#[must_use = "a loop stops when its last handle is dropped"]
pub fn start_loop_on(
&mut self,
key: &PadKey,
voice: Voice,
speaker: f32,
haptic: f32,
) -> Option<LoopHandle> {
let seed = self.roll() as u32;
let pad = self.pad(key)?;
let handle = LoopHandle::new();
let source = Loop::new(
voice,
Arc::clone(&handle.0),
Arc::clone(&self.master),
pad.sample_rate(),
seed,
);
pad.add(source, speaker, haptic);
Some(handle)
}
#[must_use = "a loop stops when its last handle is dropped"]
pub fn start_loop(&mut self, voice: Voice) -> LoopHandle {
let handle = LoopHandle::new();
let seed = self.roll() as u32;
self.mixer.add(Loop::new(
voice,
Arc::clone(&handle.0),
Arc::clone(&self.master),
self.sample_rate,
seed,
));
handle
}
pub fn set_muted(&mut self, muted: bool) {
self.muted = muted;
self.share_master();
}
pub fn is_muted(&self) -> bool {
self.muted
}
pub fn set_volume(&mut self, volume: f32) {
self.volume = volume.clamp(0.0, 1.0);
self.share_master();
}
fn silent(&self) -> bool {
self.muted || self.volume <= 0.0
}
fn share_master(&self) {
let gain = if self.muted { 0.0 } else { self.volume };
self.master.store(gain.to_bits(), Ordering::Relaxed);
}
fn roll(&mut self) -> usize {
synth::xorshift(&mut self.seed) as usize
}
}
fn play_from(mixer: &rodio::mixer::Mixer, set: &mut Set, roll: usize, gain: f32) {
if let Some(source) = take(set, roll) {
mixer.add(source.amplify(gain));
}
}
fn take(set: &mut Set, roll: usize) -> Option<SamplesBuffer> {
let index = pick(set.clips.len(), set.last, roll)?;
set.last = Some(index);
let clip = &set.clips[index];
Some(SamplesBuffer::new(
clip.channels,
clip.sample_rate,
clip.samples.clone(),
))
}
fn pick(count: usize, last: Option<usize>, roll: usize) -> Option<usize> {
match count {
0 => None,
1 => Some(0),
_ => {
let Some(last) = last.filter(|last| *last < count) else {
return Some(roll % count);
};
let choice = roll % (count - 1);
Some(if choice >= last { choice + 1 } else { choice })
}
}
}
fn decode(bytes: &'static [u8]) -> Result<Clip, rodio::decoder::DecoderError> {
let decoder = rodio::Decoder::new(std::io::Cursor::new(bytes))?;
let channels = decoder.channels();
let sample_rate = decoder.sample_rate();
Ok(Clip {
channels,
sample_rate,
samples: decoder.collect(),
})
}
#[cfg(test)]
mod tests {
use super::*;
const RATE: u32 = 48000;
fn engine() -> (Loop, LoopHandle, Arc<AtomicU32>) {
let handle = LoopHandle::new();
let master = Arc::new(AtomicU32::new(1.0f32.to_bits()));
let source = Loop::new(
Voice::Engine,
Arc::clone(&handle.0),
Arc::clone(&master),
rodio::SampleRate::new(RATE).unwrap(),
9,
);
(source, handle, master)
}
fn peak(samples: impl Iterator<Item = f32>) -> f32 {
samples.fold(0.0f32, |peak, s| peak.max(s.abs()))
}
#[test]
fn an_empty_set_plays_nothing() {
assert_eq!(pick(0, None, 7), None);
assert_eq!(pick(0, Some(0), 7), None);
}
#[test]
fn a_single_clip_is_all_there_is_to_play() {
assert_eq!(pick(1, None, 3), Some(0));
assert_eq!(pick(1, Some(0), 3), Some(0), "it has to repeat");
}
#[test]
fn the_clip_just_heard_is_never_played_again_at_once() {
for last in 0..3 {
for roll in 0..30 {
let picked = pick(3, Some(last), roll).unwrap();
assert_ne!(picked, last, "roll {roll} repeated clip {last}");
assert!(picked < 3, "picked outside the set: {picked}");
}
}
}
#[test]
fn every_other_clip_can_still_come_up() {
let mut seen = std::collections::HashSet::new();
for roll in 0..30 {
seen.insert(pick(3, Some(1), roll).unwrap());
}
assert_eq!(seen, [0, 2].into_iter().collect());
}
#[test]
fn a_first_play_may_be_any_clip() {
let mut seen = std::collections::HashSet::new();
for roll in 0..30 {
seen.insert(pick(3, None, roll).unwrap());
}
assert_eq!(seen, [0, 1, 2].into_iter().collect());
}
#[test]
fn a_stale_last_index_does_not_break_the_pick() {
let picked = pick(2, Some(9), 5).unwrap();
assert!(picked < 2);
}
#[test]
fn an_engine_under_the_loudest_one_shot_fits_under_full_scale() {
let loudest = Effect::ALL
.into_iter()
.map(Effect::gain)
.fold(0.0, f32::max);
assert!(
ENGINE_CEILING + loudest <= 1.0,
"{ENGINE_CEILING} + {loudest} clips"
);
}
#[test]
fn the_bank_is_filed_by_discriminant() {
for (index, effect) in Effect::ALL.into_iter().enumerate() {
assert_eq!(effect as usize, index, "{effect:?} is out of order");
}
}
#[test]
fn every_effect_has_its_takes_and_no_two_are_alike() {
let mut seed = 1;
let bank = render_effects(rodio::SampleRate::new(RATE).unwrap(), &mut seed);
for effect in Effect::ALL {
let takes = &bank[effect as usize].clips;
assert_eq!(takes.len(), EFFECT_TAKES, "{effect:?}");
for (i, take) in takes.iter().enumerate() {
assert_eq!(take.sample_rate.get(), RATE);
for other in &takes[i + 1..] {
assert_ne!(take.samples, other.samples, "{effect:?} repeats a take");
}
}
}
}
#[test]
fn stopping_a_loop_ends_its_source() {
let (mut source, handle, _) = engine();
assert_eq!(
source.by_ref().take(RATE as usize).count(),
RATE as usize,
"it runs while alive",
);
assert!(source.next().is_some());
handle.stop();
let fade: Vec<f32> = source.by_ref().take(RATE as usize).collect();
assert!(fade.len() < RATE as usize / 2, "the source never ended");
assert!(source.next().is_none(), "and stays ended");
let last_tenth = &fade[fade.len() * 9 / 10..];
assert!(peak(last_tenth.iter().copied()) < 0.01, "it went out loud");
}
#[test]
fn dropping_the_last_handle_stops_the_loop() {
let (mut source, handle, _) = engine();
source.by_ref().take(RATE as usize / 4).count();
drop(handle);
assert!(
source.by_ref().take(RATE as usize).count() < RATE as usize,
"nothing could ever stop it now"
);
}
#[test]
fn a_clone_keeps_the_loop_going_when_the_original_is_dropped() {
let (mut source, handle, _) = engine();
let kept = handle.clone();
drop(handle);
assert_eq!(
source.by_ref().take(RATE as usize).count(),
RATE as usize,
"the clone still holds it"
);
drop(kept);
assert!(source.by_ref().take(RATE as usize).count() < RATE as usize);
}
#[test]
fn a_loop_stopped_before_it_sounds_ends_at_once() {
let (mut source, handle, _) = engine();
handle.stop();
assert_eq!(source.next(), None);
}
#[test]
fn a_loop_fades_in_rather_than_starting_with_a_click() {
let (mut source, _handle, _) = engine();
let first = source.next().unwrap();
assert!(first.abs() < 1e-3, "opened at {first}");
}
#[test]
fn the_engine_never_exceeds_its_ceiling() {
let (mut source, handle, _) = engine();
handle.set(4.0, 1.0);
let loudest = peak(source.by_ref().take(RATE as usize * 2));
assert!(loudest <= ENGINE_CEILING + 1e-6, "{loudest}");
assert!(
loudest > ENGINE_CEILING * 0.3,
"or it is barely there: {loudest}"
);
}
#[test]
fn muting_the_master_silences_a_loop_already_running() {
let (mut source, _handle, master) = engine();
source.by_ref().take(RATE as usize / 2).count();
master.store(0.0f32.to_bits(), Ordering::Relaxed);
let after: Vec<f32> = source.by_ref().take(RATE as usize / 2).collect();
assert_eq!(after.len(), RATE as usize / 2, "muting must not end it");
assert!(peak(after[after.len() * 9 / 10..].iter().copied()) < 1e-4);
master.store(1.0f32.to_bits(), Ordering::Relaxed);
source.by_ref().take(RATE as usize / 2).count();
assert!(
peak(source.by_ref().take(RATE as usize / 4)) > 0.05,
"and unmuting brings it back"
);
}
#[test]
fn pitch_smoothing_moves_monotonically_toward_the_target() {
let mut pitch = Smoothed::new(1.0, RATE as f32, SMOOTHING_SECONDS);
let mut previous = 1.0;
for _ in 0..RATE {
let now = pitch.step(2.0);
assert!(now >= previous && now <= 2.0, "{previous} -> {now}");
previous = now;
}
assert!((previous - 2.0).abs() < 1e-3, "never got there: {previous}");
let mut previous = pitch.step(0.5);
for _ in 0..RATE {
let now = pitch.step(0.5);
assert!(now <= previous && now >= 0.5, "{previous} -> {now}");
previous = now;
}
}
#[test]
fn smoothing_takes_about_as_long_as_it_says() {
let mut gain = Smoothed::new(0.0, RATE as f32, SMOOTHING_SECONDS);
let steps = (RATE as f32 * SMOOTHING_SECONDS) as usize;
let mut value = 0.0;
for _ in 0..steps {
value = gain.step(1.0);
}
assert!((value - 0.632).abs() < 0.01, "{value}");
}
#[test]
fn the_handle_keeps_pitch_and_gain_where_the_ceiling_still_holds() {
let handle = LoopHandle::new();
handle.set(100.0, 7.0);
assert_eq!((handle.0.pitch(), handle.0.gain()), (4.0, 1.0));
handle.set(f32::NAN, f32::INFINITY);
assert_eq!((handle.0.pitch(), handle.0.gain()), (1.0, 0.0));
handle.set(0.0, -1.0);
assert_eq!((handle.0.pitch(), handle.0.gain()), (0.25, 0.0));
}
#[test]
fn a_clone_of_the_handle_drives_the_same_loop() {
let (mut source, handle, _) = engine();
handle.clone().stop();
assert_eq!(source.next(), None);
}
}