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

8.5 KiB
Raw Permalink Blame History

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