mod-player (package name react-libopenmpt-viewer) is a browser-based MOD/tracker music player that combines retro audio emulation with modern graphics. It plays tracker module files (.mod, .xm, .s3m, .it, etc.) using libopenmpt compiled to WebAssembly, and renders real-time pattern visualizations via WebGPU WGSL shaders. The app also includes a 3D studio mode (React Three Fiber), a media overlay for images/video, and PWA support.
- Frontend: React 18 (TypeScript 5.4), JSX via
react-jsx - Bundler / Dev Server: Vite 5
- Styling: Tailwind CSS 3.3 + PostCSS + Autoprefixer, with custom scrollbars in
index.css - 3D Graphics:
@react-three/fiber+@react-three/drei+three - Visualization: WebGPU (WGSL shaders), not WebGL
- Audio Backend:
libopenmpt(WASM) inside a customized AudioWorklet - Native Audio Engine: C++17 → Emscripten (
scripts/build-wasm.sh) - Module System: ES modules (
"type": "module"inpackage.json)
The audio logic is split across the Main Thread and the Audio Worklet Thread, with an optional high-performance native C++ worklet.
Managed by hooks/useLibOpenMPT.ts. Responsibilities:
- Initialize
libopenmpt(loaded from CDN inindex.htmlviawindow.libopenmptReady) - Load module files, extract pattern matrices (
utils/patternExtractor.ts) - Maintain React UI state (play/pause, volume, pan, loop, seek position)
- Send commands to the worklet via
port.postMessage() - Read high-frequency playback data via a mutable ref (
playbackStateRef) to avoid 60 Hz re-renders
File: public/worklets/openmpt-worklet.js
- This is an
AudioWorkletProcessorthat dynamicallyimport()s./libopenmpt-audioworklet.jsinside the worklet thread. - Dynamic import is used instead of static import to maintain compatibility with Chrome 113–116 (the WebGPU baseline) without requiring
addModule({ type: 'module' }). - Runs the
libopenmptrender loop, reports position (~60 Hz) back to the main thread, and handles seek/load messages. - Rule: You cannot use React state or DOM APIs inside the worklet. Communication is strictly via
port.postMessage().
Files: audio-worklet/OpenMPTWorkletEngine.ts, cpp/openmpt_wrapper.cpp, cpp/worklet_processor.cpp
- Built with
scripts/build-wasm.sh(requires Emscripten SDK). - Emscripten flags:
-sAUDIO_WORKLET=1,-sWASM_WORKERS=1,-sMODULARIZE=1,-sEXPORT_NAME=createOpenMPTModule. - Outputs:
public/worklets/openmpt-native.js,.wasm,.aw.js(these are.gitignored until built). useLibOpenMPT.tsprobes foropenmpt-native.jsat startup; if present, it instantiatesOpenMPTWorkletEngine, which creates its ownAudioContext+ worklet thread in C++ land.- The native engine polls a shared-memory
PositionInfostruct for row/BPM/channel VU data.
If the JS AudioWorklet fails to initialize WASM, hooks/useAudioGraph.ts falls back to a ScriptProcessorNode on the main thread (deprecated but functional). This is triggered by the worklet posting an error message.
- Language: WGSL (WebGPU Shading Language).
- Location: Source shaders live in
/shaders. There are 50+ versioned files (e.g.,patternv0.50.wgsl,chassisv0.40.wgsl). - Shader Groups (in
App.tsx):- Square: v0.44, v0.43, v0.40, v0.39, v0.21
- Circular: v0.50, v0.49, v0.48, v0.47, v0.46, v0.45, v0.42, v0.38, v0.35_bloom, v0.30
- Video: v0.23 (Clouds), v0.24 (Tunnel)
- Pipeline: Shaders are fetched as raw text strings (often via
fetch()or bundled strings) and passed into the WebGPU render pipeline in components likePatternDisplay.tsxandStudio3D.tsx. - Bloom: Post-processing bloom passes live in
utils/bloomPostProcessor.ts; presets are defined intypes/bloomPresets.ts. - Compatibility: WebGPU support is required for the shader visualizer. If unavailable, the app falls back to an HTML pattern renderer.
/components– React UI elements (App.tsx,PatternDisplay.tsx,Controls.tsx,Studio3D.tsx,MediaOverlay.tsx,ChannelMeters.tsx, etc.)/hooks– Core logic hooksuseLibOpenMPT.ts– Main audio bridge and stateuseAudioGraph.ts– Audio graph construction and playback startuseWorkletLoader.ts– AudioWorklet module loading with retry/diagnosticsusePlaylist.ts,useKeyboardShortcuts.ts,useWebGPURender.ts,useWebGLOverlay.ts
/audio-worklet– TypeScript wrapper for the native C++ engine (OpenMPTWorkletEngine.ts,types.ts,diagnostics.ts)/cpp– C++ source for the native worklet (openmpt_wrapper.cpp,openmpt_wrapper.h,worklet_processor.cpp,pre.js)/public/worklets– AudioWorklet JS processors served as static assetsopenmpt-worklet.js– JS worklet processor (tracked in git)libopenmpt-audioworklet.js– WASM glue for the JS workletopenmpt-native.js/.wasm/.aw.js– Generated by Emscripten (ignored in git)
/shaders– WGSL shader source files/utils–patternExtractor.ts,bloomPostProcessor.ts,remoteMedia.ts,colorSchemes.ts,gpuPacking.ts, etc./types– Shared TS types, includingbloomPresets.ts/src– Supplementary code (src/lib/paths.ts,src/shaders/,src/utils/shaderHelpers.ts)/docs– Technical guides (BLOOM.md,planning/,agent-swarm/)/scripts–build-wasm.sh,benchmark_loadFromURL.cjs,make_bezel_transparent.py/dist– Vite production build output (deployment artifact)
npm install
npm run dev # Vite dev server (needs WebGPU-enabled browser, e.g., Chrome/Edge/Arc)npm run build # tsc && vite build (uses 4GB max-old-space-size)
npm run typecheck # tsc --noEmit
npm run lint # Currently exits 0 (no active linting rules)
npm run preview # Preview the production build locallynpm run build:worklet # Alias for bash ./build-wasm.sh
npm run build:emcc # Alias for bash scripts/build-wasm.sh- Prerequisite: Emscripten SDK (emsdk) 4.0+ activated in your shell.
- The script auto-clones and builds
libopenmptfrom GitHub if headers are missing. - Outputs go to
public/worklets/openmpt-native.js,.wasm, and.aw.js.
python3 deploy.py- Builds the project with
VITE_APP_BASE_PATHautomatically derived from theREMOTE_DIRECTORYvariable insidedeploy.py. - SFTP-uploads the
dist/folder to the configured remote server. - For manual subdirectory deployment:
VITE_APP_BASE_PATH=/xm-player/ npm run build
- TypeScript strictness is high:
strict,noUnusedLocals,noUnusedParameters,noFallthroughCasesInSwitch,noUncheckedIndexedAccess,exactOptionalPropertyTypes. - File naming: PascalCase for components (
PatternDisplay.tsx), camelCase for hooks/utilities (useLibOpenMPT.ts,patternExtractor.ts). - Tailwind content paths are explicit (not a broad
**/*glob) to avoid build OOMs caused bynode_modulesmatches. - Fix comments: The codebase prefixes engineering fixes with identifiers like
AUDIO-001 FIXandTIMING FIX. - Base URL awareness: Almost all asset URLs are constructed with
import.meta.env.BASE_URLso the app works when deployed under a subdirectory.
- No unit-test framework is currently installed (no Jest/Vitest/Playwright tests in
package.json). - GitHub Actions (
.github/workflows/ci.yml) runs two jobs:lint-and-build–npm install→npm run lint→tsc --noEmit→npm run build→ verifiesdist/index.htmlanddist/assetsexist.wasm-smoke-test– Installs Emscripten, verifiesbuild-wasm.shexists, and runsshellcheck(orbash -n) for syntax validation.
- COOP/COEP headers:
vite.config.tssets:Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: credentialless- This unlocks
SharedArrayBuffer/ Atomics for Emscripten WASM workers while still allowing cross-origin CDN resources (e.g., the esm.sh React importmap and the libopenmpt script).
- CORS: Loading MOD files or WASM from external URLs can trigger CORS errors.
utils/remoteMedia.tshandles remote fetching; ensure servers send proper CORS headers. - Service Worker:
public/sw.jsis scope-aware and works under any base path. It caches module files (.mod,.xm,.s3m,.it,.mptm,.wasm) with a cache-first strategy. - PWA:
public/manifest.jsonandsw.jsprovide installability. SW registration only happens in production builds (import.meta.env.PROD).
- Worklet Caching: Browsers cache AudioWorklet files aggressively. If you edit
openmpt-worklet.jsor any worklet asset, hard-refresh or disable cache in DevTools. - Shader Imports: If you rename a shader file in
/shaders, you must update the reference inApp.tsx(theSHADER_GROUPSconstant) and in any component that fetches the file by name (e.g.,PatternDisplay.tsx). - Base Path Mismatch: Deploying to a subdirectory without setting
VITE_APP_BASE_PATHwill break shader fetches, worklet loads, and the default module fetch. Usedeploy.pyor set the env var manually before building. - Missing Native Engine:
openmpt-native.jsdoes not exist in the repo by default. If you want the native C++ worklet option, runnpm run build:emccafter installing the Emscripten SDK. - Node OOM during build: The Tailwind config was intentionally narrowed to explicit paths. Do not broaden the
contentglob to"./**/*.{js,ts,jsx,tsx}"or production builds may run out of heap memory.