use alloc::format;
use alloc::{vec, vec::Vec};
use crate::prelude::*;
use crate::tracker::import::memory::{ImportMemory, MemoryType};
use crate::tracker::import::patternslot::PatternSlot;
use super::instr_helper::InstrHelper;
use super::one_sid::OneSid;
use super::sound_fx::SoundFx;
use super::voices::Voices;
#[derive(Debug)]
pub struct SidModule {
pub sid: OneSid,
pub voices: Voices,
pub instruments: Vec<InstrRobSid>,
pub soundfx: Vec<SoundFx>,
}
impl SidModule {
pub fn to_modules(&self) -> Vec<Module> {
let mut modules: Vec<Module> = vec![];
for song_number in 0..self.voices.songs.len() {
let mut module = Module::default();
module.name = format!("{} {}", self.sid.name, song_number);
module.comment = format!(
"{} - {} (song #{})",
self.sid.copyright, self.sid.author, song_number
);
module.quirks = crate::tracker::profiles::pt();
module.origin = Some(crate::tracker::format::ModuleFormat::Sid);
module.default_tempo = 1 + self.sid.resetspd_for(song_number);
if self.sid.frame_skip_reset > 0 {
let n = self.sid.frame_skip_reset;
module.default_bpm = (module.default_bpm * n) / (n + 1);
}
let short_gate_instrs = self.sid.short_gate_instrs();
let passes = self.voices.voice_passes(song_number, &short_gate_instrs);
let num_voices = passes.len();
let song_rows = passes.iter().map(|p| p.len()).max().unwrap_or(0);
let speed = module.default_tempo as u8;
let cadence = self.sid.tempo_frac_for(song_number);
let tick_at = move |r: u32| -> u32 {
match cadence {
Some((num, den)) => (r * num).div_ceil(den),
None => r * speed as u32,
}
};
let speed_at = move |r: u32| -> u8 {
match cadence {
Some((num, den)) => {
(((r + 1) * num).div_ceil(den) - (r * num).div_ceil(den)) as u8
}
None => speed,
}
};
let mut voices_tius: Vec<Vec<crate::tracker::import::unit::TrackImportUnit>> =
Vec::with_capacity(num_voices);
for pass in passes.iter().take(num_voices) {
let voice_pattern: Vec<Vec<PatternSlot>> = pass.iter().map(|s| vec![*s]).collect();
let mut im = ImportMemory::default();
let unpacked = im.unpack_patterns(
FrequencyType::LinearFrequencies,
MemoryType::Mod,
&[vec![0]],
&[voice_pattern],
);
let voice_rows: Vec<crate::tracker::import::unit::TrackImportUnit> = unpacked
.first()
.map(|p| p.iter().map(|r| r[0].clone()).collect())
.unwrap_or_default();
voices_tius.push(voice_rows);
}
for (voice_idx, vrows) in voices_tius.iter().enumerate() {
let len = vrows.len() as u32;
if len > 0 {
module
.channel_loops
.push(crate::core::daw::loop_region::ChannelLoop {
song: 0,
channel: voice_idx as u8,
start_tick: 0,
end_tick: tick_at(len),
});
}
}
let mut tracks: Vec<Track> = Vec::new();
let mut clips: Vec<Clip> = Vec::new();
for (voice_idx, voice_rows) in voices_tius.iter().enumerate() {
let segments = crate::tracker::import::build::split_rows_by_instrument(voice_rows);
for seg in segments {
let track_idx = tracks.len() as u32;
let seg_start_row = seg.start_row;
let materialised: Vec<Cell> =
seg.rows.into_iter().map(|tiu| tiu.prepare_cell()).collect();
let seg_len = materialised.len() as u32;
tracks.push(Track::Notes {
name: format!("voice {} seg {}", voice_idx, track_idx),
instrument: seg.instrument,
rows: materialised,
muted: false,
});
let position_tick = tick_at(seg_start_row);
clips.push(Clip {
track: track_idx,
song: 0,
target_channel: voice_idx as u8,
position_tick,
speed_at_start: speed_at(seg_start_row),
track_row_offset: 0,
source_start_row: seg_start_row,
end_tick: tick_at(seg_start_row + seg_len),
});
}
}
let bpm = module.default_bpm as u16;
let mut entries: Vec<crate::core::daw::timeline::TimelineEntry> =
Vec::with_capacity(song_rows);
for r in 0..song_rows {
entries.push(crate::core::daw::timeline::TimelineEntry {
song: 0,
order_idx: 0,
pattern_idx: 0, row_idx: r as u32,
loop_iter: 0,
tick: tick_at(r as u32),
speed_at_row: speed_at(r as u32),
bpm_at_row: bpm,
});
}
module.timeline_map = crate::core::daw::timeline::TimelineMap { entries };
module.tracks = tracks;
module.clips = crate::core::daw::sorted_clips::SortedClips::from_unsorted(clips);
let idst = InstrHelper::irss_to_instruments(&self.instruments);
module.instrument = idst;
crate::tracker::import::build::dedupe_tracks_by_content(&mut module);
let max_rows = voices_tius.iter().map(|v| v.len()).max().unwrap_or(0);
let synthetic_pattern: crate::tracker::import::build::Pattern = (0..max_rows)
.map(|r| {
voices_tius
.iter()
.map(|voice| voice.get(r).cloned().unwrap_or_default())
.collect()
})
.collect();
let lanes = crate::tracker::import::extract::extract_per_track_lanes_from_patterns(
core::slice::from_ref(&synthetic_pattern),
&module.timeline_map,
&module.clips,
&module.quirks,
);
module.automation.extend(lanes);
modules.push(module);
}
modules
}
pub fn to_module(&self) -> Option<Module> {
let mut parts = self.to_modules();
if parts.is_empty() {
return None;
}
let mut merged = parts.remove(0);
merged.name = self.sid.name.into();
merged.comment = format!("{} - {}", self.sid.copyright, self.sid.author);
for (n, part) in parts.into_iter().enumerate() {
append_sub_songs(&mut merged, part, (n + 1) as u16, 0);
}
Some(merged)
}
}
pub(crate) fn song_count(module: &Module) -> u16 {
module
.timeline_map
.entries
.iter()
.map(|e| e.song)
.max()
.map_or(1, |m| m + 1)
}
pub(crate) fn append_sub_songs(
merged: &mut Module,
part: Module,
song_base: u16,
instr_base: usize,
) {
use crate::core::daw::automation::AutomationTarget as T;
use crate::tracker::import::build::EFFECT_ONLY_INSTRUMENT;
let track_base = merged.tracks.len() as u32;
merged.tracks.extend(part.tracks.into_iter().map(|mut t| {
if let Track::Notes { instrument, .. } = &mut t {
if *instrument != EFFECT_ONLY_INSTRUMENT {
*instrument += instr_base;
}
}
t
}));
let mut clips = merged.clips.snapshot();
clips.extend(part.clips.snapshot().into_iter().map(|mut c| {
c.track += track_base;
c.song += song_base;
c
}));
merged.clips = crate::core::daw::sorted_clips::SortedClips::from_unsorted(clips);
merged
.timeline_map
.entries
.extend(part.timeline_map.entries.into_iter().map(|mut e| {
e.song += song_base;
e
}));
merged
.channel_loops
.extend(part.channel_loops.into_iter().map(|mut l| {
l.song += song_base;
l
}));
merged
.automation
.extend(part.automation.into_iter().map(|mut lane| {
lane.target = match lane.target {
T::TrackVolume(i) => T::TrackVolume(i + track_base),
T::TrackPanning(i) => T::TrackPanning(i + track_base),
T::TrackPitch(i) => T::TrackPitch(i + track_base),
T::TrackChannelVolume(i) => T::TrackChannelVolume(i + track_base),
other => other,
};
lane.song += song_base;
lane
}));
}
#[cfg(feature = "sid_songs")]
impl SidModule {
pub fn get_sid_commando() -> Self {
let sid = OneSid::get_sid_commando();
sid.to_sidmodule()
}
pub fn get_sid_crazy_comets() -> Self {
let sid = OneSid::get_sid_crazy_comets();
sid.to_sidmodule()
}
pub fn get_sid_last_v8() -> Self {
let sid = OneSid::get_sid_last_v8();
sid.to_sidmodule()
}
pub fn get_sid_monty_on_the_run() -> Self {
let sid = OneSid::get_sid_monty_on_the_run();
sid.to_sidmodule()
}
pub fn get_sid_thing_on_a_spring() -> Self {
let sid = OneSid::get_sid_thing_on_a_spring();
sid.to_sidmodule()
}
pub fn get_sid_zoid() -> Self {
let sid = OneSid::get_sid_zoid();
sid.to_sidmodule()
}
pub fn get_sid_ace_2() -> Self {
let sid = OneSid::get_sid_ace_2();
sid.to_sidmodule()
}
pub fn get_sid_delta() -> Self {
let sid = OneSid::get_sid_delta();
sid.to_sidmodule()
}
pub fn get_sid_human_race() -> Self {
let sid = OneSid::get_sid_human_race();
sid.to_sidmodule()
}
pub fn get_sid_international_karate() -> Self {
let sid = OneSid::get_sid_international_karate();
sid.to_sidmodule()
}
pub fn get_sid_lightforce() -> Self {
let sid = OneSid::get_sid_lightforce();
sid.to_sidmodule()
}
pub fn get_sid_sanxion_song_1() -> Self {
let sid = OneSid::get_sid_sanxion_song_1();
sid.to_sidmodule()
}
pub fn get_sid_sanxion_song_2() -> Self {
let sid = OneSid::get_sid_sanxion_song_2();
sid.to_sidmodule()
}
pub fn get_sid_spellbound() -> Self {
let sid = OneSid::get_sid_spellbound();
sid.to_sidmodule()
}
pub fn get_sid_thrust() -> Self {
let sid = OneSid::get_sid_thrust();
sid.to_sidmodule()
}
}
impl SidModule {
pub fn for_image(image: impl Into<alloc::borrow::Cow<'static, [u8]>>) -> Vec<Self> {
let image = image.into();
let records = OneSid::records_for_image(image.clone());
if !records.is_empty() {
return records.iter().map(OneSid::to_sidmodule).collect();
}
OneSid::from_detected(image)
.map(|r| vec![r.to_sidmodule()])
.unwrap_or_default()
}
pub fn from_detected(song: impl Into<alloc::borrow::Cow<'static, [u8]>>) -> Option<Self> {
Some(OneSid::from_detected(song)?.to_sidmodule())
}
pub fn with_detected_layout(&self) -> Option<Self> {
Some(self.sid.with_detected_layout()?.to_sidmodule())
}
pub fn with_detected_stream(&self) -> Option<Self> {
let layout = crate::tracker::import::sid::detect::SidLayout::detect(&self.sid.song)?;
let mut out = self.sid.to_sidmodule();
out.voices = out.voices.with_stream(Some(layout.stream));
Some(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
const DELTA: &[u8] = include_bytes!("songs/delta.sid");
const SANXION: &[u8] = include_bytes!("songs/sanxion.sid");
const COMMANDO: &[u8] = include_bytes!("songs/commando.sid");
const LION_HEART: &[u8] = include_bytes!("songs/lion_heart.sid");
#[test]
fn merging_sub_songs_keeps_every_row() {
let sid = OneSid::delta().with_image(DELTA).to_sidmodule();
let parts = sid.to_modules();
let merged = sid.to_module().expect("delta merges");
assert_eq!(song_count(&merged) as usize, parts.len());
for (n, part) in parts.iter().enumerate() {
let rows = merged
.timeline_map
.entries
.iter()
.filter(|e| e.song as usize == n)
.count();
assert_eq!(
rows,
part.timeline_map.entries.len(),
"sub-song {n} lost rows in the merge"
);
}
merged
.verify_layers_consistent()
.expect("merged module is consistent");
}
#[test]
fn merged_sub_songs_keep_their_own_cadence() {
let sid = OneSid::delta().with_image(DELTA).to_sidmodule();
let parts = sid.to_modules();
let merged = sid.to_module().expect("delta merges");
for (n, part) in parts.iter().enumerate() {
let mine: Vec<u8> = merged
.timeline_map
.entries
.iter()
.filter(|e| e.song as usize == n)
.map(|e| e.speed_at_row)
.collect();
let theirs: Vec<u8> = part
.timeline_map
.entries
.iter()
.map(|e| e.speed_at_row)
.collect();
assert_eq!(mine, theirs, "sub-song {n} lost its own row cadence");
}
}
#[test]
fn a_file_off_disk_gets_its_transcribed_record() {
let by_record = OneSid::commando().with_image(COMMANDO).to_sidmodule();
let found = OneSid::records_for_image(COMMANDO);
assert_eq!(found.len(), 1, "commando should match exactly one record");
assert_eq!(found[0].name, by_record.sid.name);
assert_eq!(found[0].resetspd_for(1), by_record.sid.resetspd_for(1));
}
#[test]
fn two_replayers_in_one_file_both_load() {
assert_eq!(OneSid::records_for_image(SANXION).len(), 2);
let module = Module::load(SANXION).expect("sanxion loads");
assert!(
song_count(&module) >= 2,
"both sanxion replayers should contribute sub-songs, got {}",
song_count(&module)
);
module
.verify_layers_consistent()
.expect("merged sanxion is consistent");
}
#[test]
fn module_load_handles_every_readable_generation() {
for (name, image) in [
("commando (fx-mask, record)", COMMANDO),
("delta (fx-mask, multi sub-song)", DELTA),
("lion_heart (last generation)", LION_HEART),
] {
let module = Module::load(image)
.unwrap_or_else(|e| panic!("{name} should load through Module::load: {e:?}"));
assert!(!module.tracks.is_empty(), "{name} loaded with no track");
}
}
#[test]
fn the_three_marker_generation_loads_as_well() {
const OFF_THE_CUFF: &[u8] = include_bytes!("songs/off_the_cuff.sid");
let module = Module::load(OFF_THE_CUFF).expect("off the cuff loads");
assert!(!module.tracks.is_empty());
}
#[test]
fn an_undecodable_sid_is_refused_rather_than_mis_parsed() {
let mut junk = vec![0u8; 512];
junk[..4].copy_from_slice(b"PSID");
junk[4..6].copy_from_slice(&2u16.to_be_bytes()); assert!(
Module::load(&junk).is_err(),
"a PSID we cannot decode must be refused, not handed to another \
format's detector"
);
}
}