Play the music and sound effects from the ROM's sound engine (Closes #34)

The sound engine (audio/engine.asm, home/audio.asm) is ported on a byte
copy of its audio WRAM and a sound register model. A reference test runs
the cartridge's routines beside it and matches the WRAM and registers
after every UpdateSound for all songs, effects and cries, fades, the
danger tone and effect priority. apu.ts synthesizes the four channels in
an AudioWorklet; its output matches the reference emulator's APU by
spectrum. The world runs the engine twice per tick; Play or the first
press unlocks sound, pausing suspends it, Settings sets the volume.

Wired in: map music by map setup and special map music, script audio
commands, text fanfares, WaitSFX and cries that hold the script frozen
(the last page stays shown), battle music, cries, fainting, victory,
capture and the low HP tone, Pokémon Center healing, evolution, the
intro, the Pokédex's cries, PCs, marts, healing items, talk and menu
clicks, bumps, ledges, warps, boulders, the Bicycle and Surf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kawaandClaude Opus 5.5 committed 2026-10-15 15:54:02 +02:00
1 parent aff8391e55
commit 7f30c3f450
34 files changed
+3961 -74

No files matched your search

+9 -3
View File
@@ -226,8 +226,8 @@ for (const table of ["SAVED_WRAM", "WRAM"]) {
throw new Error(`${table} RAM mismatch: ${label}`);
}
}
// BATTLE, OVERWORLD, PC, INTRO, STATS, MENU, POKEGEAR, MOM, TRAINER_CARD, POKEDEX, OPTIONS and JOHTO entries are keyed by their PRET labels.
for (const table of ["BATTLE", "OVERWORLD", "PC", "INTRO", "STATS", "MENU", "POKEGEAR", "MOM", "TRAINER_CARD", "POKEDEX", "OPTIONS", "JOHTO"]) {
// BATTLE, OVERWORLD, PC, INTRO, STATS, MENU, POKEGEAR, MOM, TRAINER_CARD, POKEDEX, OPTIONS, JOHTO and AUDIO entries are keyed by their PRET labels.
for (const table of ["BATTLE", "OVERWORLD", "PC", "INTRO", "STATS", "MENU", "POKEGEAR", "MOM", "TRAINER_CARD", "POKEDEX", "OPTIONS", "JOHTO", "AUDIO"]) {
const block = new RegExp(`export const ${table} = \\{([\\s\\S]*?)\\} as const;`).exec(profile)?.[1];
if (block === undefined) throw new Error(`${table} symbol table is missing.`);
for (const [, quoted, bare, literal] of block.matchAll(/^\s+(?:"([^"]+)"|(\w+)):\s*(0x[0-9a-f]+),/gm)) {
@@ -359,7 +359,7 @@ const pretConstants = new Map();
}
}
}
const battleConstants = ["src/game/battle/constants.ts", "src/game/world/constants.ts"]
const battleConstants = ["src/game/battle/constants.ts", "src/game/world/constants.ts", "src/game/audio/constants.ts", "src/game/audio/battle-music.ts"]
.map((file) => readFileSync(file, "utf8"))
.join("\n");
for (const [, block] of battleConstants.matchAll(/export const \w+ = \{([\s\S]*?)\} as const;/g)) {
@@ -377,6 +377,12 @@ if (!profile.includes(`allFruitTrees: ${pretConstants.get("ENGINE_ALL_FRUIT_TREE
throw new Error("ENGINE_ALL_FRUIT_TREES does not match the reference.");
if (!profile.includes(`bugContestTimer: ${pretConstants.get("ENGINE_BUG_CONTEST_TIMER")},`))
throw new Error("ENGINE_BUG_CONTEST_TIMER does not match the reference.");
for (const [key, name] of [
["rocketsInRadioTower", "ENGINE_ROCKETS_IN_RADIO_TOWER"],
["rocketsInMahogany", "ENGINE_ROCKETS_IN_MAHOGANY"],
])
if (!profile.includes(`${key}: ${pretConstants.get(name)},`))
throw new Error(`${name} does not match the reference.`);
if (!profile.includes(`NUM_SPRITEMOVEDATA = ${pretConstants.get("NUM_SPRITEMOVEDATA")};`))
throw new Error("NUM_SPRITEMOVEDATA does not match the reference.");
if (!profile.includes(`NUM_ENGINE_FLAGS = ${pretConstants.get("NUM_ENGINE_FLAGS")};`))