8.5 KiB
Chiptune Support Implementation Plan
Overview
Add playback support for tracker/chiptune module formats (MOD, XM, S3M, IT, MPTM) to the Furumi web player. The implementation consists of two parts:
- Server-side — lightweight metadata parser in pure Rust (zero external dependencies)
- Client-side — playback via libopenmpt WebAssembly using AudioWorklet API
Supported Formats
| Format | Extension | Origin |
|---|---|---|
| MOD | .mod |
Amiga ProTracker |
| XM | .xm |
FastTracker II |
| S3M | .s3m |
Scream Tracker 3 |
| IT | .it |
Impulse Tracker |
| MPTM | .mptm |
OpenMPT |
Part 1: Server-Side Metadata Parser
Rationale
libopenmpt must NOT be a server dependency. All tracker formats store metadata at fixed byte offsets in their headers, making manual parsing trivial. Reading the first ~400 bytes of a file is sufficient to extract all available metadata.
Extracted Fields
- Title — song name embedded in the module header
- Channels — number of active audio channels
- Patterns — number of unique patterns in the module
- Message — song message/comment (IT/MPTM only)
Note: none of these formats have a dedicated "artist" field. Author information, when present, is typically found in the IT/MPTM song message.
Binary Format Reference
MOD
| Offset | Size | Field |
|---|---|---|
| 0 | 20 | Song title (space/null padded) |
| 952 | 128 | Pattern order table |
| 1080 | 4 | Signature (determines channel count) |
Channel count is derived from the 4-byte signature at offset 1080:
M.K.,M!K!,FLT4,4CHN→ 4 channels6CHN→ 6,8CHN/OCTA→ 8xCHN→ x channels,xxCH→ xx channels
Pattern count = max value in the order table (128 bytes at offset 952) + 1.
XM
All multi-byte values are little-endian.
| Offset | Size | Field |
|---|---|---|
| 0 | 17 | Magic: "Extended Module: " |
| 17 | 20 | Module name |
| 58 | 2 | Version number |
| 68 | 2 | Number of channels |
| 70 | 2 | Number of patterns |
S3M
| Offset | Size | Field |
|---|---|---|
| 0x00 | 28 | Song title (null-terminated) |
| 0x1C | 1 | Signature byte (0x1A) |
| 0x24 | 2 | Pattern count (LE u16) |
| 0x2C | 4 | Magic: "SCRM" |
| 0x40 | 32 | Channel settings |
Channel count = number of entries in channel settings (32 bytes) that are not 0xFF.
IT
| Offset | Size | Field |
|---|---|---|
| 0x00 | 4 | Magic: "IMPM" |
| 0x04 | 26 | Song title (null-terminated) |
| 0x26 | 2 | Pattern count (LE u16) |
| 0x2E | 2 | Special flags (bit 0 = message attached) |
| 0x36 | 2 | Message length |
| 0x38 | 4 | Message file offset |
| 0x40 | 64 | Channel panning table |
Channel count = number of entries in channel panning (64 bytes) with value < 128.
Song message: if special & 1, read message_length bytes from message_offset. Uses \r
(0x0D) as line separator.
MPTM
Parsed identically to IT. Detection:
- Legacy: magic
tpm.instead ofIMPM - Modern: magic
IMPMwith tracker version (offset 0x28) in range0x0889..=0x0FFF
Integration Points
browse.rs— add tracker extensions to the audio file whitelistmeta.rs— add a chiptune metadata branch that runs before Symphonia (which does not support tracker formats); return title, channel count, pattern count, and messagestream.rs— serve tracker files as-is (no server-side transcoding); these files are typically under 1 MB
Implementation Notes
- Zero external crate dependencies — only
std::io::Read+std::io::Seek - Read at most the first 1084 bytes for header parsing (MOD needs offset 1080 + 4 byte sig)
- For IT/MPTM messages, a second seek to
message_offsetis needed - All strings should be trimmed of null bytes and trailing whitespace
- Expected code size: ~200–300 lines of Rust
Part 2: Client-Side Playback via libopenmpt WASM
Rationale
Browsers cannot decode tracker formats natively. libopenmpt compiled to WebAssembly decodes modules into PCM samples which are then rendered through the Web Audio API. Client-side decoding keeps the server dependency-free and enables interactive features (pattern display, channel visualization) in the future.
libopenmpt WASM Source
Use the chiptune3 library (npm: chiptune3, by DrSnuggles) which bundles libopenmpt as a
self-contained AudioWorklet-compatible ES6 module.
Package contents:
| File | Size | Purpose |
|---|---|---|
chiptune3.js |
~4 KB | Main API (load, play, pause, seek) |
chiptune3.worklet.js |
~12 KB | AudioWorklet processor glue |
libopenmpt.worklet.js |
~1.7 MB | libopenmpt WASM + JS (single-file bundle) |
Available via jsDelivr CDN or can be vendored into the project.
If a newer libopenmpt version is needed, the official project provides source tarballs with an Emscripten build target:
make CONFIG=emscripten EMSCRIPTEN_TARGET=audioworkletprocessor
This produces a single ES6 module with WASM embedded inline (SINGLE_FILE=1), which is
required because AudioWorklet contexts cannot fetch separate .wasm files.
Playback Architecture
┌──────────────────────────────────────────────────────────┐
│ player.html │
│ │
│ Format detection (by file extension) │
│ ┌─────────────────────┐ ┌────────────────────────────┐ │
│ │ Standard audio │ │ Tracker module │ │
│ │ (mp3/flac/ogg/...) │ │ (mod/xm/s3m/it/mptm) │ │
│ │ │ │ │ │
│ │ <audio> element │ │ fetch() → ArrayBuffer │ │
│ │ src=/api/stream/path │ │ ↓ │ │
│ │ │ │ libopenmpt WASM decode │ │
│ │ │ │ ↓ │ │
│ │ │ │ AudioWorkletProcessor │ │
│ │ │ │ ↓ │ │
│ │ ↓ │ │ AudioContext.destination │ │
│ └────────┼─────────────┘ └────────────┼───────────────┘ │
│ └──────────┬──────────────────┘ │
│ ↓ │
│ Player controls │
│ (play/pause/seek/volume) │
│ MediaSession API │
└──────────────────────────────────────────────────────────┘
Integration Points
player.html— detect tracker format by extension; use chiptune3 API instead of<audio>element for tracker files; unify transport controls (play/pause/seek/volume) across both playback engines- WASM assets — serve
chiptune3.js,chiptune3.worklet.js, andlibopenmpt.worklet.jsvia a static file endpoint or embed them inline mod.rs(routes) — add endpoint for serving WASM assets if not embedded
Player Integration Details
The player must abstract over two playback backends behind a common interface:
play(path) — start playback (auto-detect engine by extension)
pause() — pause current playback
resume() — resume current playback
seek(seconds) — seek to position
setVolume(v) — set volume (0.0–1.0)
getDuration() — total duration in seconds
getPosition() — current position in seconds
isPlaying() — playback state
onEnded(cb) — callback when track finishes
For tracker modules, getDuration() and getPosition() are provided by libopenmpt's
get_duration_seconds() and get_position_seconds() APIs.
Considerations
- Tracker files are small (typically < 1 MB) — fetch the entire file before playback; no streaming/range-request needed
- AudioWorklet requires a secure context (HTTPS or localhost)
- The WASM bundle is ~1.7 MB — load it lazily on first tracker file playback
- MediaSession API metadata should display module title from
/api/metaresponse