diff --git a/CHIPTUNE.md b/CHIPTUNE.md new file mode 100644 index 0000000..cdaa125 --- /dev/null +++ b/CHIPTUNE.md @@ -0,0 +1,216 @@ +# 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: + +1. **Server-side** — lightweight metadata parser in pure Rust (zero external dependencies) +2. **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 channels +- `6CHN` → 6, `8CHN` / `OCTA` → 8 +- `xCHN` → 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 of `IMPM` +- Modern: magic `IMPM` with tracker version (offset 0x28) in range `0x0889..=0x0FFF` + +### Integration Points + +- **`browse.rs`** — add tracker extensions to the audio file whitelist +- **`meta.rs`** — add a chiptune metadata branch that runs before Symphonia (which does not + support tracker formats); return title, channel count, pattern count, and message +- **`stream.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_offset` is 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) │ │ +│ │ │ │ │ │ +│ │