XMrs — SoundTracker file format library
A no_std-friendly Rust library to read, edit and serialize SoundTracker
data — with pleasure.
Because "Representation is the Essence of Programming".
Supported formats
Historical module files — imported into xmrs's in-memory Module model:
| Format | Description | Feature |
|---|---|---|
| MOD | Amiga ProTracker / Soundtracker family | import_mod |
| XM | Fast Tracker II | import_xm |
| S3M | Scream Tracker 3 | import_s3m |
| IT | Impulse Tracker (incl. OpenMPT extras) | import_it |
| XI | Fast Tracker II instrument file | import_xm |
| SID | Commodore 64 / MOS6581 (WIP) | import_sid |
The umbrella feature import enables all of them at once (and is on by
default together with std).
The Module model
xmrs exposes one editor-friendly data model, regardless of what was loaded:
Module ─┬─ Instrument ─┬─ InstrDefault ─┬─ VoiceSetup (envelopes, vibrato, filter, panning)
│ │ ├─ InstrumentBehavior (NNA, DCT, DCA)
│ │ ├─ Keyboard (per-input-note sample + transposition)
│ │ ├─ InstrMidi
│ │ └─ Sample (loop & sustain-loop)
│ ├─ InstrEkn (Euclidean-rhythm instrument)
│ ├─ InstrMidi (MIDI instrument)
│ ├─ InstrOpl (Yamaha OPL)
│ ├─ InstrSid (MOS6581 SID voice)
│ └─ InstrRobSid ── InstrSid (Rob Hubbard-style)
└─ Pattern ─ Row ─ TrackUnit ─┬─ CellNote (Empty, Play(Pitch), KeyOff, NoteCut, NoteFade)
├─ TrackEffect
└─ GlobalEffect
InstrDefault is split into three orthogonal sub-types so each
concern lives in one place: [VoiceSetup] (how the voice sounds
once triggered), [InstrumentBehavior] (what happens when the same
instrument retriggers), [Keyboard] (per-input-note sample / pitch
remap, used by IT drum kits).
Per-module playback semantics are bundled into a single
[CompatibilityProfile] field on Module:
format— source-format tag (Xm,S3m,It,Mod,Unknown). Informational only — no playback decision reads it.quirks— the orthogonal switches each historical tracker flips: FT2 pitch-slide overflow, S3M period clamp, arpeggio LUT, pattern-loop resume, tremor state, IT vibrato tick-zero, pan reset policy, and so on.
Named constructors materialise the canonical configurations:
CompatibilityProfile::ft2(), it214(), it215(), st3(),
pt(), modern() (the editor-friendly default — all quirks off).
Importers pick the right constructor for the source file and
overlay any header-driven flags on top.
Module also carries a [Vec<ChannelDefault>] for per-channel
initial state (panning, channel volume, mute, surround). S3M's
channel_settings and IT's initial_channel_pan /
initial_channel_volume populate it; XM/MOD leave it empty.
Loading a file
use *;
let bytes = read?;
// Auto-detect: tries XM → S3M → IT → MOD.
let module = load?;
// Or target a specific format:
let module = load_xm?;
println!;
Format-specific constructors are also available: Module::load_mod,
load_xm, load_s3m, load_it. SID is imported via its own entry
point, xmrs::import::sid::sid_module::SidModule::load, and is not part
of the auto-detect path.
Serialization
Module derives serde::Serialize / Deserialize, so you can round-trip
it through any serde codec.
Examples
Run from the xmrs crate directory:
# Dump any supported module file:
# Dump an XI (Fast Tracker II instrument) file:
# Exercise the bundled SID parsers (Rob Hubbard tunes):
Cargo features
Defaults: ["std", "import"].
| Feature | Purpose |
|---|---|
std |
Build against std. Only forwards std to a few dependencies (bincode, serde, num_enum); the crate itself does not need std for arithmetic — see below. |
float-helpers |
Add f32 ↔ Q conversions on every fixed-point and domain type in crate::fixed. Intended for the editor / desktop side (level-meters, plotting, GUI numeric fields). Self-contained, uses only core-stable f32 operations — no math backend pulled in. Never enable it on the embedded target. |
import |
Umbrella: enables every import_* format importer. |
import_mod |
Amiga ProTracker MOD. |
import_xm |
Fast Tracker II XM (and XI instruments). |
import_s3m |
Scream Tracker 3 S3M. |
import_it |
Impulse Tracker IT. |
import_sid |
Commodore 64 SID (bundled Rob Hubbard tunes). |
demo |
Pulls in clap, std and import for the CLI examples. |
rand8 / rand16 / rand64 |
Extra xorshift widths (rand32 is always on). |
no_std builds
The crate compiles fully no_std with no math backend at all. Every
former f32 site on the runtime path has been folded to integer
Q-format arithmetic — pitch / period / frequency tables, the LFO
(waveform.rs), the IT importer's c5_speed_to_finetune (binary
search via linear_frequency_to_period), envelopes, panning, filter,
sample interpolation. No libm, no micromath, no num-traits.
bincode is pulled in only by the importer features that need it —
with no import_* enabled, it is not compiled. serde-big-array is
core (the IT keyboard table is [Option<_>; 120], beyond serde's
native const-generic limit of N ≤ 32).
# Minimal build, no importers (pre-baked blobs only):
# Embedded build, MOD + XM importers only:
Concert pitch (440 Hz / MIDI-aligned)
Notes resolve to MIDI-aligned frequencies by default: A-4 plays at exactly 440 Hz, C-4 at 261.626 Hz, etc. This means tracker output mixes cleanly with any 440-Hz-aligned source (MIDI sequencers, DAW samples, commercial audio) without a detune.
Two reference constants govern this, both in crate::fixed::tables:
| Mode | Default (MIDI-aligned) | Legacy (FT2 / PT) |
|---|---|---|
| Linear (XM/IT linear) | C4_FREQ_HZ = 8372 |
C4_FREQ_HZ_LEGACY = 8363 (≈ −2 cents) |
| Amiga (MOD/S3M/IT amiga) | AMIGA_C0_PERIOD = 6779 |
AMIGA_C0_PERIOD_LEGACY = 6848 (≈ +17 cents) |
The legacy values are exposed for bit-numerical comparison against reference players (OpenMPT, libxmp, schism, ft2-clone). Most users should leave the defaults alone — that's what makes a tracker note sound in tune against modern audio.
Embedded / footprint-sensitive builds
If you're targeting tight flash budgets, don't ship the importers.
Parse modules on a host machine, serialize with
bincode 2.x (alloc feature) —
optionally compressed with
flate2 — and load the resulting
blob on-device. You end up with a much smaller binary that still
manipulates the full Module model.
License
MIT © Sébastien Béchet. See LICENSE.