use super::amiga_sample::AmigaSample;
use super::patternslot::PatternSlot;
use bincode::error::DecodeError;
use crate::fixed::units::Volume;
use crate::import::import_memory::ImportMemory;
use crate::import::import_memory::MemoryType;
use crate::prelude::*;
use alloc::format;
use alloc::string::String;
use alloc::string::ToString;
use alloc::{vec, vec::Vec};
const MOD_15_HEADER_SIZE: usize = 600;
const MOD_31_HEADER_SIZE: usize = 1084;
const AMIGA_SAMPLE_RECORD_SIZE: usize = 30;
const POSITION_TABLE_SIZE: usize = 128;
const AMIGA_ROWS_PER_PATTERN: usize = 64;
const AMIGA_SLOT_SIZE: usize = 4;
fn tag_str_to_num_tracks(tag: &str) -> Option<u8> {
match tag {
"TDZ1" => Some(1),
"2CHN" | "TDZ2" => Some(2),
"TDZ3" => Some(3),
"M.K." | "M!K!" | "FLT4" | "NSMS" | "LARD" | "PATT" | "EXO4" | "N.T." | "M&K!" | "FEST"
| "CD61" => Some(4),
"5CHN" => Some(5),
"6CHN" => Some(6),
"7CHN" => Some(7),
"8CHN" | "CD81" | "OKTA" | "OCTA" | "FLT8" | "EXO8" => Some(8),
"9CHN" => Some(9),
t if t.ends_with("CH") || t.ends_with("CN") => {
match t[..t.len() - 2].parse::<u8>().unwrap_or(0) {
0 => None,
v => Some(v),
}
}
_ => None,
}
}
fn tag_bytes_to_num_tracks(tag: &[u8]) -> Option<u8> {
if tag.len() != 4 {
return None;
}
let s = core::str::from_utf8(tag).ok()?;
tag_str_to_num_tracks(s)
}
fn is_clean_text(bytes: &[u8]) -> bool {
bytes.iter().all(|&b| b == 0 || (0x20..=0x7E).contains(&b))
}
#[derive(Copy, Clone, Debug)]
enum AmigaVariant {
Fifteen,
ThirtyOne { num_tracks: u8 },
}
#[derive(Default, Debug)]
pub struct AmigaModule {
title: String,
samples: Vec<AmigaSample>, song_length: u8,
restart_position: u8,
positions: Vec<u8>, tag: String,
patterns: Vec<Vec<Vec<PatternSlot>>>, audio: Vec<Vec<i8>>,
}
impl AmigaModule {
fn get_number_of_tracks(&self) -> Option<u8> {
tag_str_to_num_tracks(self.tag.as_str())
}
fn get_number_of_samples(&self) -> usize {
match self.get_number_of_tracks() {
None => 15,
_ => 31,
}
}
fn get_number_of_patterns(&self) -> usize {
1 + *self.positions.iter().max().unwrap_or(&0) as usize
}
fn detect_variant(data: &[u8]) -> Result<AmigaVariant, DecodeError> {
if data.len() >= MOD_31_HEADER_SIZE {
if let Some(num_tracks) = tag_bytes_to_num_tracks(&data[0x438..0x438 + 4]) {
let v = AmigaVariant::ThirtyOne { num_tracks };
if Self::validate_structure(data, v).is_ok() {
return Ok(v);
}
}
}
if Self::validate_structure(data, AmigaVariant::Fifteen).is_ok() {
return Ok(AmigaVariant::Fifteen);
}
Err(DecodeError::Other("Not an Amiga MOD module"))
}
fn validate_structure(data: &[u8], variant: AmigaVariant) -> Result<(), DecodeError> {
let (num_samples, num_tracks, header_size) = match variant {
AmigaVariant::Fifteen => (15usize, 4u8, MOD_15_HEADER_SIZE),
AmigaVariant::ThirtyOne { num_tracks } => (31, num_tracks, MOD_31_HEADER_SIZE),
};
if data.len() < header_size {
return Err(DecodeError::Other("File too short for MOD header"));
}
if !is_clean_text(&data[..20]) {
return Err(DecodeError::Other("MOD title is not ASCII"));
}
let mut total_sample_bytes: u64 = 0;
for i in 0..num_samples {
let off = 20 + i * AMIGA_SAMPLE_RECORD_SIZE;
if !is_clean_text(&data[off..off + 22]) {
return Err(DecodeError::Other("MOD sample name is not ASCII"));
}
let length_div2 = u16::from_be_bytes([data[off + 22], data[off + 23]]);
let finetune = data[off + 24];
let volume = data[off + 25];
if finetune > 0x0F {
return Err(DecodeError::Other("MOD sample finetune > 15"));
}
if volume > 0x40 {
return Err(DecodeError::Other("MOD sample volume > 64"));
}
total_sample_bytes = total_sample_bytes.saturating_add(2 * length_div2 as u64);
}
let song_length_off = 20 + num_samples * AMIGA_SAMPLE_RECORD_SIZE;
let song_length = data[song_length_off];
if song_length == 0 || song_length > 128 {
return Err(DecodeError::Other("MOD song_length out of range"));
}
let positions_off = song_length_off + 2;
let positions = &data[positions_off..positions_off + POSITION_TABLE_SIZE];
let max_pos = *positions[..song_length as usize].iter().max().unwrap_or(&0);
if max_pos >= 128 {
return Err(DecodeError::Other("MOD position byte out of range"));
}
let pattern_size =
num_tracks as u64 * AMIGA_ROWS_PER_PATTERN as u64 * AMIGA_SLOT_SIZE as u64;
let num_patterns = max_pos as u64 + 1;
let expected = (header_size as u64)
.saturating_add(pattern_size.saturating_mul(num_patterns))
.saturating_add(total_sample_bytes);
if expected > data.len() as u64 {
return Err(DecodeError::Other("MOD file shorter than declared content"));
}
Ok(())
}
pub fn load(ser_amiga_module: &[u8]) -> Result<AmigaModule, DecodeError> {
let variant = Self::detect_variant(ser_amiga_module)?;
let mut amiga = AmigaModule {
..Default::default()
};
amiga.title = String::from_utf8_lossy(&ser_amiga_module[0..22]).to_string();
amiga.title = amiga
.title
.split('\0')
.next()
.unwrap_or("")
.trim()
.to_string();
amiga.tag = match variant {
AmigaVariant::ThirtyOne { .. } => {
String::from_utf8_lossy(&ser_amiga_module[0x438..0x438 + 4]).to_string()
}
AmigaVariant::Fifteen => String::new(),
};
let mut data = &ser_amiga_module[0x14..];
for _i in 0..amiga.get_number_of_samples() {
let (d2, sample) = AmigaSample::load(data)?;
data = d2;
amiga.samples.push(sample);
}
amiga.song_length = data[0];
amiga.restart_position = data[1];
data = &data[2..];
amiga.positions.extend_from_slice(&data[..128]);
data = &data[128..];
if amiga.get_number_of_samples() != 15 {
data = &data[4..];
}
let number_of_tracks = match amiga.get_number_of_tracks() {
Some(n) => n as usize,
None => 4, };
let number_of_patterns = amiga.get_number_of_patterns();
for _p in 0..number_of_patterns {
let mut pattern: Vec<Vec<PatternSlot>> = vec![];
for _row in 0..64 {
let mut row: Vec<PatternSlot> = vec![];
for _elt in 0..number_of_tracks {
let e = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
let element = PatternSlot::deserialize(e);
row.push(element);
data = &data[4..];
}
pattern.push(row);
}
amiga.patterns.push(pattern);
}
for i_spl in 0..amiga.samples.len() {
let l = if 2 * amiga.samples[i_spl].length_div2 as usize <= data.len() {
2 * amiga.samples[i_spl].length_div2 as usize
} else {
data.len()
};
let s = &data[0..l];
let vec_i8: Vec<i8> = s.iter().map(|&x| x as i8).collect();
amiga.audio.push(vec_i8);
data = &data[l..];
}
Result::Ok(amiga)
}
fn to_instr(&self, sample_index: usize) -> Instrument {
let mut instr: Instrument = Instrument::default();
let mut sample: Sample = self.samples[sample_index].to_sample();
sample.data = Some(SampleDataType::Mono8(self.audio[sample_index].clone()));
instr.name = sample.name.clone();
let mut idef = InstrDefault::default();
idef.sample.push(Some(sample));
idef.keyboard.sample_for_pitch = [Some(0); 120];
instr.instr_type = InstrumentType::Default(idef);
instr
}
pub fn to_module(&self) -> Module {
let mut module = Module::default();
module.name = self.title.clone();
module.comment = if self.get_number_of_samples() == 15 {
"Soundtracker (15 samples, no tag)".to_string()
} else {
format!("MOD tag: {}", self.tag)
};
module.profile = CompatibilityProfile::pt();
module.frequency_type = FrequencyType::AmigaFrequencies;
module.mix_volume = Volume::from_ratio(48, 128);
module.default_tempo = 6;
module.default_bpm = 125;
module.restart_position = if (self.restart_position as usize) < self.song_length as usize {
self.restart_position as usize
} else {
0
};
module.pattern_order = vec![self.positions[..usize::from(self.song_length)]
.to_vec()
.iter()
.map(|&x| x as usize)
.collect()];
let mut im = ImportMemory::default();
module.pattern = im.unpack_patterns(
FrequencyType::AmigaFrequencies,
MemoryType::Mod,
&module.pattern_order,
&self.patterns,
);
for i in 0..self.samples.len() {
let instr = self.to_instr(i);
module.instrument.push(instr);
}
module
}
}