use core::time::Duration;
use alloc::collections::BTreeSet;
use crate::effect::GlobalEffect;
use crate::module::Module;
pub const DEFAULT_MAX_ROWS: usize = 1 << 20;
#[derive(Debug, Clone, Copy)]
pub struct DurationOptions {
pub max_rows: usize,
pub stop_on_loop: bool,
}
impl Default for DurationOptions {
fn default() -> Self {
Self {
max_rows: DEFAULT_MAX_ROWS,
stop_on_loop: true,
}
}
}
pub trait ModuleDuration {
fn duration(&self, song: usize) -> Duration;
fn duration_with(&self, song: usize, opts: DurationOptions) -> Duration;
}
impl ModuleDuration for Module {
#[inline]
fn duration(&self, song: usize) -> Duration {
self.duration_with(song, DurationOptions::default())
}
fn duration_with(&self, song: usize, opts: DurationOptions) -> Duration {
compute_duration(self, song, opts)
}
}
pub fn compute_duration(module: &Module, song: usize, opts: DurationOptions) -> Duration {
if song >= module.pattern_order.len() {
return Duration::ZERO;
}
let order = &module.pattern_order[song];
if order.is_empty() || module.pattern.is_empty() {
return Duration::ZERO;
}
let mut speed: usize = module.default_tempo.max(1);
let mut bpm: f64 = module.default_bpm.max(1) as f64;
let mut order_idx: usize = 0;
let mut row_idx: usize = 0;
let mut loop_anchor: usize = 0;
let mut loop_iters_left: Option<usize> = None;
let mut total_secs: f64 = 0.0;
let mut visited: BTreeSet<(usize, usize, Option<usize>)> = BTreeSet::new();
let mut rows_processed: usize = 0;
loop {
if order_idx >= order.len() {
break; }
if rows_processed >= opts.max_rows {
break; }
let pat_idx = order[order_idx];
if pat_idx >= module.pattern.len() {
order_idx += 1;
row_idx = 0;
loop_iters_left = None;
continue;
}
let pattern = &module.pattern[pat_idx];
if pattern.is_empty() || row_idx >= pattern.len() {
order_idx += 1;
row_idx = 0;
loop_iters_left = None;
continue;
}
if opts.stop_on_loop {
let key = (order_idx, row_idx, loop_iters_left);
if !visited.insert(key) {
break;
}
}
rows_processed += 1;
let row = &pattern[row_idx];
let mut new_speed: Option<usize> = None;
let mut new_bpm: Option<usize> = None;
let mut bpm_slide: isize = 0;
let mut do_break: Option<usize> = None;
let mut do_jump: Option<usize> = None;
let mut loop_param: Option<usize> = None;
let mut row_repeats: usize = 1;
let mut song_end_requested: bool = false;
for unit in row.iter() {
for ge in unit.global_effects.iter() {
match ge {
GlobalEffect::Speed(n) => {
if *n > 0 {
new_speed = Some(*n);
} else if module.profile.quirks.speed_zero_ends_song {
song_end_requested = true;
}
}
GlobalEffect::Bpm(n) => {
if *n > 0 {
new_bpm = Some(*n);
}
}
GlobalEffect::BpmSlide(d) => {
bpm_slide = bpm_slide.saturating_add(*d);
}
GlobalEffect::PatternBreak(p) => do_break = Some(*p),
GlobalEffect::PositionJump(p) => do_jump = Some(*p),
GlobalEffect::PatternLoop(v) => loop_param = Some(*v),
GlobalEffect::PatternDelay { quantity, .. } => {
row_repeats = row_repeats.saturating_add(*quantity);
}
_ => {}
}
}
}
if let Some(s) = new_speed {
speed = s.max(1);
}
if let Some(b) = new_bpm {
bpm = b.max(1) as f64;
}
for _ in 0..row_repeats {
for tick in 0..speed {
if tick > 0 && bpm_slide != 0 {
let next = (bpm as isize).saturating_add(bpm_slide);
bpm = next.clamp(1, 1000) as f64;
}
total_secs += 2.5 / bpm;
}
}
if song_end_requested {
break;
}
if let Some(pos) = do_jump {
order_idx = pos;
row_idx = 0;
loop_iters_left = None;
loop_anchor = 0;
continue;
}
if let Some(target) = do_break {
order_idx += 1;
row_idx = target;
loop_iters_left = None;
loop_anchor = 0;
continue;
}
if let Some(v) = loop_param {
if v == 0 {
loop_anchor = row_idx;
row_idx += 1;
} else {
match loop_iters_left {
None => {
loop_iters_left = Some(v - 1);
row_idx = loop_anchor;
}
Some(0) => {
loop_iters_left = None;
row_idx += 1;
}
Some(n) => {
loop_iters_left = Some(n - 1);
row_idx = loop_anchor;
}
}
}
continue;
}
row_idx += 1;
if row_idx >= pattern.len() {
order_idx += 1;
row_idx = 0;
loop_iters_left = None;
loop_anchor = 0;
}
}
secs_to_duration(total_secs)
}
#[inline]
fn secs_to_duration(secs: f64) -> Duration {
if !secs.is_finite() || secs <= 0.0 {
return Duration::ZERO;
}
let nanos_total = (secs * 1_000_000_000.0).round();
if nanos_total >= (u64::MAX as f64) * 1_000_000_000.0 {
return Duration::new(u64::MAX, 999_999_999);
}
let nanos_total = nanos_total as u128;
let secs = (nanos_total / 1_000_000_000) as u64;
let nanos = (nanos_total % 1_000_000_000) as u32;
Duration::new(secs, nanos)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
use alloc::vec;
use alloc::vec::Vec;
fn row_with(n: usize, ge: Vec<GlobalEffect>) -> Row {
let mut r: Row = (0..n).map(|_| TrackUnit::default()).collect();
r[0].global_effects = ge;
r
}
fn empty_row(n: usize) -> Row {
(0..n).map(|_| TrackUnit::default()).collect()
}
fn make_module(pattern: Pattern, order: Vec<usize>) -> Module {
let mut m = Module::default();
m.pattern = vec![pattern];
m.pattern_order = vec![order];
m
}
#[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 = make_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 = make_module(pat, vec![0]);
let d = m.duration(0);
let expected = Duration::from_secs_f64(64.0 * 6.0 * 2.5 / 125.0);
let delta = if d > expected {
d - expected
} else {
expected - d
};
assert!(
delta < Duration::from_millis(1),
"got {d:?}, expected {expected:?}"
);
}
#[test]
fn speed_change_doubles_time() {
let mut pat: Pattern = (0..64).map(|_| empty_row(4)).collect();
pat[0] = row_with(4, vec![GlobalEffect::Speed(12)]);
let m = make_module(pat, vec![0]);
let d = m.duration(0);
let expected = Duration::from_secs_f64(64.0 * 12.0 * 2.5 / 125.0);
let delta = if d > expected {
d - expected
} else {
expected - d
};
assert!(
delta < Duration::from_millis(1),
"got {d:?}, expected {expected:?}"
);
}
#[test]
fn bpm_change_halves_time() {
let mut pat: Pattern = (0..64).map(|_| empty_row(4)).collect();
pat[0] = row_with(4, vec![GlobalEffect::Bpm(250)]);
let m = make_module(pat, vec![0]);
let d = m.duration(0);
let expected = Duration::from_secs_f64(64.0 * 6.0 * 2.5 / 250.0);
let delta = if d > expected {
d - expected
} else {
expected - d
};
assert!(delta < Duration::from_millis(1));
}
#[test]
fn position_jump_back_to_start_stops_at_one_pass() {
let mut pat: Pattern = (0..4).map(|_| empty_row(4)).collect();
pat[3] = row_with(4, vec![GlobalEffect::PositionJump(0)]);
let m = make_module(pat, vec![0]);
let d = m.duration(0);
let expected = Duration::from_secs_f64(4.0 * 6.0 * 2.5 / 125.0);
let delta = if d > expected {
d - expected
} else {
expected - d
};
assert!(delta < Duration::from_millis(1));
}
#[test]
fn pattern_break_skips_remaining_rows() {
let mut pat0: Pattern = (0..16).map(|_| empty_row(4)).collect();
pat0[7] = row_with(4, vec![GlobalEffect::PatternBreak(0)]);
let pat1: Pattern = (0..64).map(|_| empty_row(4)).collect();
let mut m = Module::default();
m.pattern = vec![pat0, pat1];
m.pattern_order = vec![vec![0, 1]];
let d = m.duration(0);
let expected = Duration::from_secs_f64((8 + 64) as f64 * 6.0 * 2.5 / 125.0);
let delta = if d > expected {
d - expected
} else {
expected - d
};
assert!(
delta < Duration::from_millis(1),
"got {d:?}, expected {expected:?}"
);
}
#[test]
fn pattern_delay_multiplies_row_time() {
let mut pat: Pattern = (0..4).map(|_| empty_row(4)).collect();
pat[1] = row_with(
4,
vec![GlobalEffect::PatternDelay {
quantity: 3,
tempo: false,
}],
);
let m = make_module(pat, vec![0]);
let d = m.duration(0);
let expected = Duration::from_secs_f64(7.0 * 6.0 * 2.5 / 125.0);
let delta = if d > expected {
d - expected
} else {
expected - d
};
assert!(
delta < Duration::from_millis(1),
"got {d:?}, expected {expected:?}"
);
}
#[test]
fn pattern_loop_runs_n_plus_one_times() {
let mut pat: Pattern = (0..4).map(|_| empty_row(4)).collect();
pat[0] = row_with(4, vec![GlobalEffect::PatternLoop(0)]);
pat[3] = row_with(4, vec![GlobalEffect::PatternLoop(2)]);
let m = make_module(pat, vec![0]);
let d = m.duration(0);
let expected = Duration::from_secs_f64(12.0 * 6.0 * 2.5 / 125.0);
let delta = if d > expected {
d - expected
} else {
expected - d
};
assert!(
delta < Duration::from_millis(1),
"got {d:?}, expected {expected:?}"
);
}
#[test]
fn runaway_is_capped() {
let mut pat: Pattern = vec![empty_row(4)];
pat[0] = row_with(4, vec![GlobalEffect::PositionJump(0)]);
let m = make_module(pat, vec![0]);
let d_default = m.duration(0);
let one_row = Duration::from_secs_f64(6.0 * 2.5 / 125.0);
let delta = if d_default > one_row {
d_default - one_row
} else {
one_row - d_default
};
assert!(delta < Duration::from_millis(1));
let opts = DurationOptions {
max_rows: 100,
stop_on_loop: false,
};
let d_capped = m.duration_with(0, opts);
let cap_expected = Duration::from_secs_f64(100.0 * 6.0 * 2.5 / 125.0);
let delta = if d_capped > cap_expected {
d_capped - cap_expected
} else {
cap_expected - d_capped
};
assert!(delta < Duration::from_millis(1));
}
#[test]
fn speed_zero_ends_song_when_quirk_on() {
let mut pat: Pattern = vec![empty_row(4); 4];
pat[2] = row_with(4, vec![GlobalEffect::Speed(0)]);
let mut m = make_module(pat, vec![0]);
let d_no_quirk = m.duration(0);
let four_rows = Duration::from_secs_f64(4.0 * 6.0 * 2.5 / 125.0);
let delta = if d_no_quirk > four_rows {
d_no_quirk - four_rows
} else {
four_rows - d_no_quirk
};
assert!(
delta < Duration::from_millis(1),
"no-quirk: got {d_no_quirk:?}, expected {four_rows:?}",
);
m.profile = CompatibilityProfile::pt();
let d_quirk = m.duration(0);
let three_rows = Duration::from_secs_f64(3.0 * 6.0 * 2.5 / 125.0);
let delta = if d_quirk > three_rows {
d_quirk - three_rows
} else {
three_rows - d_quirk
};
assert!(
delta < Duration::from_millis(1),
"with-quirk: got {d_quirk:?}, expected {three_rows:?}",
);
}
#[test]
fn speed_zero_with_quirk_off_keeps_old_speed() {
let mut pat: Pattern = vec![empty_row(4); 3];
pat[1] = row_with(4, vec![GlobalEffect::Speed(0)]);
let m = make_module(pat, vec![0]);
let d = m.duration(0);
let three_rows = Duration::from_secs_f64(3.0 * 6.0 * 2.5 / 125.0);
let delta = if d > three_rows {
d - three_rows
} else {
three_rows - d
};
assert!(delta < Duration::from_millis(1));
}
}