DEV Community

Junyi Zhang
Junyi Zhang

Posted 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 (requiring Java), or custom 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 third-party executables, you were simply out of luck.

The core technical challenge comes down to library availability:

  • Encoding: BRSTM encoding relies on C libraries like DspTool, which were written exclusively for native desktop environments.
  • Decoding: Decoding proprietary game audio formats like ADX, HCA, and FSB requires vgmstream, a massive C codebase representing decades of reverse-engineered audio format support.

Architecture: Three WebAssembly Modules in One Browser Tab

To bring this native toolkit to the browser, BGM Box orchestrates three distinct WebAssembly modules compiled from C via Emscripten.

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, supporting over 100+ formats (Nintendo's BRSTM/BCSTM/BFSTM, CRI's ADX/HCA, Microsoft's XMA, FMOD's FSB, and Sony's ATRAC3).

  • Emscripten Virtual Filesystem: Because vgmstream expects a POSIX filesystem, input files are written directly into memory via FS.writeFile().
  • Worker Execution: The CLI interface is invoked via callMain(['-o', '/out.wav', '/in.bcstm']), after which the decoded stream is read back using FS.readFile() within the 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)