OpenSNES
Modern Open-Source SNES Development SDK
Loading...
Searching...
No Matches
Audio & Music Tutorial

This tutorial covers SNES audio including the SPC700 sound chip, BRR samples, and SNESMOD tracker playback.

SNES Audio Architecture

The SNES has a dedicated SPC700 sound processor with:

  • 8 audio channels
  • 64KB dedicated audio RAM
  • BRR (Bit Rate Reduction) sample compression
  • Hardware ADSR envelopes
  • Echo/reverb effects

Audio Options in OpenSNES

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 (Recommended)

SNESMOD is a tracker-based audio engine supporting Impulse Tracker (.it) modules.

Setup

  1. Create music in a tracker (OpenMPT recommended)
  2. Export as Impulse Tracker (.it) format
  3. Convert with smconv tool
  4. Link soundbank with your ROM

Makefile Configuration

# Enable SNESMOD
USE_SNESMOD := 1
USE_LIB := 1
LIB_MODULES := console sprite input
# Soundbank source files
SOUNDBANK_SRC := music/mymusic.it sfx/effects.it

Basic Music Playback

#include <snes.h>
#include <snes/snesmod.h>
#include "soundbank.h"
int main(void) {
// Initialize SNESMOD (uploads SPC driver)
// Set soundbank location
snesmodSetSoundbank(SOUNDBANK_BANK);
// Load a module (defined in soundbank.h)
snesmodLoadModule(MOD_MYMUSIC);
// Start playback
snesmodPlay(0); // 0 = start from beginning
while (1) {
// MUST call every frame!
}
return 0;
}
int main(void)
Definition main.c:66
void consoleInit(void)
Initialize SNES hardware.
void WaitForVBlank(void)
Wait for next VBlank period.
void setScreenOn(void)
Enable screen display.
Definition console.h:115
void snesmodSetSoundbank(u8 bank)
Set the soundbank bank number.
void snesmodInit(void)
Initialize SNESMOD audio engine.
void snesmodPlay(u8 startPosition)
Start module playback.
void snesmodLoadModule(u16 moduleId)
Load a module from the soundbank.
void snesmodProcess(void)
Process audio commands.
OpenSNES common-case master header.
SNESMOD - Tracker-based Audio Engine.

Sound Effects

#include <snes/snesmod.h>
#include "soundbank.h"
// Load effects at startup
void init_audio(void) {
snesmodSetSoundbank(SOUNDBANK_BANK);
// Load sound effects (indices from soundbank.h)
snesmodLoadEffect(0); // Jump sound
snesmodLoadEffect(1); // Coin sound
snesmodLoadEffect(2); // Hit sound
}
// Play effect when needed
void play_jump_sound(void) {
// snesmodPlayEffect(effectId, volume, pan, pitch)
// volume: 0-127
// pan: 0=left, 128=center, 255=right
// pitch: SNESMOD_PITCH_LOW/NORMAL/HIGH (4/8/12)
}
void play_coin_sound(void) {
// Higher pitch for coins
}
#define SNESMOD_PITCH_NORMAL
Definition snesmod.h:331
#define SNESMOD_PITCH_HIGH
Definition snesmod.h:332
u8 snesmodLoadEffect(u16 sfxIndex)
Load a sound effect from the soundbank.
u8 snesmodPlayEffect(u16 effectId, u8 volume, u8 pan, u16 pitch)
Play a sound effect.

One-shot samples from WAV (wav2brr)

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:

# one-shot effect
wav2brr res/jump.wav res/jump.brr
# a looping sample (loop between sample indices 2048 and 8192)
wav2brr --loop 2048 8192 res/cello.wav res/cello.brr

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:

.section ".samples" superfree
brr_jump: .incbin "res/jump.brr"
brr_jump_end:
.ends

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):

extern u8 brr_jump[], brr_jump_end[];
audioLoadSample(0, brr_jump, (u16)(brr_jump_end - brr_jump), 0);
// ...later, when the player jumps:
u8 audioPlaySample(u8 sampleId)
Play a sample with default settings.
u8 audioLoadSample(u8 id, const u8 *brrData, u16 size, u16 loopPoint)
Load a BRR sample into SPC700 RAM.
unsigned short u16
16-bit unsigned integer (0 to 65535)
Definition types.h:53
unsigned char u8
8-bit unsigned integer (0 to 255)
Definition types.h:47

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.

Volume Control

// Set master volume (0-127)
// Fade out over time
snesmodFadeVolume(0, 4); // target=0, speed=4
// Pause/Resume
// Stop completely
void snesmodStop(void)
Stop module playback.
void snesmodPause(void)
Pause module playback.
void snesmodResume(void)
Resume module playback.
void snesmodSetModuleVolume(u8 volume)
Set module volume.
void snesmodFadeVolume(u8 targetVolume, u8 speed)
Fade module volume.

Creating Music with OpenMPT

  1. Download OpenMPT (free)
  2. Create new Impulse Tracker module
  3. Keep within SNES limits:
    • Max 8 channels
    • Total samples < 58KB
    • 32kHz max sample rate
  4. Export as .it file

smconv Tool

# Convert music for soundbank
smconv -s -o soundbank -b 1 music.it effects.it
# Output files:
# - soundbank.asm (link with ROM)
# - soundbank.h (module/effect IDs)
# - soundbank.bnk (binary data)

Pitch Constants

#define SNESMOD_PITCH_LOW 4 // 16kHz - lower pitch
#define SNESMOD_PITCH_NORMAL 8 // 32kHz - normal pitch
#define SNESMOD_PITCH_HIGH 12 // 48kHz - higher pitch

Important: Call snesmodProcess()!

You MUST call snesmodProcess() every frame! Failure to do so causes:

  • Audio glitches and stuttering
  • Command buffer overflow
  • Desynchronization
while (1) {
snesmodProcess(); // CRITICAL!
// ... game logic
}

Example: Music + SFX

#include <snes.h>
#include <snes/snesmod.h>
#include "soundbank.h"
int main(void) {
u16 pad, pad_prev = 0, pad_pressed;
// Initialize audio
snesmodSetSoundbank(SOUNDBANK_BANK);
// Load music module
snesmodLoadModule(MOD_LEVEL1);
// Load sound effects
snesmodLoadEffect(0); // Jump
snesmodLoadEffect(1); // Coin
// Start music
while (1) {
// Read input
while (REG_HVBJOY & 0x01) {}
u16 pad = REG_JOY1L | (REG_JOY1H << 8);
pad_pressed = pad & ~pad_prev;
pad_prev = pad;
if (pad == 0xFFFF) pad_pressed = 0;
// Play SFX on button press
if (pad_pressed & KEY_A) {
}
if (pad_pressed & KEY_B) {
}
}
return 0;
}
#define REG_JOY1H
Joypad 1 data high (R).
Definition registers.h:334
#define REG_JOY1L
Joypad 1 data low (R).
Definition registers.h:331
#define REG_HVBJOY
H/V blank and joypad status (R).
Definition registers.h:307
#define KEY_A
Definition input.h:81
#define KEY_B
Definition input.h:71

Memory Usage

Component Size
SPC700 Driver ~5.5KB
Sample Data Up to ~58KB
Echo Buffer ~4KB (at $D000-$FFFF)

Total audio RAM: 64KB

Tips

  1. Keep samples small - Use lower sample rates for less important sounds
  2. Reuse samples - Same sample at different pitches for variety
  3. Test on hardware - Emulator timing may differ
  4. Use echo sparingly - Takes 4KB+ of audio RAM

Examples

  • examples/audio/snesmod_music/ - Music playback demo
  • examples/audio/snesmod_sfx/ - Sound effects demo

Next Steps

The audio module (v2) — samples and effects from pure C

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.

The raw APU path (no snesmod)

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.

Hot-swapping APU programs

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):

  1. Build the APU program with APU_CHECK_RESET (from templates/memmap_spc700.inc) inside its wait loops. The idiom polls CPUIO0 for APU_RESET_MAGIC; on match it silences the DSP, acks, re-enables the IPL ROM and jumps to $FFC0 — the boot ROM is re-entrant.
  2. On the 65816, call apuReset(), then apuUpload()/apuExecute() for the next program — but NOT apuWaitBoot() (the reset already consumed the ready signal).

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.

Debugging audio: luna's spc-dump

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.