oxideav_mod/lib.rs
1//! Amiga ProTracker / SoundTracker module ("MOD") support, plus a
2//! structural parser for Scream Tracker v1 (`.stm`) files.
3//!
4//! MOD files are self-contained song data: a 20-byte title, 31 sample
5//! descriptors, a pattern order list, a 4-character signature that
6//! identifies the channel count, 64×N-channel patterns, then raw signed
7//! 8-bit sample bodies.
8//!
9//! This crate registers:
10//!
11//! - A **container** (`mod`) that slurps the entire file and emits it as
12//! a single packet. The "packets" abstraction isn't natural for MOD —
13//! playback is driven by song position + effect state, not per-packet
14//! decode — so the container just delivers the bytes to the codec.
15//! - A **mixed-stereo decoder** under codec id [`CODEC_ID_STR`] = `"mod"`.
16//! Emits one interleaved S16 stereo `AudioFrame` every ~1024 samples;
17//! the drop-in option for plug-and-play playback. Stereo pan defaults
18//! to a partial L/R separation rather than the strict Amiga hard pan
19//! — see [`player::PlayerState::set_pan_separation`] for the override
20//! and the rationale (`Protracker-effects-MODFIL12.txt` §11 itself
21//! recommends *against* full hard pan "Especially when using
22//! headphones").
23//! - A **per-channel decoder** under codec id [`CODEC_ID_PLANAR_STR`] =
24//! `"mod_planar"`. Emits planar S16P `AudioFrame`s with one plane per
25//! MOD tracker channel (4 / 6 / 8 / … / 32), post-volume but
26//! pre-pan/pre-mix. Consumers that need independent channel streams
27//! (DAWs, visualisers, per-instrument remastering) select this codec
28//! id instead of `"mod"`.
29//! - A **container** (`stm`) that recognises Scream Tracker v1 modules
30//! (pre-S3M, 4-channel-fixed), parses the header + instrument table +
31//! pattern data + sample bodies, and exposes them for downstream
32//! consumers. A minimum STM playback engine is wired via the shared
33//! [`mixer::MixerVoice`] core and [`mixer::StmC3Pitch`] pitch model;
34//! drive it via [`stm_player::StmPlayerState::render`]. The associated
35//! codec id [`CODEC_ID_STM_STR`] = `"stm"` is still a parsing-only
36//! decoder registration (it validates the packet then returns an
37//! explicit `unsupported`), because effect support and global mixer
38//! parameters are not yet feature-complete; callers wiring STM into
39//! the broader oxideav pipeline should hook up
40//! `StmPlayerState::render` directly for now.
41//! - A **container** (`xm`) that recognises FastTracker 2 Extended
42//! Module files by the 17-byte `"Extended Module: "` ASCII banner at
43//! offset 0. Parses the 336-byte file header (banner, module/tracker
44//! names, version, header size, song length, restart position,
45//! channel / pattern / instrument counts, frequency-table flag,
46//! default tempo / BPM, 256-entry order table), the variable-length
47//! bit-packed patterns (note / instrument / volume-column / effect /
48//! effect-param, each optional per mask byte), and the instrument
49//! table (per-note sample mapping, volume + panning envelopes, vibrato
50//! state, fadeout, multiple samples per instrument with delta-encoded
51//! 8- or 16-bit PCM bodies). A minimum XM playback engine is wired via
52//! the shared [`mixer::MixerVoice`] core and [`mixer::XmPitch`] pitch
53//! model (both Amiga and Linear frequency tables supported); drive it
54//! via [`xm_player::XmPlayerState::render`]. Volume + panning envelopes
55//! (tick-based linear interpolation with sustain-point hold and
56//! loop-start/loop-end looping), fadeout (on key-off / note 97), and
57//! key-off events are supported. Vibrato, tone portamento, and the
58//! bulk of the Bxy/Dxy/Exy/Fxy/Kxy/Lxy effect space remain
59//! unimplemented. The codec id [`CODEC_ID_XM_STR`] = `"xm"` remains
60//! a parsing-only decoder pending effect-set completeness — use
61//! [`xm::parse_header`] / [`xm::parse_patterns`] / [`xm::parse_instruments`]
62//! / [`xm::extract_sample_bodies`] for structural access, or drive
63//! `XmPlayerState` directly for PCM output.
64//!
65//! The tracker convention of exposing per-channel streams alongside a
66//! mixed stereo mix is shared across tracker formats — see
67//! `MEMORY.md → MOD multichannel` for the broader sketch.
68//!
69//! Decode only — there is no MOD or STM encoder, by design.
70
71pub mod container;
72pub mod decoder;
73pub mod header;
74pub mod mixer;
75pub mod player;
76pub mod samples;
77pub mod stm;
78pub mod stm_player;
79pub mod xm;
80pub mod xm_player;
81
82use oxideav_core::CodecRegistry;
83use oxideav_core::ContainerRegistry;
84use oxideav_core::RuntimeContext;
85
86/// Codec id for the mixed-stereo MOD decoder.
87pub const CODEC_ID_STR: &str = "mod";
88
89/// Codec id for the planar per-channel MOD decoder.
90pub const CODEC_ID_PLANAR_STR: &str = "mod_planar";
91
92/// Codec id for the STM (Scream Tracker v1) parsing-only decoder.
93pub const CODEC_ID_STM_STR: &str = "stm";
94
95/// Codec id for the XM (FastTracker 2 Extended Module) parsing-only decoder.
96pub const CODEC_ID_XM_STR: &str = "xm";
97
98pub fn register_codecs(reg: &mut CodecRegistry) {
99 decoder::register(reg);
100}
101
102pub fn register_containers(reg: &mut ContainerRegistry) {
103 container::register(reg);
104}
105
106/// Unified entry point: install every codec and container provided by
107/// `oxideav-mod` into a [`RuntimeContext`].
108///
109/// Also wired into [`oxideav_meta::register_all`] via the
110/// [`oxideav_core::register!`] macro below. The
111/// short-name `amiga_mod` matches the umbrella's existing trace name
112/// from the #502 sweep.
113pub fn register(ctx: &mut RuntimeContext) {
114 register_codecs(&mut ctx.codecs);
115 register_containers(&mut ctx.containers);
116}
117
118oxideav_core::register!("amiga_mod", register);
119
120#[cfg(test)]
121mod register_tests {
122 use super::*;
123
124 #[test]
125 fn register_via_runtime_context_installs_factories() {
126 let mut ctx = RuntimeContext::new();
127 register(&mut ctx);
128 assert!(
129 ctx.codecs.decoder_ids().next().is_some(),
130 "register(ctx) should install codec decoder factories"
131 );
132 assert!(
133 ctx.containers.demuxer_names().next().is_some(),
134 "register(ctx) should install container demuxer factories"
135 );
136 }
137}