use core::f32::consts::FRAC_PI_4;
use core::time::Duration;
use std::sync::Arc;
use crate::View;
use crate::math::Vec3;
use crate::sound::build::Knobs;
use crate::sound::data::{Clip, ClipFrame};
pub const MAX_VOICES: usize = 64;
pub(crate) const GLIDE: Duration = Duration::from_millis(50);
pub(crate) const STREAM_AHEAD: Duration = Duration::from_millis(1_500);
const LIMITER_KNEE: f32 = 0.75;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub(crate) struct SoundId(pub(crate) u32);
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(crate) struct Sustained {
pub(crate) sound: SoundId,
pub(crate) instance: u32,
}
impl Sustained {
pub(crate) fn of(sound: SoundId, knobs: &Knobs, sustained: bool) -> Option<Self> {
if !sustained {
if knobs.instance != 0 {
log::debug!("a one-shot is a voice of its own; ignoring the instance it names");
}
return None;
}
Some(Self {
sound,
instance: knobs.instance,
})
}
}
pub(crate) enum Command {
Play(Voicing),
Sustained(Vec<(Sustained, Voicing)>),
Volume(f32),
}
pub(crate) struct Voicing {
#[allow(dead_code)]
pub(crate) sound: SoundId,
pub(crate) clip: Arc<Clip>,
pub(crate) window: Window,
pub(crate) live: LiveKnobs,
}
impl Voicing {
pub(crate) fn audibility(&self) -> f32 {
self.live.levels.loudest()
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct LiveKnobs {
pub(crate) levels: Levels,
pub(crate) pitch: f32,
pub(crate) fade: Duration,
pub(crate) glide: Duration,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Levels {
pub(crate) left: f32,
pub(crate) right: f32,
}
impl Levels {
pub(crate) fn loudest(self) -> f32 {
self.left.max(self.right)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct Window {
pub(crate) start: ClipFrame,
pub(crate) end: ClipFrame,
pub(crate) wrap: ClipFrame,
pub(crate) looping: bool,
}
impl Window {
pub(crate) fn of(clip: &Clip, knobs: &Knobs, looping: bool) -> Option<Self> {
let total = ClipFrame::new(clip.frames);
let (start, end) = match knobs.trim {
Some((from, to)) => {
let (from, to) = (clip.rate.frame_at(from), clip.rate.frame_at(to));
if from > total || to > total {
log::debug!("a trim asks for more than the sound has; keeping what it has");
}
(from.min(total), to.min(total))
}
None => (ClipFrame::ZERO, total),
};
if end <= start {
log::debug!("a trim leaves nothing of the sound to play");
return None;
}
let wrap = match knobs.loop_from {
Some(_) if !looping => {
log::debug!("a one-shot never comes back around; ignoring where it would loop");
start
}
Some(at) if !(start..end).contains(&clip.rate.frame_at(at)) => {
log::debug!("a loop starts outside what is played; coming back to the start");
start
}
Some(at) => clip.rate.frame_at(at),
None => start,
};
Some(Self {
start,
end,
wrap,
looping,
})
}
pub(crate) fn at(&self, played: ClipFrame) -> Option<ClipFrame> {
let intro = self.end - self.start;
if played < intro {
return Some(self.start + played);
}
if !self.looping {
return None;
}
Some(self.wrap + (played - intro) % (self.end - self.wrap))
}
}
#[derive(Default)]
pub(crate) struct Declared(Vec<(Sustained, Voicing)>);
impl Declared {
pub(crate) fn declare(&mut self, sustained: Sustained, voicing: Voicing) {
match self.0.iter_mut().find(|(kept, _)| *kept == sustained) {
Some(kept) => {
log::debug!(
"a sound is sustained twice at one instance in one frame; keeping the last call"
);
kept.1 = voicing;
}
None => self.0.push((sustained, voicing)),
}
}
pub(crate) fn take(&mut self) -> Vec<(Sustained, Voicing)> {
core::mem::take(&mut self.0)
}
}
pub(crate) trait Audible {
type Pace: Copy;
fn audibility(&self, pace: Self::Pace) -> f32;
fn stop(&mut self, pace: Self::Pace);
fn rise(&mut self, pace: Self::Pace);
fn cut(&mut self, pace: Self::Pace);
fn recover(&mut self, pace: Self::Pace);
fn spent(&self, pace: Self::Pace) -> bool;
}
pub(crate) enum Playing<V> {
Virtual,
Voiced(V),
Fading(V, Fade),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum Fade {
Stop,
Cut,
}
impl<V: Audible> Playing<V> {
pub(crate) fn voice(&self) -> Option<&V> {
match self {
Self::Virtual => None,
Self::Voiced(voice) | Self::Fading(voice, _) => Some(voice),
}
}
pub(crate) fn voice_mut(&mut self) -> Option<&mut V> {
match self {
Self::Virtual => None,
Self::Voiced(voice) | Self::Fading(voice, _) => Some(voice),
}
}
pub(crate) fn declared(&mut self, pace: V::Pace) {
*self = match core::mem::replace(self, Self::Virtual) {
Self::Fading(mut voice, Fade::Stop) => {
voice.rise(pace);
Self::Voiced(voice)
}
held => held,
};
}
#[must_use]
pub(crate) fn stopped(&mut self, pace: V::Pace) -> bool {
*self = match core::mem::replace(self, Self::Virtual) {
Self::Virtual => return false,
Self::Voiced(mut voice) | Self::Fading(mut voice, _) => {
voice.stop(pace);
Self::Fading(voice, Fade::Stop)
}
};
true
}
pub(crate) fn voiced(&mut self, wins: bool, pace: V::Pace, start: impl FnOnce() -> Option<V>) {
*self = match (wins, core::mem::replace(self, Self::Virtual)) {
(true, Self::Virtual) => match start() {
Some(voice) => Self::Voiced(voice),
None => Self::Virtual,
},
(true, Self::Fading(mut voice, Fade::Cut)) => {
voice.recover(pace);
Self::Voiced(voice)
}
(false, Self::Voiced(mut voice)) => {
voice.cut(pace);
Self::Fading(voice, Fade::Cut)
}
(_, held) => held,
};
}
pub(crate) fn audibility(&self, pace: V::Pace) -> f32 {
self.voice().map_or(0.0, |voice| voice.audibility(pace))
}
pub(crate) fn spent(&self, pace: V::Pace) -> bool {
self.voice().is_none_or(|voice| voice.spent(pace))
}
pub(crate) fn landed(&mut self, pace: V::Pace) -> Option<(V, Fade)> {
match core::mem::replace(self, Self::Virtual) {
Self::Fading(voice, why) if voice.spent(pace) => Some((voice, why)),
held => {
*self = held;
None
}
}
}
}
pub(crate) trait Sustaining: Sized {
type Pace: Copy;
type Voices<'a>;
fn started(sustained: Sustained, voicing: Voicing, pace: Self::Pace) -> Self;
fn sustained(&self) -> Sustained;
fn declare(&mut self, voicing: Voicing, pace: Self::Pace);
fn stop(&mut self, pace: Self::Pace) -> bool;
fn voiced(&mut self, wins: bool, voices: &mut Self::Voices<'_>, pace: Self::Pace);
fn audibility(&self, pace: Self::Pace) -> f32;
fn settle(&mut self, pace: Self::Pace) -> bool;
}
pub(crate) struct Sustains<S: Sustaining> {
held: Vec<S>,
allocation: Allocation,
}
impl<S: Sustaining> Default for Sustains<S> {
fn default() -> Self {
Self {
held: Vec::new(),
allocation: Allocation::default(),
}
}
}
impl<S: Sustaining> Sustains<S> {
pub(crate) fn declare(&mut self, declared: Vec<(Sustained, Voicing)>, pace: S::Pace) {
let mut was = core::mem::take(&mut self.held);
let mut table = Vec::with_capacity(declared.len());
for (sustained, voicing) in declared {
let held = match was
.iter()
.position(|held| held.sustained() == sustained)
.map(|at| was.swap_remove(at))
{
Some(mut held) => {
held.declare(voicing, pace);
held
}
None => S::started(sustained, voicing, pace),
};
table.push(held);
}
table.extend(
was.into_iter()
.filter_map(|mut gone| gone.stop(pace).then_some(gone)),
);
self.held = table;
}
pub(crate) fn allocate<V: Audible<Pace = S::Pace>>(
&mut self,
shots: &mut [Playing<V>],
starting: &mut Vec<Voicing>,
voices: &mut S::Voices<'_>,
pace: S::Pace,
) {
self.allocation.rank(
self.held
.iter()
.map(|held| held.audibility(pace))
.chain(shots.iter().map(|shot| shot.audibility(pace)))
.chain(starting.iter().map(Voicing::audibility)),
);
let mut voiced = self.allocation.voiced();
for (held, wins) in self.held.iter_mut().zip(voiced.by_ref()) {
held.voiced(wins, voices, pace);
}
for (shot, wins) in shots.iter_mut().zip(voiced.by_ref()) {
shot.voiced(wins, pace, || None);
}
starting.retain(|_| {
let wins = voiced.next().unwrap_or(false);
if !wins {
log::debug!("a one-shot is quieter than what is sounding; it does not start");
}
wins
});
}
pub(crate) fn settle(&mut self, pace: S::Pace) {
self.held.retain_mut(|held| held.settle(pace));
}
pub(crate) fn iter_mut(&mut self) -> impl Iterator<Item = &mut S> {
self.held.iter_mut()
}
#[cfg(test)]
pub(crate) fn iter(&self) -> impl Iterator<Item = &S> {
self.held.iter()
}
}
#[derive(Default)]
struct Allocation {
ranked: Vec<(f32, usize)>,
voiced: Vec<bool>,
}
impl Allocation {
fn rank(&mut self, audibility: impl Iterator<Item = f32>) {
self.ranked.clear();
self.ranked
.extend(audibility.enumerate().map(|(at, level)| (level, at)));
self.voiced.clear();
self.voiced.resize(self.ranked.len(), false);
self.ranked
.sort_by(|(one, _), (other, _)| other.total_cmp(one));
for (_, at) in self
.ranked
.iter()
.take(MAX_VOICES)
.filter(|(level, _)| *level > 0.0)
{
self.voiced[*at] = true;
}
}
fn voiced(&self) -> impl Iterator<Item = bool> {
self.voiced.iter().copied()
}
}
pub(crate) fn levels(knobs: &Knobs, listener: &View) -> Levels {
let Some(position) = knobs.position else {
return Levels {
left: knobs.gain,
right: knobs.gain,
};
};
let towards = position - listener.eye();
let falloff = knobs.falloff.level(towards.length());
let forward = (listener.target() - listener.eye()).normalize_or_zero();
let side = forward.cross(listener.up()).normalize_or_zero();
let pan = towards.normalize_or(Vec3::ZERO).dot(side).clamp(-1.0, 1.0);
let angle = (pan + 1.0) * FRAC_PI_4;
Levels {
left: knobs.gain * falloff * angle.cos(),
right: knobs.gain * falloff * angle.sin(),
}
}
pub(crate) fn limit(sample: f32) -> f32 {
let over = sample.abs() - LIMITER_KNEE;
if over <= 0.0 {
return sample;
}
let room = 1.0 - LIMITER_KNEE;
(LIMITER_KNEE + room * (over / room).tanh()).copysign(sample)
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::sound::build::Falloff;
use crate::sound::data::{Body, Channels, SampleRate};
use crate::sound::output::MixRate;
pub(crate) const RATE: SampleRate = SampleRate::new(8);
pub(crate) const MIX: MixRate = MixRate::chosen(RATE);
pub(crate) const QUIET: f32 = 64.0;
pub(crate) fn tone(rate: SampleRate, hertz: f64, frames: usize) -> Vec<f32> {
(0..frames)
.map(|at| {
let turn = core::f64::consts::TAU * hertz * at as f64 / f64::from(rate);
(0.5 * turn.sin()) as f32
})
.collect()
}
pub(crate) fn counted(frames: u64) -> Arc<Clip> {
clip((0..frames).map(|at| at as f32 / QUIET).collect())
}
pub(crate) fn clip(samples: Vec<f32>) -> Arc<Clip> {
Arc::new(Clip {
rate: RATE,
channels: Channels::Mono,
frames: samples.len() as u64,
body: Body::Samples(samples.into()),
})
}
pub(crate) fn knobs() -> Knobs {
Knobs {
gain: 1.0,
pitch: 1.0,
position: None,
falloff: Falloff::DEFAULT,
fade: Duration::ZERO,
glide: Duration::ZERO,
trim: None,
loop_from: None,
instance: 0,
}
}
pub(crate) fn sustained(sound: u32) -> Sustained {
Sustained {
sound: SoundId(sound),
instance: 0,
}
}
pub(crate) fn seconds(frames: u64) -> Duration {
Duration::from_secs_f64(frames as f64 / f64::from(RATE))
}
pub(crate) fn voicing(clip: &Arc<Clip>, knobs: &Knobs, looping: bool) -> Voicing {
leveled(clip, knobs, looping, 1.0)
}
pub(crate) fn leveled(clip: &Arc<Clip>, knobs: &Knobs, looping: bool, level: f32) -> Voicing {
Voicing {
sound: SoundId(0),
clip: Arc::clone(clip),
window: Window::of(clip, knobs, looping).expect("the window holds something"),
live: LiveKnobs {
levels: Levels {
left: level,
right: level,
},
pitch: knobs.pitch,
fade: knobs.fade,
glide: knobs.glide,
},
}
}
#[test]
fn a_value_sustained_twice_in_one_frame_goes_out_once_from_the_last_call() {
let clip = counted(10);
let faster = Knobs {
pitch: 2.0,
..knobs()
};
let mut declared = Declared::default();
declared.declare(sustained(1), voicing(&clip, &knobs(), true));
declared.declare(sustained(1), voicing(&clip, &faster, true));
declared.declare(sustained(2), voicing(&clip, &knobs(), true));
let set = declared.take();
assert!(
matches!(set.as_slice(), [(first, kept), (second, _)]
if *first == sustained(1) && kept.live.pitch == faster.pitch && *second == sustained(2)),
"one entry apiece, the first from its last call, in declaration order"
);
assert!(declared.take().is_empty(), "and the frame starts empty");
}
#[derive(Default)]
struct Fake {
done: Vec<&'static str>,
spent: bool,
}
impl Audible for Fake {
type Pace = ();
fn audibility(&self, _: ()) -> f32 {
1.0
}
fn stop(&mut self, _: ()) {
self.done.push("stop");
}
fn rise(&mut self, _: ()) {
self.done.push("rise");
}
fn cut(&mut self, _: ()) {
self.done.push("cut");
}
fn recover(&mut self, _: ()) {
self.done.push("recover");
}
fn spent(&self, _: ()) -> bool {
self.spent
}
}
fn done(playing: &Playing<Fake>) -> Vec<&'static str> {
playing
.voice()
.map(|voice| voice.done.clone())
.unwrap_or_default()
}
#[test]
fn a_value_declared_again_while_it_fades_out_rises_back_and_keeps_its_voice() {
let mut playing = Playing::Voiced(Fake::default());
assert!(playing.stopped(()), "a voice fades before the value goes");
playing.declared(());
assert!(matches!(playing, Playing::Voiced(_)));
assert_eq!(done(&playing), ["stop", "rise"], "and it plays on");
}
#[test]
fn a_value_with_no_voice_is_over_where_no_set_declares_it() {
let mut playing: Playing<Fake> = Playing::Virtual;
assert!(!playing.stopped(()), "nothing fades, so nothing is held");
}
#[test]
fn a_value_the_rank_cuts_keeps_its_voice_until_the_fade_lands() {
let mut playing = Playing::Voiced(Fake::default());
playing.voiced(false, (), || None);
assert_eq!(done(&playing), ["cut"], "the rank leaves it out");
assert!(playing.landed(()).is_none(), "and the fade runs on");
playing.voice_mut().expect("it holds a voice").spent = true;
let (_, why) = playing.landed(()).expect("the fade landed");
assert_eq!(why, Fade::Cut, "so the value goes on virtual");
assert!(matches!(playing, Playing::Virtual));
}
#[test]
fn a_value_the_rank_voices_again_rises_back_and_keeps_the_voice_it_had() {
let mut playing = Playing::Voiced(Fake::default());
playing.voiced(false, (), || None);
playing.voiced(true, (), || None);
assert!(matches!(playing, Playing::Voiced(_)), "the voice it had");
assert_eq!(done(&playing), ["cut", "recover"]);
}
#[test]
fn a_declaration_never_takes_back_the_cut_the_rank_made() {
let mut playing = Playing::Voiced(Fake::default());
playing.voiced(false, (), || None);
playing.declared(());
assert!(matches!(playing, Playing::Fading(_, Fade::Cut)));
assert_eq!(done(&playing), ["cut"], "only the rank voices it again");
}
#[test]
fn the_rank_has_no_say_over_a_value_no_set_declares() {
let mut playing = Playing::Voiced(Fake::default());
assert!(playing.stopped(()), "the value is on its way out");
playing.voiced(true, (), || Some(Fake::default()));
playing.voiced(false, (), || None);
assert!(matches!(playing, Playing::Fading(_, Fade::Stop)));
assert_eq!(done(&playing), ["stop"], "neither risen nor cut again");
}
#[test]
fn the_backend_starts_a_voice_for_a_virtual_value_the_rank_keeps() {
let mut playing: Playing<Fake> = Playing::Virtual;
let mut refused: Playing<Fake> = Playing::Virtual;
playing.voiced(true, (), || Some(Fake::default()));
refused.voiced(true, (), || None);
assert!(matches!(playing, Playing::Voiced(_)));
assert!(
matches!(refused, Playing::Virtual),
"and one the backend made no voice for stays virtual"
);
}
#[test]
fn the_rank_voices_the_loudest_and_nothing_at_no_level() {
let mut allocation = Allocation::default();
allocation.rank([0.5, 1.0, 0.0, 0.25].into_iter());
assert_eq!(
allocation.voiced().collect::<Vec<bool>>(),
[true, true, false, true],
"room for all of them, but never for one at no level"
);
}
#[test]
fn the_rank_cuts_at_the_cap_and_ties_keep_the_order_they_came_in() {
let over = MAX_VOICES + 8;
let mut allocation = Allocation::default();
allocation.rank(core::iter::repeat_n(0.5, over));
let voiced: Vec<bool> = allocation.voiced().collect();
assert_eq!(voiced.len(), over, "every one of them is ranked");
assert_eq!(
voiced.iter().filter(|voiced| **voiced).count(),
MAX_VOICES,
"and the cap is what is voiced"
);
assert!(
voiced[..MAX_VOICES].iter().all(|voiced| *voiced),
"the ones declared first, since nothing is louder than another"
);
}
#[test]
fn the_rank_takes_the_loudest_however_late_it_came_in() {
let mut levels = vec![0.5; MAX_VOICES];
levels.push(1.0);
let mut allocation = Allocation::default();
allocation.rank(levels.into_iter());
let voiced: Vec<bool> = allocation.voiced().collect();
assert!(voiced[MAX_VOICES], "the loudest, declared last of all");
assert_eq!(
voiced.iter().filter(|voiced| **voiced).count(),
MAX_VOICES,
"and one of the rest lost its voice for it"
);
assert!(!voiced[MAX_VOICES - 1], "the last of the ones that tied");
}
#[test]
fn a_trim_clamps_to_the_clip_and_an_empty_one_plays_nothing() {
let clip = counted(10);
let past_the_end = Knobs {
trim: Some((seconds(4), seconds(40))),
..knobs()
};
let backwards = Knobs {
trim: Some((seconds(6), seconds(2))),
..knobs()
};
let window = Window::of(&clip, &past_the_end, false).expect("what is there still plays");
assert_eq!((window.start.get(), window.end.get()), (4, 10));
assert!(Window::of(&clip, &backwards, false).is_none());
}
#[test]
fn a_loop_outside_the_window_comes_back_to_its_start_instead() {
let clip = counted(10);
let outside = Knobs {
trim: Some((seconds(4), seconds(8))),
loop_from: Some(seconds(1)),
..knobs()
};
let inside = Knobs {
loop_from: Some(seconds(6)),
..outside
};
assert_eq!(
Window::of(&clip, &outside, true)
.expect("it plays")
.wrap
.get(),
4
);
assert_eq!(
Window::of(&clip, &inside, true)
.expect("it plays")
.wrap
.get(),
6
);
assert_eq!(
Window::of(&clip, &inside, false)
.expect("it plays")
.wrap
.get(),
4,
"a one-shot never comes back around"
);
}
#[test]
fn a_window_walks_its_intro_once_and_its_body_forever() {
let clip = counted(10);
let looped = Knobs {
trim: Some((seconds(2), seconds(8))),
loop_from: Some(seconds(5)),
..knobs()
};
let window = Window::of(&clip, &looped, true).expect("it plays");
let walked: Vec<u64> = (0..12u64)
.filter_map(|played| window.at(ClipFrame::new(played)).map(ClipFrame::get))
.collect();
assert_eq!(walked, [2, 3, 4, 5, 6, 7, 5, 6, 7, 5, 6, 7]);
let once = Window::of(&clip, &looped, false).expect("it plays");
assert_eq!(once.at(ClipFrame::new(5)), Some(ClipFrame::new(7)));
assert_eq!(once.at(ClipFrame::new(6)), None, "and then it is over");
}
#[test]
fn one_value_at_two_instances_is_two_sustained_values_and_a_one_shot_is_none() {
let second = Knobs {
instance: 1,
..knobs()
};
assert_eq!(
Sustained::of(SoundId(3), &knobs(), true),
Some(sustained(3))
);
assert_eq!(
Sustained::of(SoundId(3), &second, true),
Some(Sustained {
sound: SoundId(3),
instance: 1
}),
"which the first one is not"
);
assert_eq!(
Sustained::of(SoundId(3), &second, false),
None,
"and a one-shot keeps no voice alive to name"
);
}
#[test]
fn the_limiter_leaves_a_quiet_mix_alone_and_bends_a_loud_one() {
assert_eq!(limit(0.5), 0.5);
assert_eq!(limit(-LIMITER_KNEE), -LIMITER_KNEE);
assert!(limit(1.0) > LIMITER_KNEE && limit(1.0) < 1.0);
assert!(limit(64.0) <= 1.0, "nothing ever leaves past one");
assert_eq!(limit(-4.0), -limit(4.0), "and it bends both ways alike");
}
#[test]
fn a_placed_sound_is_panned_and_fades_with_distance() {
let listener = View::look_at(Vec3::ZERO, Vec3::NEG_Z);
let placed = |position| {
levels(
&Knobs {
position: Some(position),
falloff: Falloff::DEFAULT.with_range(10.0),
..knobs()
},
&listener,
)
};
let right = placed(Vec3::X);
let left = placed(Vec3::NEG_X);
let ahead = placed(Vec3::NEG_Z);
assert!(right.right > right.left, "to the right is heard right");
assert!(
(left.left - right.right).abs() < 1e-6 && (left.right - right.left).abs() < 1e-6,
"and one side is the other side turned around"
);
assert!(
(ahead.left - ahead.right).abs() < 1e-6,
"and straight ahead is heard in both"
);
assert!(placed(Vec3::NEG_Z * 5.0).left < ahead.left, "fading out");
assert_eq!(placed(Vec3::NEG_Z * 20.0).left, 0.0, "to nothing at range");
}
#[test]
fn a_placed_sound_holds_its_level_to_the_reference_and_halves_at_twice_it() {
let listener = View::look_at(Vec3::ZERO, Vec3::NEG_Z);
let straight = FRAC_PI_4.cos();
let level = |meters: f32, falloff| {
levels(
&Knobs {
position: Some(Vec3::NEG_Z * meters),
falloff,
..knobs()
},
&listener,
)
.left
/ straight
};
let far = Falloff::DEFAULT.with_reference(2.0).with_range(1_000.0);
let near = Falloff::DEFAULT.with_range(10.0);
assert_eq!(level(1.0, far), 1.0, "full level within the reference");
assert_eq!(level(2.0, far), 1.0, "and at the reference itself");
assert!(
(level(4.0, far) - 0.5).abs() < 2e-3,
"half of it at twice the reference, but for the shift's share"
);
assert_eq!(level(10.0, near), 0.0, "nothing at the range");
assert_eq!(level(20.0, near), 0.0, "and nothing past it");
}
#[test]
fn a_sound_with_no_place_is_heard_the_same_in_both_ears() {
let listener = View::look_at(Vec3::ZERO, Vec3::NEG_Z);
let heard = levels(
&Knobs {
gain: 0.5,
..knobs()
},
&listener,
);
assert_eq!(
heard,
Levels {
left: 0.5,
right: 0.5
}
);
}
}