use core::time::Duration;
use crate::core::module::Module;
pub trait ModuleDuration {
fn duration(&self, song: usize) -> Duration;
}
impl ModuleDuration for Module {
fn duration(&self, song: usize) -> Duration {
compute_duration(self, song)
}
}
pub fn compute_duration(module: &Module, song: usize) -> Duration {
if song > u16::MAX as usize {
return Duration::ZERO;
}
let song_u16 = song as u16;
let mut total_nanos: u64 = 0;
for e in &module.timeline_map.entries {
if e.song != song_u16 {
continue;
}
let bpm = e.bpm_at_row.max(1) as u64;
let scaled = (e.speed_at_row as u64) * 2_500_000_000;
let row_nanos = (scaled + bpm / 2) / bpm;
total_nanos = total_nanos.saturating_add(row_nanos);
}
let secs = total_nanos / 1_000_000_000;
let nanos = (total_nanos % 1_000_000_000) as u32;
Duration::new(secs, nanos)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
use crate::tracker::import::build::{build_timeline_layer, Pattern, Row};
use alloc::vec;
use alloc::vec::Vec;
fn nav_row(n: usize, ne: Vec<NavigationEffect>) -> Row {
let mut r: Row = (0..n)
.map(|_| crate::tracker::import::unit::TrackImportUnit::default())
.collect();
r[0].navigation = ne;
r
}
fn song_row(n: usize, sle: Vec<SongLevelEffect>) -> Row {
let mut r: Row = (0..n)
.map(|_| crate::tracker::import::unit::TrackImportUnit::default())
.collect();
r[0].song_level = sle;
r
}
fn empty_row(n: usize) -> Row {
(0..n)
.map(|_| crate::tracker::import::unit::TrackImportUnit::default())
.collect()
}
fn build_module(pattern: Pattern, order: Vec<usize>) -> Module {
let mut m = Module::default();
let pattern_order = vec![order];
let patterns = vec![pattern];
build_timeline_layer(&mut m, &pattern_order, &patterns);
m
}
fn expected_duration(rows: u64, speed: u64, bpm: u64) -> Duration {
let total_nanos = (rows * speed * 2_500_000_000 + bpm / 2) / bpm;
let secs = total_nanos / 1_000_000_000;
let nanos = (total_nanos % 1_000_000_000) as u32;
Duration::new(secs, nanos)
}
fn close_to(d: Duration, expected: Duration) -> bool {
let delta = d.abs_diff(expected);
delta < Duration::from_millis(1)
}
#[test]
fn empty_module_is_zero() {
let m = Module::default();
assert_eq!(m.duration(0), Duration::ZERO);
}
#[test]
fn out_of_range_song_is_zero() {
let m = build_module(vec![empty_row(4); 8], vec![0]);
assert_eq!(m.duration(42), Duration::ZERO);
}
#[test]
fn defaults_one_pattern_64_rows() {
let pat: Pattern = (0..64).map(|_| empty_row(4)).collect();
let m = build_module(pat, vec![0]);
let expected = expected_duration(64, 6, 125);
assert!(close_to(m.duration(0), expected), "got {:?}", m.duration(0));
}
#[test]
fn speed_change_doubles_time() {
let mut pat: Pattern = (0..64).map(|_| empty_row(4)).collect();
pat[0] = song_row(4, vec![SongLevelEffect::Speed(12)]);
let m = build_module(pat, vec![0]);
let expected = expected_duration(64, 12, 125);
assert!(close_to(m.duration(0), expected));
}
#[test]
fn bpm_change_halves_time() {
let mut pat: Pattern = (0..64).map(|_| empty_row(4)).collect();
pat[0] = song_row(4, vec![SongLevelEffect::Bpm(250)]);
let m = build_module(pat, vec![0]);
let expected = expected_duration(64, 6, 250);
assert!(close_to(m.duration(0), expected));
}
#[test]
fn position_jump_back_to_start_stops_at_one_pass() {
let mut pat: Pattern = (0..4).map(|_| empty_row(4)).collect();
pat[3] = nav_row(4, vec![NavigationEffect::PositionJump(0)]);
let m = build_module(pat, vec![0]);
let expected = expected_duration(4, 6, 125);
assert!(close_to(m.duration(0), expected));
}
#[test]
fn pattern_break_skips_remaining_rows() {
let mut pat0: Pattern = (0..16).map(|_| empty_row(4)).collect();
pat0[7] = nav_row(4, vec![NavigationEffect::PatternBreak(0)]);
let pat1: Pattern = (0..64).map(|_| empty_row(4)).collect();
let mut m = Module::default();
build_timeline_layer(&mut m, &[vec![0, 1]], &[pat0, pat1]);
let expected = expected_duration(8 + 64, 6, 125);
assert!(close_to(m.duration(0), expected));
}
#[test]
fn pattern_delay_multiplies_row_time() {
let mut pat: Pattern = (0..4).map(|_| empty_row(4)).collect();
pat[1] = nav_row(
4,
vec![NavigationEffect::PatternDelay {
quantity: 3,
tempo: false,
}],
);
let m = build_module(pat, vec![0]);
let expected = expected_duration(7, 6, 125);
assert!(close_to(m.duration(0), expected));
}
#[test]
fn pattern_loop_runs_n_plus_one_times() {
let mut pat: Pattern = (0..4).map(|_| empty_row(4)).collect();
pat[0] = nav_row(4, vec![NavigationEffect::PatternLoop(0)]);
pat[3] = nav_row(4, vec![NavigationEffect::PatternLoop(2)]);
let m = build_module(pat, vec![0]);
let expected = expected_duration(12, 6, 125);
assert!(close_to(m.duration(0), expected));
}
#[test]
fn speed_zero_ends_song_when_quirk_on() {
let mut pat: Pattern = vec![empty_row(4); 4];
pat[2] = song_row(4, vec![SongLevelEffect::Speed(0)]);
let m = build_module(pat.clone(), vec![0]);
let four_rows = expected_duration(4, 6, 125);
assert!(close_to(m.duration(0), four_rows));
let mut m = Module {
quirks: crate::core::compatibility::PlaybackQuirks {
speed_zero_ends_song: true,
..Default::default()
},
..Default::default()
};
build_timeline_layer(&mut m, &[vec![0]], &[pat]);
let three_rows = expected_duration(3, 6, 125);
assert!(close_to(m.duration(0), three_rows));
}
}