use crate::callbacks::Hook;
use crate::rng::StreamRng;
pub const HARD_CUT_MIN_FRAMES: u64 = 50;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SwitchRequest {
pub hard_cut: bool,
}
#[derive(Clone, Debug)]
pub struct PresetControl {
beat_sensitivity: f32,
aspect_correction: bool,
easter_egg: f32,
locked: bool,
preset_duration: f64,
soft_cut_duration: f64,
hard_cut_duration: f64,
hard_cut_duration_reported: f64,
hard_cut_enabled: bool,
hard_cut_sensitivity: f32,
now: f64,
preset_time: f64,
preset_duration_a: f64,
smoothing: bool,
smooth_start: f64,
notified: bool,
frame_count: u64,
previous_vol: f32,
rng: StreamRng,
on_request: Hook<SwitchRequest>,
md3_time_rand: Option<f64>,
}
impl PresetControl {
pub fn new(seed: u64) -> Self {
let mut c = Self {
beat_sensitivity: 1.0,
aspect_correction: true,
easter_egg: 1.0,
locked: false,
preset_duration: 30.0,
soft_cut_duration: 3.0,
hard_cut_duration: 20.0,
hard_cut_duration_reported: 20.0,
hard_cut_enabled: false,
hard_cut_sensitivity: 2.0,
now: 0.0,
preset_time: 0.0,
preset_duration_a: 0.0,
smoothing: false,
smooth_start: 0.0,
notified: false,
frame_count: 0,
previous_vol: 0.0,
rng: StreamRng::new(seed, "preset_duration"),
on_request: Hook::none(),
md3_time_rand: None,
};
c.preset_duration_a = c.sampled_preset_duration();
c
}
pub fn set_switch_requested_callback(
&mut self,
f: Option<impl Fn(&SwitchRequest) + Send + Sync + 'static>,
) {
self.on_request.set(f);
}
pub fn set_md3_time_rand(&mut self, rand: Option<f64>) {
self.md3_time_rand = rand.map(|r| r.max(0.0));
}
pub fn md3_time_rand(&self) -> Option<f64> {
self.md3_time_rand
}
pub fn set_beat_sensitivity(&mut self, sensitivity: f32) {
self.beat_sensitivity = sensitivity.clamp(0.0, 2.0);
}
pub fn beat_sensitivity(&self) -> f32 {
self.beat_sensitivity
}
pub fn set_aspect_correction(&mut self, enabled: bool) {
self.aspect_correction = enabled;
}
pub fn aspect_correction(&self) -> bool {
self.aspect_correction
}
pub fn set_easter_egg(&mut self, value: f32) {
self.easter_egg = value;
}
pub fn easter_egg(&self) -> f32 {
self.easter_egg
}
pub fn set_preset_locked(&mut self, locked: bool) {
self.locked = locked;
self.notified = locked;
}
pub fn preset_locked(&self) -> bool {
self.locked
}
pub fn set_preset_duration(&mut self, seconds: f64) {
self.preset_duration = seconds;
}
pub fn preset_duration(&self) -> f64 {
self.preset_duration
}
pub fn set_soft_cut_duration(&mut self, seconds: f64) {
self.soft_cut_duration = seconds;
}
pub fn soft_cut_duration(&self) -> f64 {
self.soft_cut_duration
}
pub fn set_hard_cut_enabled(&mut self, enabled: bool) {
self.hard_cut_enabled = enabled;
}
pub fn hard_cut_enabled(&self) -> bool {
self.hard_cut_enabled
}
pub fn set_hard_cut_duration(&mut self, seconds: f64) {
self.hard_cut_duration = seconds;
self.hard_cut_duration_reported = seconds.trunc();
}
#[allow(clippy::misnamed_getters)]
pub fn hard_cut_duration(&self) -> f64 {
self.hard_cut_duration_reported
}
pub fn set_hard_cut_sensitivity(&mut self, sensitivity: f32) {
self.hard_cut_sensitivity = sensitivity;
}
pub fn hard_cut_sensitivity(&self) -> f32 {
self.hard_cut_sensitivity
}
pub fn sampled_preset_duration(&mut self) -> f64 {
if let Some(rand) = self.md3_time_rand {
let r = f64::from(self.rng.next32() % 1000);
return self.soft_cut_duration + self.preset_duration + r * rand * 0.001;
}
if f64::from(self.easter_egg) < 0.001 {
return self.preset_duration;
}
let u1 = ((self.rng.next64() >> 11) as f64 + 1.0) / (1u64 << 53) as f64;
let u2 = (self.rng.next64() >> 11) as f64 / (1u64 << 53) as f64;
let z = (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos();
(self.preset_duration + f64::from(self.easter_egg) * z).max(1.0)
}
pub fn start_preset(&mut self, time: f64) {
self.now = time;
self.notified = self.locked;
self.smoothing = false;
self.preset_time = time;
self.preset_duration_a = self.sampled_preset_duration();
}
pub fn start_smoothing(&mut self, time: f64) {
self.now = time;
self.notified = self.locked;
if self.smoothing {
self.end_smoothing();
}
self.smoothing = true;
self.smooth_start = time;
}
fn end_smoothing(&mut self) {
self.smoothing = false;
self.preset_time = self.smooth_start;
self.preset_duration_a = self.sampled_preset_duration();
}
pub fn is_smoothing(&self) -> bool {
self.smoothing
}
pub fn frame_time(&self) -> f64 {
self.now
}
pub fn preset_progress(&self) -> f64 {
if self.smoothing {
return 1.0;
}
((self.now - self.preset_time) / self.preset_duration_a).min(1.0)
}
fn can_hard_cut(&self) -> bool {
self.now - self.preset_time > self.hard_cut_duration
}
pub fn update(&mut self, time: f64, vol: f32) -> Option<SwitchRequest> {
self.update_with_auto_cuts(time, vol, 0)
}
pub fn update_with_auto_cuts(
&mut self,
time: f64,
vol: f32,
auto_cuts: u32,
) -> Option<SwitchRequest> {
self.now = time;
let mut request = None;
if !self.notified {
if self.preset_progress() >= 1.0 && !self.smoothing {
self.notified = true;
request = Some(SwitchRequest { hard_cut: false });
} else if self.hard_cut_enabled
&& self.frame_count > HARD_CUT_MIN_FRAMES
&& vol - self.previous_vol > self.hard_cut_sensitivity
&& self.can_hard_cut()
{
self.notified = true;
request = Some(SwitchRequest { hard_cut: true });
}
}
if auto_cuts > 0 && !self.locked {
self.notified = true;
request = Some(SwitchRequest { hard_cut: true });
}
if self.smoothing && (self.now - self.smooth_start) / self.soft_cut_duration >= 1.0 {
self.end_smoothing();
}
self.frame_count += 1;
self.previous_vol = vol;
if let Some(r) = &request {
self.on_request.fire(r);
}
request
}
}
#[cfg(test)]
mod tests {
use super::*;
fn run(
c: &mut PresetControl,
from: u32,
to: u32,
fps: f64,
vol: impl Fn(u32) -> f32,
) -> Vec<(u32, SwitchRequest)> {
let mut out = Vec::new();
for n in from..to {
if let Some(r) = c.update(f64::from(n) / fps, vol(n)) {
out.push((n, r));
}
}
out
}
#[test]
fn defaults_and_clamps_follow_projectm() {
let mut c = PresetControl::new(1);
assert_eq!(c.beat_sensitivity(), 1.0);
assert!(c.aspect_correction());
assert_eq!(c.easter_egg(), 1.0);
assert!(!c.preset_locked() && !c.hard_cut_enabled());
assert_eq!((c.preset_duration(), c.soft_cut_duration()), (30.0, 3.0));
assert_eq!(
(c.hard_cut_duration(), c.hard_cut_sensitivity()),
(20.0, 2.0)
);
c.set_beat_sensitivity(7.0);
assert_eq!(c.beat_sensitivity(), 2.0);
c.set_beat_sensitivity(-1.0);
assert_eq!(c.beat_sensitivity(), 0.0);
c.set_beat_sensitivity(0.25);
assert_eq!(c.beat_sensitivity(), 0.25);
c.set_aspect_correction(false);
assert!(!c.aspect_correction());
c.set_hard_cut_duration(12.75);
assert_eq!(c.hard_cut_duration(), 12.0);
}
#[test]
fn display_time_up_requests_a_soft_cut_once() {
let mut c = PresetControl::new(1);
c.set_easter_egg(0.0); c.set_preset_duration(2.0);
c.start_preset(0.0);
let got = run(&mut c, 0, 600, 60.0, |_| 0.0);
assert_eq!(got, vec![(120, SwitchRequest { hard_cut: false })]);
c.start_preset(10.0);
assert_eq!(c.preset_progress(), 0.0);
assert!(run(&mut c, 600, 700, 60.0, |_| 0.0).is_empty()); assert_eq!(run(&mut c, 700, 800, 60.0, |_| 0.0).len(), 1);
}
#[test]
fn hard_cut_needs_flag_frames_minimum_time_and_a_volume_jump() {
let spike = |n: u32| if n == 900 { 5.0 } else { 0.0 };
let mut c = PresetControl::new(1);
c.set_easter_egg(0.0);
c.set_preset_duration(1e6);
c.set_hard_cut_duration(10.0);
c.start_preset(0.0);
assert!(run(&mut c, 0, 1000, 60.0, spike).is_empty());
let mut c = PresetControl::new(1);
c.set_easter_egg(0.0);
c.set_preset_duration(1e6);
c.set_hard_cut_enabled(true);
c.set_hard_cut_duration(10.0);
c.start_preset(0.0);
let vol = |n: u32| if n == 30 || n == 900 { 5.0 } else { 0.0 };
assert_eq!(
run(&mut c, 0, 1000, 60.0, vol),
vec![(900, SwitchRequest { hard_cut: true })]
);
let mut c = PresetControl::new(1);
c.set_easter_egg(0.0);
c.set_preset_duration(1e6);
c.set_hard_cut_enabled(true);
c.set_hard_cut_duration(1.0);
c.start_preset(0.0);
let jump = |n: u32| if n == 300 { 1.5 } else { 0.0 };
assert!(run(&mut c, 0, 400, 60.0, jump).is_empty());
let mut c = PresetControl::new(1);
c.set_easter_egg(0.0);
c.set_preset_duration(1e6);
c.set_hard_cut_enabled(true);
c.set_hard_cut_duration(1.0);
c.set_hard_cut_sensitivity(1.0);
c.start_preset(0.0);
assert_eq!(run(&mut c, 0, 400, 60.0, jump).len(), 1);
}
#[test]
fn lock_suppresses_requests_and_unlock_rearms_the_current_preset() {
let mut c = PresetControl::new(1);
c.set_easter_egg(0.0);
c.set_preset_duration(1.0);
c.set_preset_locked(true);
c.start_preset(0.0);
assert!(run(&mut c, 0, 300, 60.0, |_| 0.0).is_empty());
c.set_preset_locked(false);
assert_eq!(
run(&mut c, 300, 301, 60.0, |_| 0.0),
vec![(300, SwitchRequest { hard_cut: false })]
);
assert!(run(&mut c, 301, 600, 60.0, |_| 0.0).is_empty());
c.start_preset(10.0);
assert_eq!(run(&mut c, 600, 800, 60.0, |_| 0.0).len(), 1);
}
#[test]
fn soft_cut_state_machine_blocks_requests_then_restarts_the_timer() {
let mut c = PresetControl::new(1);
c.set_easter_egg(0.0);
c.set_preset_duration(5.0);
c.set_soft_cut_duration(2.0);
c.start_preset(0.0);
c.start_smoothing(1.0);
assert!(c.is_smoothing());
assert_eq!(c.preset_progress(), 1.0); assert!(run(&mut c, 60, 179, 60.0, |_| 0.0).is_empty());
assert!(c.is_smoothing());
run(&mut c, 179, 182, 60.0, |_| 0.0);
assert!(!c.is_smoothing());
let got = run(&mut c, 182, 400, 60.0, |_| 0.0);
assert_eq!(got.len(), 1);
assert_eq!(got[0].0, 360);
assert!(!got[0].1.hard_cut);
}
#[test]
fn easter_egg_samples_a_seeded_gaussian_of_at_least_one_second() {
let mut a = PresetControl::new(7);
let mut b = PresetControl::new(7);
a.set_easter_egg(5.0);
b.set_easter_egg(5.0);
let xs: Vec<f64> = (0..2000).map(|_| a.sampled_preset_duration()).collect();
let ys: Vec<f64> = (0..2000).map(|_| b.sampled_preset_duration()).collect();
assert_eq!(xs, ys, "same seed, same samples");
assert!(xs.iter().all(|&x| x >= 1.0));
let mean = xs.iter().sum::<f64>() / xs.len() as f64;
let var = xs.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / xs.len() as f64;
assert!((mean - 30.0).abs() < 0.5, "mean {mean}");
assert!((var.sqrt() - 5.0).abs() < 0.5, "sigma {}", var.sqrt());
assert!(xs.iter().any(|&x| (x - 30.0).abs() > 1.0), "durations vary");
a.set_easter_egg(0.0005);
assert_eq!(a.sampled_preset_duration(), 30.0);
let mut other = PresetControl::new(8);
other.set_easter_egg(5.0);
assert_ne!(other.sampled_preset_duration(), xs[0]);
}
#[test]
fn auto_cuts_request_a_hard_cut_independent_of_the_projectm_settings() {
let mut c = PresetControl::new(1);
c.set_easter_egg(0.0);
c.set_preset_duration(1e6);
c.start_preset(0.0);
let r = c.update_with_auto_cuts(0.5, 0.0, 1);
assert_eq!(r, Some(SwitchRequest { hard_cut: true }));
c.start_preset(1.0);
assert_eq!(
c.update_with_auto_cuts(1.1, 0.0, 2),
Some(SwitchRequest { hard_cut: true })
);
c.start_preset(2.0);
assert_eq!(c.update_with_auto_cuts(2.1, 0.0, 0), None);
}
#[test]
fn an_auto_cut_wins_over_the_display_time_and_a_lock_suppresses_it() {
let mut c = PresetControl::new(1);
c.set_easter_egg(0.0);
c.set_preset_duration(1.0);
c.start_preset(0.0);
assert_eq!(
c.update_with_auto_cuts(2.0, 0.0, 1),
Some(SwitchRequest { hard_cut: true })
);
c.start_preset(3.0);
c.set_preset_locked(true);
assert_eq!(c.update_with_auto_cuts(3.5, 0.0, 1), None);
}
#[test]
fn md3_display_time_adds_the_blend_time_and_a_seeded_random_part() {
let mut c = PresetControl::new(3);
c.set_preset_duration(16.0);
c.set_soft_cut_duration(2.7);
c.set_md3_time_rand(Some(10.0));
assert_eq!(c.md3_time_rand(), Some(10.0));
let xs: Vec<f64> = (0..500).map(|_| c.sampled_preset_duration()).collect();
assert!(xs.iter().all(|&x| (18.7..=28.69 + 1e-9).contains(&x)));
assert!(xs.iter().any(|&x| x > 25.0) && xs.iter().any(|&x| x < 22.0));
let mut d = PresetControl::new(3);
d.set_preset_duration(16.0);
d.set_soft_cut_duration(2.7);
d.set_md3_time_rand(Some(10.0));
let ys: Vec<f64> = (0..500).map(|_| d.sampled_preset_duration()).collect();
assert_eq!(xs, ys, "same seed, same samples");
c.set_md3_time_rand(Some(-4.0));
assert_eq!(c.md3_time_rand(), Some(0.0));
c.set_easter_egg(5.0);
assert!((c.sampled_preset_duration() - 18.7).abs() < 1e-12);
c.set_md3_time_rand(None);
c.set_easter_egg(0.0);
assert_eq!(c.sampled_preset_duration(), 16.0);
}
}