This tutorial covers SNES audio including the SPC700 sound chip, BRR samples, and SNESMOD tracker playback.
The SNES has a dedicated SPC700 sound processor with:
| Method | Best For | Size | Complexity |
|---|---|---|---|
| Direct BRR samples | One-shot SFX, voice clips | per sample | Low |
| SNESMOD | Music + tracker SFX | ~5.5KB | Medium |
Reach for direct BRR samples when you want a jump, a hit, a coin, or a voice clip played on demand — you load a .brr into the SPC700 and trigger it from C (see soundboard — the audio v2 engine, driven entirely from C). Reach for SNESMOD when you want music, or SFX that share a tracker soundbank with it. The two can coexist. To make a .brr from your own audio, see One-shot samples from WAV below.
SNESMOD is a tracker-based audio engine supporting Impulse Tracker (.it) modules.
SNESMOD is built around tracker modules. For a plain one-shot effect — a jump, a hit, a UI blip, a bit of speech — the lighter path is a direct BRR sample: convert an audio file to the SNES's BRR format once, bake it into the ROM, and trigger it from C. This is what soundboard — the audio v2 engine, driven entirely from C does.
1. Convert your WAV to BRR. wav2brr (built by make tools) turns a PCM .wav into a .brr using the same encoder smconv uses on its own samples:
Input is PCM WAV (8- or 16-bit, mono or stereo — stereo is downmixed). Keep the source at or below 32 kHz; that is the DSP's ceiling, and a higher rate just plays back sharp. Pass -v to see the block count, loop offset, and a ready-to-paste audioLoadSample() line.
2. Bake the .brr into the ROM. Put it in a data.asm with a label and an end label, exactly as the example does:
3. Load it once, play it on demand. The label becomes a C symbol; the size is the two labels subtracted, and the loop point is the byte offset wav2brr reported (0 for a one-shot):
See Sample Management in the API reference for the full sample API (audioLoadSample, audioPlaySample, audioUnloadSample).
A .brr is a build input, like a converted PNG — generate it with wav2brr and commit it next to your source WAV. There is no automatic .wav → .brr build step yet; run the tool when the source audio changes.
You MUST call snesmodProcess() every frame! Failure to do so causes:
| Component | Size |
|---|---|
| SPC700 Driver | ~5.5KB |
| Sample Data | Up to ~58KB |
| Echo Buffer | ~4KB (at $D000-$FFFF) |
Total audio RAM: 64KB
For sound effects and sample playback, LIB_MODULES += audio gives you the full engine with no SPC700 code of your own: the lib ships a resident driver (built from source at lib build time) and audio.h's 22 functions drive it — audioInit(), audioLoadSample() (BRR streamed into APU RAM at runtime), audioPlaySampleEx() (volume/pan/ pitch, 8-voice round-robin polyphony), per-voice ADSR/GAIN, and a configurable echo with FIR filter. Every call is bounded — the API returns AUDIO_ERR_TIMEOUT rather than hanging. Worked example: audio/soundboard. Main-thread only; one engine per ROM (don't link audio and snesmod together).
Choosing a path: snesmod for tracker music (IT modules), audio for C-driven samples and DSP effects, apu (below) for writing your own SPC700 program.
Since the SPC700 arc, the SDK has a second audio path: the apu module uploads a wla-spc700-assembled program straight through the IPL boot-ROM protocol — apuWaitBoot(), apuUpload(), apuExecute() — giving full DSP control (voices, ADSR, echo, pitch) with no tracker involved. Worked example: audio/speech_synth (phoneme-bank speech — upload, DSP config, per-phoneme sequencing). The two paths are exclusive: don't link apu and snesmod in one ROM.
APU-side memory layout matters: the flat binary is laid out by wlalink -b, and an .ORG section that overlaps your growing code OVERWRITES it silently (the SPC700-arc sequencer died exactly this way during development). Budget the code page before placing the sample directory.
apuWaitBoot() only works once — the IPL handshake is consumed at boot. To replace the running program later, use the cooperative reset protocol (worked example: audio/apu_switch):
Two contract points, both learned the hard way in apu_switch: apuReset() blocks forever on a program that never polls for the magic, and the next program receives the DSP dirty — a residual ADSR1 bit 7 from the previous program silently overrides GAIN, so every register a voice depends on must be written explicitly.
luna spc-dump runs a ROM and exports the complete APU state — 64 KB of ARAM plus all 128 DSP registers — as a standard playable .spc. Diffing two dumps (yours vs a reference, or two instants of your own ROM) answers in seconds what ears cannot localize: upload integrity, directory/loop addresses, per-voice ADSR/pitch/envelope state, phoneme/note schedules. --audio-out (WAV capture) complements it for spectral verification.