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
//! `Module::load_*` impls — auto-detect a tracker file's format
//! from its header and dispatch to the matching format-specific
//! loader (`load_mod`, `load_xm`, `load_s3m`, `load_it`, `load_dw`,
//! `load_sid`). SID is settled first and exclusively, off its PSID/RSID
//! magic; DW is auto-detected even without a magic header — its probe
//! quadruple is unique enough — and so goes last.
use crate::prelude::*;
use crate::tracker::import::bin_reader::ImportError;
// The module-level gate in `crate::tracker::import::mod` only compiles
// this file when at least one of MOD/XM/S3M/IT/DW is enabled, so
// `load` below always has at least one live branch and `source` is
// always used.
impl Module {
/// Try to import an Amiga ProTracker MOD file.
#[cfg(feature = "import_mod")]
pub fn load_mod(source: &[u8]) -> Result<Self, ImportError> {
use super::amiga::amiga_module::AmigaModule;
Ok(AmigaModule::load(source)?.to_module())
}
/// Try to import a Fast Tracker II XM module file.
#[cfg(feature = "import_xm")]
pub fn load_xm(source: &[u8]) -> Result<Self, ImportError> {
use super::xm::xmmodule::XmModule;
Ok(XmModule::load(source)?.to_module())
}
/// Try to import a ScreamTracker 3 S3M module file.
#[cfg(feature = "import_s3m")]
pub fn load_s3m(source: &[u8]) -> Result<Self, ImportError> {
use super::s3m::s3m_module::S3mModule;
Ok(S3mModule::load(source)?.to_module())
}
/// Try to import an Impulse Tracker IT module file.
#[cfg(feature = "import_it")]
pub fn load_it(source: &[u8]) -> Result<Self, ImportError> {
use super::it::it_module::ItModule;
Ok(ItModule::load(source)?.to_module())
}
/// Try to import a David Whittaker custom Amiga `.dw` file.
/// The detection layer runs a probe-quadruple scan (`LEA /
/// LEA / MOVEQ / DBF`) over the first ~4 KB of the payload
/// and walks the BSR call graph from there; no magic header
/// is required.
#[cfg(feature = "import_dw")]
pub fn load_dw(source: &[u8]) -> Result<Self, ImportError> {
use super::dw::dw_module::DwModule;
Ok(DwModule::load(source)?.to_module())
}
/// Import a `.dw` from bytes and give it an explicit title.
///
/// The `.dw` format carries no textual title — the byte-only
/// [`Self::load_dw`] therefore tags every module with the same
/// `"David Whittaker (.dw)"` placeholder, which is useless once
/// several songs sit side by side in an editor. This variant runs
/// the exact same importer, then overrides [`Module::name`] with
/// `name` (an empty `name` is ignored, keeping the placeholder).
///
/// Titling needs only `alloc`, so this works in `no_std` builds:
/// a caller that already has the bytes and a name (from any
/// source) can title the module without pulling in `std`. The
/// std-only [`Self::load_dw_path`] is a thin convenience over this.
#[cfg(feature = "import_dw")]
pub fn load_dw_named(source: &[u8], name: &str) -> Result<Self, ImportError> {
let mut module = Self::load_dw(source)?;
if !name.is_empty() {
module.name = name.into();
}
Ok(module)
}
/// Import a `.dw` file from disk and title it after the file name.
///
/// Reads `path`, then delegates to [`Self::load_dw_named`] with the
/// file stem (the name minus its extension, e.g. `xenon2.dw` →
/// `xenon2`). Requires `std` for the filesystem read; the titling
/// itself does not — see [`Self::load_dw_named`].
#[cfg(all(feature = "import_dw", feature = "std"))]
pub fn load_dw_path<P: AsRef<std::path::Path>>(path: P) -> Result<Self, ImportError> {
let path = path.as_ref();
let data = std::fs::read(path).map_err(|_| ImportError::Other("dw: cannot read file"))?;
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
Self::load_dw_named(&data, stem)
}
/// Try to import a Commodore 64 `.sid` file (PSID / RSID container).
///
/// Three Rob Hubbard player generations are recognised, and the answer is
/// only as good as what each one puts in the image:
///
/// - The **last** generation needs nothing else — every table and every
/// effect comes out of the bytes ([`super::sid::g2::to_module`]).
/// - The **three-marker** generation, its predecessor, goes through the
/// same reader; its pitch is not finished (see
/// `sid/THREE_MARKER_GENERATION.md`).
/// - The **fx-mask** family (v10..v30) has its layout recovered by
/// [`super::sid::detect::SidLayout`], but its *behaviour* — row cadence,
/// effect semantics — is not in the image. When the file is one of the
/// fifteen tunes someone transcribed, it is recognised by fingerprint and
/// gets those settings; otherwise they stay at neutral defaults, which
/// means the right notes at the wrong tempo.
///
/// Sub-songs become sub-songs of the returned module (`song` 0..n), the way
/// `.dw` files already work — a `.sid` holding several tunes is one module
/// with several songs, not several modules.
///
/// One file can hold more than one replayer: Sanxion ships two, one per
/// song. Each contributes its own sub-songs, with its own instrument table
/// appended and rebased.
#[cfg(feature = "import_sid")]
pub fn load_sid(source: &[u8]) -> Result<Self, ImportError> {
use super::sid::sid_module::{append_sub_songs, song_count, SidModule};
if !matches!(source.get(..4), Some(b"PSID") | Some(b"RSID")) {
return Err(ImportError::Other("sid: not a PSID/RSID file"));
}
// The last generation first: it is fully readable, so when it claims a
// file there is nothing to be gained from the other paths.
if let Some(m) = super::sid::g2::to_module(source) {
return Ok(m);
}
// The importer holds the image for the module's lifetime and the caller
// only lent us a slice, so take a copy — a few kilobytes.
let image: alloc::borrow::Cow<'static, [u8]> = source.to_vec().into();
// Filter first, then fold: a replayer that decodes to nothing must not
// decide the outcome just by being listed first. Sanxion's two are
// independent, and so are a file's chances of yielding either.
let mut parts = SidModule::for_image(image)
.into_iter()
.filter_map(|p| p.to_module());
let mut merged = parts
.next()
.ok_or(ImportError::Other("sid: unrecognised replayer"))?;
for part in parts {
let (song_base, instr_base) = (song_count(&merged), merged.instrument.len());
merged.instrument.extend(part.instrument.iter().cloned());
append_sub_songs(&mut merged, part, song_base, instr_base);
}
// A transcribed record carries the tune's title; a detected one has
// nothing to offer and names every tune "SID". The PSID header has had
// the answer all along — three fixed 32-byte fields — so read it rather
// than hand a folder of rips back under one name.
if merged.name.is_empty() || merged.name == "SID" {
use alloc::string::String;
let text = |off: usize| -> String {
source
.get(off..off + 32)
.map(|b| {
b.iter()
.take_while(|&&c| c != 0)
.map(|&c| c as char)
.collect::<String>()
.trim()
.into()
})
.unwrap_or_default()
};
let title = text(0x16);
if !title.is_empty() {
merged.name = title;
}
merged.comment = alloc::format!("{} - {}", text(0x56), text(0x36));
}
Ok(merged)
}
/// Try to auto-detect and import any supported historical module
/// file (SID / MOD / XM / S3M / IT / DW). SID is settled first, off its
/// magic; the rest are tried in the order XM → S3M → IT → MOD → DW. The
/// Amiga MOD format is tried late because its detection accepts almost
/// anything; DW comes after MOD because its detection is structural
/// (no magic header at all) — running it last avoids ever
/// stealing a valid MOD parse. Only formats whose `import_*`
/// feature is enabled are attempted.
///
/// Every SID generation this crate reads goes through here now, sub-songs
/// included — see [`Self::load_sid`] for what each one is worth. Nothing
/// needs a dedicated entry point any more.
pub fn load(source: &[u8]) -> Result<Self, ImportError> {
// `.sid` is settled first and exclusively, because it is the one format
// here that says what it is: a four-byte `PSID`/`RSID` magic no tracker
// module can carry. Trying it last instead would let MOD — whose
// detection "accepts almost anything" — claim a tune this crate cannot
// decode and hand back noise. Failing here is the honest answer.
#[cfg(feature = "import_sid")]
if matches!(source.get(..4), Some(b"PSID") | Some(b"RSID")) {
return Self::load_sid(source);
}
#[cfg(feature = "import_xm")]
if let Ok(m) = Self::load_xm(source) {
return Ok(m);
};
#[cfg(feature = "import_s3m")]
if let Ok(m) = Self::load_s3m(source) {
return Ok(m);
};
#[cfg(feature = "import_it")]
if let Ok(m) = Self::load_it(source) {
return Ok(m);
};
// The amiga format is tried late because it accepts almost
// anything as a 15-sample Soundtracker variant.
#[cfg(feature = "import_mod")]
if let Ok(m) = Self::load_mod(source) {
return Ok(m);
};
// DW has no header magic at all — its detector scans for
// an opcode quadruple in the first few KB. Running it
// last keeps it from intercepting a MOD that happens to
// begin with the right bytes.
#[cfg(feature = "import_dw")]
if let Ok(m) = Self::load_dw(source) {
return Ok(m);
};
// When no MOD/XM/S3M/IT/DW feature is enabled the whole
// `import_loader` module is gated out in `import::mod`, so
// we never reach this arm with `source` being the only live
// binding — the compiler always sees at least one of the
// `#[cfg]` blocks above as active. The `let _ = source;` is
// a belt-and-braces silencer for the rare case where only
// one `load_*` is compiled and a stubborn linter still
// complains.
let _ = source;
Err(ImportError::Other("Unknown data?"))
}
}