use core::hash::Hash;
use core::time::Duration;
use crate::math::Vec3;
use crate::sound::SoundData;
use crate::sound::mixer::GLIDE;
use crate::{Assets, Catalog};
const SLOWEST_PITCH: f32 = 0.01;
pub trait Sounds: Catalog + Hash + Eq + Clone {
fn build(&self, assets: &Assets) -> SoundData;
fn gain(self, gain: f32) -> SoundCue<Self> {
SoundCue::from(self).gain(gain)
}
fn pitch(self, pitch: f32) -> SoundCue<Self> {
SoundCue::from(self).pitch(pitch)
}
fn at(self, position: Vec3) -> SoundCue<Self> {
SoundCue::from(self).at(position)
}
fn range(self, range: f32) -> SoundCue<Self> {
SoundCue::from(self).range(range)
}
fn reference(self, reference: f32) -> SoundCue<Self> {
SoundCue::from(self).reference(reference)
}
fn fade(self, fade: Duration) -> SoundCue<Self> {
SoundCue::from(self).fade(fade)
}
fn glide(self, glide: Duration) -> SoundCue<Self> {
SoundCue::from(self).glide(glide)
}
fn trim_to(self, start: Duration, end: Duration) -> SoundCue<Self> {
SoundCue::from(self).trim_to(start, end)
}
fn loop_from(self, at: Duration) -> SoundCue<Self> {
SoundCue::from(self).loop_from(at)
}
fn instance(self, nth: u32) -> SoundCue<Self> {
SoundCue::from(self).instance(nth)
}
}
#[must_use = "a sound is only played once play or sustain takes it"]
#[derive(Debug)]
pub struct SoundCue<S: Sounds> {
sound: S,
knobs: Knobs,
}
impl<S: Sounds> SoundCue<S> {
pub const DEFAULT_RANGE: f32 = Falloff::DEFAULT.range;
pub const DEFAULT_REFERENCE: f32 = Falloff::DEFAULT.reference;
pub const DEFAULT_FADE: Duration = Duration::from_millis(5);
pub const DEFAULT_GLIDE: Duration = GLIDE;
pub fn gain(mut self, gain: f32) -> Self {
self.knobs.gain = gain.max(0.0);
self
}
pub fn pitch(mut self, pitch: f32) -> Self {
self.knobs.pitch = pitch.max(SLOWEST_PITCH);
self
}
pub fn at(mut self, position: Vec3) -> Self {
self.knobs.position = Some(position);
self
}
pub fn range(mut self, range: f32) -> Self {
self.knobs.falloff = self.knobs.falloff.with_range(range);
self
}
pub fn reference(mut self, reference: f32) -> Self {
self.knobs.falloff = self.knobs.falloff.with_reference(reference);
self
}
pub fn fade(mut self, fade: Duration) -> Self {
self.knobs.fade = fade;
self
}
pub fn glide(mut self, glide: Duration) -> Self {
self.knobs.glide = glide;
self
}
pub fn trim_to(mut self, start: Duration, end: Duration) -> Self {
self.knobs.trim = Some((start, end));
self
}
pub fn loop_from(mut self, at: Duration) -> Self {
self.knobs.loop_from = Some(at);
self
}
pub fn instance(mut self, nth: u32) -> Self {
self.knobs.instance = nth;
self
}
pub(crate) fn split(self) -> (S, Knobs) {
(self.sound, self.knobs)
}
}
impl<S: Sounds> From<S> for SoundCue<S> {
fn from(sound: S) -> Self {
Self {
sound,
knobs: Knobs {
gain: 1.0,
pitch: 1.0,
position: None,
falloff: Falloff::DEFAULT,
fade: Self::DEFAULT_FADE,
glide: Self::DEFAULT_GLIDE,
trim: None,
loop_from: None,
instance: 0,
},
}
}
}
impl<S: Sounds> Clone for SoundCue<S> {
fn clone(&self) -> Self {
Self {
sound: self.sound.clone(),
knobs: self.knobs,
}
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Knobs {
pub(crate) gain: f32,
pub(crate) pitch: f32,
pub(crate) position: Option<Vec3>,
pub(crate) falloff: Falloff,
pub(crate) fade: Duration,
pub(crate) glide: Duration,
pub(crate) trim: Option<(Duration, Duration)>,
pub(crate) loop_from: Option<Duration>,
pub(crate) instance: u32,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Falloff {
reference: f32,
range: f32,
}
impl Falloff {
pub(crate) const DEFAULT: Self = Self {
reference: 1.0,
range: 50.0,
};
const SMALLEST: f32 = f32::EPSILON;
pub(crate) fn with_reference(self, reference: f32) -> Self {
Self {
reference: reference.clamp(Self::SMALLEST, self.range),
..self
}
}
pub(crate) fn with_range(self, range: f32) -> Self {
let range = range.max(Self::SMALLEST);
Self {
reference: self.reference.min(range),
range,
}
}
pub(crate) fn level(self, distance: f32) -> f32 {
if distance <= self.reference {
return 1.0;
}
let shift = self.reference / self.range;
((self.reference / distance - shift) / (1.0 - shift)).clamp(0.0, 1.0)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum NoSounds {}
impl Catalog for NoSounds {
fn catalog() -> Vec<Self> {
Vec::new()
}
}
impl Sounds for NoSounds {
fn build(&self, _assets: &Assets) -> SoundData {
match *self {}
}
}
#[cfg(test)]
mod tests {
use super::*;
const DISTANCES: [f32; 7] = [0.0, 0.5, 1.0, 2.0, 10.0, 50.0, 1e9];
#[test]
fn a_reference_past_the_range_or_at_or_under_zero_leaves_a_level_that_never_rises() {
let past = Falloff::DEFAULT.with_range(2.0).with_reference(50.0);
let none = Falloff::DEFAULT.with_reference(0.0);
let negative = Falloff::DEFAULT.with_reference(-5.0).with_range(-1.0);
for falloff in [past, none, negative] {
let mut nearer = 1.0;
for distance in DISTANCES {
let level = falloff.level(distance);
assert!(
(0.0..=nearer).contains(&level),
"{falloff:?} is heard at {level} from {distance} meters"
);
nearer = level;
}
}
}
#[test]
fn a_reference_and_a_range_are_heard_the_same_whichever_is_set_first() {
let reference_first = Falloff::DEFAULT.with_reference(50.0).with_range(2.0);
let range_first = Falloff::DEFAULT.with_range(2.0).with_reference(50.0);
for distance in DISTANCES {
assert_eq!(
reference_first.level(distance),
range_first.level(distance),
"from {distance} meters"
);
}
}
}