Files
furumi-ng/docs/CHIPTUNE.md
AB-UK 8d70a5133a
All checks were successful
Publish Metadata Agent Image (dev) / build-and-push-image (push) Successful in 1m17s
Publish Web Player Image (dev) / build-and-push-image (push) Successful in 1m14s
Disabled obsolete CI
2026-03-20 00:49:27 +00:00

217 lines
8.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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