use serde::{Deserialize, Serialize};
use crate::core::cell::Cell;
use crate::core::compatibility::PlaybackQuirks;
use crate::core::daw::automation::AutomationLane;
use crate::core::daw::clip::Clip;
use crate::core::daw::sorted_clips::SortedClips;
use crate::core::daw::timeline::TimelineMap;
use crate::core::daw::track::Track;
use crate::core::fixed::units::{ChannelVolume, Panning, Volume};
use crate::core::instrument::Instrument;
#[cfg(feature = "import_it")]
use crate::tracker::mix_plugin::MixPlugins;
use crate::tracker::period::FrequencyType;
use alloc::string::String;
use alloc::string::ToString;
use alloc::{vec, vec::Vec};
#[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
pub struct ChannelDefault {
pub panning: Option<Panning>,
pub volume: Option<ChannelVolume>,
pub muted: bool,
pub surround: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub struct PatternHighlight {
pub beat: u8,
pub measure: u8,
}
impl Default for PatternHighlight {
fn default() -> Self {
Self {
beat: 4,
measure: 16,
}
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Module {
pub name: String,
pub comment: String,
pub quirks: PlaybackQuirks,
#[cfg(any(
feature = "import_mod",
feature = "import_xm",
feature = "import_s3m",
feature = "import_it",
feature = "import_sid",
feature = "import_dw",
))]
pub origin: Option<crate::tracker::format::ModuleFormat>,
pub frequency_type: FrequencyType,
pub default_tempo: usize,
pub default_bpm: usize,
pub channel_names: Vec<String>,
pub channel_defaults: Vec<ChannelDefault>,
pub instrument: Vec<Instrument>,
pub midi_macros: Option<MidiMacros>,
pub pattern_highlight: PatternHighlight,
#[cfg(feature = "import_it")]
pub mix_plugins: Option<MixPlugins>,
pub pitch_wheel_depth: u8,
pub mix_volume: Volume,
#[serde(default)]
pub tracks: Vec<Track>,
#[serde(default)]
pub clips: SortedClips,
#[serde(default)]
pub automation: Vec<AutomationLane>,
#[serde(default)]
pub timeline_map: TimelineMap,
#[serde(default)]
pub song_loop_to: Option<u32>,
#[serde(default)]
pub channel_loops: Vec<crate::core::daw::loop_region::ChannelLoop>,
#[serde(default)]
pub channel_inserts: Vec<crate::core::daw::device::DeviceChain>,
#[serde(default)]
pub buses: Vec<crate::core::daw::device::Bus>,
#[serde(default)]
pub channel_sends: Vec<Vec<crate::core::daw::device::Send>>,
#[serde(default)]
pub master_chain: crate::core::daw::device::DeviceChain,
#[serde(default)]
pub assets: Vec<crate::core::sample::Sample>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct MidiMacros {
pub global: Vec<Vec<u8>>,
pub parametric: Vec<Vec<u8>>,
pub fixed: Vec<Vec<u8>>,
}
impl Default for Module {
fn default() -> Self {
Module {
name: "".to_string(),
comment: "".to_string(),
quirks: PlaybackQuirks::default(),
#[cfg(any(
feature = "import_mod",
feature = "import_xm",
feature = "import_s3m",
feature = "import_it",
feature = "import_sid",
feature = "import_dw",
))]
origin: None,
frequency_type: FrequencyType::LinearFrequencies,
default_tempo: 6,
default_bpm: 125,
channel_names: vec![],
channel_defaults: vec![],
instrument: vec![],
midi_macros: None,
pattern_highlight: PatternHighlight::default(),
#[cfg(feature = "import_it")]
mix_plugins: None,
pitch_wheel_depth: 2,
mix_volume: Volume::FULL,
tracks: vec![],
clips: SortedClips::new(),
automation: vec![],
timeline_map: TimelineMap::default(),
song_loop_to: None,
channel_loops: vec![],
channel_inserts: vec![],
buses: vec![],
channel_sends: vec![],
master_chain: crate::core::daw::device::DeviceChain::default(),
assets: vec![],
}
}
}
impl Module {
pub fn copy_block(
&self,
tracks: &[u32],
row_start: u32,
row_end: u32,
) -> alloc::vec::Vec<alloc::vec::Vec<Cell>> {
use alloc::vec::Vec;
if row_end <= row_start {
return Vec::new();
}
let len = (row_end - row_start) as usize;
tracks
.iter()
.map(|&t| {
let mut col: Vec<Cell> = Vec::with_capacity(len);
let track = self.tracks.get(t as usize);
for r in row_start..row_end {
let cell = track.map(|tr| tr.cell_at(r)).unwrap_or_default();
col.push(cell);
}
col
})
.collect()
}
pub fn lane_for(
&self,
target: crate::core::daw::automation::AutomationTarget,
song: u16,
) -> Option<&AutomationLane> {
self.automation
.iter()
.find(|l| l.target == target && l.song == song)
}
pub fn lanes_for<'a>(
&'a self,
target: crate::core::daw::automation::AutomationTarget,
song: u16,
) -> impl Iterator<Item = &'a AutomationLane> + 'a {
self.automation
.iter()
.filter(move |l| l.target == target && l.song == song)
}
pub fn get_num_channels(&self) -> usize {
self.clips
.iter()
.map(|c| c.target_channel as usize + 1)
.max()
.unwrap_or(0)
}
pub fn push_asset(&mut self, sample: crate::core::sample::Sample) -> usize {
let idx = self.assets.len();
self.assets.push(sample);
idx
}
pub fn track_audio(&self, track_idx: usize) -> Option<(&crate::core::sample::Sample, u32)> {
use crate::core::daw::track::{AudioSource, Track};
let (source, rate) = match self.tracks.get(track_idx)? {
Track::Audio {
source,
source_rate,
..
} => (source, *source_rate),
_ => return None,
};
let sample = match source {
AudioSource::Inline(s) => s,
AudioSource::Pooled(i) => self.assets.get(*i)?,
};
Some((sample, rate))
}
pub fn song_length(&self, song: usize) -> usize {
self.timeline_map.order_count(song)
}
pub fn order_pattern_idx(&self, song: usize, order_idx: usize) -> Option<usize> {
self.timeline_map
.find_order_entry(song, order_idx)
.map(|e| e.pattern_idx as usize)
}
pub fn pattern_row_count(&self, pat_idx: usize) -> usize {
self.timeline_map.row_count_in_pattern(pat_idx)
}
pub fn find_overlapping_clip(&self, candidate: &Clip, ignore: &[u32]) -> Option<u32> {
let range = self
.clips
.lane_range(candidate.song, candidate.target_channel);
let lane_start = range.start;
let cand_end = candidate.end_tick(self);
for (offset, existing) in self.clips.as_slice()[range].iter().enumerate() {
let global_idx = (lane_start + offset) as u32;
if ignore.contains(&global_idx) {
continue;
}
let existing_end = existing.end_tick(self);
let disjoint =
cand_end <= existing.position_tick || existing_end <= candidate.position_tick;
if !disjoint {
return Some(global_idx);
}
}
None
}
pub fn channel_loop(
&self,
song: u16,
channel: u8,
) -> Option<crate::core::daw::loop_region::ChannelLoop> {
self.channel_loops
.iter()
.find(|l| l.song == song && l.channel == channel)
.copied()
}
pub fn row_at(
&self,
song: usize,
pat_idx: usize,
row_idx: usize,
) -> alloc::vec::Vec<(Cell, Option<usize>)> {
let playhead = match self.timeline_map.find_entry(song, pat_idx, row_idx) {
Some(e) => e.tick,
None => {
return (0..self.get_num_channels())
.map(|_| (Cell::default(), None))
.collect()
}
};
self.row_at_with_playhead(song, pat_idx, row_idx, playhead)
}
pub fn row_at_with_playhead(
&self,
song: usize,
_pat_idx: usize,
row_idx: usize,
playhead_tick: u32,
) -> alloc::vec::Vec<(Cell, Option<usize>)> {
let num_channels = self.get_num_channels();
let mut out: alloc::vec::Vec<(Cell, Option<usize>)> =
(0..num_channels).map(|_| (Cell::default(), None)).collect();
let abs_tick = playhead_tick;
let song_u16 = song as u16;
let global_row = row_idx as u32;
for (ch_idx, slot) in out.iter_mut().enumerate() {
let (local_tick, local_row) = match self.channel_loop(song_u16, ch_idx as u8) {
Some(l) => {
let folded = l.fold_tick(abs_tick);
if folded == abs_tick {
(abs_tick, global_row)
} else {
match self.timeline_map.entry_at_tick(song, folded) {
Some(e) => (folded, e.row_idx),
None => continue,
}
}
}
None => (abs_tick, global_row),
};
let Some((_, clip)) = self.clips.active_at(song_u16, ch_idx as u8, local_tick) else {
continue;
};
if local_tick >= clip.end_tick {
continue;
}
if local_row < clip.source_start_row {
continue;
}
let row_in_track = local_row - clip.source_start_row;
if row_in_track < clip.track_row_offset {
continue;
}
let track = match clip.track_in(self) {
Some(t) => t,
None => continue,
};
let track_instr_idx = track.instrument();
let track_instr =
if track_instr_idx == crate::tracker::import::build::EFFECT_ONLY_INSTRUMENT {
None
} else {
Some(track_instr_idx)
};
match track {
Track::Notes { rows, .. } => {
if (row_in_track as usize) >= rows.len() {
continue;
}
slot.0 = rows[row_in_track as usize].clone();
slot.1 = track_instr;
}
Track::Euclidean {
events,
steps,
rotation,
prototype,
..
} => {
let steps_u32 = (*steps).max(1) as u32;
let pos = row_in_track % steps_u32;
let rot = (*rotation as u32) % steps_u32;
let bjork_idx = ((pos + steps_u32 - rot) % steps_u32) as usize;
let canonical = crate::core::daw::euclidean::euclidean_pattern(*events, *steps);
if canonical.get(bjork_idx).copied().unwrap_or(false) {
slot.0 = prototype.clone();
slot.1 = track_instr;
}
}
Track::Audio { .. } => {}
}
}
out
}
pub fn verify_layers_consistent(&self) -> Result<(), LayerInconsistency> {
for w in self.clips.as_slice().windows(2) {
let a = (w[0].song, w[0].target_channel, w[0].position_tick);
let b = (w[1].song, w[1].target_channel, w[1].position_tick);
if a > b {
return Err(LayerInconsistency::ClipsUnsorted);
}
}
for (i, clip) in self.clips.iter().enumerate() {
if clip.track as usize >= self.tracks.len() {
return Err(LayerInconsistency::ClipTrackOutOfRange {
clip_idx: i,
track: clip.track,
});
}
}
for i in 0..self.clips.len() {
let ci = &self.clips[i];
let end_i = ci.end_tick(self);
for j in (i + 1)..self.clips.len() {
let cj = &self.clips[j];
if cj.song != ci.song || cj.target_channel != ci.target_channel {
continue;
}
if cj.position_tick >= end_i {
break;
}
return Err(LayerInconsistency::ClipsOverlap {
clip_a: i,
clip_b: j,
});
}
}
for (i, t) in self.tracks.iter().enumerate() {
let instr = t.instrument();
if instr == crate::tracker::import::build::EFFECT_ONLY_INSTRUMENT {
continue;
}
if instr >= self.instrument.len() {
return Err(LayerInconsistency::TrackInstrumentMismatch {
track_idx: i,
row: 0,
});
}
}
for (i, lane) in self.automation.iter().enumerate() {
if let Some(points) = lane.points() {
let mut last_tick: Option<u32> = None;
for (j, p) in points.iter().enumerate() {
if !p.value.is_compatible_with(lane.target) {
return Err(LayerInconsistency::AutomationTypeMismatch {
lane_idx: i,
point_idx: j,
});
}
if let Some(lt) = last_tick {
if p.tick < lt {
return Err(LayerInconsistency::AutomationPointsUnsorted {
lane_idx: i,
});
}
}
last_tick = Some(p.tick);
}
}
}
let mut last_song: u16 = 0;
let mut last_tick: u32 = 0;
let mut first = true;
for (i, e) in self.timeline_map.entries.iter().enumerate() {
if first || e.song != last_song {
first = false;
last_song = e.song;
last_tick = e.tick;
continue;
}
if e.tick < last_tick {
return Err(LayerInconsistency::TimelineMapMismatch { entry_idx: i });
}
last_tick = e.tick;
}
for (i, l) in self.channel_loops.iter().enumerate() {
if l.start_tick >= l.end_tick {
return Err(LayerInconsistency::ChannelLoopEmpty { loop_idx: i });
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LayerInconsistency {
ClipsUnsorted,
ClipTrackOutOfRange {
clip_idx: usize,
track: u32,
},
ClipsOverlap {
clip_a: usize,
clip_b: usize,
},
TimelineMapMismatch {
entry_idx: usize,
},
AutomationTypeMismatch {
lane_idx: usize,
point_idx: usize,
},
AutomationPointsUnsorted {
lane_idx: usize,
},
ChannelLoopEmpty {
loop_idx: usize,
},
TrackInstrumentMismatch {
track_idx: usize,
row: usize,
},
}
#[cfg(test)]
mod channel_loop_tests {
use super::*;
use crate::core::cell::{Cell, CellEvent};
use crate::core::daw::clip::Clip;
use crate::core::daw::loop_region::ChannelLoop;
use crate::core::daw::sorted_clips::SortedClips;
use crate::core::daw::timeline::{TimelineEntry, TimelineMap};
use crate::core::daw::track::Track;
use crate::core::fixed::units::Volume;
use crate::core::pitch::Pitch;
#[test]
fn row_at_folds_looping_lane() {
let pitch = |r: u32| Pitch::try_from((60 + r) as u8).unwrap();
let mut rows = alloc::vec::Vec::new();
for r in 0..4u32 {
rows.push(Cell {
event: CellEvent::NoteOn {
pitch: pitch(r),
velocity: Volume::FULL,
},
..Cell::default()
});
}
let mut m = Module::default();
m.tracks.push(Track::Notes {
name: "t".into(),
instrument: 0,
rows,
muted: false,
});
m.clips = SortedClips::from_unsorted(alloc::vec![Clip {
track: 0,
song: 0,
target_channel: 0,
position_tick: 0,
speed_at_start: 3,
track_row_offset: 0,
source_start_row: 0,
end_tick: 12,
}]);
let mut entries = alloc::vec::Vec::new();
for r in 0..4u32 {
entries.push(TimelineEntry {
song: 0,
order_idx: 0,
pattern_idx: 0,
row_idx: r,
loop_iter: 0,
tick: r * 3,
speed_at_row: 3,
bpm_at_row: 125,
});
}
m.timeline_map = TimelineMap { entries };
let played = |m: &Module, row: u32| -> Option<Pitch> {
match m.row_at(0, 0, row as usize)[0].0.event {
CellEvent::NoteOn { pitch, .. } => Some(pitch),
_ => None,
}
};
for r in 0..4u32 {
assert_eq!(played(&m, r), Some(pitch(r)), "unlooped row {r}");
}
m.channel_loops.push(ChannelLoop {
song: 0,
channel: 0,
start_tick: 0,
end_tick: 6,
});
assert_eq!(played(&m, 0), Some(pitch(0)));
assert_eq!(played(&m, 1), Some(pitch(1)));
assert_eq!(
played(&m, 2),
Some(pitch(0)),
"row 2 (tick 6) folds to row 0"
);
assert_eq!(
played(&m, 3),
Some(pitch(1)),
"row 3 (tick 9) folds to row 1"
);
}
#[test]
fn empty_channel_loop_is_invalid() {
let mut m = Module::default();
m.channel_loops.push(ChannelLoop {
song: 0,
channel: 0,
start_tick: 8,
end_tick: 8,
});
assert!(matches!(
m.verify_layers_consistent(),
Err(LayerInconsistency::ChannelLoopEmpty { loop_idx: 0 })
));
}
}