Skip to content

L32 — Audio

Goal

Initialize the mixer, play SFX on button presses, and toggle XM music. Never forget mixer_try_play().

In plain English

N64 audio is mixed on the RSP. Your CPU:

  1. Loads samples from ROM (wav64_load, xm64player_open)
  2. Starts voices on mixer channels (wav64_play, xm64player_play)
  3. Every frame, calls mixer_try_play() so buffers keep flowing

What you will see

bash
make -C lessons/l32-audio
InputSound
ACollect SFX
BUI blip
STARTWin jingle
ZMusic on/off

Core code shape

c
audio_init(48000, 4);   /* must be >= wav64 sample rate (Opus → 48 kHz) */
mixer_init(16);
wav64_init_compression(3);  /* match Makefile --wav-compress 3 */

wav64_t *sfx = wav64_load("rom:/collect.wav64", NULL);
xm64player_t music;
xm64player_open(&music, "rom:/music.xm64");
xm64player_set_loop(&music, true);

// each frame, after input/render:
mixer_try_play();

Mixer channel map (important)

Course assets ship a stereo collect.wav. Stereo SFX occupies two mixer channels (ch and ch+1). Put UI/win on later mono channels and start XM after them:

c
CH_COLLECT = 0  /* stereo → also uses 1 */
CH_UI      = 2
CH_WIN     = 3
CH_BGM     = 4  /* music.xm uses 8 channels → 4..11 */

Overlapping a mono voice onto the stereo pair (or restarting win on the collect channel while Opus is decoding) can assert:

samplebuffer_get: window … not contiguous

Sample rate vs audio_init

Our pipeline converts WAVs with Opus (--wav-compress 3), which produces 48 kHz wav64 files. If you call audio_init(44100, …), playing SFX asserts:

frequency 48000 exceeds configured limit 44095 on channel N

Use audio_init(48000, 4) (or resample assets to match a lower output rate).

Volume etiquette

Keep music under SFX (~0.5–0.6). Avoid stacking five loud one-shots on the same channel without planning.

Exercises

  1. Map collect SFX to a different button.
  2. Lower music volume with xm64player_set_vol.

Full lesson source

The blocks below are imported from the real repository files at build time (VitePress <<< snippets). They are not hand-copied into this markdown.

lessons/l32-audio/Makefile · lessons/l32-audio/src/main.c

lessons/l32-audio/Makefile
make
ROMNAME   := l32_audio
ROM_TITLE := "L32 Audio"

include ../../common/lesson.mk
lessons/l32-audio/src/main.c
c
/**
 * L32 — Audio (mixer + WAV SFX + XM music)
 * ============================================================================
 *
 * LEARNING GOAL
 * -------------
 * Make the N64 play sounds. Audio is mixed on the RSP; your job is to:
 *   1. Init audio + mixer once
 *   2. Load wav64 / xm64 from the ROM
 *   3. Start playback on a channel when something happens
 *   4. Call mixer_try_play() EVERY FRAME (or audio glitches / stops)
 *
 * CHANNELS
 * --------
 * Think of mixer channels as simultaneous "voice slots".
 * We reserve:
 *   0 = SFX A (collect)
 *   1 = SFX B (ui)
 *   2+ = music (XM may use several starting at 2)
 *
 * ASSETS (converted at build by common/lesson.mk)
 * -----------------------------------------------
 *   assets/collect.wav → filesystem/collect.wav64
 *   assets/ui.wav      → ui.wav64
 *   assets/win.wav     → win.wav64
 *   assets/music.xm    → music.xm64
 *
 * CONTROLS: A collect, B ui, START win, Z music toggle
 * BUILD:    make -C lessons/l32-audio
 * DOCS:     docs/guide/m5/l32-audio.md
 */

#include <libdragon.h>
#include <stdio.h>

/* collect is stereo (0+1); win/ui mono; XM (8 ch) from CH_BGM */
#define CH_COLLECT 0
#define CH_UI      2
#define CH_WIN     3
#define CH_BGM     4

int main(void)
{
    debug_init_isviewer();
    debug_init_usblog();
    display_init(RESOLUTION_320x240, DEPTH_16_BPP, 2, GAMMA_NONE, FILTERS_RESAMPLE);
    rdpq_init();
    rdpq_text_register_font(1, rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_VAR));
    joypad_init();

    /* Need DFS before rom:/ audio paths work. */
    dfs_init(DFS_DEFAULT_LOCATION);

    /*
     * audio_init(output_hz, num_buffers)
     * mixer_init(max_channels)
     * wav64_init_compression(level) must match how files were converted
     *   (we use --wav-compress 3 = Opus in the Makefile rule).
     *
     * Opus wav64 is stored at 48 kHz. audio_init must be >= that rate or
     * wav64_play asserts: "frequency 48000 exceeds configured limit …".
     */
    audio_init(48000, 4);
    mixer_init(16);
    wav64_init_compression(3);

    /* Load SFX into RAM-ish streaming structures. NULL = default load params. */
    wav64_t *sfx_collect = wav64_load("rom:/collect.wav64", NULL);
    wav64_t *sfx_ui = wav64_load("rom:/ui.wav64", NULL);
    wav64_t *sfx_win = wav64_load("rom:/win.wav64", NULL);

    /* XM music player — multi-channel tracker music, great size on cartridge. */
    xm64player_t music;
    xm64player_open(&music, "rom:/music.xm64");
    xm64player_set_loop(&music, true); /* restart when the song ends */
    bool music_on = false;

    char line[64];
    int plays = 0; /* how many one-shots we've fired (for the HUD) */

    while (1) {
        joypad_poll();
        /* "Pressed" = edge this frame (not held). Perfect for one-shots. */
        joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1);

        if (pressed.a && sfx_collect) {
            /* Play on channel 0. Starting again interrupts the previous SFX on 0. */
            wav64_play(sfx_collect, CH_COLLECT);
            plays++;
        }
        if (pressed.b && sfx_ui) {
            wav64_play(sfx_ui, CH_UI);
            plays++;
        }
        if (pressed.z) {
            music_on = !music_on;
            if (music_on) {
                /* first_ch = CH_BGM; XM may occupy several channels from here. */
                xm64player_play(&music, CH_BGM);
            } else {
                xm64player_stop(&music);
            }
        }
        if (pressed.start && sfx_win) {
            wav64_play(sfx_win, CH_WIN);
        }

        /* Simple 2D UI — no Tiny3D this lesson. */
        surface_t *disp = display_get();
        rdpq_attach(disp, NULL);
        rdpq_clear((color_t){ .r = 20, .g = 24, .b = 40, .a = 255 });
        rdpq_text_print(NULL, 1, 20, 40, "L32 — Audio");
        rdpq_text_print(NULL, 1, 20, 70, "A collect  B ui  START win");
        rdpq_text_print(NULL, 1, 20, 90, "Z music on/off");
        snprintf(line, sizeof(line), "plays=%d  music=%s", plays, music_on ? "ON" : "OFF");
        rdpq_text_print(NULL, 1, 20, 120, line);
        rdpq_text_print(NULL, 1, 20, 160, "mixer_try_play every frame");
        rdpq_detach_show();

        /*
         * CRITICAL: mix audio for any free output buffer.
         * Call this once per loop even if nothing is playing.
         * Capstone and all game ROMs do the same at the end of the frame.
         */
        mixer_try_play();
    }
}

Next

L33 — HUD.

N64 Educator v1.2.2 — libdragon + Tiny3D · branch master