use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Music {
pub tempo: f64,
pub beats: Vec<f64>,
pub tolerance: f64,
pub on_beat: usize,
pub cuts: usize,
pub chance: f64,
}
impl Music {
#[allow(clippy::cast_precision_loss)]
pub fn ratio(&self) -> f64 {
if self.cuts == 0 {
0.0
} else {
self.on_beat as f64 / self.cuts as f64
}
}
pub fn cut_to_beat(&self) -> bool {
self.cuts >= 3 && self.ratio() >= (2.0 * self.chance).max(0.5)
}
}
pub fn nearest(beats: &[f64], t: f64) -> Option<f64> {
let i = beats.partition_point(|&b| b < t);
[i.checked_sub(1), Some(i)]
.into_iter()
.flatten()
.filter_map(|j| beats.get(j))
.map(|&b| b - t)
.min_by(|a, b| a.abs().total_cmp(&b.abs()))
}
#[allow(clippy::cast_precision_loss)]
pub fn align(tempo: f64, beats: &[f64], cuts: &[f64], tolerance: f64) -> Music {
let beats: Vec<f64> = beats
.iter()
.map(|b| (b * 1000.0).round() / 1000.0)
.collect();
let on_beat = cuts
.iter()
.filter(|&&c| nearest(&beats, c).is_some_and(|d| d.abs() <= tolerance))
.count();
let chance = (2.0 * tolerance * tempo / 60.0).min(1.0);
Music {
tempo: (tempo * 10.0).round() / 10.0,
beats,
tolerance,
on_beat,
cuts: cuts.len(),
chance: (chance * 1000.0).round() / 1000.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cuts_on_the_beat() {
let beats: Vec<f64> = (0..20).map(|i| f64::from(i) * 0.5).collect();
let m = align(120.0, &beats, &[1.02, 2.0, 3.49, 4.25], 0.05);
assert_eq!((m.on_beat, m.cuts), (3, 4));
assert!((m.chance - 0.2).abs() < 1e-9);
assert!(m.cut_to_beat());
assert_eq!(
nearest(&beats, 4.3).map(|d| (d * 100.0).round()),
Some(20.0)
);
assert_eq!(nearest(&[], 1.0), None);
}
}