use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use crate::core::daw::timeline::{TimelineEntry, TimelineMap};
use crate::core::effect::{NavigationEffect, SongLevelEffect};
use crate::core::module::Module;
use super::Pattern;
const MAX_ROWS: usize = 1 << 20;
pub fn build_timeline_map(
module: &Module,
pattern_order: &[Vec<usize>],
pattern: &[Pattern],
) -> (TimelineMap, Option<u32>) {
let mut entries: Vec<TimelineEntry> = Vec::new();
let mut wrap_tick_song0: Option<u32> = None;
for song in 0..pattern_order.len() {
let wrap = walk_one_song(module, pattern_order, pattern, song, &mut entries);
if song == 0 {
wrap_tick_song0 = wrap;
}
}
(TimelineMap { entries }, wrap_tick_song0)
}
fn walk_one_song(
module: &Module,
pattern_order: &[Vec<usize>],
patterns: &[Pattern],
song: usize,
out: &mut Vec<TimelineEntry>,
) -> Option<u32> {
let order = &pattern_order[song];
if order.is_empty() || patterns.is_empty() {
return None;
}
let song_u16 = song.min(u16::MAX as usize) as u16;
let mut speed: usize = module.default_tempo.max(1);
let mut bpm: u32 = module.default_bpm.max(1) as u32;
let mut order_idx: usize = 0;
let mut row_idx: usize = 0;
let nch = patterns
.iter()
.filter_map(|p| p.first())
.map(|row| row.len())
.max()
.unwrap_or(1)
.max(1);
let mut loop_anchor: alloc::vec::Vec<usize> = alloc::vec![0; nch];
let mut loop_counter: alloc::vec::Vec<Option<usize>> = alloc::vec![None; nch];
let mut loop_iter_count: u16 = 0;
let e60_leaks = module.quirks.e60_leaks_to_next_pattern;
let mut ft2_leak_row: Option<usize> = None;
let mut tick_acc: u32 = 0;
let mut visited: BTreeMap<(usize, usize, alloc::vec::Vec<Option<usize>>), u32> =
BTreeMap::new();
let mut rows_processed: usize = 0;
loop {
if order_idx >= order.len() || rows_processed >= MAX_ROWS {
return None;
}
let pat_idx = order[order_idx];
if pat_idx >= patterns.len() {
order_idx += 1;
row_idx = 0;
loop_counter.fill(None);
loop_iter_count = 0;
ft2_leak_row = None;
continue;
}
let pattern = &patterns[pat_idx];
if pattern.is_empty() || row_idx >= pattern.len() {
order_idx += 1;
row_idx = 0;
loop_counter.fill(None);
loop_iter_count = 0;
ft2_leak_row = None;
continue;
}
let key = (order_idx, row_idx, loop_counter.clone());
if let Some(&first_tick) = visited.get(&key) {
return Some(first_tick);
}
visited.insert(key, tick_acc);
rows_processed += 1;
let row = &pattern[row_idx];
let mut new_speed: Option<usize> = None;
let mut new_bpm: Option<usize> = None;
let mut do_break: Option<usize> = None;
let mut do_jump: Option<usize> = None;
let mut loop_params: alloc::vec::Vec<Option<usize>> = alloc::vec![None; nch];
let mut any_loop_param = false;
let mut row_repeats: usize = 1;
let mut song_end_requested = false;
for (ch, unit) in row.iter().enumerate() {
for sle in unit.song_level.iter() {
match sle {
SongLevelEffect::Speed(n) => {
if *n > 0 {
new_speed = Some(*n);
} else if module.quirks.speed_zero_ends_song {
song_end_requested = true;
}
}
SongLevelEffect::Bpm(n) if *n > 0 => {
new_bpm = Some(*n);
}
_ => {}
}
}
for ne in unit.navigation.iter() {
match ne {
NavigationEffect::PatternBreak(p) => do_break = Some(*p),
NavigationEffect::PositionJump(p) => do_jump = Some(*p),
NavigationEffect::PatternLoop(v) => {
if ch < nch {
loop_params[ch] = Some(*v);
any_loop_param = true;
}
}
NavigationEffect::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 u32;
}
let speed_u8 = speed.min(255) as u8;
let bpm_u16 = bpm.min(u16::MAX as u32) as u16;
let row_repeats_u8 = row_repeats.min(255) as u8;
for repeat in 0..row_repeats_u8 {
out.push(TimelineEntry {
song: song_u16,
order_idx: order_idx as u32,
pattern_idx: pat_idx as u32,
row_idx: row_idx as u32,
loop_iter: loop_iter_count,
tick: tick_acc + repeat as u32 * speed as u32,
speed_at_row: speed_u8,
bpm_at_row: bpm_u16,
});
}
tick_acc = tick_acc.saturating_add(speed as u32 * row_repeats as u32);
if song_end_requested {
return None;
}
if let Some(pos) = do_jump {
order_idx = pos;
row_idx = 0;
loop_counter.fill(None);
loop_anchor.fill(0);
loop_iter_count = 0;
ft2_leak_row = None;
continue;
}
let mut loop_jump: Option<usize> = None;
let mut any_expired = false;
if any_loop_param {
for ch in 0..nch {
let Some(v) = loop_params[ch] else { continue };
if v == 0 {
loop_anchor[ch] = row_idx; continue;
}
match loop_counter[ch] {
None => {
loop_counter[ch] = Some(v - 1);
loop_jump = Some(loop_anchor[ch]);
}
Some(0) => {
loop_counter[ch] = None;
any_expired = true;
if module.quirks.pattern_loop_resumes {
loop_anchor[ch] = row_idx + 1;
}
}
Some(n) => {
loop_counter[ch] = Some(n - 1);
loop_jump = Some(loop_anchor[ch]);
}
}
}
}
if let Some(target) = do_break {
let any_active = loop_counter.iter().any(|c| c.is_some());
let suppressed = module.quirks.active_loop_suppresses_break && any_active;
if !suppressed {
let landing = if module.quirks.pattern_loop_break_resets_row
&& loop_jump.is_some()
{
0
} else {
target
};
order_idx += 1;
row_idx = landing;
loop_counter.fill(None);
loop_anchor.fill(0);
loop_iter_count = 0;
ft2_leak_row = None;
continue;
}
}
if let Some(anchor) = loop_jump {
loop_iter_count = loop_iter_count.saturating_add(1);
row_idx = anchor;
if e60_leaks {
ft2_leak_row = Some(anchor);
}
continue;
}
if any_expired {
loop_iter_count = 0;
}
row_idx += 1;
if row_idx >= pattern.len() {
order_idx += 1;
let leaked = ft2_leak_row.take();
row_idx = match leaked {
Some(r) if e60_leaks => {
let next_len = order
.get(order_idx)
.and_then(|&pi| patterns.get(pi))
.map(|p| p.len())
.unwrap_or(0);
if r < next_len {
r
} else {
0
}
}
_ => 0,
};
loop_counter.fill(None);
loop_anchor.fill(0);
loop_iter_count = 0;
}
}
}
#[cfg(test)]
mod walker_wrap_tests {
use super::*;
use crate::core::compatibility::PlaybackQuirks;
use crate::core::effect::NavigationEffect;
use crate::core::module::Module;
use crate::tracker::import::unit::TrackImportUnit;
use alloc::vec;
fn empty_row() -> Vec<TrackImportUnit> {
vec![TrackImportUnit::default()]
}
fn empty_pat(rows: usize) -> Pattern {
(0..rows).map(|_| empty_row()).collect()
}
#[test]
fn position_jump_back_records_first_visit_tick_as_wrap() {
let mut pat_a = empty_pat(4);
let mut pat_b = empty_pat(4);
let mut bxx_unit = TrackImportUnit::default();
bxx_unit.navigation.push(NavigationEffect::PositionJump(1));
pat_b[3][0] = bxx_unit;
let module = Module::default();
let order = vec![0usize, 1];
let patterns = vec![pat_a.clone(), pat_b.clone()];
let mut out = Vec::new();
let wrap = walk_one_song(&module, &[order], &patterns, 0, &mut out);
assert_eq!(out.len(), 8, "every order-0 + order-1 row plays once");
let expected_wrap_tick = 4 * module.default_tempo as u32;
assert_eq!(
wrap,
Some(expected_wrap_tick),
"B01 on last row should record order-1-row-0 tick as wrap target",
);
}
#[test]
fn linear_song_without_jumps_reports_no_wrap() {
let module = Module::default();
let order = vec![0usize, 1];
let patterns = vec![empty_pat(4), empty_pat(4)];
let mut out = Vec::new();
let wrap = walk_one_song(&module, &[order], &patterns, 0, &mut out);
assert_eq!(out.len(), 8);
assert_eq!(wrap, None);
}
fn pat_nch(rows: usize, nch: usize) -> Pattern {
(0..rows)
.map(|_| (0..nch).map(|_| TrackImportUnit::default()).collect())
.collect()
}
#[test]
fn pattern_loop_on_nonzero_channel_still_repeats() {
let mut pat = pat_nch(6, 4);
pat[1][2].navigation.push(NavigationEffect::PatternLoop(0)); pat[4][2].navigation.push(NavigationEffect::PatternLoop(1)); let module = Module::default();
let mut out = Vec::new();
walk_one_song(&module, &[vec![0usize]], &[pat], 0, &mut out);
let repeats: Vec<u32> = out
.iter()
.filter(|e| e.loop_iter == 1)
.map(|e| e.row_idx)
.collect();
assert_eq!(repeats, vec![1, 2, 3, 4], "loop body rows 1..=4 replay once");
}
#[test]
fn it_active_loop_suppresses_same_row_break() {
let mut pat0 = pat_nch(4, 2);
pat0[0][0].navigation.push(NavigationEffect::PatternLoop(0)); pat0[2][0].navigation.push(NavigationEffect::PatternLoop(1)); pat0[2][1].navigation.push(NavigationEffect::PatternBreak(0)); let patterns = vec![pat0, empty_pat(4)];
let order = vec![0usize, 1];
let plain = Module::default();
let mut out_plain = Vec::new();
walk_one_song(&plain, &[order.clone()], &patterns, 0, &mut out_plain);
assert!(
!out_plain.iter().any(|e| e.loop_iter == 1),
"without the quirk the same-row break preempts the loop",
);
let mut it = Module::default();
it.quirks.active_loop_suppresses_break = true;
let mut out_it = Vec::new();
walk_one_song(&it, &[order], &patterns, 0, &mut out_it);
assert!(
out_it.iter().any(|e| e.loop_iter == 1),
"IT suppresses the break so the loop runs once",
);
}
#[test]
fn ft2_e60_leaks_loop_start_row_to_next_pattern() {
let mut pat0 = empty_pat(6);
pat0[2][0].navigation.push(NavigationEffect::PatternLoop(0)); pat0[4][0].navigation.push(NavigationEffect::PatternLoop(1)); let patterns = vec![pat0, empty_pat(6)];
let order = vec![0usize, 1];
let first_order1_row = |m: &Module| -> u32 {
let mut out = Vec::new();
walk_one_song(m, &[order.clone()], &patterns, 0, &mut out);
out.iter()
.find(|e| e.order_idx == 1)
.map(|e| e.row_idx)
.expect("order 1 is reached")
};
assert_eq!(first_order1_row(&Module::default()), 0);
let mut ft2 = Module::default();
ft2.quirks.e60_leaks_to_next_pattern = true;
assert_eq!(first_order1_row(&ft2), 2);
}
#[test]
fn pt_loop_plus_break_lands_at_row_zero() {
let mut pat0 = pat_nch(4, 2);
pat0[0][0].navigation.push(NavigationEffect::PatternLoop(0)); pat0[2][0].navigation.push(NavigationEffect::PatternLoop(1)); pat0[2][1].navigation.push(NavigationEffect::PatternBreak(3)); let patterns = vec![pat0, empty_pat(6)];
let order = vec![0usize, 1];
let first_order1_row = |m: &Module| -> u32 {
let mut out = Vec::new();
walk_one_song(m, &[order.clone()], &patterns, 0, &mut out);
out.iter()
.find(|e| e.order_idx == 1)
.map(|e| e.row_idx)
.expect("order 1 is reached")
};
assert_eq!(first_order1_row(&Module::default()), 3);
let mut pt = Module::default();
pt.quirks.pattern_loop_break_resets_row = true;
assert_eq!(first_order1_row(&pt), 0);
}
#[test]
fn walker_matches_reference_replayer_oracle() {
use crate::tracker::profiles;
let seq = |q: &PlaybackQuirks, order: Vec<usize>, pats: Vec<Pattern>| -> Vec<(u32, u32)> {
let mut m = Module::default();
m.quirks = *q;
let mut out = Vec::new();
walk_one_song(&m, &[order], &pats, 0, &mut out);
out.iter().map(|e| (e.order_idx, e.row_idx)).collect()
};
let lp = |p: &mut Pattern, r: usize, ch: usize, v: usize| {
p[r][ch].navigation.push(NavigationEffect::PatternLoop(v))
};
let br = |p: &mut Pattern, r: usize, ch: usize, t: usize| {
p[r][ch].navigation.push(NavigationEffect::PatternBreak(t))
};
let row = |o: u32, r: u32| (o, r);
let mut a = pat_nch(6, 1);
lp(&mut a, 1, 0, 0);
lp(&mut a, 4, 0, 1);
let exp_a = vec![
row(0, 0), row(0, 1), row(0, 2), row(0, 3), row(0, 4),
row(0, 1), row(0, 2), row(0, 3), row(0, 4), row(0, 5),
];
for q in [profiles::pt(), profiles::ft2(), profiles::it214()] {
assert_eq!(seq(&q, vec![0], vec![a.clone()]), exp_a, "case A");
}
let mut b = pat_nch(6, 1);
lp(&mut b, 2, 0, 0);
lp(&mut b, 4, 0, 1);
let head_b = vec![
row(0, 0), row(0, 1), row(0, 2), row(0, 3), row(0, 4),
row(0, 2), row(0, 3), row(0, 4), row(0, 5),
];
let mut exp_b_noleak = head_b.clone();
exp_b_noleak.extend([row(1, 0), row(1, 1), row(1, 2), row(1, 3), row(1, 4), row(1, 5)]);
let mut exp_b_leak = head_b;
exp_b_leak.extend([row(1, 2), row(1, 3), row(1, 4), row(1, 5)]);
assert_eq!(seq(&profiles::pt(), vec![0, 1], vec![b.clone(), pat_nch(6, 1)]), exp_b_noleak);
assert_eq!(seq(&profiles::it214(), vec![0, 1], vec![b.clone(), pat_nch(6, 1)]), exp_b_noleak);
assert_eq!(seq(&profiles::ft2(), vec![0, 1], vec![b, pat_nch(6, 1)]), exp_b_leak);
let mut c = pat_nch(4, 2);
lp(&mut c, 0, 0, 0);
lp(&mut c, 2, 0, 1);
br(&mut c, 2, 1, 3);
assert_eq!(
seq(&profiles::pt(), vec![0, 1], vec![c.clone(), pat_nch(6, 2)]),
vec![row(0, 0), row(0, 1), row(0, 2), row(1, 0), row(1, 1), row(1, 2), row(1, 3), row(1, 4), row(1, 5)],
);
assert_eq!(
seq(&profiles::ft2(), vec![0, 1], vec![c.clone(), pat_nch(6, 2)]),
vec![row(0, 0), row(0, 1), row(0, 2), row(1, 3), row(1, 4), row(1, 5)],
);
assert_eq!(
seq(&profiles::it214(), vec![0, 1], vec![c, pat_nch(6, 2)]),
vec![row(0, 0), row(0, 1), row(0, 2), row(0, 0), row(0, 1), row(0, 2), row(1, 3), row(1, 4), row(1, 5)],
);
}
}