Files
furumi-ng/CHIPTUNE.md

217 lines
8.5 KiB
Markdown
Raw Normal View History

2026-03-18 00:46:16 +00:00
# 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: ~200300 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`, and
`libopenmpt.worklet.js` via 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.01.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/meta` response