1use oxideav_core::{Error, Result};
51
52pub const XM_BANNER: &[u8; 17] = b"Extended Module: ";
55
56pub const XM_ID_BYTE_OFFSET: usize = 37;
58
59pub const XM_VERSION_0104: u16 = 0x0104;
61
62pub const XM_HEADER_SIZE_OFFSET: usize = 60;
64
65pub const XM_ORDER_TABLE_OFFSET: usize = 80;
70
71pub const XM_ORDER_TABLE_SIZE: usize = 256;
73
74pub const XM_MIN_HEADER_LEN: usize = XM_ORDER_TABLE_OFFSET + XM_ORDER_TABLE_SIZE;
76
77pub const XM_PATTERN_HEADER_SIZE: u32 = 9;
81
82pub const XM_SAMPLE_HEADER_SIZE: u32 = 0x28;
84
85pub const XM_INSTRUMENT_HEADER_SIZE_WITH_SAMPLES: u32 = 0x107;
89
90pub const XM_FLAG_LINEAR_FREQ_TABLE: u16 = 0x0001;
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum XmFrequencyTable {
96 Amiga,
98 Linear,
100}
101
102#[derive(Clone, Debug)]
105pub struct XmHeader {
106 pub module_name: String,
107 pub tracker_name: String,
108 pub version: u16,
110 pub header_size: u32,
112 pub song_length: u16,
113 pub restart_position: u16,
114 pub num_channels: u16,
115 pub num_patterns: u16,
116 pub num_instruments: u16,
117 pub flags: u16,
118 pub frequency_table: XmFrequencyTable,
119 pub default_tempo: u16,
120 pub default_bpm: u16,
121 pub order: Vec<u8>,
123}
124
125#[derive(Clone, Debug)]
127pub struct XmPattern {
128 pub header_length: u32,
129 pub packing_type: u8,
131 pub num_rows: u16,
132 pub packed_size: u16,
133 pub rows: Vec<Vec<XmCell>>,
135}
136
137#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
141pub struct XmCell {
142 pub note: u8,
144 pub instrument: u8,
146 pub volume: u8,
148 pub effect_type: u8,
150 pub effect_param: u8,
152}
153
154#[derive(Clone, Copy, Debug, PartialEq, Eq)]
161pub enum XmVolume {
162 Empty,
163 SetVolume(u8),
165 VolumeSlideDown(u8),
167 VolumeSlideUp(u8),
169 FineVolumeSlideDown(u8),
171 FineVolumeSlideUp(u8),
173 SetVibratoSpeed(u8),
175 Vibrato(u8),
177 SetPanning(u8),
179 PanningSlideLeft(u8),
181 PanningSlideRight(u8),
183 TonePorta(u8),
185}
186
187impl XmCell {
188 pub fn volume_kind(&self) -> XmVolume {
190 match self.volume {
191 0 => XmVolume::Empty,
192 v @ 0x10..=0x50 => XmVolume::SetVolume(v - 0x10),
193 v @ 0x60..=0x6F => XmVolume::VolumeSlideDown(v & 0x0F),
194 v @ 0x70..=0x7F => XmVolume::VolumeSlideUp(v & 0x0F),
195 v @ 0x80..=0x8F => XmVolume::FineVolumeSlideDown(v & 0x0F),
196 v @ 0x90..=0x9F => XmVolume::FineVolumeSlideUp(v & 0x0F),
197 v @ 0xA0..=0xAF => XmVolume::SetVibratoSpeed(v & 0x0F),
198 v @ 0xB0..=0xBF => XmVolume::Vibrato(v & 0x0F),
199 v @ 0xC0..=0xCF => XmVolume::SetPanning(v & 0x0F),
200 v @ 0xD0..=0xDF => XmVolume::PanningSlideLeft(v & 0x0F),
201 v @ 0xE0..=0xEF => XmVolume::PanningSlideRight(v & 0x0F),
202 v @ 0xF0..=0xFF => XmVolume::TonePorta(v & 0x0F),
203 _ => XmVolume::Empty,
204 }
205 }
206
207 pub fn is_note_off(&self) -> bool {
209 self.note == 97
210 }
211
212 pub fn has_note(&self) -> bool {
214 (1..=96).contains(&self.note)
215 }
216}
217
218#[derive(Clone, Debug, Default)]
220pub struct XmEnvelope {
221 pub points: Vec<(u16, u16)>,
222 pub sustain_point: u8,
223 pub loop_start_point: u8,
224 pub loop_end_point: u8,
225 pub type_bits: u8,
227}
228
229impl XmEnvelope {
230 pub fn is_on(&self) -> bool {
231 self.type_bits & 0x01 != 0
232 }
233 pub fn has_sustain(&self) -> bool {
234 self.type_bits & 0x02 != 0
235 }
236 pub fn has_loop(&self) -> bool {
237 self.type_bits & 0x04 != 0
238 }
239}
240
241#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
243pub enum XmSampleLoopMode {
244 #[default]
245 None,
246 Forward,
247 PingPong,
248}
249
250impl XmSampleLoopMode {
251 pub fn from_type_byte(b: u8) -> Self {
252 match b & 0x03 {
253 1 => XmSampleLoopMode::Forward,
254 2 => XmSampleLoopMode::PingPong,
255 _ => XmSampleLoopMode::None,
256 }
257 }
258}
259
260impl crate::mixer::SampleSource for XmSampleHeader {
261 fn len(&self) -> usize {
262 if self.is_16_bit {
263 self.pcm16.len()
264 } else {
265 self.pcm8.len()
266 }
267 }
268 fn loop_start(&self) -> usize {
269 let div = if self.is_16_bit { 2 } else { 1 };
272 if matches!(self.loop_mode, XmSampleLoopMode::None) {
273 0
274 } else {
275 (self.loop_start as usize / div).min(self.len())
276 }
277 }
278 fn loop_end(&self) -> usize {
279 let div = if self.is_16_bit { 2 } else { 1 };
280 if matches!(self.loop_mode, XmSampleLoopMode::None) {
281 self.len()
282 } else {
283 let end = (self.loop_start + self.loop_length) as usize / div;
284 end.min(self.len())
285 }
286 }
287 fn loop_kind(&self) -> crate::mixer::LoopKind {
288 match self.loop_mode {
289 XmSampleLoopMode::None => crate::mixer::LoopKind::None,
290 XmSampleLoopMode::Forward => crate::mixer::LoopKind::Forward,
291 XmSampleLoopMode::PingPong => crate::mixer::LoopKind::PingPong,
292 }
293 }
294 fn at(&self, idx: usize) -> f32 {
295 if self.is_16_bit {
296 self.pcm16.get(idx).copied().unwrap_or(0) as f32 / 32768.0
297 } else {
298 self.pcm8.get(idx).copied().unwrap_or(0) as f32 / 128.0
299 }
300 }
301}
302
303#[derive(Clone, Debug, Default)]
306pub struct XmSampleHeader {
307 pub name: String,
308 pub length: u32,
311 pub loop_start: u32,
312 pub loop_length: u32,
313 pub volume: u8,
314 pub finetune: i8,
316 pub type_byte: u8,
317 pub panning: u8,
318 pub relative_note: i8,
320 pub loop_mode: XmSampleLoopMode,
321 pub is_16_bit: bool,
322 pub pcm16: Vec<i16>,
325 pub pcm8: Vec<i8>,
327}
328
329#[derive(Clone, Debug, Default)]
333pub struct XmInstrument {
334 pub name: String,
335 pub header_size: u32,
338 pub instrument_type: u8,
340 pub num_samples: u16,
341 pub sample_header_size: u32,
344 pub sample_map: Vec<u8>,
347 pub volume_envelope: XmEnvelope,
348 pub panning_envelope: XmEnvelope,
349 pub vibrato_type: u8,
350 pub vibrato_sweep: u8,
351 pub vibrato_depth: u8,
352 pub vibrato_rate: u8,
353 pub volume_fadeout: u16,
354 pub samples: Vec<XmSampleHeader>,
355 pub sample_data_offset: usize,
358}
359
360pub fn is_xm(bytes: &[u8]) -> bool {
365 bytes.len() >= XM_BANNER.len() && &bytes[..XM_BANNER.len()] == XM_BANNER.as_slice()
366}
367
368fn read_u16_le(bytes: &[u8], off: usize) -> u16 {
369 u16::from_le_bytes([bytes[off], bytes[off + 1]])
370}
371
372fn read_u32_le(bytes: &[u8], off: usize) -> u32 {
373 u32::from_le_bytes([bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]])
374}
375
376fn trim_fixed_string(bytes: &[u8]) -> String {
377 let end = bytes
381 .iter()
382 .rposition(|&b| b != 0 && b != b' ')
383 .map(|i| i + 1)
384 .unwrap_or(0);
385 String::from_utf8_lossy(&bytes[..end]).to_string()
386}
387
388pub fn parse_header(bytes: &[u8]) -> Result<XmHeader> {
396 if bytes.len() < XM_MIN_HEADER_LEN {
397 return Err(Error::NeedMore);
398 }
399 if !is_xm(bytes) {
400 return Err(Error::invalid(
401 "XM: missing 'Extended Module: ' banner at offset 0",
402 ));
403 }
404 if bytes[XM_ID_BYTE_OFFSET] != 0x1A {
405 return Err(Error::invalid("XM: missing 0x1A marker byte at offset 37"));
406 }
407
408 let module_name = trim_fixed_string(&bytes[17..37]);
409 let tracker_name = trim_fixed_string(&bytes[38..58]);
410 let version = read_u16_le(bytes, 58);
411 let header_size = read_u32_le(bytes, XM_HEADER_SIZE_OFFSET);
412 let song_length = read_u16_le(bytes, 64);
413 let restart_position = read_u16_le(bytes, 66);
414 let num_channels = read_u16_le(bytes, 68);
415 let num_patterns = read_u16_le(bytes, 70);
416 let num_instruments = read_u16_le(bytes, 72);
417 let flags = read_u16_le(bytes, 74);
418 let default_tempo = read_u16_le(bytes, 76);
419 let default_bpm = read_u16_le(bytes, 78);
420
421 if !(1..=32).contains(&num_channels) {
422 return Err(Error::invalid(format!(
423 "XM: implausible channel count {num_channels} (expected 1..=32)"
424 )));
425 }
426 if num_patterns > 256 {
427 return Err(Error::invalid(format!(
428 "XM: implausible pattern count {num_patterns} (expected <=256)"
429 )));
430 }
431 if num_instruments > 128 {
432 return Err(Error::invalid(format!(
433 "XM: implausible instrument count {num_instruments} (expected <=128)"
434 )));
435 }
436
437 let frequency_table = if flags & XM_FLAG_LINEAR_FREQ_TABLE != 0 {
438 XmFrequencyTable::Linear
439 } else {
440 XmFrequencyTable::Amiga
441 };
442
443 let order = bytes[XM_ORDER_TABLE_OFFSET..XM_ORDER_TABLE_OFFSET + XM_ORDER_TABLE_SIZE].to_vec();
444
445 Ok(XmHeader {
446 module_name,
447 tracker_name,
448 version,
449 header_size,
450 song_length,
451 restart_position,
452 num_channels,
453 num_patterns,
454 num_instruments,
455 flags,
456 frequency_table,
457 default_tempo,
458 default_bpm,
459 order,
460 })
461}
462
463pub fn pattern_data_offset(header: &XmHeader) -> usize {
472 XM_HEADER_SIZE_OFFSET + header.header_size as usize
473}
474
475fn decode_packed_cell(data: &[u8], cur: usize) -> (XmCell, usize) {
489 if cur >= data.len() {
490 return (XmCell::default(), 0);
491 }
492 let first = data[cur];
493 let mut cell = XmCell::default();
494 if first & 0x80 != 0 {
495 let mask = first & 0x7F;
496 let mut off = 1usize;
497 let grab = |off: &mut usize, data: &[u8]| -> u8 {
498 let p = cur + *off;
499 *off += 1;
500 if p < data.len() {
501 data[p]
502 } else {
503 0
504 }
505 };
506 if mask & 0x01 != 0 {
507 cell.note = grab(&mut off, data);
508 }
509 if mask & 0x02 != 0 {
510 cell.instrument = grab(&mut off, data);
511 }
512 if mask & 0x04 != 0 {
513 cell.volume = grab(&mut off, data);
514 }
515 if mask & 0x08 != 0 {
516 cell.effect_type = grab(&mut off, data);
517 }
518 if mask & 0x10 != 0 {
519 cell.effect_param = grab(&mut off, data);
520 }
521 (cell, off)
522 } else {
523 cell.note = first;
525 let grab = |rel: usize| -> u8 {
526 let p = cur + rel;
527 if p < data.len() {
528 data[p]
529 } else {
530 0
531 }
532 };
533 cell.instrument = grab(1);
534 cell.volume = grab(2);
535 cell.effect_type = grab(3);
536 cell.effect_param = grab(4);
537 (cell, 5)
538 }
539}
540
541pub fn parse_patterns(header: &XmHeader, bytes: &[u8]) -> Result<(Vec<XmPattern>, usize)> {
555 let mut cur = pattern_data_offset(header);
556 let mut out = Vec::with_capacity(header.num_patterns as usize);
557 let num_channels = header.num_channels as usize;
558
559 for pat_idx in 0..header.num_patterns as usize {
560 if cur + 9 > bytes.len() {
561 return Err(Error::invalid(format!(
562 "XM: truncated pattern header #{pat_idx} at offset {cur}"
563 )));
564 }
565 let header_length = read_u32_le(bytes, cur);
566 let packing_type = bytes[cur + 4];
567 let num_rows = read_u16_le(bytes, cur + 5);
568 let packed_size = read_u16_le(bytes, cur + 7);
569
570 let data_start = cur
573 .checked_add(header_length as usize)
574 .ok_or_else(|| Error::invalid("XM: pattern header_length overflow"))?;
575
576 let mut rows: Vec<Vec<XmCell>> = Vec::with_capacity(num_rows as usize);
577
578 if packed_size == 0 {
579 for _ in 0..num_rows {
581 rows.push(vec![XmCell::default(); num_channels]);
582 }
583 } else {
584 let start = data_start.min(bytes.len());
592 let data_end = data_start
593 .saturating_add(packed_size as usize)
594 .min(bytes.len());
595 let slice = &bytes[start..data_end];
596 let mut inner = 0usize;
597 for _ in 0..num_rows {
598 let mut row = Vec::with_capacity(num_channels);
599 for _ in 0..num_channels {
600 let (cell, consumed) = decode_packed_cell(slice, inner);
601 inner += consumed;
602 row.push(cell);
603 }
604 rows.push(row);
605 }
606 }
607
608 cur = data_start.saturating_add(packed_size as usize);
613
614 out.push(XmPattern {
615 header_length,
616 packing_type,
617 num_rows,
618 packed_size,
619 rows,
620 });
621 }
622
623 Ok((out, cur))
624}
625
626fn parse_envelope_points(bytes: &[u8; 48], num_points: u8) -> Vec<(u16, u16)> {
632 let n = num_points.min(12) as usize;
633 let mut out = Vec::with_capacity(n);
634 for i in 0..n {
635 let off = i * 4;
636 let x = u16::from_le_bytes([bytes[off], bytes[off + 1]]);
637 let y = u16::from_le_bytes([bytes[off + 2], bytes[off + 3]]);
638 out.push((x, y));
639 }
640 out
641}
642
643fn parse_one_instrument(bytes: &[u8], cur: usize) -> Result<(XmInstrument, usize)> {
647 if cur + 29 > bytes.len() {
648 return Err(Error::invalid(format!(
649 "XM: truncated instrument header at offset {cur}"
650 )));
651 }
652 let header_size = read_u32_le(bytes, cur);
653 if header_size < 29 {
654 return Err(Error::invalid(format!(
655 "XM: nonsensical instrument header_size {header_size} at {cur}"
656 )));
657 }
658 let name = trim_fixed_string(&bytes[cur + 4..cur + 26]);
659 let instrument_type = bytes[cur + 26];
660 let num_samples = read_u16_le(bytes, cur + 27);
661
662 let mut inst = XmInstrument {
663 name,
664 header_size,
665 instrument_type,
666 num_samples,
667 ..Default::default()
668 };
669
670 if num_samples == 0 {
671 let next = cur.saturating_add(header_size as usize).min(bytes.len());
674 inst.sample_data_offset = next;
675 return Ok((inst, next));
676 }
677
678 if cur + header_size as usize > bytes.len() {
680 return Err(Error::invalid(format!(
681 "XM: truncated extended instrument block (need {} bytes at {cur})",
682 header_size
683 )));
684 }
685 let ext_base = cur + 29;
686 inst.sample_header_size = read_u32_le(bytes, ext_base);
688
689 let map_start = ext_base + 4;
691 if map_start + 96 > bytes.len() {
692 return Err(Error::invalid("XM: truncated instrument sample-number map"));
693 }
694 inst.sample_map = bytes[map_start..map_start + 96].to_vec();
695
696 let vol_env_start = ext_base + 100;
698 let pan_env_start = ext_base + 148;
700 if pan_env_start + 48 > bytes.len() {
701 return Err(Error::invalid("XM: truncated instrument envelope tables"));
702 }
703 let vol_env_raw: [u8; 48] = bytes[vol_env_start..vol_env_start + 48].try_into().unwrap();
704 let pan_env_raw: [u8; 48] = bytes[pan_env_start..pan_env_start + 48].try_into().unwrap();
705
706 let f = ext_base + 196;
708 if f + 16 > bytes.len() {
709 return Err(Error::invalid("XM: truncated instrument fixed-byte block"));
710 }
711 let num_vol_points = bytes[f];
712 let num_pan_points = bytes[f + 1];
713 let vol_sustain = bytes[f + 2];
714 let vol_loop_start = bytes[f + 3];
715 let vol_loop_end = bytes[f + 4];
716 let pan_sustain = bytes[f + 5];
717 let pan_loop_start = bytes[f + 6];
718 let pan_loop_end = bytes[f + 7];
719 let vol_type = bytes[f + 8];
720 let pan_type = bytes[f + 9];
721 inst.vibrato_type = bytes[f + 10];
722 inst.vibrato_sweep = bytes[f + 11];
723 inst.vibrato_depth = bytes[f + 12];
724 inst.vibrato_rate = bytes[f + 13];
725 inst.volume_fadeout = read_u16_le(bytes, f + 14);
726 inst.volume_envelope = XmEnvelope {
729 points: parse_envelope_points(&vol_env_raw, num_vol_points),
730 sustain_point: vol_sustain,
731 loop_start_point: vol_loop_start,
732 loop_end_point: vol_loop_end,
733 type_bits: vol_type,
734 };
735 inst.panning_envelope = XmEnvelope {
736 points: parse_envelope_points(&pan_env_raw, num_pan_points),
737 sustain_point: pan_sustain,
738 loop_start_point: pan_loop_start,
739 loop_end_point: pan_loop_end,
740 type_bits: pan_type,
741 };
742
743 if inst.sample_header_size < 40 {
748 return Err(Error::invalid(format!(
749 "XM: sample_header_size {} too small (expected >=40)",
750 inst.sample_header_size
751 )));
752 }
753
754 let headers_start = cur + header_size as usize;
755 let mut hcur = headers_start;
756 for i in 0..num_samples as usize {
757 if hcur + inst.sample_header_size as usize > bytes.len() {
758 return Err(Error::invalid(format!(
759 "XM: truncated sample header #{i} in instrument"
760 )));
761 }
762 let length = read_u32_le(bytes, hcur);
763 let loop_start = read_u32_le(bytes, hcur + 4);
764 let loop_length = read_u32_le(bytes, hcur + 8);
765 let volume = bytes[hcur + 12];
766 let finetune = bytes[hcur + 13] as i8;
767 let type_byte = bytes[hcur + 14];
768 let panning = bytes[hcur + 15];
769 let relative_note = bytes[hcur + 16] as i8;
770 let name = if hcur + 18 + 22 <= bytes.len() {
772 trim_fixed_string(&bytes[hcur + 18..hcur + 40])
773 } else {
774 String::new()
775 };
776 let loop_mode = XmSampleLoopMode::from_type_byte(type_byte);
777 let is_16_bit = type_byte & 0x10 != 0;
778 inst.samples.push(XmSampleHeader {
779 name,
780 length,
781 loop_start,
782 loop_length,
783 volume,
784 finetune,
785 type_byte,
786 panning,
787 relative_note,
788 loop_mode,
789 is_16_bit,
790 pcm16: Vec::new(),
791 pcm8: Vec::new(),
792 });
793 hcur += inst.sample_header_size as usize;
794 }
795
796 inst.sample_data_offset = hcur;
797 Ok((inst, hcur))
798}
799
800pub fn parse_instruments(
809 header: &XmHeader,
810 bytes: &[u8],
811 instruments_offset: usize,
812) -> Result<Vec<XmInstrument>> {
813 let mut out = Vec::with_capacity(header.num_instruments as usize);
814 let mut cur = instruments_offset;
815 for i in 0..header.num_instruments as usize {
816 let (inst, _next) = parse_one_instrument(bytes, cur)
817 .map_err(|e| Error::invalid(format!("XM: failed to parse instrument #{i}: {e}")))?;
818 let pcm_bytes: usize = inst.samples.iter().map(|s| s.length as usize).sum();
823 cur = inst.sample_data_offset.saturating_add(pcm_bytes);
824 out.push(inst);
825 }
826 Ok(out)
827}
828
829pub fn extract_sample_bodies(instruments: &mut [XmInstrument], bytes: &[u8]) {
837 for inst in instruments.iter_mut() {
838 let mut cur = inst.sample_data_offset;
839 for sample in inst.samples.iter_mut() {
840 let length_bytes = (sample.length as usize).min(bytes.len().saturating_sub(cur));
841 let slice = &bytes[cur..cur + length_bytes];
842 if sample.is_16_bit {
843 let n_frames = length_bytes / 2;
844 let mut out = Vec::with_capacity(n_frames);
845 let mut old: i16 = 0;
846 for i in 0..n_frames {
847 let delta = i16::from_le_bytes([slice[i * 2], slice[i * 2 + 1]]);
848 old = old.wrapping_add(delta);
849 out.push(old);
850 }
851 sample.pcm16 = out;
852 } else {
853 let mut out = Vec::with_capacity(length_bytes);
854 let mut old: i8 = 0;
855 for &b in slice {
856 let delta = b as i8;
857 old = old.wrapping_add(delta);
858 out.push(old);
859 }
860 sample.pcm8 = out;
861 }
862 cur += length_bytes;
863 }
864 }
865}
866
867pub fn estimate_duration_micros(header: &XmHeader, patterns: &[XmPattern]) -> i64 {
872 if patterns.is_empty() {
873 return 0;
874 }
875 let bpm = header.default_bpm.max(1) as i64;
876 let tempo = header.default_tempo.max(1) as i64;
877 let ticks_per_sec = (5 * bpm) / 2;
879 if ticks_per_sec < 1 {
880 return 0;
881 }
882 let song_length = header.song_length.max(1) as usize;
883 let mut total_rows: u64 = 0;
884 for idx in 0..song_length {
885 let pat_idx = *header.order.get(idx).unwrap_or(&0) as usize;
886 let rows = patterns
887 .get(pat_idx)
888 .map(|p| p.num_rows as u64)
889 .unwrap_or(64);
890 total_rows = total_rows.saturating_add(rows);
891 }
892 (total_rows as i64).saturating_mul(tempo) * 1_000_000 / ticks_per_sec.max(1)
894}
895
896#[cfg(test)]
897mod tests {
898 use super::*;
899
900 fn build_header(
907 num_channels: u16,
908 num_patterns: u16,
909 num_instruments: u16,
910 linear: bool,
911 ) -> Vec<u8> {
912 let mut out = vec![0u8; XM_MIN_HEADER_LEN];
913 out[0..17].copy_from_slice(XM_BANNER);
914 let name = b"hello xm ";
915 out[17..17 + name.len()].copy_from_slice(name);
916 out[XM_ID_BYTE_OFFSET] = 0x1A;
917 let tracker = b"oxideav-test ";
918 out[38..38 + tracker.len()].copy_from_slice(tracker);
919 out[58..60].copy_from_slice(&XM_VERSION_0104.to_le_bytes());
921 out[60..64].copy_from_slice(&0x114u32.to_le_bytes());
923 out[64..66].copy_from_slice(&1u16.to_le_bytes()); out[66..68].copy_from_slice(&0u16.to_le_bytes()); out[68..70].copy_from_slice(&num_channels.to_le_bytes());
926 out[70..72].copy_from_slice(&num_patterns.to_le_bytes());
927 out[72..74].copy_from_slice(&num_instruments.to_le_bytes());
928 let flags = if linear { 1u16 } else { 0u16 };
929 out[74..76].copy_from_slice(&flags.to_le_bytes());
930 out[76..78].copy_from_slice(&6u16.to_le_bytes()); out[78..80].copy_from_slice(&125u16.to_le_bytes()); for i in 1..XM_ORDER_TABLE_SIZE {
934 out[XM_ORDER_TABLE_OFFSET + i] = 0xFF;
935 }
936 out
937 }
938
939 fn build_pattern_block(num_rows: u16, packed: &[u8]) -> Vec<u8> {
941 let mut out = Vec::new();
942 out.extend_from_slice(&XM_PATTERN_HEADER_SIZE.to_le_bytes()); out.push(0); out.extend_from_slice(&num_rows.to_le_bytes());
945 out.extend_from_slice(&(packed.len() as u16).to_le_bytes());
946 out.extend_from_slice(packed);
947 out
948 }
949
950 fn build_empty_instrument(name: &[u8]) -> Vec<u8> {
954 let mut out = Vec::new();
955 const HSIZE: u32 = 0x21;
960 out.extend_from_slice(&HSIZE.to_le_bytes());
961 let mut nbuf = [0u8; 22];
962 let n = name.len().min(22);
963 nbuf[..n].copy_from_slice(&name[..n]);
964 out.extend_from_slice(&nbuf);
965 out.push(0); out.extend_from_slice(&0u16.to_le_bytes()); while out.len() < HSIZE as usize {
968 out.push(0);
969 }
970 out
971 }
972
973 fn build_one_sample_instrument(name: &[u8], sample_body: &[u8]) -> Vec<u8> {
977 let mut out = Vec::new();
978 const HSIZE: u32 = XM_INSTRUMENT_HEADER_SIZE_WITH_SAMPLES; out.extend_from_slice(&HSIZE.to_le_bytes());
980 let mut nbuf = [0u8; 22];
981 let n = name.len().min(22);
982 nbuf[..n].copy_from_slice(&name[..n]);
983 out.extend_from_slice(&nbuf);
984 out.push(0); out.extend_from_slice(&1u16.to_le_bytes()); out.extend_from_slice(&XM_SAMPLE_HEADER_SIZE.to_le_bytes());
991 out.extend(std::iter::repeat_n(0u8, 96));
993 let mut vol_env = [0u8; 48];
995 vol_env[0..2].copy_from_slice(&0u16.to_le_bytes()); vol_env[2..4].copy_from_slice(&0u16.to_le_bytes()); vol_env[4..6].copy_from_slice(&64u16.to_le_bytes()); vol_env[6..8].copy_from_slice(&64u16.to_le_bytes()); out.extend_from_slice(&vol_env);
1000 out.extend_from_slice(&[0u8; 48]);
1002 out.push(2);
1004 out.push(0);
1006 out.push(0);
1008 out.push(0);
1009 out.push(0);
1010 out.push(0);
1012 out.push(0);
1013 out.push(0);
1014 out.push(0x01);
1016 out.push(0);
1018 out.push(0);
1020 out.push(0);
1021 out.push(0);
1022 out.push(0);
1023 out.extend_from_slice(&512u16.to_le_bytes());
1025 out.extend_from_slice(&0u16.to_le_bytes());
1027 while out.len() < HSIZE as usize {
1029 out.push(0);
1030 }
1031
1032 out.extend_from_slice(&(sample_body.len() as u32).to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.push(0x40); out.push(0); out.push(0); out.push(128); out.push(0); out.push(0); let mut sname = [0u8; 22];
1043 let s = b"snd";
1044 sname[..s.len()].copy_from_slice(s);
1045 out.extend_from_slice(&sname);
1046
1047 out.extend_from_slice(sample_body);
1049 out
1050 }
1051
1052 #[test]
1053 fn is_xm_accepts_canonical_banner() {
1054 let bytes = build_header(4, 0, 0, false);
1055 assert!(is_xm(&bytes));
1056 }
1057
1058 #[test]
1059 fn is_xm_rejects_lowercase_banner() {
1060 let mut bytes = build_header(4, 0, 0, false);
1061 bytes[9] = b'm';
1064 assert!(!is_xm(&bytes));
1065 }
1066
1067 #[test]
1068 fn is_xm_rejects_short_buffer() {
1069 assert!(!is_xm(b"Extended Module"));
1070 }
1071
1072 #[test]
1073 fn parse_header_populates_core_fields() {
1074 let bytes = build_header(8, 2, 3, true);
1075 let h = parse_header(&bytes).unwrap();
1076 assert_eq!(h.module_name, "hello xm");
1077 assert_eq!(h.tracker_name, "oxideav-test");
1078 assert_eq!(h.version, XM_VERSION_0104);
1079 assert_eq!(h.header_size, 0x114);
1080 assert_eq!(h.song_length, 1);
1081 assert_eq!(h.num_channels, 8);
1082 assert_eq!(h.num_patterns, 2);
1083 assert_eq!(h.num_instruments, 3);
1084 assert_eq!(h.default_tempo, 6);
1085 assert_eq!(h.default_bpm, 125);
1086 assert_eq!(h.frequency_table, XmFrequencyTable::Linear);
1087 assert_eq!(h.order.len(), XM_ORDER_TABLE_SIZE);
1088 assert_eq!(h.order[0], 0);
1089 assert_eq!(h.order[1], 0xFF);
1090 }
1091
1092 #[test]
1093 fn parse_header_rejects_missing_id_byte() {
1094 let mut bytes = build_header(4, 0, 0, false);
1095 bytes[XM_ID_BYTE_OFFSET] = 0;
1096 assert!(parse_header(&bytes).is_err());
1097 }
1098
1099 #[test]
1100 fn parse_header_rejects_zero_channels() {
1101 let mut bytes = build_header(0, 0, 0, false);
1102 bytes[68..70].copy_from_slice(&0u16.to_le_bytes());
1104 assert!(parse_header(&bytes).is_err());
1105 }
1106
1107 #[test]
1108 fn parse_header_needs_full_order_table() {
1109 let bytes = build_header(4, 0, 0, false);
1110 let short = &bytes[..XM_ORDER_TABLE_OFFSET];
1111 matches!(parse_header(short), Err(Error::NeedMore));
1112 }
1113
1114 #[test]
1115 fn parse_patterns_all_empty_synthesizes_defaults() {
1116 let mut bytes = build_header(4, 1, 0, false);
1117 bytes.extend(build_pattern_block(8, &[]));
1119 let h = parse_header(&bytes).unwrap();
1120 let (pats, end) = parse_patterns(&h, &bytes).unwrap();
1121 assert_eq!(pats.len(), 1);
1122 assert_eq!(pats[0].num_rows, 8);
1123 assert_eq!(pats[0].packed_size, 0);
1124 assert_eq!(pats[0].rows.len(), 8);
1125 assert_eq!(pats[0].rows[0].len(), 4);
1126 for row in &pats[0].rows {
1127 for cell in row {
1128 assert_eq!(*cell, XmCell::default());
1129 }
1130 }
1131 assert_eq!(end, pattern_data_offset(&h) + 9);
1133 }
1134
1135 #[test]
1136 fn parse_patterns_hostile_header_length_does_not_panic() {
1137 let mut bytes = build_header(1, 1, 0, false);
1149 let mut block = Vec::new();
1152 block.extend_from_slice(&0xFFFFu32.to_le_bytes()); block.push(0); block.extend_from_slice(&1u16.to_le_bytes()); block.extend_from_slice(&1u16.to_le_bytes()); bytes.extend(block);
1160
1161 let h = parse_header(&bytes).unwrap();
1162 let (pats, _end) = parse_patterns(&h, &bytes)
1163 .expect("hostile header_length must clamp rather than panic on the slice index");
1164 assert_eq!(pats.len(), 1);
1165 assert_eq!(pats[0].rows.len(), 1);
1166 assert_eq!(pats[0].rows[0].len(), 1);
1167 assert_eq!(pats[0].rows[0][0], XmCell::default());
1168 }
1169
1170 #[test]
1171 fn parse_patterns_unpacked_cell_form() {
1172 let mut bytes = build_header(2, 1, 0, false);
1173 let mut packed = Vec::new();
1177 packed.extend_from_slice(&[48, 1, 0x40, 0x0C, 0x20]);
1178 packed.push(0x80); bytes.extend(build_pattern_block(1, &packed));
1180
1181 let h = parse_header(&bytes).unwrap();
1182 let (pats, _end) = parse_patterns(&h, &bytes).unwrap();
1183 let c0 = pats[0].rows[0][0];
1184 assert_eq!(c0.note, 48);
1185 assert_eq!(c0.instrument, 1);
1186 assert_eq!(c0.volume, 0x40);
1187 assert_eq!(c0.effect_type, 0x0C);
1188 assert_eq!(c0.effect_param, 0x20);
1189 assert!(c0.has_note());
1190 assert!(!c0.is_note_off());
1191 let c1 = pats[0].rows[0][1];
1192 assert_eq!(c1, XmCell::default());
1193 }
1194
1195 #[test]
1196 fn parse_patterns_packed_cell_selective_mask() {
1197 let mut bytes = build_header(1, 1, 0, false);
1198 let mut packed = Vec::new();
1201 packed.extend_from_slice(&[0x80 | 0x05, 50, 0x12]); bytes.extend(build_pattern_block(1, &packed));
1203
1204 let h = parse_header(&bytes).unwrap();
1205 let (pats, _end) = parse_patterns(&h, &bytes).unwrap();
1206 let c = pats[0].rows[0][0];
1207 assert_eq!(c.note, 50);
1208 assert_eq!(c.instrument, 0);
1209 assert_eq!(c.volume, 0x12);
1210 assert_eq!(c.effect_type, 0);
1211 assert_eq!(c.effect_param, 0);
1212 }
1213
1214 #[test]
1215 fn xm_volume_kinds_classify_correctly() {
1216 let mk = |v: u8| XmCell {
1217 volume: v,
1218 ..XmCell::default()
1219 };
1220 assert_eq!(mk(0).volume_kind(), XmVolume::Empty);
1221 assert_eq!(mk(0x10).volume_kind(), XmVolume::SetVolume(0));
1222 assert_eq!(mk(0x50).volume_kind(), XmVolume::SetVolume(0x40));
1223 assert_eq!(mk(0x63).volume_kind(), XmVolume::VolumeSlideDown(3));
1224 assert_eq!(mk(0x77).volume_kind(), XmVolume::VolumeSlideUp(7));
1225 assert_eq!(mk(0x8F).volume_kind(), XmVolume::FineVolumeSlideDown(0x0F));
1226 assert_eq!(mk(0x9A).volume_kind(), XmVolume::FineVolumeSlideUp(0x0A));
1227 assert_eq!(mk(0xA4).volume_kind(), XmVolume::SetVibratoSpeed(4));
1228 assert_eq!(mk(0xB5).volume_kind(), XmVolume::Vibrato(5));
1229 assert_eq!(mk(0xC0).volume_kind(), XmVolume::SetPanning(0));
1230 assert_eq!(mk(0xD2).volume_kind(), XmVolume::PanningSlideLeft(2));
1231 assert_eq!(mk(0xE8).volume_kind(), XmVolume::PanningSlideRight(8));
1232 assert_eq!(mk(0xF9).volume_kind(), XmVolume::TonePorta(9));
1233 }
1234
1235 #[test]
1236 fn parse_instruments_zero_samples() {
1237 let mut bytes = build_header(4, 0, 2, false);
1238 bytes.extend(build_empty_instrument(b"empty1"));
1239 bytes.extend(build_empty_instrument(b"another"));
1240 let h = parse_header(&bytes).unwrap();
1241 let offset = pattern_data_offset(&h);
1242 let insts = parse_instruments(&h, &bytes, offset).unwrap();
1243 assert_eq!(insts.len(), 2);
1244 assert_eq!(insts[0].name, "empty1");
1245 assert_eq!(insts[0].num_samples, 0);
1246 assert!(insts[0].samples.is_empty());
1247 assert_eq!(insts[1].name, "another");
1248 }
1249
1250 #[test]
1251 fn parse_instrument_with_one_sample_decodes_envelope_and_sample_header() {
1252 let mut bytes = build_header(4, 0, 1, false);
1253 let body = [1i8, 2, 3, 4];
1255 let body_bytes: Vec<u8> = body.iter().map(|&b| b as u8).collect();
1256 bytes.extend(build_one_sample_instrument(b"kick", &body_bytes));
1257
1258 let h = parse_header(&bytes).unwrap();
1259 let offset = pattern_data_offset(&h);
1260 let mut insts = parse_instruments(&h, &bytes, offset).unwrap();
1261 assert_eq!(insts.len(), 1);
1262 let inst = &insts[0];
1263 assert_eq!(inst.name, "kick");
1264 assert_eq!(inst.num_samples, 1);
1265 assert_eq!(inst.sample_header_size, XM_SAMPLE_HEADER_SIZE);
1266 assert_eq!(inst.sample_map.len(), 96);
1267 assert_eq!(inst.volume_envelope.points.len(), 2);
1268 assert_eq!(inst.volume_envelope.points[1], (64, 64));
1269 assert!(inst.volume_envelope.is_on());
1270 assert!(!inst.volume_envelope.has_sustain());
1271 assert_eq!(inst.samples.len(), 1);
1272 let s = &inst.samples[0];
1273 assert_eq!(s.length, body.len() as u32);
1274 assert_eq!(s.volume, 0x40);
1275 assert_eq!(s.panning, 128);
1276 assert!(!s.is_16_bit);
1277 assert_eq!(s.loop_mode, XmSampleLoopMode::None);
1278 assert_eq!(s.name, "snd");
1279
1280 extract_sample_bodies(&mut insts, &bytes);
1282 let decoded = &insts[0].samples[0].pcm8;
1283 assert_eq!(decoded, &[1, 3, 6, 10]);
1284 }
1285
1286 #[test]
1287 fn extract_sample_bodies_handles_truncated_body() {
1288 let mut bytes = build_header(4, 0, 1, false);
1289 let body_bytes = vec![1u8, 2, 3, 4, 5];
1290 bytes.extend(build_one_sample_instrument(b"s", &body_bytes));
1291 let drop = 2;
1293 bytes.truncate(bytes.len() - drop);
1294
1295 let h = parse_header(&bytes).unwrap();
1296 let mut insts = parse_instruments(&h, &bytes, pattern_data_offset(&h)).unwrap();
1297 extract_sample_bodies(&mut insts, &bytes);
1298 assert_eq!(insts[0].samples[0].pcm8.len(), body_bytes.len() - drop);
1299 }
1300
1301 #[test]
1302 fn decode_packed_cell_empty_mask_byte() {
1303 let (cell, used) = decode_packed_cell(&[0x80], 0);
1305 assert_eq!(cell, XmCell::default());
1306 assert_eq!(used, 1);
1307 }
1308
1309 #[test]
1310 fn pattern_data_offset_is_standard_for_default_header_size() {
1311 let bytes = build_header(4, 0, 0, false);
1312 let h = parse_header(&bytes).unwrap();
1313 assert_eq!(pattern_data_offset(&h), 0x150);
1318 assert_eq!(0x150, 336);
1319 }
1320
1321 #[test]
1322 fn estimate_duration_micros_is_positive_for_nonempty_song() {
1323 let mut bytes = build_header(4, 1, 0, false);
1324 bytes.extend(build_pattern_block(64, &[]));
1325 let h = parse_header(&bytes).unwrap();
1326 let (pats, _) = parse_patterns(&h, &bytes).unwrap();
1327 let us = estimate_duration_micros(&h, &pats);
1328 assert!(us > 0, "estimate_duration_micros returned {us}");
1329 }
1330}