#![forbid(unsafe_code)]
#![allow(dead_code)]
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
pub mod core;
pub mod tracker;
pub mod generators;
pub mod edit;
pub mod prelude;
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(42, 42);
}
#[cfg(feature = "import_xm")]
#[test]
fn load_empty_xm() {
let data = include_bytes!("../examples/empty.xm");
let module = crate::core::module::Module::load_xm(data);
assert!(
module.is_ok(),
"Failed to load empty.xm: {:?}",
module.err()
);
}
#[cfg(feature = "import_xm")]
#[test]
fn load_note_xm() {
let data = include_bytes!("../examples/note.xm");
let module = crate::core::module::Module::load_xm(data).expect("Failed to load note.xm");
assert!(
!module.instrument.is_empty(),
"Module should have instruments"
);
assert!(
!module.timeline_map.entries.is_empty(),
"timeline_map should be populated"
);
assert!(!module.tracks.is_empty(), "tracks should be populated");
module
.verify_layers_consistent()
.expect("DAW layer should be consistent after import");
}
#[cfg(feature = "import_sid")]
#[test]
fn load_sid_commando_has_consistent_layer() {
let sid = crate::tracker::import::sid::sid_module::SidModule::get_sid_commando();
let modules = sid.to_modules();
assert!(
!modules.is_empty(),
"SID should produce at least one Module"
);
for (i, m) in modules.iter().enumerate() {
assert!(
!m.tracks.is_empty(),
"SID module {} should have tracks after build_timeline_layer",
i
);
m.verify_layers_consistent()
.unwrap_or_else(|e| panic!("SID module {} inconsistent: {:?}", i, e));
}
}
#[cfg(feature = "import_xm")]
#[test]
fn load_empty_xm_has_consistent_layer() {
let data = include_bytes!("../examples/empty.xm");
let module = crate::core::module::Module::load_xm(data).expect("load");
module
.verify_layers_consistent()
.expect("DAW layer should be consistent on empty.xm");
}
#[cfg(feature = "import_xm")]
#[test]
fn load_xi_instrument() {
let data = include_bytes!("../examples/instr.xi");
let xmi = crate::tracker::import::xm::xi_instrument::XiInstrument::load(data);
assert!(xmi.is_ok(), "Failed to load instr.xi: {:?}", xmi.err());
}
#[cfg(feature = "import_xm")]
#[test]
fn autodetect_xm() {
let data = include_bytes!("../examples/note.xm");
let module = crate::core::module::Module::load(data);
assert!(module.is_ok(), "Autodetect should find XM format");
}
#[cfg(all(feature = "import_dw", feature = "std"))]
#[test]
fn autodetect_dw_xenon2() {
use crate::tracker::format::ModuleFormat;
let path = "/home/user/Downloads/xenon2 (title).dw";
let Ok(data) = std::fs::read(path) else {
eprintln!("skipped: {} not found", path);
return;
};
let module = crate::core::module::Module::load(&data)
.expect("autodetect should accept the dw payload");
assert_eq!(
module.origin,
Some(ModuleFormat::Dw),
"autodetect must tag as Dw"
);
assert!(
!module.tracks.is_empty(),
"autodetected DW module should carry tracks"
);
module
.verify_layers_consistent()
.expect("autodetected DW DAW layer must be consistent");
}
#[cfg(all(feature = "import_dw", feature = "std"))]
#[test]
fn load_all_dw_modules_from_music_dir() {
use crate::tracker::format::ModuleFormat;
use crate::tracker::import::dw::dw_module::DwModule;
let dir = "/home/user/Music/dw";
let Ok(entries) = std::fs::read_dir(dir) else {
eprintln!("skipped: {} not found", dir);
return;
};
let mut paths: alloc::vec::Vec<std::path::PathBuf> = entries
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("dw"))
.collect();
paths.sort();
let mut failures: alloc::vec::Vec<alloc::string::String> = alloc::vec::Vec::new();
let mut total = 0usize;
let mut ok_count = 0usize;
let mut skipped = 0usize;
for path in &paths {
total += 1;
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("<?>");
let data = match std::fs::read(path) {
Ok(d) => d,
Err(e) => {
failures.push(alloc::format!("{}: read failed: {}", name, e));
continue;
}
};
let dw = match DwModule::load(&data) {
Ok(m) => m,
Err(crate::tracker::import::bin_reader::ImportError::InvalidMagic(
"dw_no_song",
)) => {
skipped += 1;
eprintln!(" -- {} : no song content (sfx/placeholder bank)", name);
continue;
}
Err(e) => {
failures.push(alloc::format!("{}: DwModule::load: {:?}", name, e));
continue;
}
};
if dw.samples.is_empty() {
failures.push(alloc::format!("{}: zero samples parsed", name));
continue;
}
let module = dw.to_module();
if module.instrument.is_empty() {
failures.push(alloc::format!("{}: empty instrument bank", name));
continue;
}
if let Err(e) = module.verify_layers_consistent() {
failures.push(alloc::format!("{}: DAW layer inconsistent: {:?}", name, e));
continue;
}
match crate::core::module::Module::load(&data) {
Ok(auto) => {
if auto.origin != Some(ModuleFormat::Dw) {
failures.push(alloc::format!(
"{}: autodetect produced origin {:?}, expected Dw",
name,
auto.origin
));
}
}
Err(e) => failures.push(alloc::format!("{}: autodetect failed: {:?}", name, e)),
}
ok_count += 1;
}
eprintln!(
"load_all_dw_modules_from_music_dir: {} files, {} ok, {} sfx/placeholder, {} failures",
total,
ok_count,
skipped,
failures.len()
);
if !failures.is_empty() {
for f in &failures {
eprintln!(" FAIL {}", f);
}
panic!("{} module(s) failed to load cleanly", failures.len());
}
}
#[cfg(any(
feature = "import_xm",
feature = "import_mod",
feature = "import_s3m",
feature = "import_it"
))]
fn run_format_load_gate(
ext: &str,
loader: impl Fn(
&[u8],
) -> Result<
crate::core::module::Module,
crate::tracker::import::bin_reader::ImportError,
>,
) {
fn collect(
dir: &std::path::Path,
ext: &str,
out: &mut alloc::vec::Vec<std::path::PathBuf>,
) {
let Ok(rd) = std::fs::read_dir(dir) else {
return;
};
for e in rd.flatten() {
let p = e.path();
if p.is_dir() {
collect(&p, ext, out);
} else if p
.extension()
.and_then(|x| x.to_str())
.is_some_and(|x| x.eq_ignore_ascii_case(ext))
{
out.push(p);
}
}
}
let root = "/home/user/Music";
let mut paths = alloc::vec::Vec::new();
collect(std::path::Path::new(root), ext, &mut paths);
if paths.is_empty() {
eprintln!("skipped: no .{} under {}", ext, root);
return;
}
paths.sort();
let mut total = 0usize;
let mut ok_count = 0usize;
let mut rejected: alloc::vec::Vec<alloc::string::String> = alloc::vec::Vec::new();
let mut failures: alloc::vec::Vec<alloc::string::String> = alloc::vec::Vec::new();
for path in &paths {
total += 1;
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("<?>");
let data = match std::fs::read(path) {
Ok(d) => d,
Err(e) => {
failures.push(alloc::format!("{}: read failed: {}", name, e));
continue;
}
};
let loaded = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| loader(&data)));
match loaded {
Ok(Ok(module)) => {
if let Err(e) = module.verify_layers_consistent() {
failures.push(alloc::format!("{}: DAW layer inconsistent: {:?}", name, e));
continue;
}
ok_count += 1;
}
Ok(Err(e)) => {
rejected.push(alloc::format!("{} ({:?})", name, e));
}
Err(_) => {
failures.push(alloc::format!("{}: PANIC during load/to_module", name));
}
}
}
eprintln!(
"run_format_load_gate[{}]: {} files, {} ok, {} rejected (foreign/damaged), {} failures",
ext,
total,
ok_count,
rejected.len(),
failures.len()
);
for r in &rejected {
eprintln!(" -- rejected: {}", r);
}
if !failures.is_empty() {
for f in &failures {
eprintln!(" FAIL {}", f);
}
panic!(
"{} .{} module(s) failed to load cleanly",
failures.len(),
ext
);
}
assert!(
rejected.len() * 4 <= total,
"run_format_load_gate[{}]: {}/{} files rejected — looks like a loader regression, not just mislabeled inputs",
ext,
rejected.len(),
total
);
}
#[cfg(feature = "import_it")]
#[test]
fn load_all_it_modules_from_music_dir() {
run_format_load_gate("it", crate::core::module::Module::load_it);
}
#[cfg(feature = "import_xm")]
#[test]
fn load_all_xm_modules_from_music_dir() {
run_format_load_gate("xm", crate::core::module::Module::load_xm);
}
#[cfg(feature = "import_mod")]
#[test]
fn load_all_mod_modules_from_music_dir() {
run_format_load_gate("mod", crate::core::module::Module::load_mod);
}
#[cfg(feature = "import_s3m")]
#[test]
fn load_all_s3m_modules_from_music_dir() {
run_format_load_gate("s3m", crate::core::module::Module::load_s3m);
}
#[cfg(all(feature = "import_dw", feature = "std"))]
#[test]
fn load_dw_xenon2_from_home() {
use crate::tracker::import::dw::dw_module::DwModule;
let path = "/home/user/Downloads/xenon2 (title).dw";
let Ok(data) = std::fs::read(path) else {
eprintln!("skipped: {} not found", path);
return;
};
let dw = match DwModule::load(&data) {
Ok(m) => m,
Err(e) => panic!("DwModule::load({}) failed: {:?}", path, e),
};
let pcm_total: usize = dw.samples.iter().map(|s| s.pcm.len()).sum();
let looping = dw.samples.iter().filter(|s| s.is_looping()).count();
eprintln!(
"dw.xenon 2: variant {:?}, period table {:?}, {} samples ({} looping), {} PCM bytes",
dw.variant,
dw.period_table,
dw.samples.len(),
looping,
pcm_total,
);
for s in &dw.samples {
if let Some(start) = s.loop_start {
eprintln!(
" sample {:02}: loop_start={} len={} freq={}Hz",
s.index, start, s.length, s.frequency
);
}
}
if let Some(ss) = &dw.sub_song {
eprintln!(
" sub-song: speed={}, delay_speed={}, channel offsets={:04x?}",
ss.speed, ss.delay_speed, ss.channel_position_offsets,
);
}
for (ch, list) in dw.position_lists.iter().enumerate() {
eprintln!(
" position list ch{}: {} entries{}",
ch,
list.len(),
match list.loop_to {
Some(i) => alloc::format!(", loop_to={}", i),
None => alloc::string::String::new(),
}
);
}
let total_track_bytes: usize = dw.tracks.iter().map(|t| t.bytes.len()).sum();
let total_notes: usize = dw.tracks.iter().map(|t| t.note_count()).sum();
let total_cmds: usize = dw.tracks.iter().map(|t| t.command_count()).sum();
eprintln!(
" tracks: {} unique, {} bytes total, {} notes / {} commands",
dw.tracks.len(),
total_track_bytes,
total_notes,
total_cmds,
);
if let (Some(shortest), Some(longest)) = (
dw.tracks.iter().map(|t| t.bytes.len()).min(),
dw.tracks.iter().map(|t| t.bytes.len()).max(),
) {
eprintln!(" track length range: {}..={} bytes", shortest, longest);
}
use crate::tracker::import::dw::event::DwTrackEvent;
let mut notes = 0usize;
let mut long_waits = 0usize;
let mut set_samples = 0usize;
let mut slides = 0usize;
let mut vibratos = 0usize;
let mut speeds = 0usize;
let mut transposes = 0usize;
let mut effect8 = 0usize;
let mut effect9 = 0usize;
let mut waits_until_next = 0usize;
let mut other_cmds = 0usize;
let mut set_arpeggios = 0usize;
let mut set_envelopes = 0usize;
for t in &dw.tracks {
for ev in &t.events {
match ev {
DwTrackEvent::Note(_) => notes += 1,
DwTrackEvent::LongWait(_) => long_waits += 1,
DwTrackEvent::SetSample(_) => set_samples += 1,
DwTrackEvent::Slide { .. } => slides += 1,
DwTrackEvent::StartVibrato { .. } | DwTrackEvent::StopVibrato => vibratos += 1,
DwTrackEvent::SetSpeed(_) => speeds += 1,
DwTrackEvent::GlobalTranspose(_) => transposes += 1,
DwTrackEvent::Effect8(_) => effect8 += 1,
DwTrackEvent::Effect9(_) => effect9 += 1,
DwTrackEvent::WaitUntilNextRow => waits_until_next += 1,
DwTrackEvent::EndOfTrack => {}
DwTrackEvent::SetVolumeEnvelope(_) => set_envelopes += 1,
DwTrackEvent::SetPitchArpeggio(_) => set_arpeggios += 1,
_ => other_cmds += 1,
}
}
}
eprintln!(
" events: notes={} long_wait={} set_sample={} set_arp={} set_env={} slide={} vibrato={} speed={} transpose={} fx8={} fx9={} wait_row={} other={}",
notes,
long_waits,
set_samples,
set_arpeggios,
set_envelopes,
slides,
vibratos,
speeds,
transposes,
effect8,
effect9,
waits_until_next,
other_cmds,
);
let layout = crate::tracker::import::dw::detect::detect(&data).expect("re-detect");
let feat = layout.features;
eprintln!(
" features: delay_counter={} delay_multiply={} square_waveform={} sample_transpose={} arpeggio={} envelopes={}",
feat.enable_delay_counter,
feat.enable_delay_multiply,
feat.enable_square_waveform,
feat.enable_sample_transpose,
feat.enable_arpeggio,
feat.enable_envelopes,
);
eprintln!(
" vibrato={} half_volume={} volume_fade={} channel_transpose={}",
feat.enable_vibrato,
feat.enable_half_volume,
feat.enable_volume_fade,
feat.enable_channel_transpose,
);
use crate::tracker::import::dw::runtime::{Simulator, TickEvent};
let mut sim = Simulator::new(&dw);
let trace = sim.run();
let halted = sim.halted;
let final_frame = sim.frame;
let mut note_count = [0usize; 4];
let mut sample_set = [0usize; 4];
let mut mute_count = [0usize; 4];
let mut speed_changes = 0usize;
let mut channel_finished = [false; 4];
let mut song_end = false;
let mut notes_with_envelope = 0usize;
let mut notes_with_full_volume = 0usize;
for (_, ev) in &trace {
match ev {
TickEvent::NoteOn {
channel, volume, ..
} => {
note_count[*channel as usize] += 1;
if volume.is_some() {
notes_with_envelope += 1;
} else {
notes_with_full_volume += 1;
}
}
TickEvent::SampleSet { channel, .. } => sample_set[*channel as usize] += 1,
TickEvent::Mute { channel } => mute_count[*channel as usize] += 1,
TickEvent::SpeedChange { .. } => speed_changes += 1,
TickEvent::ChannelFinished { channel } => {
channel_finished[*channel as usize] = true
}
TickEvent::SongEnd { .. } => song_end = true,
TickEvent::VibratoStart { .. }
| TickEvent::VibratoStop { .. }
| TickEvent::SlideStart { .. }
| TickEvent::SlideStop { .. }
| TickEvent::Arpeggio { .. } => {}
}
}
eprintln!(
" simulation: {} frames (~{}s @ 50Hz), halted={}, trace_events={}",
final_frame,
final_frame / 50,
halted,
trace.len(),
);
eprintln!(
" notes per channel: ch0={} ch1={} ch2={} ch3={}",
note_count[0], note_count[1], note_count[2], note_count[3]
);
eprintln!(
" volume envelopes: {} parsed, {} notes shaped, {} unshaped",
dw.volume_envelopes.len(),
notes_with_envelope,
notes_with_full_volume,
);
eprintln!(
" sample_sets per ch: ch0={} ch1={} ch2={} ch3={}",
sample_set[0], sample_set[1], sample_set[2], sample_set[3]
);
eprintln!(
" finished per ch: {:?}, song_end={}, speed_changes={}, total_mutes={}",
channel_finished,
song_end,
speed_changes,
mute_count.iter().sum::<usize>(),
);
let buckets = 10usize;
let last = final_frame.max(1);
let mut hist: [[usize; 10]; 4] = [[0; 10]; 4];
for (frame, ev) in &trace {
if let TickEvent::NoteOn { channel, .. } = ev {
let ch = (*channel as usize).min(3);
let bucket = ((*frame as u64 * buckets as u64 - 1) / last as u64) as usize;
let bucket = bucket.min(buckets - 1);
hist[ch][bucket] += 1;
}
}
for (ch, h) in hist.iter().enumerate().take(4) {
eprintln!(" ch{} note distribution (10% buckets): {:?}", ch, h);
}
eprintln!(
" final channel state: ch0 pos={}/{} cursor={} sp_cnt={} | ch1 pos={}/{} | ch2 pos={}/{} | ch3 pos={}/{}",
sim.channels[0].position_index,
dw.position_lists[0].len(),
sim.channels[0].event_cursor,
sim.channels[0].speed_counter,
sim.channels[1].position_index,
dw.position_lists[1].len(),
sim.channels[2].position_index,
dw.position_lists[2].len(),
sim.channels[3].position_index,
dw.position_lists[3].len(),
);
let module = dw.to_module();
assert_eq!(
module.instrument.len(),
dw.samples.len(),
"every DwSample should yield exactly one Instrument"
);
assert_eq!(
module.origin,
Some(crate::tracker::format::ModuleFormat::Dw),
"origin should be tagged as Dw"
);
module
.verify_layers_consistent()
.expect("DW DAW layer should be consistent");
let cell_count: usize = module
.tracks
.iter()
.map(|t| match t {
crate::core::daw::track::Track::Notes { rows, .. } => rows.len(),
_ => 0,
})
.sum();
let triggered: usize = module
.tracks
.iter()
.map(|t| match t {
crate::core::daw::track::Track::Notes { rows, .. } => rows
.iter()
.filter(|c| !matches!(c.event, crate::core::cell::CellEvent::None))
.count(),
_ => 0,
})
.sum();
let pre_dedup_segments = dw.segment_count_for_diagnostics();
eprintln!(
" daw layer: {} segments → {} tracks (dedup), {} clips, {} timeline entries, {} cells ({} triggered)",
pre_dedup_segments,
module.tracks.len(),
module.clips.as_slice().len(),
module.timeline_map.entries.len(),
cell_count,
triggered,
);
}
}