DEV Community

Junyi Zhang
Junyi Zhang

Posted on • Edited on

How I Built a BRSTM Converter with WebAssembly — Technical Deep Dive

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     |
+-------------------------------------------------------------------+

Enter fullscreen mode Exit fullscreen mode

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 _malloc and 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.js compiled with Emscripten, bundled with Browserify to ensure compatibility with importScripts() 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, ... ]

Enter fullscreen mode Exit fullscreen mode

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];

Enter fullscreen mode Exit fullscreen mode

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:

  1. Persistent Worker Allocation: Web Workers are instantiated once and kept alive throughout the session.
  2. Transferable Objects: Large ArrayBuffer payloads exchanged via postMessage() use transferables (postMessage(data, [data.buffer])), transferring ownership zero-copy rather than cloning large audio arrays.
  3. 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.comConvert, Play, Loop. All in one box.

Top comments (0)