use alloc::vec::Vec;
use crate::core::module::Module;
use crate::tracker::import::unit::TrackImportUnit;
mod dedup;
mod segments;
mod timeline_map;
pub use dedup::dedupe_tracks_by_content;
pub use segments::{extract_tracks_and_clips, split_rows_by_instrument, TrackSegment};
pub use timeline_map::build_timeline_map;
pub type Row = Vec<TrackImportUnit>;
pub type Pattern = Vec<Row>;
pub const EFFECT_ONLY_INSTRUMENT: usize = usize::MAX;
fn pad_instruments_to_track_refs(module: &mut Module) {
let max_ref = module
.tracks
.iter()
.map(|t| t.instrument())
.filter(|&i| i != EFFECT_ONLY_INSTRUMENT)
.max();
if let Some(max_ref) = max_ref {
if max_ref >= module.instrument.len() {
module
.instrument
.resize_with(max_ref + 1, crate::core::instrument::Instrument::default);
}
}
}
pub fn set_song_loop_to_restart_byte(module: &mut Module, song: u16, restart_byte: usize) {
if module.song_loop_to.is_some() {
return;
}
if restart_byte == 0 {
return;
}
let entry = module
.timeline_map
.entries
.iter()
.find(|e| {
e.song == song
&& e.order_idx as usize == restart_byte
&& e.row_idx == 0
&& e.loop_iter == 0
})
.copied();
if let Some(e) = entry {
module.song_loop_to = Some(e.tick);
}
}
pub fn build_timeline_layer(
module: &mut Module,
pattern_order: &[Vec<usize>],
pattern: &[Pattern],
) {
let (timeline_map, walker_wrap_tick) = build_timeline_map(module, pattern_order, pattern);
module.timeline_map = timeline_map;
if let Some(tick) = walker_wrap_tick {
module.song_loop_to = Some(tick);
}
let (tracks, clips) = extract_tracks_and_clips(pattern, &module.timeline_map);
module.tracks = tracks;
module.clips = crate::core::daw::sorted_clips::SortedClips::from_unsorted(clips);
dedupe_tracks_by_content(module);
crate::core::daw::euclidean::auto_convert_strict_euclidean_tracks(module);
pad_instruments_to_track_refs(module);
let mut lanes = crate::tracker::import::extract::extract_per_track_lanes_from_patterns(
pattern,
&module.timeline_map,
&module.clips,
&module.quirks,
);
lanes.extend(
crate::tracker::import::extract::extract_song_lanes_from_patterns(
pattern,
&module.timeline_map,
),
);
module.automation.extend(lanes);
}
#[cfg(test)]
mod restart_tests {
use super::*;
use alloc::vec;
fn empty_pattern(channels: usize, rows: usize) -> Pattern {
(0..rows)
.map(|_| (0..channels).map(|_| TrackImportUnit::default()).collect())
.collect()
}
#[test]
fn song_loop_to_matches_restart_byte_tick() {
use crate::core::module::Module;
let order = vec![0, 1, 2];
let patterns = vec![empty_pattern(1, 4); 3];
let mut module = Module::default();
build_timeline_layer(&mut module, &[order], &patterns);
set_song_loop_to_restart_byte(&mut module, 0, 1);
assert_eq!(module.song_loop_to, Some(24));
}
#[test]
fn song_loop_to_noop_for_restart_zero() {
use crate::core::module::Module;
let order = vec![0, 1, 2];
let patterns = vec![empty_pattern(1, 4); 3];
let mut module = Module::default();
build_timeline_layer(&mut module, &[order], &patterns);
set_song_loop_to_restart_byte(&mut module, 0, 0);
assert_eq!(module.song_loop_to, None);
}
#[test]
fn song_loop_to_noop_for_out_of_range_restart_byte() {
use crate::core::module::Module;
let order = vec![0, 1, 2];
let patterns = vec![empty_pattern(1, 4); 3];
let mut module = Module::default();
build_timeline_layer(&mut module, &[order], &patterns);
set_song_loop_to_restart_byte(&mut module, 0, 0x7F);
assert_eq!(module.song_loop_to, None);
}
#[test]
fn build_timeline_layer_extracts_lanes_in_mirror() {
use crate::core::cell_note::CellNote;
use crate::core::daw::automation::{AutomationTarget, LaneKind};
use crate::core::effect::SongLevelEffect;
use crate::core::fixed::fixed::Q8_8;
use crate::core::fixed::units::{PitchDelta, Volume};
use crate::core::module::Module;
use crate::core::pitch::Pitch;
use crate::tracker::import::effect::TrackImportEffect;
use crate::tracker::import::unit::TrackImportUnit;
let mut row0 = TrackImportUnit::default();
row0.note = CellNote::Play(Pitch::C5);
row0.instrument = Some(0);
row0.velocity = Volume::FULL;
row0.effects = alloc::vec![TrackImportEffect::Vibrato(
Q8_8::from_int(3),
PitchDelta::from_q8_8_i16(6)
)];
row0.song_level = alloc::vec![SongLevelEffect::Bpm(140)];
let pat: Pattern = (0..4)
.map(|r| {
if r == 0 {
alloc::vec![row0.clone()]
} else {
alloc::vec![TrackImportUnit::default()]
}
})
.collect();
let mut module = Module::default();
module
.instrument
.push(crate::core::instrument::Instrument::default());
build_timeline_layer(&mut module, &[vec![0]], &[pat]);
let has_vibrato_lane = module.automation.iter().any(|l| {
matches!(l.target, AutomationTarget::TrackPitch(_))
&& matches!(l.kind, LaneKind::Lfo { .. })
});
let has_bpm_points = module
.automation
.iter()
.any(|l| l.target == AutomationTarget::Bpm && matches!(l.kind, LaneKind::Points(_)));
assert!(
has_vibrato_lane,
"expected an Lfo lane on TrackPitch after extraction"
);
assert!(
has_bpm_points,
"expected a Points lane on Bpm after extraction"
);
}
#[test]
fn out_of_range_instrument_reference_pads_instrument_vec() {
use crate::core::cell_note::CellNote;
use crate::core::module::Module;
use crate::core::pitch::Pitch;
use crate::tracker::import::unit::TrackImportUnit;
let mut module = Module::default();
module
.instrument
.push(crate::core::instrument::Instrument::default());
let mut cell = TrackImportUnit::default();
cell.note = CellNote::Play(Pitch::C5);
cell.instrument = Some(5);
let pat: Pattern = alloc::vec![alloc::vec![cell]];
build_timeline_layer(&mut module, &[alloc::vec![0]], &[pat]);
assert!(
module.instrument.len() >= 6,
"instrument vec padded to cover the out-of-range index 5"
);
assert!(module.verify_layers_consistent().is_ok());
}
}