1use oxideav_core::{Error, Result};
25
26pub const HEADER_FIXED_SIZE: usize = 1084;
27pub const PATTERN_ROWS: usize = 64;
28pub const SAMPLE_COUNT: usize = 31;
29pub const ORDER_TABLE_SIZE: usize = 128;
30
31#[derive(Clone, Debug)]
32pub struct Sample {
33 pub name: String,
34 pub length: u32,
36 pub finetune: i8,
38 pub volume: u8,
40 pub repeat_start: u32,
42 pub repeat_length: u32,
44}
45
46#[derive(Clone, Debug)]
47pub struct ModHeader {
48 pub title: String,
49 pub samples: Vec<Sample>,
50 pub song_length: u8,
51 pub restart: u8,
52 pub order: Vec<u8>,
53 pub signature: [u8; 4],
54 pub channels: u8,
55 pub n_patterns: u8,
57}
58
59impl ModHeader {
60 pub fn pattern_data_offset(&self) -> usize {
62 HEADER_FIXED_SIZE
63 }
64
65 pub fn pattern_data_size(&self) -> usize {
67 self.n_patterns as usize * PATTERN_ROWS * self.channels as usize * 4
68 }
69
70 pub fn sample_data_offset(&self) -> usize {
72 HEADER_FIXED_SIZE + self.pattern_data_size()
73 }
74}
75
76pub fn parse_header(bytes: &[u8]) -> Result<ModHeader> {
77 if bytes.len() < HEADER_FIXED_SIZE {
78 return Err(Error::NeedMore);
79 }
80 let title = read_padded_ascii(&bytes[0..20]);
81
82 let mut samples = Vec::with_capacity(SAMPLE_COUNT);
83 for i in 0..SAMPLE_COUNT {
84 let off = 20 + i * 30;
85 let name = read_padded_ascii(&bytes[off..off + 22]);
86 let len_words = u16::from_be_bytes([bytes[off + 22], bytes[off + 23]]) as u32;
87 let finetune_raw = bytes[off + 24] & 0x0F;
88 let finetune = if finetune_raw & 0x08 != 0 {
89 (finetune_raw as i8) - 16
90 } else {
91 finetune_raw as i8
92 };
93 let volume = bytes[off + 25].min(64);
94 let repeat_start_words = u16::from_be_bytes([bytes[off + 26], bytes[off + 27]]) as u32;
95 let repeat_length_words = u16::from_be_bytes([bytes[off + 28], bytes[off + 29]]) as u32;
96 samples.push(Sample {
97 name,
98 length: len_words.saturating_mul(2),
99 finetune,
100 volume,
101 repeat_start: repeat_start_words.saturating_mul(2),
102 repeat_length: repeat_length_words.saturating_mul(2),
103 });
104 }
105
106 let song_length = bytes[950];
107 let restart = bytes[951];
108 let order: Vec<u8> = bytes[952..952 + ORDER_TABLE_SIZE].to_vec();
109
110 let mut signature = [0u8; 4];
111 signature.copy_from_slice(&bytes[1080..1084]);
112 let channels = channels_from_signature(&signature)?;
113
114 let n_patterns = 1 + *order.iter().take(song_length as usize).max().unwrap_or(&0);
115
116 Ok(ModHeader {
117 title,
118 samples,
119 song_length,
120 restart,
121 order,
122 signature,
123 channels,
124 n_patterns,
125 })
126}
127
128fn channels_from_signature(sig: &[u8; 4]) -> Result<u8> {
129 match sig {
130 b"M.K." | b"M!K!" | b"FLT4" | b"4CHN" => Ok(4),
131 b"6CHN" => Ok(6),
132 b"8CHN" | b"OCTA" | b"CD81" | b"FLT8" => Ok(8),
133 other if other[2] == b'C' && other[3] == b'H' => {
135 let tens = (other[0] as char).to_digit(10);
136 let ones = (other[1] as char).to_digit(10);
137 match (tens, ones) {
138 (Some(t), Some(o)) => {
139 let n = (t * 10 + o) as u8;
140 if (10..=32).contains(&n) {
141 Ok(n)
142 } else {
143 Err(Error::unsupported(format!(
144 "MOD: unsupported channel count {n}"
145 )))
146 }
147 }
148 _ => Err(Error::invalid(format!(
149 "MOD: unknown signature {:?}",
150 std::str::from_utf8(other).unwrap_or("????")
151 ))),
152 }
153 }
154 _ => Err(Error::invalid(format!(
155 "MOD: unknown signature {:?}",
156 std::str::from_utf8(sig).unwrap_or("????")
157 ))),
158 }
159}
160
161fn read_padded_ascii(bytes: &[u8]) -> String {
162 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
163 String::from_utf8_lossy(&bytes[..end])
164 .trim_end()
165 .to_string()
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 fn make_fake_mod(channels: &[u8; 4], song_length: u8) -> Vec<u8> {
173 let mut out = vec![0u8; HEADER_FIXED_SIZE];
174 out[0..8].copy_from_slice(b"test\0\0\0\0");
175 out[950] = song_length;
178 out[951] = 0x7F;
179 for i in 0..song_length as usize {
180 out[952 + i] = 0;
181 }
182 out[1080..1084].copy_from_slice(channels);
183 out
184 }
185
186 #[test]
187 fn signature_mk() {
188 let h = parse_header(&make_fake_mod(b"M.K.", 1)).unwrap();
189 assert_eq!(h.channels, 4);
190 assert_eq!(h.signature, *b"M.K.");
191 assert_eq!(h.song_length, 1);
192 assert_eq!(h.samples.len(), 31);
193 }
194
195 #[test]
196 fn signature_6chn() {
197 let h = parse_header(&make_fake_mod(b"6CHN", 2)).unwrap();
198 assert_eq!(h.channels, 6);
199 }
200
201 #[test]
202 fn signature_14ch() {
203 let h = parse_header(&make_fake_mod(b"14CH", 1)).unwrap();
204 assert_eq!(h.channels, 14);
205 }
206
207 #[test]
208 fn rejects_unknown_signature() {
209 let bytes = make_fake_mod(b"XXXX", 1);
210 assert!(parse_header(&bytes).is_err());
211 }
212}