xmrs 0.11.1

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
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
use super::serde_helper::{deserialize_string_12, deserialize_string_28, deserialize_string_4};
use bincode;
use bincode::error::DecodeError;
use serde::Deserialize;

use crate::fixed::units::{ChannelVolume, Finetune, Frequency, Panning, Period, Volume};
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::format;
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=amiga (module memory marker), 2=DP30ADPCM
    /// — but no major decoder honours this byte. Schism's
    /// `fmt/s3m.c:279` skips it as "packing info (never used)"
    /// and st3play's `digdata.h:91` notes ADPCM is "not used in
    /// st3play". Treated identically here: every sample is read
    /// as raw PCM.
    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> {
        // The `pack` byte is essentially never honoured by real
        // S3M decoders. ScreamTracker 3 itself documented it as
        // `0=normal(disk), 1=amiga(module), 2=ADPCM`, but
        // st3play's `digdata.h:91` explicitly notes that ADPCM
        // is "not used in st3play", and schism's `fmt/s3m.c:279`
        // skips the byte with the comment `"packing info (never
        // used)"`. We mirror that: just decode every sample as
        // raw PCM regardless of the packing nibble. Real-world
        // S3M files with non-zero pack are vanishingly rare, and
        // the cost of being stricter than the reference players
        // is rejecting files they would happily play.

        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 {
                dst.push(u8_pcm_to_i8(left_channel[i]));
                dst.push(u8_pcm_to_i8(right_channel[i]));
            }
        } else {
            for &v in p.iter().take(length) {
                dst.push(u8_pcm_to_i8(v))
            }
        }
        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 {
                dst.push(u16_pcm_to_i16(left_channel[i]));
                dst.push(u16_pcm_to_i16(right_channel[i]));
            }
        } else {
            for &v in p.iter().take(length) {
                dst.push(u16_pcm_to_i16(v))
            }
        }
        dst
    }
}

/// Convert an unsigned-PCM byte to signed-PCM by flipping the
/// midpoint bit. S3M stores 8-bit PCM samples as unsigned
/// (`0..=255`, silence at `128`); the audio path expects signed
/// (`-128..=127`, silence at `0`). XOR with the MSB inverts the
/// high bit, mapping `0 ↔ -128`, `128 ↔ 0`, `255 ↔ +127`.
#[inline]
const fn u8_pcm_to_i8(u: u8) -> i8 {
    (u ^ 0x80) as i8
}

/// 16-bit equivalent of [`u8_pcm_to_i8`].
#[inline]
const fn u16_pcm_to_i16(u: u16) -> i16 {
    (u ^ 0x8000) as i16
}

#[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;
        // C2-speed → (relative_pitch, finetune) using the Q-format
        // pitch tables. `c4freq_to_relative_pitch` returns
        // `(i8 semitones, PitchDelta Q8.8 fractional)`. Map the
        // fractional `PitchDelta` raw into a `Finetune` (Q1.15)
        // by `<< 7` (256→32768 = 2^7 widening). The fractional
        // part of `PitchDelta` here is bounded to ±0.5 semitone
        // by the splitting convention, well inside Q1.15 range.
        let f = Frequency::from_hz(self.c2spd);
        let rn = ph.c4freq_to_relative_pitch(f);
        i_opl.finetune =
            crate::fixed::units::Finetune::from_q15_i32_sat((rn.1.as_q8_8_i32()) << 7);
        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>>>,
    /// Song message (free text), when the file has flag bit 0x80
    /// set and `ptr_special` points at usable text. The S3M format
    /// uses CR (0x0D) as line separator; we normalise to LF.
    /// Empty when no readable message was found.
    message: String,
}

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);
        }

        // Optional song message via the `ptr_special` parapointer.
        // Triggered by flag bit 0x80 in the masterflags. The format
        // is poorly standardised — most files that use it write
        // null-terminated CR-separated ASCII at `ptr_special * 16`
        // — so the extractor is best-effort: trim at the first NUL,
        // require the result to be mostly printable, and bail
        // silently otherwise. Failure here never aborts the load,
        // because most S3M files don't have a usable message at
        // all and we don't want to reject them for that.
        if s3m.header.flags & 0x80 != 0 && s3m.header.ptr_special != 0 {
            s3m.message = Self::extract_song_message(ser_s3m_module, s3m.header.ptr_special)
                .unwrap_or_default();
        }

        Ok(s3m)
    }

    /// Read up to 1024 bytes of null-terminated text at the
    /// parapointer offset. Returns `None` when the data doesn't
    /// look like text (less than ~75% printable bytes), so we
    /// silently fall back to "no message" instead of polluting
    /// `Module.comment` with binary noise from a tracker that
    /// repurposed `ptr_special` for something else.
    fn extract_song_message(data: &[u8], ptr_special: u16) -> Option<String> {
        let offset = (ptr_special as usize).checked_mul(16)?;
        if offset >= data.len() {
            return None;
        }
        let raw = &data[offset..];
        // Cap the scan to avoid running over the file when no NUL
        // appears (some files store fixed-width unterminated text).
        let scan_len = raw.len().min(1024);
        let scanned = &raw[..scan_len];
        let end = scanned.iter().position(|&b| b == 0).unwrap_or(scan_len);
        let text_bytes = &scanned[..end];
        if text_bytes.is_empty() {
            return None;
        }
        // Sanity gate: at least 75% of bytes must be either
        // printable ASCII, common whitespace, or the CR/LF that
        // S3M uses as line separator. Pure binary fails this and
        // is rejected outright.
        let printable = text_bytes
            .iter()
            .filter(|&&b| b == b'\r' || b == b'\n' || b == b'\t' || (0x20..=0x7E).contains(&b))
            .count();
        if printable * 4 < text_bytes.len() * 3 {
            return None;
        }
        // Normalise: CR → LF (S3M's native line separator is CR;
        // editors / terminals expect LF). `from_utf8_lossy`
        // substitutes U+FFFD for any stray non-ASCII byte that
        // slipped past the gate above.
        let normalised: Vec<u8> = text_bytes
            .iter()
            .map(|&b| if b == b'\r' { b'\n' } else { b })
            .collect();
        Some(String::from_utf8_lossy(&normalised).trim().to_string())
    }

    // 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(Panning::from_byte_255(0xC0)), // soft right
                        ..ChannelDefault::default()
                    }
                } else {
                    ChannelDefault {
                        panning: Some(Panning::from_byte_255(0x40)), // 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();
        // ST3's `version` byte pair (high nibble = tracker ID, low
        // 12 bits = version) — propagated to the comment so editors
        // can surface it. Mirrors what the XM importer does with its
        // header `tracker_name` + `version_number`. We render it as
        // a single hex word because the per-tracker layout of the
        // upper nibble is non-uniform (ST3 = 0x1, IT-export = 0x2,
        // Schism = 0x4, OpenMPT = 0x5, …) and a fixed major.minor
        // split would lie for half the corpus.
        //
        // Optional song message (when the file's flag 0x80 +
        // ptr_special pointed at usable text) is appended below
        // the version line — same pattern as the IT importer
        // appending `self.message` to its own comment.
        module.comment = if self.message.is_empty() {
            format!("S3M version 0x{:04X}", self.header.version)
        } else {
            format!(
                "S3M version 0x{:04X}\n\n{}",
                self.header.version, self.message
            )
        };
        // 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 = Volume::from_ratio((self.header.master_volume & 0x7F) as i32, 128);
        // 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).
                //
                // Truncated to integer `Period` after the migration
                // (was `(113.25, 856.0)`). The lost 0.25 sub-period
                // precision is well below an audible cent of pitch
                // and matches the typed `Period` storage.
                p.quirks.period_clamp =
                    Some((Period::from_raw(113), Period::from_raw(856)));
            }
            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`.
                    // 8372 Hz is the modern MIDI-aligned C-4 reference.
                    let c2spd_hz = if pcm.c2spd == 0 {
                        crate::fixed::tables::C4_FREQ_HZ
                    } else {
                        pcm.c2spd
                    };
                    let f = Frequency::from_hz(c2spd_hz);
                    let rn = ph.c4freq_to_relative_pitch(f);
                    // `rn.1` is `PitchDelta` (Q8.8 semitones, in
                    // `[-0.5, +0.5]` after round-to-nearest);
                    // `Sample.finetune` is `Finetune` (Q1.15
                    // where ±1 = ±1 semitone). Conversion is a
                    // simple `<< 7` (256 → 32768), with
                    // saturation guarding the corner cases.
                    let finetune = {
                        // PitchDelta is Q8.8 in `[-0.5, +0.5]`,
                        // Finetune is Q1.15 where `±1 = ±1
                        // semitone`. Width promotion is `<< 7`
                        // (256 → 32768), saturated.
                        let pd_raw = rn.1.as_q8_8_i32();
                        Finetune::from_q15_i32_sat(pd_raw << 7)
                    };
                    let sample = Sample {
                        name: s3m_meta_instr.filename.clone(),
                        relative_pitch: rn.0,
                        finetune,
                        volume: ChannelVolume::from_byte_64(pcm.volume),
                        // S3M has no per-sample default note volume.
                        default_note_volume: Volume::FULL,
                        panning: Panning::CENTER,
                        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
    }
}