Static disassembly told us that the Super Mario World SPC700 program lives at LoROM bank $0E. It does. But that 32 KiB block is mostly other ROM data that the APU never touches. The actual samples the S-DSP plays back are not there. We traced the chip for 1000 frames with a native snes9x-devkit and found them hiding in ROM bank $07 — loaded by the driver at runtime, not by the static extraction we had been reading for weeks.

What the APU actually is
The SNES Audio Processing Unit is a separate computer. It has a Sony SPC700 8-bit CPU running at roughly 1.024 MHz (the S-SMP's 24.576 MHz master clock divided by 24), a Nintendo S-DSP that mixes eight stereo voices at 32 kHz, and 64 KB of APU RAM (ARAM) shared between the SPC700 and the DSP. The main 5A22 CPU talks to the SPC700 over four 8-bit I/O ports, and the SPC700 keeps its own 64 KB memory map. It cannot read the cartridge ROM directly. Every byte the DSP plays has to be copied into APU RAM, or ARAM, first.
The DSP does not play raw PCM. It plays BRR — Bit Rate Reduction — a 9-byte block format that packs sixteen 4-bit nibble samples plus a header (range, filter, loop, end). BRR gives a 32:9 compression ratio, which is why 32 KB of ARAM can hold several seconds of sample data. We use the SnesLab BRR reference and the Wikipedia BRR overview as ground truth for the format; our decoder in sn-brr-audio.sh matches the exact integer filter coefficients from the Snes9x SPC_DSP.cpp source.
The static story, and why it is incomplete
Super Mario World uses the N-SPC Prototype v0.0 sound engine — also called Kankichi-kun — the earliest variant of Nintendo's common SPC driver, used only in SMW and Pilotwings. SnesLab documents the engine's Prototype variant with a VCMD table at $0F90 relative to the driver base and the ROM program at $0E8000. That is where our disassembly started.
Using snes2asm and spcdas, we extracted 32 KiB from $0E8000. It looks like an audio driver. It has the right reset vector, the right APUIO port references, and a VCMD dispatch table. But when we ran sn diff-firmware to compare the static extraction against a runtime ARAM dump, only about 12 KiB matched. The rest — roughly 20 KiB of the 32 KiB bank — was ROM data the driver never uploaded.
# 32 KiB extracted from ROM bank $0E:8000 (file offset 0x070000)
# Runtime ARAM dump after 60/300/1000 frames
# diff-firmware classes: CODE/DATA/RELOCATED/ZERO_INIT
Driver region (ARAM $0200-$30D4): ~11.9 KiB
Static 32 KiB bank contains ~17 KiB that was never loaded ZERO_INIT does not mean silence. It means that address range in ARAM was never written by the SPC700. The gap is not a timing issue: we captured 60 frames, 300 frames, and 1000 frames, and the non-driver portion of the bank stayed identical. The static extraction simply overestimates the driver because it includes unrelated ROM data from the same bank.
Where the samples actually come from
The BRR data in ARAM starts around $8000. To find its source, sn-analyze-spc.sh takes 64-byte signatures from runtime ARAM at 1 KiB intervals and searches the ROM. The result is unambiguous: for Super Mario World, ARAM $8000 matches ROM $078004 and ARAM $9000 matches ROM $078F58. Both are in ROM bank $07, not bank $0E.
The source directory — a small table that maps each instrument number to a BRR start address and loop address — is placed at $0200. The driver itself sits just above it. The BRR samples sit far above, at $8000+. The driver is only about 145 bytes in our homebrew sys_test ROM and a few KiB in SMW; the samples are the bulk of the audio data, and they come from a different bank than the disassembly we started with.
The tools that made this visible
All of this work depends on the native snes9x-devkit binary we built from the CatGame sn/emu module. Unlike the old AppImage, it runs headless, dumps audio as WAV, and can trace the SPC700 program counter per instruction. The relevant commands are now part of the SNES games page and the reverse-engineering CLI.
./scripts/build.sh snes --emu
./build/linux-debug/bin/snes9x-devkit \
--rom data/sn/roms/snes/oem/Super\ Mario\ World\ \(USA\).sfc \
--frames 300 --trace-spc /tmp/spc.trace --trace-max 500000 \
--dump-audio /tmp/smw.wav
sn analyze spc --capture 60 rom.sfc
sn diff-firmware static.bin runtime-aram.bin
sn brr-audio --verify /tmp/smw.wav The trace output lets us see which code blocks execute, where branches go, and which VCMDs the music parser actually hits. The diff-firmware pass gives us a relocation map: which bytes in the static bank match ARAM, which are relocated, and which were never written. Combining the two tells us the true driver boundary and the BRR origin.
What this means for original driver design
We are not just documenting SMW. We are designing our own SPC700 driver for homebrew SNES ROMs built with llvm-mos. If we copied the static extraction from bank $0E blindly, we would ship 20 KiB of junk and miss the actual sample loader. Worse, we would not understand the contract between the 65816 CPU and the SPC700: the 65816 must upload the driver, then the driver must upload samples from ROM bank $07. That two-stage copy is the architecture.
Our homebrew sys_test ROM now uses a compact resident driver. The layout is the same pattern: a source directory at $0200, a driver at $0210, four tiny BRR samples at $02B0, and the DSP registers controlled through a latched command protocol. The 65816 side writes $2140-$2143 (CPU-side APUIO), the SPC700 side sees $F4-$F7, and the driver echoes the command back after latching parameters to avoid a race. This is the same pattern the N-SPC engine uses, simplified for our test scenes.
Coverage, caveats, and what is next
SMW's title screen music only exercises a fraction of the N-SPC engine. Our 2000-frame trace covers 101 of 1748 branch/call edges (about 5%) and 401 unique program counters. Most of the unexecuted paths are instrument setup, echo, and other VCMD handlers that trigger in levels, the world map, or boss fights. The coverage is frame-count dependent: 300 frames gives 61 edges, 2000 frames gives 101. To get full coverage we need save-state captures at multiple scenes, which is a roadmap feature for the devkit.
| Capability | Status | Notes |
|---|---|---|
Native snes9x-devkit build | implemented | >60 FPS on SMW, SDL3 display mode, headless capture |
| Per-instruction SPC trace | implemented | Configurable SPC_TRACE_MAX, call graph output |
| Diff-firmware relocation map | implemented | Identifies static vs. runtime, CODE/DATA/ZERO_INIT/RELOCATED |
| BRR origin signature scan | implemented | Verified for SMW bank $07 |
| BRR roundtrip verification | roadmap | Cross-correlate decoded BRR against emulator WAV |
| Multi-scene coverage | roadmap | Save states at title, map, level, boss |
The BRR roundtrip verifier is the next logical step. It would decode every BRR block back to PCM, capture the emulator output for the same SRCN, and run a spectral cross-correlation to prove our decoder matches the S-DSP. If you want the engineering context for the web side of this work, the companion post on running Snes9x at 60 FPS in WebAssembly covers the browser half of the same toolchain.
Cross-links
References
- SnesLab: N-SPC Prototype (Super Mario World)
- SnesLab: Bit Rate Reduction (BRR)
- Wikipedia: Bit Rate Reduction
- SPC-700 Programming Information (snesmusic.org)
- Snes9x source repository
- CatGame internal:
sn/shared/snes_emu_s9x_shim.cpp,scripts/lib/sn/sn-analyze-spc.sh,scripts/lib/sn/sn-diff-firmware.sh,sn/roms/snes/shared/spc_resident.c
