use core::f32::consts::FRAC_PI_4;
use core::time::Duration;
use crate::sound::data::ClipFrame;
use crate::sound::mixer::Levels;
pub(crate) const REPITCH: Duration = Duration::from_millis(100);
pub(crate) struct Scheduled<'a> {
pub(crate) ends: &'a [f64],
pub(crate) until: f64,
pub(crate) produced: ClipFrame,
pub(crate) rate: f64,
}
impl Scheduled<'_> {
pub(crate) fn cut(&self, now: f64) -> Cut {
let ends = self.ends.iter().copied().filter(|end| *end > now);
let at = ends
.fold(f64::INFINITY, f64::min)
.min(now + REPITCH.as_secs_f64())
.min(self.until)
.max(now);
Cut {
at,
produced: self
.produced
.back(((self.until - at) * self.rate).round() as u64),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Cut {
pub(crate) at: f64,
pub(crate) produced: ClipFrame,
}
pub(crate) fn split(levels: &Levels) -> (f32, f32) {
let gain = levels.left.hypot(levels.right);
let pan = levels.right.atan2(levels.left) / FRAC_PI_4 - 1.0;
(gain, pan.clamp(-1.0, 1.0))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_streamed_repitch_cuts_the_buffers_scheduled_ahead_within_its_bound() {
let now = 10.0;
let cut = |ends: &[f64], until: f64, produced: u64| {
Scheduled {
ends,
until,
produced: ClipFrame::new(produced),
rate: 100.0,
}
.cut(now)
};
assert_eq!(
cut(&[10.05, 10.55], 10.55, 100),
Cut {
at: 10.05,
produced: ClipFrame::new(50)
},
"the buffer playing now ends inside the bound, so the cut is there"
);
let bounded = cut(&[10.55], 10.55, 100);
assert!(
bounded.at <= now + REPITCH.as_secs_f64(),
"and with none ending inside it, the cut is no further off than the bound"
);
assert_eq!(
bounded.produced,
ClipFrame::new(55),
"leaving the frames up to the cut to play"
);
assert_eq!(
cut(&[], 9.9, 100),
Cut {
at: now,
produced: ClipFrame::new(100)
},
"and a voice with nothing scheduled is cut where it stands"
);
}
#[test]
fn the_page_hears_the_levels_the_mixer_would_make() {
for gain in [0.25f32, 1.0, 1.5] {
for pan in [-1.0f32, -0.5, 0.0, 0.7, 1.0] {
let angle = (pan + 1.0) * FRAC_PI_4;
let made = Levels {
left: gain * angle.cos(),
right: gain * angle.sin(),
};
let (heard, panned) = split(&made);
let back = (panned + 1.0) * FRAC_PI_4;
assert!(
(heard * back.cos() - made.left).abs() < 1e-5
&& (heard * back.sin() - made.right).abs() < 1e-5,
"gain {gain} pan {pan} came back as gain {heard} pan {panned}"
);
}
}
}
}