xmrs 0.15.0

Read, edit and serialize SoundTracker music with pleasure — MOD/XM/S3M/IT/DW import plus SID & OPL chip synthesis, no_std.
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
//! Read-only view of the importer-side, **pre-DAW** representation.
//!
//! Every format importer already parses its file into a faithful
//! mirror of the on-disk structures (`AmigaModule`, `XmModule`,
//! `S3mModule`, `ItModule`) before `to_module()` folds that into the
//! normalised [`Module`] and `build_timeline_layer` projects it onto
//! the DAW layer. Those per-format structs keep their fields private
//! — their layout tracks the file formats, not a public contract —
//! so until now the only way out of an importer was the normalised
//! `Module`.
//!
//! That is the wrong altitude for a whole class of consumers:
//! corpus analysis, censuses, near-duplicate detection, format
//! forensics. They want what the *file* said, not what the player
//! needs:
//!
//! * [`PatternSlot::effect_type`] / [`PatternSlot::effect_parameter`]
//!   are the **raw bytes**, not a semantic [`TrackEffect`]. A
//!   fingerprint built on them is stable across xmrs releases; one
//!   built on the normalised effect enum moves whenever an effect's
//!   translation is refined.
//! * The order list is a plain `&[u8]`, not dissolved into a
//!   `TimelineMap`.
//! * A pattern is a `[row][channel]` grid, directly hashable, with
//!   no clip/segment reconstruction.
//! * Nothing downstream of `load()` runs — no segment splitting, no
//!   automation extraction, no DAW build.
//!
//! The trade-off is the flip side of the same coin: this layer is
//! **not comparable across formats**. A `effect_type` of `0x04` is
//! Vibrato in MOD and a volume slide in S3M. Cross-format work needs
//! either the normalised [`Module`] or the one thing that *is*
//! format-neutral here — the sample PCM, exposed via [`RawPcm`].
//!
//! Written against the trait rather than one importer's type: this module is
//! compiled as soon as *any* of MOD / XM / S3M / IT is enabled, so naming a
//! concrete one (`ItModule::load(&bytes)`, the obvious way to reach it) would
//! be an example that does not compile in half the builds that have the module.
//!
//! ```
//! use xmrs::tracker::import::raw::RawModule;
//!
//! /// Sum every raw effect byte in a file — a fingerprint that is stable
//! /// across xmrs releases, because it never touches the normalised effects.
//! fn effect_fingerprint<M: RawModule>(m: &M) -> u64 {
//!     let mut sum = 0u64;
//!     for pat in m.raw_patterns() {
//!         for row in pat {
//!             for slot in row {
//!                 sum += slot.effect_type as u64 + slot.effect_parameter as u64;
//!             }
//!         }
//!     }
//!     sum
//! }
//! ```
//!
//! [`Module`]: crate::core::module::Module
//! [`TrackEffect`]: crate::core::effect::TrackEffect
// The `pub mod raw;` declaration in the parent carries its own doc comment, and
// rustdoc resolves this whole merged block in the *parent's* scope — where
// neither `PatternSlot` (its module is `pub(crate)`) nor `RawPcm` is nameable.
// Spelling the targets out absolutely is what keeps the links live.
//! [`PatternSlot::effect_type`]: crate::tracker::import::raw::PatternSlot::effect_type
//! [`PatternSlot::effect_parameter`]: crate::tracker::import::raw::PatternSlot::effect_parameter
//! [`RawPcm`]: crate::tracker::import::raw::RawPcm

use alloc::vec::Vec;
use core::time::Duration;

use crate::core::sample::SampleDataType;

/// The shared, importer-side pattern cell. Re-exported here because
/// the defining module stays `pub(crate)`: this is the one supported
/// path to it.
pub use crate::tracker::import::patternslot::PatternSlot;

/// Borrowed sample PCM, in whatever width and channel count the file
/// actually stored. The owning variant is
/// [`SampleDataType`]; this is its zero-copy counterpart, so walking
/// a corpus never clones a sample bank.
///
/// Unlike everything else in this module, PCM **is** comparable
/// across formats: the same 8-bit loop lifted from a MOD into an XM
/// is the same bytes in both.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RawPcm<'a> {
    /// The slot exists but carries no sample data (an empty
    /// instrument, or a header with no body).
    Empty,
    Mono8(&'a [i8]),
    Mono16(&'a [i16]),
    Stereo8(&'a [i8]),
    Stereo16(&'a [i16]),
    StereoFloat(&'a [f32]),
}

impl<'a> RawPcm<'a> {
    /// Length in **frames** (stereo variants count a left+right pair
    /// once), matching [`SampleDataType::len`].
    pub fn len(&self) -> usize {
        match self {
            RawPcm::Empty => 0,
            RawPcm::Mono8(v) => v.len(),
            RawPcm::Mono16(v) => v.len(),
            RawPcm::Stereo8(v) => v.len() / 2,
            RawPcm::Stereo16(v) => v.len() / 2,
            RawPcm::StereoFloat(v) => v.len() / 2,
        }
    }

    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    /// Size of the payload in bytes, i.e. what a byte-oriented view
    /// of it would span.
    pub fn byte_len(&self) -> usize {
        match self {
            RawPcm::Empty => 0,
            RawPcm::Mono8(v) | RawPcm::Stereo8(v) => v.len(),
            RawPcm::Mono16(v) | RawPcm::Stereo16(v) => v.len() * 2,
            RawPcm::StereoFloat(v) => v.len() * 4,
        }
    }
}

// There is deliberately no `as_bytes()`: reinterpreting `&[i16]` as
// `&[u8]` needs `unsafe`, and this module is under
// `#![forbid(unsafe_code)]`. A consumer that wants to hash the
// payload should match on the variant and feed the typed slice to
// its hasher, or reinterpret it on its own side (`bytemuck::cast_
// slice`) where it — not xmrs — owns that decision. Note that any
// byte-level hash of a 16-bit or float variant is host-endian, and
// so not portable across architectures.

impl<'a> From<&'a SampleDataType> for RawPcm<'a> {
    fn from(d: &'a SampleDataType) -> Self {
        match d {
            SampleDataType::Mono8(v) => RawPcm::Mono8(v),
            SampleDataType::Mono16(v) => RawPcm::Mono16(v),
            SampleDataType::Stereo8(v) => RawPcm::Stereo8(v),
            SampleDataType::Stereo16(v) => RawPcm::Stereo16(v),
            SampleDataType::StereoFloat(v) => RawPcm::StereoFloat(v),
        }
    }
}

/// One sample slot as the file declared it.
///
/// Deliberately minimal. Volume, panning, finetune and relative
/// pitch are *not* here: each format encodes them on its own scale
/// (MOD's finetune nibble is a signed 4-bit index, XM's is `i8/127`,
/// IT's is a C5 frequency), so a single field would have to pick an
/// interpretation — which is exactly what this layer exists to
/// avoid. Read those from the normalised [`Sample`] instead.
///
/// [`Sample`]: crate::core::sample::Sample
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RawSample<'a> {
    /// The name as stored, trimmed of padding. Often not a name at
    /// all: empty sample slots are the traditional place to leave
    /// greetings, credits and dates.
    pub name: &'a str,
    pub pcm: RawPcm<'a>,
    /// Loop start, **in frames**. Formats that store this in bytes
    /// (XM, 16-bit) or in words (MOD) are converted, because that is
    /// a unit, not an interpretation. The file's value is passed
    /// through otherwise — in particular the out-of-range clamping
    /// that `to_sample()` applies is *not* done here, so a nonsense
    /// loop stays visible as nonsense.
    pub loop_start: u32,
    /// Loop length, in frames. Same conversion rules as
    /// [`Self::loop_start`].
    pub loop_length: u32,
    /// Playback rate the file assigns to the sample's reference
    /// note, in Hz — S3M's `C2Spd` and IT's `C5Speed`. `None` on
    /// MOD and XM, which have no such field: they express pitch
    /// relative to an Amiga period table via a finetune nibble
    /// (MOD) or a relative-note plus finetune pair (XM).
    ///
    /// Two samples holding identical PCM at different rates are
    /// *not* the same sound, so this belongs in any identity
    /// comparison that crosses S3M/IT boundaries.
    pub sample_rate: Option<u32>,
}

/// One entry of IT's edit-history block.
///
/// Impulse Tracker 2.07+ and OpenMPT append one record per editing
/// session: when it started, and how long it lasted. Nothing else in
/// a tracker module is dated, which makes this the only intrinsic
/// evidence of *when* a file was worked on — and, between two files
/// sharing a history prefix, of which one came later.
///
/// Coverage is partial by nature: files written by older trackers
/// carry no block at all, and OpenMPT can be configured not to emit
/// one. An empty history means "not recorded", never "never edited".
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RawEditEntry {
    /// `(year, month, day)`, decoded from the FAT date word.
    pub date: (u16, u8, u8),
    /// `(hour, minute, second)`, decoded from the FAT time word.
    /// FAT stores seconds in units of two, so this is even.
    pub time: (u8, u8, u8),
    /// How long the session lasted, converted from MS-DOS ticks.
    pub run_time: Duration,
}

/// What the file says about the tool that wrote it.
///
/// The four formats disagree on how to answer that, so every field
/// is optional and the caller reads whichever its format populates.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RawTracker<'a> {
    /// Free-form tag: XM's `tracker_name` (a 20-byte string such as
    /// `"FastTracker v2.00"`, which trackers routinely lie in), or
    /// MOD's four-character format tag (`M.K.`, `6CHN`, `FLT8`, …).
    /// `None` on IT and S3M, which identify their tool numerically.
    pub tag: Option<&'a str>,
    /// Version word as stored: XM's `version_number`, IT's
    /// `created_with_tracker`, S3M's `version`.
    pub version: Option<u16>,
    /// IT only: `compatible_with_tracker`, the oldest version that
    /// can still load the file. Together with [`Self::version`] it
    /// brackets the authoring tool more tightly than either alone.
    pub compatible_with: Option<u16>,
}

/// Read-only access to an importer's pre-DAW state.
///
/// Implemented by [`AmigaModule`], [`XmModule`], [`S3mModule`] and
/// [`ItModule`]. Not implemented by `SidModule` or `DwModule`: both
/// are synthesiser-native (register streams, not sample banks over a
/// pattern grid), and `DwModule` already exposes its fields
/// directly.
///
/// [`AmigaModule`]: crate::tracker::import::amiga::amiga_module::AmigaModule
/// [`XmModule`]: crate::tracker::import::xm::xmmodule::XmModule
/// [`S3mModule`]: crate::tracker::import::s3m::s3m_module::S3mModule
/// [`ItModule`]: crate::tracker::import::it::it_module::ItModule
pub trait RawModule {
    /// Song title as stored. May be empty.
    fn raw_title(&self) -> &str;

    /// Free-text song message (IT and S3M carry one; MOD and XM do
    /// not and return `""`). Line endings are normalised to `\n`.
    fn raw_message(&self) -> &str {
        ""
    }

    /// The order list, truncated to the song length the file
    /// declares — not the fixed-size on-disk array. May contain the
    /// format's separator and end markers (`0xFE` / `0xFF` in
    /// S3M/IT); they are passed through rather than filtered, since
    /// which values are markers is format knowledge.
    fn raw_orders(&self) -> &[u8];

    fn raw_pattern_count(&self) -> usize;

    /// One pattern as a `[row][channel]` grid. `None` past the end.
    ///
    /// Row counts vary per pattern (XM and IT both allow it) and
    /// empty patterns are legal, so neither dimension can be assumed
    /// constant across the return values.
    fn raw_pattern(&self, idx: usize) -> Option<&[Vec<PatternSlot>]>;

    fn raw_sample_count(&self) -> usize;

    /// One sample slot. `None` past the end; a slot that exists but
    /// holds no PCM yields [`RawPcm::Empty`], which is a different
    /// thing and worth keeping distinct.
    fn raw_sample(&self, idx: usize) -> Option<RawSample<'_>>;

    /// Number of *instrument* slots, which is not the sample count
    /// on formats where the two are distinct: IT and XM map notes to
    /// samples through an instrument layer, so one instrument can
    /// reference many samples. MOD and S3M have no such layer and
    /// default to the sample count.
    fn raw_instrument_count(&self) -> usize {
        self.raw_sample_count()
    }

    /// The instrument slot's own name, distinct from the names of
    /// the samples it references. Defaults to the sample name on
    /// formats without an instrument layer.
    fn raw_instrument_name(&self, idx: usize) -> Option<&str> {
        self.raw_sample(idx).map(|s| s.name)
    }

    /// Ticks per row at song start (the "speed" register, `Axx` in
    /// MOD/XM, `Axx` in S3M/IT). `None` on MOD, whose format has no
    /// field for it — ProTracker starts at 6 by convention and any
    /// other value arrives through an `Fxx` effect.
    fn raw_initial_speed(&self) -> Option<u8> {
        None
    }

    /// Beats per minute at song start. `None` on MOD for the same
    /// reason as [`Self::raw_initial_speed`]; the convention is 125.
    ///
    /// This is the *initial* value only. Any `Fxx`/`Txx` in the
    /// patterns moves it, so a module's tempo is a trajectory, not a
    /// number — treat this as a starting point, not a description.
    fn raw_initial_bpm(&self) -> Option<u8> {
        None
    }

    /// Number of channels. Defaults to the width of the first
    /// non-empty pattern, which is what the patterns actually use;
    /// formats that state it in their header override this.
    fn raw_channel_count(&self) -> usize {
        for idx in 0..self.raw_pattern_count() {
            if let Some(rows) = self.raw_pattern(idx) {
                if let Some(row) = rows.first() {
                    if !row.is_empty() {
                        return row.len();
                    }
                }
            }
        }
        0
    }

    /// What the file claims about its authoring tool.
    fn raw_tracker(&self) -> RawTracker<'_> {
        RawTracker::default()
    }

    /// Author-given name of one pattern. IT only (the `PNAM` chunk);
    /// `None` elsewhere, and `None` for an unnamed pattern.
    fn raw_pattern_name(&self, _idx: usize) -> Option<&str> {
        None
    }

    /// Author-given name of one channel. IT only (the `CNAM` chunk);
    /// `None` elsewhere. Frequently used for section labels and
    /// credits rather than for instrument roles.
    fn raw_channel_name(&self, _idx: usize) -> Option<&str> {
        None
    }

    /// IT's edit history, oldest first. Empty on every other format,
    /// and on IT files that carry no such block — see
    /// [`RawEditEntry`] for why an empty result is not evidence of
    /// anything.
    fn raw_edit_history(&self) -> Vec<RawEditEntry> {
        Vec::new()
    }

    /// Iterator over every pattern grid.
    fn raw_patterns(&self) -> RawPatterns<'_, Self>
    where
        Self: Sized,
    {
        RawPatterns {
            module: self,
            next: 0,
        }
    }

    /// Iterator over every sample slot.
    fn raw_samples(&self) -> RawSamples<'_, Self>
    where
        Self: Sized,
    {
        RawSamples {
            module: self,
            next: 0,
        }
    }
}

/// Iterator returned by [`RawModule::raw_patterns`].
pub struct RawPatterns<'a, M: RawModule> {
    module: &'a M,
    next: usize,
}

impl<'a, M: RawModule> Iterator for RawPatterns<'a, M> {
    type Item = &'a [Vec<PatternSlot>];

    fn next(&mut self) -> Option<Self::Item> {
        let item = self.module.raw_pattern(self.next)?;
        self.next += 1;
        Some(item)
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        let n = self.module.raw_pattern_count().saturating_sub(self.next);
        (n, Some(n))
    }
}

/// Iterator returned by [`RawModule::raw_samples`].
pub struct RawSamples<'a, M: RawModule> {
    module: &'a M,
    next: usize,
}

impl<'a, M: RawModule> Iterator for RawSamples<'a, M> {
    type Item = RawSample<'a>;

    fn next(&mut self) -> Option<Self::Item> {
        let item = self.module.raw_sample(self.next)?;
        self.next += 1;
        Some(item)
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        let n = self.module.raw_sample_count().saturating_sub(self.next);
        (n, Some(n))
    }
}