The Problem: No Browser-Based BRSTM Tool Existed
Every existing BRSTM converter was a desktop app — BrawlBox for Windows, Looping Audio Converter (a .NET Framework app written in C#), or command-line tools that needed compiling from source. If you were on a Mac, on Linux, or wanted to quickly convert a file without installing anything, you were out of luck.
The core challenge: BRSTM encoding requires C libraries (DspTool) that were never designed to run in a browser. And decoding game audio formats like ADX, HCA, and FSB requires vgmstream, a massive C codebase with decades of reverse-engineered format support.
Architecture: Three WebAssembly Modules in One Browser Tab
The workflow splits processing away from the main UI thread into background Web Workers, maintaining a butter-smooth 60 FPS user interface during complex encoding tasks:
+-------------------------------------------------------------------+
| MAIN THREAD (UI) |
| - Drag & Drop Handling - Audio Preview (Web Audio API) |
| - Progress Bar Rendering - SEO & Route State |
+-------------------------------------------------------------------+
|
postMessage()
v
+-------------------------------------------------------------------+
| WEB WORKERS (Background) |
| |
| 1. VgmstreamWorker 2. EncoderWorker 3. Export Worker |
| [vgmstream.wasm] [sound.wasm] [lame / vorbis] |
| Decode to PCM Encode to BRSTM Export MP3/OGG |
+-------------------------------------------------------------------+
1. vgmstream.wasm — The Universal Decoder (4.1 MB)
vgmstream is the industry-standard game audio decoding library. It supports 100+ formats — from Nintendo's BRSTM/BCSTM/BFSTM to CRI's ADX/HCA, Microsoft's XMA, FMOD's FSB, and Sony's ATRAC3. The vgmstream project provides a pre-built WASM binary, which I wrapped in a Web Worker so decoding never blocks the UI thread.
The hardest part was integrating with the Emscripten virtual filesystem that vgmstream's WASM build uses internally. Files are written to the virtual FS via FS.writeFile(), decoded via vgmstream's CLI interface (callMain(['-o', '/out.wav', '/in.bcstm'])), then read back from FS.readFile() — all inside a Web Worker.
2. sound.wasm — The BRSTM Encoder (176 KB)
The encoding process relies on DspTool (developed by Alex Barney), an MIT-licensed C library designed for generating BRSTM, BCSTM, and BFSTM files.
- Takes raw PCM audio data and outputs a fully compliant Nintendo audio file complete with loop point metadata.
-
Memory Management: PCM data is pushed into the WASM heap using Emscripten’s
_mallocand converted to match Nintendo DSP ADPCM standards.
3. lame.min.js + vorbis-encoder-bundle — The Export Encoders
For reverse conversions (BRSTM → MP3, BCSTM → OGG, FSB → MP3), browser-native encoders are required:
-
MP3: Handled efficiently via
lamejs. -
OGG Vorbis:
libvorbis.jscompiled with Emscripten, bundled with Browserify to ensure compatibility withimportScripts()inside Web Workers.
Technical Deep-Dive: The Interleaved vs. Planar PCM Bug
During development, multi-channel audio output resulted in harsh, garbled noise. Tracking this down required comparing hex dumps of PCM buffers against expected byte layouts.
Interleaved PCM (vgmstream output / Web Audio API):
[ L0, R0, L1, R1, L2, R2, ... ]
Planar PCM (DspTool expectation):
[ L0, L1, L2, ... ] [ R0, R1, R2, ... ]
The bug stemmed from an indexing calculation error during the deinterleaving pass:
// ❌ Incorrect Deinterleaving Formula
const sample = pcm[channelIndex * sampleLength + i];
// ✅ Correct Deinterleaving Formula
const sample = pcm[i * totalChannels + channelIndex];
By ensuring proper data transformation before allocation to the WASM heap, multi-channel audio was rendered cleanly without UI thread stutter.
Web Worker Memory Optimization
Loading a 4.1 MB WASM module multiple times can quickly lead to heavy memory consumption. To keep performance optimal:
- Persistent Worker Allocation: Web Workers are instantiated once and kept alive throughout the session.
-
Transferable Objects: Large
ArrayBufferpayloads exchanged viapostMessage()use transferables (postMessage(data, [data.buffer])), transferring ownership zero-copy rather than cloning large audio arrays. - WASM Module Caching: Compiled WebAssembly modules are cached in IndexedDB to bypass recompilation overhead on subsequent page visits.
Try It Out
BGM Box is 100% free and open about its technical implementation. Every conversion process executes entirely inside your client browser — no server-side uploading or processing required.
👉 bgmbox.com — Convert, Play, Loop. All in one box.
Top comments (0)