Skip to content

Commit

Permalink
xm: add a sanity check
Browse files Browse the repository at this point in the history
  • Loading branch information
B0ney committed Aug 26, 2024
1 parent 6f4da08 commit 771c354
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 6 deletions.
33 changes: 27 additions & 6 deletions src/load/format/xm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ const FLAG_STEREO: u8 = 1 << 5;
const INSTRUMENT_SIZE: u32 = 263;
const MINIMUM_INSTRUMENT_SIZE: u32 = 29;

const PADDING_LIMIT: u32 = 2 * 1024 * 1024;
const PADDING_LIMIT: u32 = 44100 * 10; // TODO

const ADPCM_COMPRESSION_TABLE_SIZE: u32 = 16;

/// Determine if given bytes could be an Extended Module.
Expand Down Expand Up @@ -187,7 +188,7 @@ pub fn load(buffer: Vec<u8>, source: Option<PathBuf>) -> Result<Module, Error> {
// See: Page 16 in "The Unofficial XM File Format Specification"
let length_bytes = match smp.pcm_type == PcmType::ADPCM {
true => ADPCM_COMPRESSION_TABLE_SIZE + ((smp.length + 1) / 2),
_ => smp.length,
false => smp.length,
};

// Apparently, it is common for samples to report their sizes beyond what the file can store.
Expand All @@ -198,25 +199,29 @@ pub fn load(buffer: Vec<u8>, source: Option<PathBuf>) -> Result<Module, Error> {
//
// We need to add extra padding as loop points may point to them.
if smp.pointer + length_bytes > buffer.len() as u32 {
extra_padding = buffer.len() as u32 - smp.pointer + length_bytes;
samples.push(smp);
extra_padding = (smp.pointer + length_bytes) - buffer.len() as u32;

if extra_padding < PADDING_LIMIT {
samples.push(smp);
}

break 'parse_instrument;
}

file.skip_bytes(length_bytes as i64)?;

samples.push(smp);
}
}

sanity_check_samples(&mut samples);

samples
};

let mut buffer = buffer;

if extra_padding > 0 {
let new_len = buffer.len() + extra_padding.clamp(0, PADDING_LIMIT) as usize;
let new_len = buffer.len() + extra_padding as usize;
buffer.resize(new_len, 0);
info!("Padded last sample with {} extra bytes", extra_padding);
}
Expand All @@ -232,3 +237,19 @@ pub fn load(buffer: Vec<u8>, source: Option<PathBuf>) -> Result<Module, Error> {
samples: samples.into(),
})
}

/// HACK: Removes some really cursed samples. I really need a better heuristic.
/// For "xenia3.xm", xmodits reports last sample to be 5Hz and has a duration of ~34.5 hours.
fn sanity_check_samples(samples: &mut Vec<Sample>) {
use std::time::Duration;

samples.retain(|smp| {
let less_than_15_mins =
Duration::from_secs_f32(smp.length_frames() as f32 / smp.rate as f32)
< Duration::from_secs(60 * 15);

let sensible_sample_rate = (256..=384_000).contains(&smp.rate);

sensible_sample_rate && less_than_15_mins
})
}
Binary file added tests/modules/xm/xenia3.xm
Binary file not shown.
12 changes: 12 additions & 0 deletions tests/test_xm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ check_sample_number! {
with: 26
}

// This module is cursed.
//
// Openmpt reports 7 samples, but the last sample is complete and utter garbage.
// Attempting to save samples with it will fail on the last one.
//
// source: https://modarchive.org/module.php?193712
check_sample_number! {
test_xm_cursed_sample,
path: include_bytes!("modules/xm/xenia3.xm"),
with: 6
}

// An ordinary xm file. Mainly for sanity checks.
//
// source: https://modarchive.org/module.php?191384
Expand Down

0 comments on commit 771c354

Please sign in to comment.