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

Music Composition

The GBA has four PSG (Programmable Sound Generator) channels: two square waves, one wave (sample) channel, and one noise channel. Rather than manually writing register values, stdgba lets you compose music using Strudel notation (a text-based mini-language for patterns) and compiles it to an optimised event table at build time.

Quick Start

#include <gba/music>
#include <gba/peripherals>
#include <gba/bios>

using namespace gba::music;
using namespace gba::music::literals;

int main() {
    // Enable sound output
    gba::reg_soundcnt_x = { .master_enable = true };
    gba::reg_soundcnt_l = {
        .volume_right = 7, .volume_left = 7,
        .enable_1_right = true, .enable_1_left = true,
        .enable_2_right = true, .enable_2_left = true,
        .enable_3_right = true, .enable_3_left = true,
        .enable_4_right = true, .enable_4_left = true
    };
    gba::reg_soundcnt_h = { .psg_volume = 2 };

    // Compile a simple melody
    static constexpr auto music = compile(note("c4 e4 g4 c5"));

    // Play it in a loop
    auto player = music_player<music>{};
    while (player()) {
        gba::VBlankIntrWait();
    }
}

Pattern Syntax

Patterns use Strudel notation. Here’s the reference:

SyntaxMeaningExample
c4 e4 g4Sequence (space-separated notes)"c4 e4 g4"
~Rest (silence)"c4 ~ g4"
_Hold/tie (sustain, no retrigger)"c4 _ _" (hold for 3 steps)
[a b]Subdivision (fit into one parent step)"[c4 d4] e4"
<a b c>Alternating (cycle through each step)"<c4 d4 e4>"
<a, b>Parallel layers (commas create stacked voices)"<c4, g3>"
a@3Elongation (weight = 3)"c4@3 e4"
a!3Replicate (repeat 3 times equally)"c4!3"
a*2Fast (play 2x in one step)"c4*2"
a/2Slow (stretch over 2 cycles)"c4/2"
(3,8)Euclidean rhythm (Bjorklund: 3 pulses in 8 steps)"c4(3,8)"
eb3Flat notation (Eb3 = D#3)"eb3 f3 g3"

Creating Melodies with note()

note() is the main function for creating pitched patterns:

// Single melody (auto-assigned to square 1)
auto melody = note("c4 e4 g4 c5");

// With modifiers
auto fast = note("c4*2 e4*2");  // Double speed
auto slow = note("c4/2");        // Stretch over 2 cycles
auto rests = note("c4 ~ ~ e4");  // With silences

All notes from C2 to B8 are supported. Octave-1 notes (C1-B1) are rejected at compile time because the PSG hardware cannot represent those frequencies.

Multi-Voice Patterns with Stacking

Create parallel voices using commas inside <>:

// Two voices: melody (sq1) + bass (sq2)
static constexpr auto music = compile(
    note("<c4 e4 g4 c5, c3 c3 c3 c3>")
);

// Or use the stack() combinator
static constexpr auto music = compile(
    stack(
        note("c4 e4 g4 c5"),
        note("c3 c3 c3 c3"),
        s("bd sd bd sd")  // Drums on noise channel
    )
);

The layers are auto-assigned to channels in order: square 1 -> square 2 -> wave -> noise.

PSG Channels (CH1-CH4)

Use one page per channel when you need hardware details:

Quick inline examples:

using namespace gba::music;
using namespace gba::music::literals;

auto lead = "c4 e4 g4 c5"_sq1;
auto bass = note("c3 c3 g2 g2").channel(channel::sq2);
auto pad = note("c4 _ g4 _").channel(channel::wav, waves::triangle);
auto drums = s("bd sd hh sd");

static constexpr auto song = compile(loop(stack(lead, bass, pad, drums)));

Drums with s()

The s() function creates drum patterns using Strudel percussion names. It auto-assigns to the noise channel:

// Kick + snare beat
auto beat = s("bd sd bd sd");

// Euclidean kick pattern
auto kick = s("bd(3,8)");

// Complex drum pattern
auto drums = s("bd [sd rim]*2 bd sd");

20 drum presets are supported: bd, sd, hh, oh, cp, rs, rim, lt, mt, ht, cb, cr, rd, hc, mc, lc, cl, sh, ma, ag.

Chaining with Sequential (seq())

Combine multiple patterns sequentially. Instrument changes are emitted at boundaries:

static constexpr auto music = compile(
    loop(
        seq(
            note("c4 e4 g4 c5"),
            note("d4 f4 a4 d5"),
            note("e4 g4 b4 e5")
        )
    )
);

Compile-Time Tempos

By default, compile() uses 0.5 cycles-per-second (120 BPM in 4/4). Override it:

// Explicit BPM
static constexpr auto music = compile<120_bpm>(note("c4 e4 g4"));

// Or cycles-per-second
static constexpr auto music = compile<1_cps>(note("c4 e4 g4"));

// Or cycles-per-minute
static constexpr auto music = compile<30_cpm>(note("c4 e4 g4"));

Pattern Functions

All patterns support transformation methods:

auto melody = note("c4 e4 g4 c5");

melody.add(12);       // Transpose up one octave
melody.sub(5);        // Transpose down 5 semitones
melody.rev();         // Reverse the sequence
melody.ply(2);        // Stutter (repeat each note 2x)
melody.press();       // Staccato (half duration + rest)
melody.late(1, 8);    // Shift 1/8 cycle later (swing)

User-Defined Literal Shorthands

For convenience, single-note assignments use UDLs:

using namespace gba::music::literals;

auto melody = "c4 e4 g4"_sq1;   // Assign to square 1
auto bass = "c3 c3"_sq2;         // Assign to square 2
auto sample = "c4 d4"_wav;       // Use wave channel
auto drums = "bd sd hh"_s;       // Drums (noise channel)

WAV Channel & Custom Waveforms

The wave channel (CH3) can play 4-bit sampled audio. Use built-in waveforms or embed .wav files:

For a deeper guide to wav_embed(), resampling limits, and custom sample authoring, see Embedded WAV Samples.

// Built-in waveforms
using namespace gba::music::waves;

auto melody = note("c4 e4 g4").channel(channel::wav, sine);

// Embed a .wav file (requires C++26 #embed and GCC 15+)
static constexpr auto piano = gba::music::wav_embed([] {
    return std::to_array<unsigned char>({
#embed "Piano.wav"
    });
});

static constexpr auto music = compile(
    note("<c4 e4 g4, c3>")
        .channels(layer_cfg{channel::wav, piano}, channel::sq2)
);

Playing Music

Use music_player with NTTP (non-type template parameter) syntax:

static constexpr auto music = compile(note("c4 e4 g4 c5"));

auto player = music_player<music>{};  // Pass as template argument

// Play in VBlank loop
while (player()) {
    gba::VBlankIntrWait();
}

music_player::operator() returns false when the pattern ends (for non-looping patterns) or loops forever.

Performance

Music playback uses tail-call recursive dispatch over compile-time batches. Per-frame cost:

  • Idle frame (no events): ~400 cycles (~0.6% of VBlank)
  • 4-channel batch dispatch: ~760 cycles (~1.1% of VBlank)

This leaves >99% of VBlank budget for game logic.