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
First workflow: one file-backed song
This route is for a musician who wants to hear a song in a map before learning
the chip-program API. Start with an existing mod scaffold and a WAV or Ogg
Vorbis file supported by the project's LÖVE desktop build. The WAV in the
finished Tutorial 09 example is the repository's
known-good starting sample. The engine passes the path to
love.audio.newSource, so use that sample first when diagnosing a format
problem; try another file only after the baseline works.
The bundled sample is examples/tutorial-09/finished/assets/town.wav; when
copying it into a new mod, place it at mods/my_mod/assets/town.wav.
Keep the complete manifest at mods/my_mod/manifest.json, the entry file at
mods/my_mod/main.lua, and the audio at mods/my_mod/assets/; see the
manifest reference for the required fields.
From the engine repository root, place the file at
mods/my_mod/assets/town.wav and use this complete main.lua:
Example status: runnable complete file; edit the marked id, map, and asset path.
return function(mod)
mod.content.music:register("Music_ModTown", {
file = mod.assets:path("assets/town.wav"),
})
mod.content.map_songs:override("PALLET_TOWN", "Music_ModTown")
end
Replace PALLET_TOWN with the map id you intend to test. Enable the mod,
enter that map, and listen for the new cue. If the old song remains, check
the mod is enabled for the selected game and that the map id and file path
match; read the manager error feed before changing the song definition. A
successful run keeps the game running and plays the replacement cue. If it
does not, restore the original map-song entry, disable the mod, and restart
before trying one change at a time.
modkit bounce is a separate chip-authoring tool. It renders chip-program
songs to WAV; it does not preview a file-backed song or prove that a map cue
is wired correctly.
Songs, sound effects, and cries are data. Every def is shape-dispatched
per record (src/core/Music.lua, src/core/Sound.lua), so file-backed
songs, authored chiptunes, and the vanilla chip soundtrack can coexist in one
dataset. There is no global audio mode to select.
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
on Gen 1 and Gen 2 but is deprecated in favor of these; Gen 3 reports it as
having no target.)
Song def shapes
Example status: outline; compare the three supported record shapes here.
-- 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
Example status: outline; adapt the ids and asset paths for your mod.
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:
Example status: runnable insertion; place this ChipAsm registration in main.lua.
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 1–16), pitch (explicit 0–11), rest, octave (1–8),
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
Example status: runnable insertion; place this map/hook code in main.lua.
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.
Gen 2 and Gen 3
The four registries are shared names on every game. Keys are the selected game's own ids, so a portable mod registers its own songs and maps them per game rather than reusing a Gen 1 map id.
| Gen 1 | Gen 2 (Gold, Silver, Crystal) | Gen 3 (FRLG, RSE) | |
|---|---|---|---|
music, sfx, cries, map_songs |
yes | yes, same Data.audio target |
yes, played by src/core/game3/mod_audio.lua |
| File-backed defs | yes | yes | yes |
| ChipAsm defs | yes | yes | yes, through the Game Boy chip engine |
audio (v1) |
deprecated | deprecated | no target |
music.select hook |
yes | yes | no Gen 3 call site |
On Gen 3 a row keyed by a native name replaces the cart sound: a song id
such as MUS_..., an effect such as SE_PIN, or a species id for a cry.
map_songs keys are Gen 3 map ids, and a mod song wins over the map
header's music. Gen 3 has no vanilla records in these tables to patch, so
a cry retune like the PIKACHU example above is Gen 1 and Gen 2 only; use
register with a file instead. Check mod:supports("hooks", "music.select") before relying on the hook in an api 3 mod.
Check and recover
The first successful checkpoint is the replacement cue playing in the chosen map while the game remains running. If it does not, restore the original map song entry, disable the mod, and restart with the known-good Tutorial 09 WAV before changing the path or registry id.
Start here
Playing
Tutorials
- Choose a tutorial
- 01 Sprite and Text Tweak
- 02 Balance Patch
- 03 New Species
- 04 New Item and Ball
- 05 New Move
- 06 NPC and Dialogue
- 07 New Map
- 08 Quest
- 09 Custom Music
- 10 New Mechanic
- 11 Custom UI Screen
- 12 Mini Total Conversion
Creative guides
Concepts
Recipes and reference
Author and developer tools
Home · Start Playing · Glossary · Troubleshooting · Repository · Community
Game data is imported locally from a supported ROM supplied by the player. Shared mods contain original work or transform recipes, never extracted game content. Check each guide's target game before using its examples.