Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

DMA PCM Playback

The GBA has two Direct Sound channels for playing signed 8-bit PCM. A hardware timer sets the sample rate while DMA keeps the sound FIFO supplied without a CPU write for every sample.

The playback pipeline

Direct Sound A consumes one sample whenever its selected timer overflows. FIFO A holds 32 bytes; when it falls to half full, the hardware asks DMA1 for another four words.

Playback requires four pieces of hardware configuration:

  1. reg_soundcnt_x enables the sound hardware.
  2. reg_soundcnt_h routes Direct Sound A to the speakers, selects timer 0, and clears FIFO A.
  3. dma::to_fifo_a() configures DMA1 for repeating, FIFO-triggered word transfers.
  4. Timer 0 overflows at the required sample rate.

Direct Sound samples range from -128 to 127, with 0 representing silence. WAV files store 8-bit PCM as unsigned values, so they must be converted before playback.

Looping a sine wave

The first demo generates a 32-sample sine wave and streams it continuously:

#include <gba/bios>
#include <gba/dma>
#include <gba/interrupt>
#include <gba/peripherals>

#include <array>
#include <cstdint>

namespace {

    constexpr unsigned short samples_per_frame = 96;
    constexpr unsigned short timer_period = 280896 / samples_per_frame;
    constexpr unsigned short timer_reload = 65536 - timer_period;

    constexpr std::array<std::int8_t, 32> sine_period{
        0, 25,  49,  71,  90,  106,  117,  125,  127,  125,  117,  106,  90,  71,  49,  25,
        0, -25, -49, -71, -90, -106, -117, -125, -127, -125, -117, -106, -90, -71, -49, -25,
    };

    constexpr auto make_sample_buffer() {
        std::array<std::int8_t, samples_per_frame + sine_period.size()> result{};
        for (std::size_t index = 0; index < result.size(); ++index) {
            result[index] = sine_period[index % sine_period.size()];
        }
        return result;
    }

    alignas(4) constexpr auto samples = make_sample_buffer();

    void rewind_audio_dma() {
        gba::reg_dmacnt_h[1] = {};
        gba::reg_dma[1] = gba::dma::to_fifo_a(samples.data());
    }

} // namespace

int main() {
    gba::irq_handler = {[](gba::irq flags) {
        if (flags.vblank) {
            rewind_audio_dma();
        }
    }};
    gba::reg_dispstat = {.enable_irq_vblank = true};
    gba::reg_ie = {.vblank = true};

    gba::reg_soundcnt_x = {.master_enable = true};
    gba::reg_soundcnt_h = {
        .dma_a_volume = true,
        .dma_a_right = true,
        .dma_a_left = true,
        .dma_a_reset = true,
    };

    rewind_audio_dma();
    gba::reg_tmcnt_l_reload[0] = timer_reload;
    gba::reg_tmcnt_h[0] = {.cycles = gba::cycles_1, .enabled = true};
    gba::reg_ime = true;

    while (true) {
        gba::VBlankIntrWait();
    }
}

The GBA takes 280896 CPU cycles to draw one frame. The demo plays 96 samples per frame, giving an exact timer period:

timer period = 280896 / 96 = 2926 cycles
sample rate = 16777216 / 2926 = 5734 Hz
timer reload = 65536 - 2926 = 62610

The 32-sample waveform repeats three times per frame. At VBlank, the interrupt handler restarts DMA1 from the beginning of the buffer without breaking the waveform’s phase.

The DMA repeat flag keeps the channel active, but does not reset its source address. The sample buffer therefore includes one extra waveform period for FIFO read-ahead, preventing the final DMA burst from reading beyond the array.

Playing an embedded WAV once

embed::wav() loads an uncompressed mono 8-bit PCM WAV at compile time. It converts unsigned WAV samples to signed Direct Sound samples, provides a ready-to-write sound.timer configuration and whole-frame playback duration, then uses the rest of the final frame to fade the last sample to zero. A silent FIFO DMA lookahead follows the fade.

The second demo plays Piano.wav when the program starts. Press A to play it again:

#include <gba/bios>
#include <gba/dma>
#include <gba/embed>
#include <gba/interrupt>
#include <gba/keyinput>
#include <gba/peripherals>

#include <array>
#include <cstddef>

namespace {

    static constexpr auto sound = gba::embed::wav<>([] {
        return std::to_array<unsigned char>({
#embed "Piano.wav"
        });
    });

    volatile std::size_t frames_remaining{};

    void stop_sound() {
        gba::reg_tmcnt_h[0] = {};
        gba::reg_dmacnt_h[1] = {};
        gba::reg_soundcnt_h = {
            .dma_a_volume = true,
            .dma_a_right = true,
            .dma_a_left = true,
            .dma_a_reset = true,
        };
        frames_remaining = 0;
    }

    void play_sound() {
        stop_sound();
        gba::reg_dma[1] = gba::dma::to_fifo_a(sound.samples.data());
        frames_remaining = sound.frame_count;
        gba::reg_tmcnt[0] = sound.timer;
    }

} // namespace

int main() {
    gba::irq_handler = {[](gba::irq flags) {
        if (flags.vblank && frames_remaining != 0) {
            frames_remaining = frames_remaining - 1;
            if (frames_remaining == 0) {
                stop_sound();
            }
        }
    }};
    gba::reg_dispstat = {.enable_irq_vblank = true};
    gba::reg_ie = {.vblank = true};

    gba::reg_soundcnt_x = {.master_enable = true};
    gba::reg_ime = true;

    gba::keypad keys;
    while (true) {
        gba::VBlankIntrWait();
        keys = gba::reg_keyinput;
        if (keys.pressed(gba::key_a)) {
            play_sound();
        }
    }
}

The sample clock is the demo’s only timer. Playback starts immediately after VBlank, then the VBlank handler counts sound.frame_count video frames and stops timer 0 and DMA1 after the recording has finished. The generated DMA source fades to zero through the rest of the final frame and includes a silent 16-sample FIFO lookahead, so frame-quantized stopping cannot read past the buffer or produce a noise burst.

Piano.wav is mono 8-bit PCM at 8363 Hz. The demo uses embed::wav<16384>() to linearly resample it at compile time. This gives timer 0 an exact 1024-cycle period and moves low-rate playback images above the most audible range. Omitting the template argument preserves the source sample rate.

The same WAV is used by the Embedded WAV Samples demo for the hardware wave channel. That path extracts a short 4-bit waveform; embed::wav() retains the complete PCM recording for Direct Sound.

Next steps