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:
1 parent
aff8391e55
commit
7f30c3f450
34 files changed
+3961
-74
No files matched your search
@@ -86,8 +86,22 @@ export interface TextContext {
|
||||
/** Native handlers for text_asm, keyed by the command's ROM offset. They
|
||||
* return where text continues, as the original routine returns in hl. */
|
||||
asm?: (offset: number) => number | undefined;
|
||||
/** TextCommand_SOUND's sound effect (TextSFX), with the length of the
|
||||
* text printed before it. */
|
||||
onSound?: (sfx: number, at: number) => void;
|
||||
}
|
||||
|
||||
/** TextSFX: the text commands that play a sound effect, and its id. */
|
||||
const TEXT_SFX: Readonly<Record<number, number>> = {
|
||||
0x0b: 0x00, // SFX_DEX_FANFARE_50_79
|
||||
0x0e: 0x09, // SFX_DEX_FANFARE_20_49
|
||||
0x0f: 0x01, // SFX_ITEM
|
||||
0x10: 0x02, // SFX_CAUGHT_MON
|
||||
0x11: 0x0a, // SFX_DEX_FANFARE_80_109
|
||||
0x12: 0x2d, // SFX_FANFARE
|
||||
0x13: 0x2c, // SFX_SLOT_MACHINE_START
|
||||
};
|
||||
|
||||
export const defaultTextContext = (player: string): TextContext => ({
|
||||
player,
|
||||
// New games initialize these names before the player can choose them.
|
||||
@@ -252,6 +266,7 @@ export function readTextEnd(
|
||||
break;
|
||||
}
|
||||
case 0x0a:
|
||||
break; // Timed pauses belong to presentation.
|
||||
case 0x0b:
|
||||
case 0x0e:
|
||||
case 0x0f:
|
||||
@@ -259,7 +274,8 @@ export function readTextEnd(
|
||||
case 0x11:
|
||||
case 0x12:
|
||||
case 0x13:
|
||||
break; // Timed pauses and sound cues belong to presentation/audio.
|
||||
context.onSound?.(TEXT_SFX[command], output.length);
|
||||
break;
|
||||
case 0x0c:
|
||||
output += "…".repeat(reader.byte(p++));
|
||||
break;
|
||||
@@ -275,10 +291,12 @@ export function readTextEnd(
|
||||
output += context.weekday;
|
||||
break;
|
||||
case 0x16: {
|
||||
const before = output.length,
|
||||
onSound = context.onSound;
|
||||
const nested = readTextEnd(
|
||||
reader,
|
||||
reader.near(p, reader.byte(p + 2)),
|
||||
context,
|
||||
onSound ? { ...context, onSound: (sfx, at) => onSound(sfx, before + at) } : context,
|
||||
depth + 1,
|
||||
);
|
||||
output += nested.text;
|
||||
|
||||
Reference in new issue
Block a user