1
Guide Audio Authoring
bryanthaboi edited this page 2026-07-19 16:19:15 -04:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Audio authoring

Songs, sound effects, and cries are data. Every def is shape-dispatched per record (src/core/Music.lua, src/core/Sound.lua), so one file-backed song, one authored chiptune, and the whole vanilla chip soundtrack coexist in a single dataset — there is no global "audio mode" to flip and no way for one def to brick the rest.

The four registries

Registry Target Keyed by
music Data.audio.songs song id (Music_...)
sfx Data.audio.sfx effect id (Save, Trade_Machine, ...)
cries Data.audio.cries species id
map_songs Data.audio.mapSongs map id → music id

(The v1 audio registry still works for whole-key swaps of Data.audio but is deprecated in favor of these.)

Song def shapes

-- rom chip reference (what the importer writes)
{ address = 0x822E, bank = 0x02 }

-- file-backed: file plays once as the intro, loopFile loops after
{ file = "mods/my_mod/assets/town.ogg",
  loopFile = "mods/my_mod/assets/town_loop.ogg" }

-- authored chip program
ChipAsm.song({ tempo = 140, channels = { { program = { ... } } } })

A def with only file loops that file; a missing loopFile degrades to the intro alone. A def that fails to load is skipped with a logged, mod-attributed error when its cue fires — the rest of the soundtrack plays on.

SFX and cries

mod.content.sfx:register("SFX_MOD_CHIME", { file = mod.assets:path("assets/chime.ogg") })
mod.content.cries:register("MODMON", { file = mod.assets:path("assets/cry.wav") })
mod.content.cries:patch("PIKACHU", { pitch = 200 })   -- retune a vanilla cry

Cry shapes: { file }, a { base = "<species>", pitch?, length? } derivation borrowing another cry's header, a raw { header, pitch, length }, or a chip program via ChipAsm.sfx.

ChipAsm — authentic channel programs

src.audio.ChipAsm (a supported require) assembles note-event Lua tables into the same channel bytecode the ROM's own songs run on:

local ChipAsm = require("src.audio.ChipAsm")

local song = ChipAsm.song({
  tempo = 140,                     -- rides the first channel
  channels = {
    { program = {                  -- channel 1 (squares 1-2, wave 3, noise 4)
        { label = "top" },
        { notetype = { speed = 12, volume = 15, fade = 2 } },
        { octave = 4 },
        { note = "C", len = 4 }, { note = "E", len = 4 },
        { rest = 4 },
        { loop = { count = 0, to = "top" } },
    } },
    { hw = 4, program = {          -- drums live on channel 4
        { drum = 3, len = 8 },
        { loop = { count = 0, to = 1 } },
    } },
  },
})
mod.content.music:register("Music_ModSong", song)

Event vocabulary (one key per event, src/audio/ChipAsm.lua): note (+len 116), pitch (explicit 011), rest, octave (18), notetype (speed, volume, fade; wave channel takes waveLevel/ waveInstrument), drum (channel 4 only), label / call / ret / loop = { count, to } (count 0 = forever), vibrato, slide, duty, dutyPattern, tempo, pan, perfectPitch, squareNote/noiseNote (effect programs), pitchSweep. subroutines = { name = { rows } } on a channel spec serves call. Assembly errors name the channel and event index. ChipAsm.sfx builds effect programs the same way for the sfx registry.

Render without booting the game:

python3 tools/modkit.py bounce Music_ModSong --seconds 30

Retargeting and the hook

mod.content.map_songs:override("PALLET_TOWN", "Music_ModSong")

mod.hooks:wrap("music.select", function(next, song, ctx)
  if ctx.reason == "battle" and ctx.trainerId == "OPP_BROCK" then
    return "Music_ModSong"
  end
  return next(song, ctx)
end)

Every song choice — map themes, battle themes, jingles, scenes — passes through music.select (ctx.reason: "map", "battle", "victory", "once", "direct"; plus mapId, mapSong, onBike, surfing, kind, trainerId). music.started / music.stopped / sound.played announce what actually played.

Worked path: Tutorial 09.