xmrs 0.10.2

A library to edit SoundTracker data with pleasure
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
use super::serde_helper::{deserialize_string_12, deserialize_string_28, deserialize_string_4};
use bincode;
use bincode::error::DecodeError;
use serde::Deserialize;

use crate::import::import_memory::ImportMemory;
use crate::import::import_memory::MemoryType;
use crate::import::orders_helper;
use crate::import::patternslot::PatternSlot;
use crate::prelude::*;

use alloc::string::String;
use alloc::string::ToString;
use alloc::{vec, vec::Vec};

use super::s3m_effect::S3mEffect;
use crate::import::import_memory::S3mImportFlags;

#[repr(C)]
#[derive(Default, Deserialize, Debug)]
struct S3mHeader {
    #[serde(deserialize_with = "deserialize_string_28")]
    title: String,
    /// 0x1A
    sig1: u8,
    song_type: u8,
    reserved1: u16,
    order_count: u16,
    instrument_count: u16,
    pattern_count: u16,
    flags: u16,
    version: u16,
    sample_type: u16,
    /// SCRM
    #[serde(deserialize_with = "deserialize_string_4")]
    sig2: String,
    global_volume: u8,
    speed: u8,
    tempo: u8,
    master_volume: u8, // bit7: stereo, bits 6-0: volume
    ultra_click_removal: u8,
    pan: u8,
    reserved2: [u8; 8],
    ptr_special: u16, // parapointer to additional data ? see flags, bit7
    channel_settings: [u8; 32],
}

#[repr(C)]
#[derive(Deserialize, Debug, Default)]
struct S3mPcmInstr {
    /// offset = ((ptr_data_h << 16) | ptr_data_l) * 16
    ptr_data_h: u8,
    ptr_data_l: u16,

    len: u32,
    loop_start: u32,
    loop_end: u32,

    /// 0-63
    volume: u8,
    dsk: u8,
    /// 0=unpacked, 1=DP30ADPCM
    pack: u8,
    // 1=loop on, 2=stereo (data left then data right), 4=s16le
    flags: u8,
    /// sample rate for middle-c note (C-4)
    c2spd: u32,
    internal: [u8; 12],
    #[serde(deserialize_with = "deserialize_string_28")]
    title: String,
    /// SCRS
    #[serde(deserialize_with = "deserialize_string_4")]
    sig: String,
}

impl S3mPcmInstr {
    fn is_loop(&self) -> bool {
        self.flags & 1 != 0
    }

    fn is_stereo(&self) -> bool {
        self.flags & 2 != 0
    }

    fn is_16bits(&self) -> bool {
        self.flags & 4 != 0
    }

    fn get_sample_offset(&self) -> usize {
        (((self.ptr_data_h as usize) << 16) | (self.ptr_data_l as usize)) << 4
    }

    fn get_sample_data(&self, data: &[u8]) -> Result<SampleDataType, DecodeError> {
        let offset = self.get_sample_offset();

        if offset > data.len() {
            return Err(DecodeError::Other("S3M sample offset out of bounds"));
        }
        let available = data.len() - offset;

        // `self.len` is a count of sample *values* on disk (not frames, not
        // raw bytes). For 16-bit samples each value takes 2 bytes, hence
        // `bytes_per_value`. Stereo is handled downstream: `convert_*_sample`
        // splits the buffer in half (L then R), so stereo gets half as many
        // final frames as `self.len`, which matches the original behaviour.
        let bytes_per_value: usize = if self.is_16bits() { 2 } else { 1 };
        let wanted_bytes = (self.len as usize).saturating_mul(bytes_per_value);

        // Truncate to what is actually available — fixes "miracle man.s3m"
        // and other broken S3Ms where the sample header over-claims. We
        // must keep whole values (2 bytes for 16-bit), otherwise the
        // u8→u16 conversion would fail on an odd tail.
        let usable_bytes = wanted_bytes.min(available);
        let usable_bytes = usable_bytes - (usable_bytes % bytes_per_value);
        let len = usable_bytes / bytes_per_value; // count in sample *values*

        let dst = if self.is_16bits() {
            let src = Self::convert_u8_to_u16_vec(&data[offset..offset + len * 2])?;
            if self.is_stereo() {
                SampleDataType::Stereo16(self.convert_16bit_sample(src.as_slice()))
            } else {
                SampleDataType::Mono16(self.convert_16bit_sample(src.as_slice()))
            }
        } else {
            let src = &data[offset..offset + len];
            if self.is_stereo() {
                SampleDataType::Stereo8(self.convert_8bit_sample(src))
            } else {
                SampleDataType::Mono8(self.convert_8bit_sample(src))
            }
        };

        Ok(dst)
    }

    fn convert_u8_to_u16_vec(input: &[u8]) -> Result<Vec<u16>, DecodeError> {
        if !input.len().is_multiple_of(2) {
            return Err(DecodeError::Other("input is odd!"));
        }

        let mut output = Vec::with_capacity(input.len() / 2);

        for chunk in input.chunks_exact(2) {
            let value = u16::from_le_bytes([chunk[0], chunk[1]]);
            output.push(value);
        }
        Ok(output)
    }

    fn convert_8bit_sample(&self, p: &[u8]) -> Vec<i8> {
        let length = p.len();
        let mut dst: Vec<i8> = vec![];

        if self.is_stereo() {
            let half_length = length / 2;
            let (left_channel, right_channel) = p.split_at(half_length);

            for i in 0..half_length {
                let l = left_channel[i] ^ 0x80;
                let r = right_channel[i] ^ 0x80;
                dst.push(l as i8);
                dst.push(r as i8);
            }
        } else {
            for &v in p.iter().take(length) {
                dst.push((v ^ 0x80) as i8)
            }
        }
        dst
    }

    fn convert_16bit_sample(&self, p: &[u16]) -> Vec<i16> {
        let length = p.len();
        let mut dst: Vec<i16> = vec![];

        if self.is_stereo() {
            let half_length = length / 2;
            let (left_channel, right_channel) = p.split_at(half_length);

            for i in 0..half_length {
                let l = left_channel[i] ^ 0x8000;
                let r = right_channel[i] ^ 0x8000;
                dst.push(l as i16);
                dst.push(r as i16);
            }
        } else {
            for &v in p.iter().take(length) {
                dst.push((v ^ 0x8000) as i16)
            }
        }
        dst
    }
}

#[repr(C)]
#[derive(Deserialize, Debug)]
struct S3mOplInstr {
    reserved1: [u8; 3],

    mod0: u8,
    car1: u8,
    mod2: u8,
    car3: u8,
    mod4: u8,
    car5: u8,
    mod6: u8,
    car7: u8,
    mod8: u8,
    car9: u8,
    mod10: u8,
    unused11: u8,

    volume: u8,
    dsk: u8,
    reserved2: u16,
    c2spd: u32,
    internal: [u8; 12],
    #[serde(deserialize_with = "deserialize_string_28")]
    pub title: String,
    /// SCRI
    #[serde(deserialize_with = "deserialize_string_4")]
    sig: String,
}

impl S3mOplInstr {
    pub fn to_instr_opl(&self) -> InstrOpl {
        // Match the PCM path — S3M is Amiga-period natively.
        let ph = PeriodHelper::new(FrequencyType::AmigaFrequencies, false);
        let mut i_opl = InstrOpl::default();
        i_opl.element.modulator = MdiOpl {
            ksl: ((self.mod2 & 0b0100_0000) >> 6) | (self.mod2 >> 7),
            multiple: self.mod0 & 0b0000_1111,
            feedback: self.mod10 >> 1,
            attack: self.mod4 >> 4,
            sustain: self.mod6 >> 4,
            eg: (self.mod0 & 0b0010_0000) != 0, // OPL reg 0x20 bit 5: EG type (sustain)
            decay: self.mod4 & 0b0000_1111,
            release: self.mod6 & 0b0000_1111,
            total_level: self.mod2 & 0b0011_1111,
            am: (self.mod0 & 0b1000_0000) != 0, // OPL reg 0x20 bit 7: AM (tremolo)
            vib: (self.mod0 & 0b0100_0000) != 0,
            ksr: (self.mod0 & 0b0001_0000) != 0,
            con: (self.mod10 & 0b0000_0001) != 0,
        };
        i_opl.element.carrier = MdiOpl {
            ksl: ((self.car3 & 0b0100_0000) >> 6) | (self.car3 >> 7),
            multiple: self.car1 & 0b0000_1111,
            feedback: self.mod10 >> 1, // no way for carrier
            attack: self.car5 >> 4,
            sustain: self.car7 >> 4,
            eg: (self.car1 & 0b0010_0000) != 0, // OPL reg 0x20 bit 5: EG type (sustain)
            decay: self.car5 & 0b0000_1111,
            release: self.car7 & 0b0000_1111,
            total_level: self.car3 & 0b0011_1111,
            am: (self.car1 & 0b1000_0000) != 0, // OPL reg 0x20 bit 7: AM (tremolo)
            vib: (self.car1 & 0b0100_0000) != 0,
            ksr: (self.car1 & 0b0001_0000) != 0,
            con: (self.mod10 & 0b0000_0001) != 0, // no way for carrier
        };
        i_opl.element.modulator_wave_select = self.mod8;
        i_opl.element.carrier_wave_select = self.car9;
        i_opl.volume = self.volume;
        let rn = ph.c4freq_to_relative_pitch(self.c2spd as f32);
        i_opl.finetune = rn.1;
        i_opl.relative_pitch = rn.0;

        i_opl
    }
}

#[derive(Deserialize, Debug)]
enum S3mInstrument {
    PcmInstrument(S3mPcmInstr),
    OplInstrument(S3mOplInstr),
}

#[repr(C)]
#[derive(Deserialize, Debug)]
struct S3mMetaInstrument {
    discriminator: u8,
    #[serde(deserialize_with = "deserialize_string_12")]
    filename: String,
    value: S3mInstrument,
    sample: Option<SampleDataType>,
}

impl S3mMetaInstrument {
    /// Placeholder for an empty instrument slot (parapointer == 0).
    /// Needed to preserve 1:1 indexing between file slots and `self.instruments`,
    /// since S3M pattern slots reference instruments by absolute index.
    fn empty() -> Self {
        Self {
            discriminator: 0,
            filename: String::new(),
            value: S3mInstrument::PcmInstrument(S3mPcmInstr::default()),
            sample: None,
        }
    }

    fn new(data: &[u8], offset: usize) -> Result<Self, DecodeError> {
        let discriminator = data[offset];
        let filename = String::from_utf8_lossy(&data[offset + 1..offset + 13])
            .trim_end_matches('\0')
            .to_string();
        let mut sample = None;
        let value = match discriminator {
            0 => {
                // Empty Instrument, we use PcmInstrument not to forget informations
                let i = bincode::serde::decode_from_slice::<S3mPcmInstr, _>(
                    &data[offset + 13..],
                    bincode::config::legacy(),
                )?
                .0;
                S3mInstrument::PcmInstrument(i)
            }
            1 => {
                let i = bincode::serde::decode_from_slice::<S3mPcmInstr, _>(
                    &data[offset + 13..],
                    bincode::config::legacy(),
                )?
                .0;
                if i.sig != "SCRS" {
                    return Err(DecodeError::Other("Not a SCRS instr?"));
                }
                sample = Some(i.get_sample_data(data)?);
                S3mInstrument::PcmInstrument(i)
            }
            2..=7 => {
                let i = bincode::serde::decode_from_slice::<S3mOplInstr, _>(
                    &data[offset + 13..],
                    bincode::config::legacy(),
                )?
                .0;
                if i.sig != "SCRI" {
                    return Err(DecodeError::Other("Not a SCRI instr?"));
                }
                S3mInstrument::OplInstrument(i)
            }
            _ => S3mInstrument::PcmInstrument(S3mPcmInstr::default()),
        };
        Ok(Self {
            discriminator,
            filename,
            value,
            sample,
        })
    }
}

#[derive(Default, Deserialize, Debug)]
pub struct S3mModule {
    header: S3mHeader,
    positions: Vec<u8>,
    instruments: Vec<S3mMetaInstrument>,
    patterns: Vec<Vec<Vec<PatternSlot>>>,
}

impl S3mModule {
    pub fn load(ser_s3m_module: &[u8]) -> Result<S3mModule, DecodeError> {
        let mut s3m = S3mModule {
            ..Default::default()
        };

        // === load header

        let s = 96;
        if ser_s3m_module.len() < s {
            return Result::Err(DecodeError::Other("Not an S3M module?"));
        }
        let data = &ser_s3m_module[0..s];
        s3m.header =
            bincode::serde::decode_from_slice::<S3mHeader, _>(data, bincode::config::legacy())?.0;
        let data = &ser_s3m_module[s..];

        if s3m.header.sig1 != 0x1A || s3m.header.song_type != 0x10 || s3m.header.sig2 != "SCRM" {
            return Result::Err(DecodeError::Other("Not an S3M module?"));
        }

        // === positions offsets

        let s = s3m.header.order_count as usize;
        if data.len() < s {
            return Result::Err(DecodeError::Other("Not an S3M module?"));
        }
        s3m.positions = data[0..s].to_vec();
        let data = &data[s..];

        // remove pattern separators (254)
        s3m.positions.retain(|&x| x != 254);
        // cut on first eop (255)
        s3m.positions = s3m
            .positions
            .iter()
            .take_while(|&x| *x != 255)
            .cloned()
            .collect();

        // === sample offsets

        let s = 2 * s3m.header.instrument_count as usize;
        if data.len() < s {
            return Result::Err(DecodeError::Other("Not an S3M module?"));
        }
        let sample_offsets: Vec<u32> = data[0..s]
            .chunks(2)
            .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], 0, 0]) << 4)
            .collect();
        let data = &data[s..];

        // === pattern offsets

        let s = 2 * s3m.header.pattern_count as usize;
        if data.len() < s {
            return Result::Err(DecodeError::Other("Not an S3M module?"));
        }
        let pattern_offsets: Vec<u32> = data[0..s]
            .chunks(2)
            .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], 0, 0]) << 4)
            .collect();

        // === Samples
        //
        // S3M pattern slots reference instruments by absolute 1-based index,
        // so empty slots (parapointer == 0) MUST be kept as placeholders
        // to preserve the mapping.

        for offset in sample_offsets {
            if offset == 0 {
                s3m.instruments.push(S3mMetaInstrument::empty());
                continue;
            }
            s3m.instruments
                .push(S3mMetaInstrument::new(ser_s3m_module, offset as usize)?);
        }

        // === Patterns
        //
        // Order list entries reference patterns by absolute index, so empty
        // slots (parapointer == 0) MUST be kept as placeholders (mirroring the
        // IT loader convention) instead of being skipped.

        for offset in pattern_offsets {
            if offset == 0 {
                s3m.patterns.push(vec![vec![]]);
                continue;
            }
            let data = &ser_s3m_module[offset as usize..];
            let len: u16 = data[0] as u16 | (data[1] as u16) << 8;
            let data = &data[2..len as usize];
            let mut d2 = data;
            let mut pattern: Vec<Vec<PatternSlot>> = vec![];
            while !d2.is_empty() {
                let (pss, next) = Self::process_pattern_row(d2)?;
                pattern.push(pss);
                d2 = next;
            }
            S3mEffect::resolve_pattern_memory(&mut pattern);
            s3m.patterns.push(pattern);
        }

        Ok(s3m)
    }

    // load one pattern row
    fn process_pattern_row(data: &[u8]) -> Result<(Vec<PatternSlot>, &[u8]), DecodeError> {
        let mut d2 = data;
        // IMPORTANT: 255 is the sentinel for "no volume command". Using 0
        // here would be interpreted by `s3m_walk_volume` as an explicit
        // Volume(0.0), which silences every channel on every row that has
        // no volume column — i.e. the vast majority of S3M rows.
        let mut pss: Vec<PatternSlot> = vec![
            PatternSlot {
                volume: 255,
                ..PatternSlot::default()
            };
            32
        ];
        while !d2.is_empty() {
            match Self::decode_pattern_slot(d2) {
                Some((channel, ps, next)) => {
                    pss[channel] = ps;
                    d2 = next;
                }
                None => {
                    d2 = &d2[1..];
                    break;
                }
            }
        }
        Ok((pss, d2))
    }

    // return channel, PatternSlot, next data
    fn decode_pattern_slot(packed_data: &[u8]) -> Option<(usize, PatternSlot, &[u8])> {
        let mut k = 0;

        let what = packed_data[k];
        k += 1;

        if what == 0 {
            return None; // EOL
        }

        let channel = (what & 0x1F) as usize;

        // 255 is the "no volume command" sentinel — same reason as in
        // `process_pattern_row`. Must be set BEFORE the volume-column
        // branch so that when the slot has no volume column (bit 0x40 of
        // `what` clear), the slot doesn't accidentally carry volume 0.
        let mut slot: PatternSlot = PatternSlot {
            volume: 255,
            ..PatternSlot::default()
        };

        // Note+Instr?
        if what & 0x20 != 0 {
            if k < packed_data.len() {
                let note = packed_data[k];
                slot.note = match note {
                    254 => CellNote::KeyOff,
                    255 => CellNote::Empty,
                    _ => {
                        let tmp_pitch = (note & 0xF) + (note >> 4) * 12;
                        if tmp_pitch > 96 {
                            CellNote::Empty
                        } else {
                            match Pitch::try_from(tmp_pitch) {
                                Ok(p) => CellNote::Play(p),
                                Err(_) => CellNote::Empty,
                            }
                        }
                    }
                };
                k += 1;
            }

            if k < packed_data.len() {
                slot.instrument = if packed_data[k] != 0 {
                    Some(packed_data[k] as usize - 1)
                } else {
                    None
                };
                k += 1;
            }
        }

        // Volume?
        //
        // Valid ranges:
        //   0..=64       → set volume
        //   0x80..=0xC0  → MPT-style "set panning" (left=0x80, right=0xC0)
        //   anything else → invalid → map to 255 (the "no command" sentinel)
        if what & 0x40 != 0 && k < packed_data.len() {
            let v = packed_data[k];
            slot.volume = if v <= 64 || (0x80..=0xC0).contains(&v) {
                v
            } else {
                255
            };
            k += 1;
        }

        // effect and data?
        if what & 0x80 != 0 {
            if k < packed_data.len() {
                slot.effect_type = packed_data[k];
                k += 1;
            }

            if k < packed_data.len() {
                slot.effect_parameter = packed_data[k];
                k += 1;
            }
        }

        Some((channel, slot, &packed_data[k..]))
    }

    /// Inject S3M header defaults (global volume) as effects on
    /// row 0 of the first pattern that playback will start with.
    /// This avoids renumbering patterns/positions — which would
    /// break any `goto(position, …)` call made by the player.
    ///
    /// Global volume is written as `Vxx` (effect 22) on any channel
    /// that hasn't already been used on row 0. If every slot on
    /// that row already carries an effect, the default is silently
    /// dropped — better to lose a default than overwrite explicit
    /// user data. Mirrors real tracker behaviour: state is
    /// established at song-start; a `goto` mid-song forfeits that
    /// state, which is correct.
    ///
    /// Per-channel panning used to ride the same path
    /// (`Xxx` on row 0), but it now lives on
    /// [`Module::channel_defaults`] — a clean header-side
    /// data path that doesn't depend on row 0 having a free slot.
    fn inject_header_defaults(&self, patterns: &mut [Vec<Vec<PatternSlot>>]) {
        let first_pat = match self.positions.first() {
            Some(&p) => p as usize,
            None => return,
        };
        let row = match patterns.get_mut(first_pat).and_then(|p| p.first_mut()) {
            Some(r) if !r.is_empty() => r,
            _ => return,
        };

        // Global volume (Vxx = effect 22). Any channel can carry a global
        // effect; pick the first free slot.
        let gv = self.header.global_volume.min(64);
        if gv != 64 {
            if let Some(slot) = row
                .iter_mut()
                .find(|s| s.effect_type == 0 && s.effect_parameter == 0)
            {
                slot.effect_type = 22;
                slot.effect_parameter = gv;
            }
        }
    }

    /// Build the per-channel default state from S3M's
    /// `channel_settings` block. Returns a vector aligned on the
    /// channel slots declared by the file. Each entry's pan and
    /// mute flag come straight from the channel byte.
    ///
    /// Mapping mirrors schism's `fmt/s3m.c:208-213`:
    /// - bit 0x80 → channel disabled (muted)
    /// - bit 0x10 → Adlib (no PCM pan; left at default)
    /// - bit 0x08 set → R-side default (~0.78)
    /// - bit 0x08 clear → L-side default (~0.22)
    fn header_channel_defaults(&self) -> Vec<ChannelDefault> {
        self.header
            .channel_settings
            .iter()
            .map(|&s| {
                if s == 0xFF {
                    // Unused channel slot — leave fully default.
                    ChannelDefault::default()
                } else if s & 0x80 != 0 {
                    // Disabled channel.
                    ChannelDefault {
                        muted: true,
                        ..ChannelDefault::default()
                    }
                } else if s & 0x10 != 0 {
                    // Adlib — no PCM pan.
                    ChannelDefault::default()
                } else if s & 0x08 != 0 {
                    ChannelDefault {
                        panning: Some(0xC0 as f32 / 256.0), // soft right
                        ..ChannelDefault::default()
                    }
                } else {
                    ChannelDefault {
                        panning: Some(0x40 as f32 / 256.0), // soft left
                        ..ChannelDefault::default()
                    }
                }
            })
            .collect()
    }

    pub fn to_module(&self) -> Module {
        // S3M is natively an Amiga-period format — Scream Tracker 3
        // inherited the Amiga clock-divider period math, and every
        // portamento/slide parameter in the wild was authored against
        // that model (including its natural period-saturation at high
        // pitches). Playing S3M data through Linear period math makes
        // extreme Fxy/Exy values (F80 in PANIC.S3M, etc.) sweep far
        // beyond where ST3 would saturate, producing aliased "shriek"
        // frequencies that don't exist in the original playback.
        //
        // Unlike IT, S3M has no file flag to select Linear mode — the
        // format predates that option. All S3M files should be treated
        // as Amiga.
        let ph = PeriodHelper::new(FrequencyType::AmigaFrequencies, false);
        let mut module = Module::default();

        // ST3 masterflags (see dig.c:setmasterflags). Bits we care
        // about:
        //   * bit 0x01 — `oldstvib`: ST2-compatibility vibrato
        //       (depth doubled). Absorbed at import.
        //   * bit 0x10 — `amigalimits`: period clamped to Amiga
        //       hardware range at runtime. Flipped into
        //       `module.quirks.period_clamp`.
        //   * bit 0x40 — `fastvolslide`: Dxy / Nxy apply on every
        //       tick. Absorbed at import via double-emission.
        // Other flags (bit 0x08 "vol0 opt", bit 0x20 "stereo") are
        // either performance hints or have no replay impact.
        let s3m_mflags = self.header.flags;
        let import_flags = S3mImportFlags {
            fastvolslide: s3m_mflags & 0x40 != 0,
            oldstvib: s3m_mflags & 0x01 != 0,
        };
        let amigalimits = s3m_mflags & 0x10 != 0;

        module.name = self.header.title.clone();
        module.comment = "XmRs reader".to_string();
        // S3M's master-volume byte is encoded with bit 7 = stereo
        // flag and bits 6..0 = mixing volume in 0..127 range. Schism
        // (`fmt/s3m.c:166`) reads the same byte and stores it as
        // `mixing_volume`. We mirror that: strip bit 7 and normalise
        // by 128 to land in the same [0..1] domain the IT importer
        // uses, so the player's mix chain treats S3M and IT
        // uniformly. (We deliberately skip schism's optional ST3-era
        // rescaling at `fmt/s3m.c:233` — it only applies to a
        // legacy variant and would silently change levels for
        // mainstream S3Ms.)
        module.mix_volume = ((self.header.master_volume & 0x7F) as f32) / 128.0;
        // Canonical ST3 replay profile. The amigalimits masterflag
        // is per-file, so we overlay it on top of the base profile.
        module.profile = {
            let mut p = CompatibilityProfile::st3();
            if amigalimits {
                // ST3 `aspd` range `[453, 3424]` → xmrs Amiga units
                // `[113.25, 856]` (xmrs periods are 1/4 of ST3's).
                p.quirks.period_clamp = Some((113.25, 856.0));
            }
            p
        };
        module.frequency_type = FrequencyType::AmigaFrequencies;
        module.default_tempo = self.header.speed as usize;
        module.default_bpm = self.header.tempo as usize;

        // Inject header defaults (channel panning, global volume) into
        // row 0 of the first-played pattern. This preserves the original
        // position/pattern indexing — any `goto(position, …)` call keeps
        // referencing the same patterns it did before these defaults
        // were materialised.
        let mut patterns_for_unpack = self.patterns.clone();
        self.inject_header_defaults(&mut patterns_for_unpack);

        module.channel_defaults = self.header_channel_defaults();

        module.pattern_order = orders_helper::parse_orders(&self.positions);

        let mut im = ImportMemory::default();
        im.s3m_flags = import_flags;
        module.pattern = im.unpack_patterns(
            module.frequency_type,
            MemoryType::S3m,
            &module.pattern_order,
            &patterns_for_unpack,
        );

        for s3m_meta_instr in &self.instruments {
            match &s3m_meta_instr.value {
                S3mInstrument::PcmInstrument(pcm) => {
                    // Prepare sample
                    let data = s3m_meta_instr.sample.clone();
                    // Guard against degenerate c2spd values (0, or absurdly
                    // small) producing NaN/inf in `c4freq_to_relative_pitch`.
                    // 8363 Hz is the historical Amiga/ST3 default for C-4.
                    let c2spd = if pcm.c2spd == 0 {
                        PeriodHelper::C4_FREQ as u32
                    } else {
                        pcm.c2spd
                    };
                    let rn = ph.c4freq_to_relative_pitch(c2spd as f32);
                    let sample = Sample {
                        name: s3m_meta_instr.filename.clone(),
                        relative_pitch: rn.0,
                        finetune: rn.1,
                        volume: pcm.volume as f32 / 64.0,
                        // S3M has no per-sample default note volume.
                        default_note_volume: 1.0,
                        panning: 0.5,
                        loop_flag: if pcm.is_loop() {
                            LoopType::Forward
                        } else {
                            LoopType::No
                        },
                        loop_start: pcm.loop_start,
                        loop_length: pcm.loop_end.saturating_sub(pcm.loop_start),
                        sustain_loop_flag: LoopType::No,
                        sustain_loop_start: 0,
                        sustain_loop_length: 0,
                        data,
                    };

                    // Create InstrDefault
                    let mut instr_def = InstrDefault::default();
                    instr_def.sample.push(Some(sample));
                    instr_def.keyboard.sample_for_pitch = [Some(0); 120];

                    // Create Instrument
                    let mut instr = Instrument::default();
                    instr.name = pcm.title.clone();
                    instr.instr_type = InstrumentType::Default(instr_def);

                    // Add Instrument to module
                    module.instrument.push(instr);
                }
                S3mInstrument::OplInstrument(opl) => {
                    // Create Instrument
                    let mut instr = Instrument::default();
                    instr.name = opl.title.clone();
                    instr.instr_type = InstrumentType::Opl(opl.to_instr_opl());

                    // Add Instrument to module
                    module.instrument.push(instr);
                }
            }
        }

        module
    }
}