Skip to main content

oxideav_mod/
header.rs

1//! ProTracker / SoundTracker MOD header parser.
2//!
3//! Layout (little-endian is not used — all multi-byte fields are
4//! big-endian):
5//!
6//! ```text
7//! Offset 0        20 bytes      Song title (null-padded ASCII)
8//! Offset 20      31 * 30 bytes  Sample definitions:
9//!                                 22 bytes name
10//!                                  2 bytes length (in 16-bit words, BE)
11//!                                  1 byte  finetune  (low 4 bits, signed)
12//!                                  1 byte  volume    (0..64)
13//!                                  2 bytes repeat-start (words, BE)
14//!                                  2 bytes repeat-length (words, BE)
15//! Offset 950      1 byte        Song length (1..128)
16//! Offset 951      1 byte        Restart byte (0x7F typical)
17//! Offset 952    128 bytes       Pattern-order table
18//! Offset 1080     4 bytes       Signature: "M.K.", "M!K!", "4CHN",
19//!                                "6CHN", "8CHN", "xxCH" (xx 10..32)
20//! Offset 1084      …            Pattern data: 64 rows × channels × 4 bytes
21//! After patterns               Raw sample bodies (signed 8-bit)
22//! ```
23
24use 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    /// Sample length in *samples* (spec stores words — we've doubled).
35    pub length: u32,
36    /// Finetune value, signed 4-bit (-8..=7).
37    pub finetune: i8,
38    /// Volume 0..=64.
39    pub volume: u8,
40    /// Loop start in samples.
41    pub repeat_start: u32,
42    /// Loop length in samples (0 or 2 = no loop).
43    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    /// Number of distinct patterns referenced by the order table.
56    pub n_patterns: u8,
57}
58
59impl ModHeader {
60    /// Total size of the header block preceding sample data (in bytes).
61    pub fn pattern_data_offset(&self) -> usize {
62        HEADER_FIXED_SIZE
63    }
64
65    /// Size of the pattern data region in bytes.
66    pub fn pattern_data_size(&self) -> usize {
67        self.n_patterns as usize * PATTERN_ROWS * self.channels as usize * 4
68    }
69
70    /// Absolute offset where sample bodies begin.
71    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        // "xxCH" with xx in 10..=32 (Fast Tracker / TakeTracker).
134        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        // sample 0: empty
176        // song length + order table
177        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}