Skip to content

Tutorial 09 Custom Music

bryanthaboi edited this page Jul 19, 2026 · 1 revision

Tutorial 09 — Custom music

Goal. Add a file-based song, retarget a map's theme, author an authentic chiptune natively, and swap themes conditionally with a hook.

Prerequisites. Tutorial 07 (a map to score); Guide: Audio Authoring.

Files created.

mods/tutorial_09_music/
├── manifest.json
├── main.lua
└── assets/
    ├── town.ogg
    └── town_loop.ogg

Steps

  1. Register a file-based song. Song defs are shape-dispatched per record — a { file = ... } def streams audio while every other song keeps playing through the chip synth:

    return function(mod)
      mod.content.music:register("Music_ModTown", {
        file = mod.assets:path("assets/town.ogg"),
        loopFile = mod.assets:path("assets/town_loop.ogg"),
      })
    end

    file plays once as the intro, then loopFile loops; a def with only file loops it. Outcome: the def validates; nothing plays it yet.

  2. Retarget a map's theme.

    mod.content.map_songs:override("MODROUTE", "Music_ModTown")

    map_songs maps map id → music id (Data.audio.mapSongs). Outcome: entering MODROUTE plays your song. Retargeting a vanilla map (map_songs:override("PALLET_TOWN", "Music_ModTown")) is the same one-liner.

  3. Author a chiptune. src.audio.ChipAsm is a supported require — note-event Lua tables assembled into the same channel bytecode the ROM's own songs run on:

    local ChipAsm = require("src.audio.ChipAsm")
    
    mod.content.music:register("Music_ModChip", ChipAsm.song({
      tempo = 140,
      channels = {
        { program = {
            { label = "top" },
            { notetype = { speed = 12, volume = 15, fade = 2 } },
            { octave = 4 },
            { note = "C", len = 4 },
            { note = "E", len = 4 },
            { note = "G", len = 4 },
            { octave = 5 },
            { note = "C", len = 8 },
            { loop = { count = 0, to = "top" } },   -- loop forever
        } },
      },
    }))

    Each event table is one command — note/len, rest, octave, notetype (speed/volume/fade), drum (channel 4), label, call, loop = { count, to } (count 0 = forever), vibrato, duty, tempo, and friends. ChipAsm.song returns a { chip = ... } def the music registry accepts directly. Outcome: a genuine GB channel program with no audio file in the mod. (Full event vocabulary: Audio Authoring.)

  4. Conditional music with the hook. Every song choice — map themes, battle themes, jingles — passes through one choke point:

    mod.hooks:wrap("music.select", function(next, song, ctx)
      if ctx.reason == "map" and ctx.surfing then
        return "Music_ModChip"
      end
      return next(song, ctx)
    end)

    Outcome: surfing anywhere plays your chip track; everything else is untouched. ctx carries reason, mapId, mapSong, onBike, surfing, kind/battleKind, trainerId (Reference: Hooks).

Checkpoint

MODROUTE plays the file song with a clean intro→loop seam; the chip song plays while surfing; disabling the mod restores every vanilla theme. Nothing about base audio was disabled to get here.

Common pitfalls

  • Forgetting loopFile. The theme plays its intro and stops; loop material belongs in loopFile (or make file self-looping).
  • A bad file path. A def whose file cannot load is skipped with a logged, mod-attributed error the next time its cue fires — the rest of the game's audio keeps playing. Watch the log, not silence.
  • Song id collisions. music:register("Music_Cities1", ...) collides with the imported def; override is the verb for replacing a vanilla song.

Next: Tutorial 10 — New Mechanic.

Clone this wiki locally