diff --git a/projects/mazegame/CLAUDE.md b/projects/mazegame/CLAUDE.md index cebaee3..14205af 100644 --- a/projects/mazegame/CLAUDE.md +++ b/projects/mazegame/CLAUDE.md @@ -8,7 +8,8 @@ An MSX2+ maze game written in C against the **MSXgl** library (`../../engine`), into a 256KB **Konami SCC mapper ROM**. SCREEN 8 (G7, 256 colors), 32×32-pixel tiles, player always centered with smooth easing scroll. The whole game is one translation unit (`mazegame.c`) plus ~28 generated data files holding bitmap assets and per-screen music in -banked ROM segments. Each screen has its own looping **MSX-Music (FM/OPLL)** track. +banked ROM segments. Each screen has its own looping **MSX-Music (FM/OPLL)** track, with +**ayFX** sound effects layered on the PSG. ## Build & run @@ -70,7 +71,10 @@ All converters encode pixels as SCREEN 8 **GRB** (bits 7–5 = G3, 4–2 = R3, 1 `to_grb()`. Any new color/asset code must use the same encoding. Sprite segments (16/17) have no checked-in converter — edit them as data directly. -### Music — per-screen lVGM (MSX-Music / FM) +### Music & sound — per-screen lVGM (MSX-Music / FM) + ayFX (PSG) + +Music and sound effects use **different chips**, so they mix with zero contention: looping +**MSX-Music (FM/OPLL)** tracks on the YM2413, and **ayFX** sound effects on the PSG. Each screen has its own looping track played by MSXgl's `vgm/lvgm_player`. The tracks are **lVGM** (compact VGM) files in `music/`, all **MSX-Music (YM2413 / OPLL / FM)** — so FM must @@ -83,12 +87,12 @@ so `./build.sh run` gives openMSX the FM-PAC (`-ext fmpac`). The PSG carries no bytes as `const u8 g_Mus*[N]` in the matching `s{26..29}_b2.c`). See `~/Repositories/msx-music-generator` (the generator + its `msxgl-example/README.md`). - **Decode runs in the VBlank interrupt (this is the key design point).** `MusicVBlank()` is - installed as the `H_TIMI` hook (`BIOS_SetHookCallback`) and calls `LVGM_Decode()` once per - VBlank — so the tempo is **steady regardless of how long a frame's game work takes**. (An - earlier design decoded once per `Halt()` in the main loop; the heavy per-step VDP work in - `ScrollDraw` could overrun a VBlank, so the decode rate — and the music — slowed *only while - moving*. The ISR decouples them.) The game loops therefore contain **no** decode calls; they - are plain `Halt()`/busy-waits as before the music existed. + installed as the `H_TIMI` hook (`BIOS_SetHookCallback`) and each VBlank calls `LVGM_Decode()` + (music) + `ayFX_Update()` + `PSG_Apply()` (SFX) — so the tempo is **steady regardless of how + long a frame's game work takes**. (An earlier design decoded once per `Halt()` in the main + loop; the heavy per-step VDP work in `ScrollDraw` could overrun a VBlank, so the decode rate — + and the music — slowed *only while moving*. The ISR decouples them.) The game loops therefore + contain **no** decode calls; they are plain `Halt()`/busy-waits as before the music existed. - **Banking — why it's race-free:** the tracks are compiled at the bank-2 link address but at runtime `PlayTrack()` maps the current track into the **bank-3 window** (`0xA000-0xBFFF`) and leaves it there for the whole screen. Gameplay only ever switches **bank 2** (`0x8000`) for @@ -98,10 +102,37 @@ so `./build.sh run` gives openMSX the FM-PAC (`-ext fmpac`). The PSG carries no `LVGM_Play(0xA000, TRUE)`) so `MusicVBlank` can't decode mid-switch. `MSXMusic_Initialize()` and the hook install run once at the top of `main()`; `MusicVBlank` is a no-op until a track is playing (`LVGM_IsPlaying()`). -- **Config:** `LibModules` adds `psg`, `msx-music`, `vgm/lvgm_player`; `msxgl_config.h` sets - `LVGM_USE_PSG`/`LVGM_USE_MSXMUSIC` on (rest off) and `PSG_ACCESS = PSG_DIRECT`. +- **Config:** `LibModules` adds `psg`, `msx-music`, `vgm/lvgm_player`, `ayfx/ayfx_player`; + `msxgl_config.h` sets `LVGM_USE_PSG`/`LVGM_USE_MSXMUSIC` on (rest off), `PSG_ACCESS = + PSG_INDIRECT` and `AYFX_BUFFER = AYFX_BUFFER_DEFAULT` (ayFX writes the PSG buffer that + `PSG_Apply` flushes), and `PSG_USE_EXTRA = TRUE`. - The tracks are authored at **60 Hz**; on a 50 Hz (PAL) machine they play a constant ~17 % slower (the decode rate is the display refresh). That's uniform, not movement-specific. + +**Sound effects (ayFX, PSG).** An 8-effect ayFX bank in `sfx/sfx_bank.afb` (with a descriptive +`sfx_bank.txt`), embedded as `sfx_bank.h` (`g_SFXBank[]`) in **fixed main ROM** so the interrupt +reads it with no bank switching. Trigger with the `PlaySfx(id, prio)` helper; ids and priorities +(0 = highest, never interrupted; footsteps lowest so any UI/finish sound cuts over them): + +| id | effect | trigger | prio | +|----|--------|---------|------| +| 0-2 | cat paws (random) | each move | lowest | +| 4 | fanfare | landing on the exit | highest | +| 5 | map unfold | press **M** (minimap) | mid | +| 6 | laugh | press **C** (cheat) | mid | +| 7 | ta-daa | advancing title / hint / finish screen | high | + +(id 3 is unused.) Two interrupt-safety rules make this work — **break either and you get noise +and phantom input**: +- **All joystick reads go through `ReadJoy()`**, which wraps `Joystick_Read` in + `DisableInterrupt()`/`EnableInterrupt()`. `Joystick_Read` polls the PSG directly (R#14/R#15); + the ISR's `PSG_Apply` also touches the PSG, so an un-guarded read gets corrupted *and* can + misdirect a port-select write into a sound register. (`Keyboard_Read` uses the PPI, not the + PSG, so it needs no guard.) `PlaySfx` and `PlayTrack` likewise run under DI/EI. +- **The PSG mixer is muted at startup** (`PSG_SetMixer(0); PSG_Apply();` before the hook is + installed). The zero-initialized PSG buffer has the mixer at `0` = *all channels enabled* + (active-low), which `PSG_Apply` would drive as noise; ayFX re-enables its own channel when it + plays an effect. `ayFX_InitBank`/`SetChannel(PSG_CHANNEL_A)`/`SetMode(FIXED)` run once in `main`. - The boot **logo animation is intentionally silent** (no track). ### VRAM layout (off-screen caches above the visible 256×212) @@ -138,6 +169,13 @@ Consequences to respect when editing: - Fill targets in `ScrollDraw` are **base-relative** but the fill *timing* (which animation step is safe to write each row/col) is **base-invariant**; at `base = 0` every expression reduces exactly to the pre-torus code, which is the easy case to reason about. +- **Strip fills must respect BOTH axis bases, not just the scroll axis.** The bulk HMMM fills + handle the scroll-axis base in their destination, and the orthogonal axis as follows: + vertical strips are **pre-rotated by `g_BaseX` in staging** (`PreRenderStrip`), so the + full-width row copies stay at destination x=0; horizontal column fills go through + `FillStripColumns`, which **splits the 224-row copy at the 256-row wrap** for `g_BaseY`. + (Omitting either writes the new edge 2/4/6 tiles rotated whenever the other axis' base is + non-zero — the maze *looks* wrong while collision stays right: phantom walls/passages.) - Vertical has only 32px of slack in the 256-row torus, so one new tile-row is pre-filled (off-screen slack) and the other is deferred until it scrolls off; horizontal fills progressively as columns wrap. This is the delicate part — verify long straight runs in diff --git a/projects/mazegame/emul/rom/mazegame.rom b/projects/mazegame/emul/rom/mazegame.rom index 0165761..b125acc 100644 Binary files a/projects/mazegame/emul/rom/mazegame.rom and b/projects/mazegame/emul/rom/mazegame.rom differ diff --git a/projects/mazegame/mazegame.c b/projects/mazegame/mazegame.c index a041ae7..387d71e 100644 --- a/projects/mazegame/mazegame.c +++ b/projects/mazegame/mazegame.c @@ -11,6 +11,8 @@ #include "psg.h" #include "msx-music.h" #include "vgm/lvgm_player.h" +#include "ayfx/ayfx_player.h" +#include "sfx_bank.h" // g_SFXBank[] — ayFX sound-effect bank (fixed main ROM) #include //============================================================================= @@ -119,12 +121,52 @@ u8 g_BaseY; // resting vertical scroll offset (mult #define MUS_WINDOW ((const u8*)0xA000) // bank-3 window the track is mapped into -// H_TIMI (VBlank) hook: advance the current track one frame. No-op until a track is -// playing. Reads only from the bank-3 window, which nothing else touches. +// Sound effects (ayFX on the PSG, mixed over the FM music). Bank ids (see +// sfx/sfx_bank.txt): 0-2 cat paws (movement, chosen at random), 4 fanfare (reaching +// the exit), 5 map unfold (M / minimap), 6 laugh (C / cheat), 7 ta-daa (advancing a +// screen). Priority: 0 = highest (never interrupted); higher number = interrupted +// more easily. Footsteps are lowest so any UI/finish sound cuts over them. +#define SFX_FINISH 4 +#define SFX_MINIMAP 5 +#define SFX_CHEAT 6 +#define SFX_SCREEN 7 +#define SFX_PRIO_FINISH 0 +#define SFX_PRIO_SCREEN 1 +#define SFX_PRIO_ACTION 2 // M / C +#define SFX_PRIO_MOVE 4 // footsteps + +// H_TIMI (VBlank) hook: advance the music one frame (if a track is playing) and the +// current sound effect one frame, then flush the PSG buffer. Music reads from the +// bank-3 window; SFX reads the bank in fixed main ROM — neither needs bank switching. void MusicVBlank(void) { if (LVGM_IsPlaying()) LVGM_Decode(); + ayFX_Update(); + PSG_Apply(); +} + +// Trigger a bank sound effect. ayFX state is set up here (main context) while the +// VBlank hook reads it, so disable interrupts across the call. +static void PlaySfx(u8 id, u8 prio) +{ + DisableInterrupt(); + ayFX_PlayBank(id, prio); + EnableInterrupt(); +} + +// Read joystick 1 with interrupts disabled. Joystick_Read drives the PSG directly +// (selects/writes R#15, reads R#14); the VBlank hook's PSG_Apply also touches the +// PSG. If the hook fired mid-read it would corrupt the returned data AND could +// misdirect a port-select write into a sound register (audible noise). All joystick +// reads in the game must go through this wrapper. (Keyboard_Read uses the PPI, not +// the PSG, so it is race-free and needs no protection.) +static u8 ReadJoy(void) +{ + DisableInterrupt(); + u8 j = Joystick_Read(JOY_PORT_1); + EnableInterrupt(); + return j; } // Switch to a looping track: silence the previous track's lingering note (key-off all @@ -584,7 +626,14 @@ static void PreRenderStrip(i8 dx, i8 dy) for (u8 ty = ya; ty <= yb; ty++) { for (u8 tx = xa; tx <= xb; tx++) { u8 srcX = GetTileSrcX(ox + (i8)tx, oy + (i8)ty); - u16 stx = (u16)(tx - xa) * TILE_W; + // Vertical strips are pasted by full-width HMMMs at VRAM x=0, so the + // staging row must already be rotated by the horizontal torus base: + // screen column 0 lives at VRAM column g_BaseX. Tiles are 32-aligned + // and the base is a multiple of 64, so no tile straddles the wrap. + // Horizontal strips are pasted column-by-column with an explicit + // destination x, so their staging stays unrotated (x = 0..63). + u16 stx = (dy != 0) ? (u8)(tx * TILE_W + g_BaseX) + : (u16)(tx - xa) * TILE_W; u16 sty = STAGING_Y + (u16)(ty - ya) * TILE_W; VDP_CommandHMMM(srcX, TILE_CACHE_Y, stx, sty, TILE_W, TILE_W); VDP_CommandWait(); @@ -592,6 +641,22 @@ static void PreRenderStrip(i8 dx, i8 dy) } } +// Paste a w-wide, 224-tall column of the horizontal staging strip into the +// visible torus. Screen row 0 lives at VRAM row g_BaseY, so the copy is split +// at the 256-row wrap when the vertical base is non-zero (base 64/128/192 → +// 192/128/64 rows before the wrap, remainder continuing at VRAM row 0). +static void FillStripColumns(u8 srcX, u8 dstX, u8 w) +{ + u8 by = g_BaseY; + if (by == 0) { + VDP_CommandHMMM(srcX, STAGING_Y, dstX, 0, w, 224); + } else { + u16 h1 = 256u - by; // rows before the torus wrap + VDP_CommandHMMM(srcX, STAGING_Y, dstX, by, w, h1); + VDP_CommandHMMM(srcX, STAGING_Y + h1, dstX, 0, w, (u16)(224u - h1)); + } +} + // Wrap-around (torus) scroll: animate the hardware scroll register from the // resting base to base±64, rendering the newly exposed edge into the VRAM cells // that scroll off the opposite side. Nothing is bulk-shifted or blanked @@ -617,7 +682,9 @@ u8 ScrollDraw(i8 dx, i8 dy) // tile-rows exactly one is already off-screen (in the slack just past the // viewport) and can be filled now; the other overlaps on-screen VRAM and is // deferred into the loop. At base=0 these reduce to the original rows 224/0 - // (dy=1) and 224/192 (dy=-1). Staging holds the new tiles at rows 0 / +32. + // (dy=1) and 224/192 (dy=-1). Staging holds the new tiles at rows 0 / +32, + // already rotated by g_BaseX (see PreRenderStrip), so these full-width + // copies stay at destination x=0 regardless of the horizontal base. if (dy == 1) { // New bottom tile-row 5 → VRAM row (base+224) (off-screen slack). Staging row 0. VDP_CommandHMMM(0, STAGING_Y, 0, (u8)(base + 224u), 256, 32); VDP_CommandWait(); @@ -676,10 +743,10 @@ u8 ScrollDraw(i8 dx, i8 dy) u8 cur = g_AO[i]; u8 w = cur - prev; if (dx == 1) { - VDP_CommandHMMM(prev, STAGING_Y, (u8)(base + prev), 0, w, 224); + FillStripColumns(prev, (u8)(base + prev), w); } else { u8 fs = (u8)(64u - cur); - VDP_CommandHMMM(fs, STAGING_Y, (u8)(base + 192u + fs), 0, w, 224); + FillStripColumns(fs, (u8)(base + 192u + fs), w); } u8 new_vx = (u8)((i16)SPR_DRAW_X + (i16)dx * (i16)cur); g_SprFrame = (g_SprFrame + 1) & 3; @@ -693,7 +760,7 @@ u8 ScrollDraw(i8 dx, i8 dy) // Read keyboard and joystick at the final animation frame (for chaining). u8 row8 = Keyboard_Read(8); - g_Joy = Joystick_Read(JOY_PORT_1); + g_Joy = ReadJoy(); // Commit the new resting base. The newly exposed edge is already in VRAM (from // the fills), the old content is already at the correct torus position, and the @@ -863,7 +930,7 @@ void WriteImgRow(u8 first_seg, u8 y) // Returns 1 if any key (rows 0-8) or any joystick direction/button is active. static u8 AnyInput(void) { - u8 joy = Joystick_Read(JOY_PORT_1); + u8 joy = ReadJoy(); if (JOY_GET_DIR(joy) != 0 || IS_JOY_PRESSED(joy, JOY_INPUT_TRIGGER_A) || IS_JOY_PRESSED(joy, JOY_INPUT_TRIGGER_B)) return 1; @@ -877,6 +944,7 @@ static void WaitKeySequence(void) { while (AnyInput()); while (!AnyInput()); + PlaySfx(SFX_SCREEN, SFX_PRIO_SCREEN); // ta-daa on the advancing keypress while (AnyInput()); } @@ -931,6 +999,7 @@ void TitleScreen(void) // Wait for any key or joystick input, then release while (!AnyInput()); + PlaySfx(SFX_SCREEN, SFX_PRIO_SCREEN); // ta-daa on the advancing keypress while (AnyInput()); } @@ -995,9 +1064,19 @@ static void ShowWinScreen(void) void main(void) { - // Init the MSX-Music (OPLL) chip once, then install the VBlank hook that decodes - // the current track every frame (MusicVBlank is a no-op until a track is playing). + // Init the MSX-Music (OPLL) chip, load the ayFX bank onto PSG channel A, then + // install the VBlank hook that advances music + SFX every frame (the hook is a + // no-op for music until a track plays, and silent for SFX until one is triggered). MSXMusic_Initialize(); + ayFX_InitBank((void*)g_SFXBank); + ayFX_SetChannel(PSG_CHANNEL_A); + ayFX_SetMode(AYFX_MODE_FIXED); + // Silence the PSG before the per-frame PSG_Apply starts: the zero-initialized + // register buffer has the mixer at 0 = all channels ENABLED, which PSG_Apply would + // drive as noise. Disable every channel; ayFX re-enables its own channel when it + // plays an effect (and drops back to volume 0 when the effect ends). + PSG_SetMixer(0); + PSG_Apply(); BIOS_SetHookCallback(H_TIMI, MusicVBlank); ShowLogoAnimation(); @@ -1090,7 +1169,7 @@ void main(void) do { UpdateTimer(); row8 = Keyboard_Read(8); - g_Joy = Joystick_Read(JOY_PORT_1); + g_Joy = ReadJoy(); } while (IS_KEY_PRESSED(row8, KEY_UP) || IS_KEY_PRESSED(row8, KEY_DOWN) || IS_KEY_PRESSED(row8, KEY_LEFT) || IS_KEY_PRESSED(row8, KEY_RIGHT) || JOY_GET_DIR(g_Joy) != 0); @@ -1102,7 +1181,7 @@ void main(void) row3 = Keyboard_Read(3); row4 = Keyboard_Read(4); row5 = Keyboard_Read(5); - g_Joy = Joystick_Read(JOY_PORT_1); + g_Joy = ReadJoy(); } while (!IS_KEY_PRESSED(row8, KEY_UP) && !IS_KEY_PRESSED(row8, KEY_DOWN) && !IS_KEY_PRESSED(row8, KEY_LEFT) && !IS_KEY_PRESSED(row8, KEY_RIGHT) && !IS_KEY_PRESSED(row3, KEY_C) && !IS_KEY_PRESSED(row4, KEY_M) && @@ -1114,6 +1193,7 @@ void main(void) // at least one of its up/left/right neighbours has an open passage to // it; drop the player on the first such neighbour. if (IS_KEY_PRESSED(row3, KEY_C)) { + PlaySfx(SFX_CHEAT, SFX_PRIO_ACTION); u8 ey = MAZE_H - 1; if (g_DW[g_EX][ey-1] == 0) { // open above -> step down g_MX = g_EX; g_MY = ey - 1; @@ -1129,6 +1209,7 @@ void main(void) // M key or joystick button A: show minimap while held if (IS_KEY_PRESSED(row4, KEY_M) || IS_JOY_PRESSED(g_Joy, JOY_INPUT_TRIGGER_A)) { + PlaySfx(SFX_MINIMAP, SFX_PRIO_ACTION); VDP_EnableDisplay(FALSE); // Minimap is drawn at absolute VRAM (0,0); show it with zero // scroll offset regardless of the current torus base. @@ -1141,7 +1222,7 @@ void main(void) do { Halt(); row4 = Keyboard_Read(4); - g_Joy = Joystick_Read(JOY_PORT_1); + g_Joy = ReadJoy(); } while (IS_KEY_PRESSED(row4, KEY_M) || IS_JOY_PRESSED(g_Joy, JOY_INPUT_TRIGGER_A)); // Restore game view (DrawFull includes DrawSprite). Put the // scroll offset back to the current torus base first. @@ -1192,10 +1273,12 @@ void main(void) g_MX += (u8)dx; g_MY += (u8)dy; + PlaySfx((u8)(rand() % 3), SFX_PRIO_MOVE); // random cat-paw footstep (id 0-2) row8 = ScrollDraw(dx, dy); // Win condition: reached exit cell if (g_MY == MAZE_H-1 && g_MX == g_EX) { + PlaySfx(SFX_FINISH, SFX_PRIO_FINISH); // fanfare over the win screen ShowWinScreen(); break; // back to outer loop → title screen → new maze } diff --git a/projects/mazegame/msxgl_config.h b/projects/mazegame/msxgl_config.h index 4aa5e72..3b2f569 100644 --- a/projects/mazegame/msxgl_config.h +++ b/projects/mazegame/msxgl_config.h @@ -49,16 +49,19 @@ #define INPUT_KB_UPDATE_MIN 0 #define INPUT_KB_UPDATE_MAX 8 -// Music: screen tracks are lVGM (MSX-Music / YM2413 / OPLL). The PSG parser is -// kept so the player links cleanly, but the tracks carry only FM data. -// PSG: direct register access (no PSG_Apply needed) — no PSG data is ever played. +// Music: screen tracks are lVGM (MSX-Music / YM2413 / OPLL); sound effects are +// ayFX on the PSG, mixed over the FM music each frame. PSG is in buffered +// (indirect) mode so ayFX writes g_PSG_Regs and PSG_Apply() flushes it once a +// frame (the tracks carry no PSG data, so the PSG belongs entirely to the SFX). #define PSG_CHIP PSG_INTERNAL -#define PSG_ACCESS PSG_DIRECT +#define PSG_ACCESS PSG_INDIRECT #define PSG_USE_NOTES FALSE -#define PSG_USE_EXTRA FALSE +#define PSG_USE_EXTRA TRUE #define PSG_USE_RESUME FALSE // MSX-Music (the chip the music actually uses). #define MSXMUSIC_USE_RESUME FALSE +// ayFX sound effects: use the PSG module's register buffer (no PT3 player here). +#define AYFX_BUFFER AYFX_BUFFER_DEFAULT // lVGM replayer: enable only the chips these tracks target (PSG parser + MSX-Music). #define LVGM_USE_PSG TRUE // parser kept for clean linking; tracks carry no PSG data #define LVGM_USE_MSXMUSIC TRUE // MSX-Music (OPLL) — the music format used diff --git a/projects/mazegame/project_config.js b/projects/mazegame/project_config.js index 9206d7e..51c3484 100644 --- a/projects/mazegame/project_config.js +++ b/projects/mazegame/project_config.js @@ -2,7 +2,7 @@ ProjName = "mazegame"; ProjModules = [ ProjName ]; -LibModules = [ "system", "bios", "vdp", "input", "psg", "msx-music", "vgm/lvgm_player" ]; +LibModules = [ "system", "bios", "vdp", "input", "psg", "msx-music", "vgm/lvgm_player", "ayfx/ayfx_player" ]; Machine = "2P"; Target = "ROM_KONAMI_SCC"; diff --git a/projects/mazegame/sfx/sfx_bank.afb b/projects/mazegame/sfx/sfx_bank.afb new file mode 100644 index 0000000..cd50c78 Binary files /dev/null and b/projects/mazegame/sfx/sfx_bank.afb differ diff --git a/projects/mazegame/sfx/sfx_bank.txt b/projects/mazegame/sfx/sfx_bank.txt new file mode 100644 index 0000000..3c62903 --- /dev/null +++ b/projects/mazegame/sfx/sfx_bank.txt @@ -0,0 +1,28 @@ +; MSX ayFX sound-effect bank +; sfx_bank.afb — 8 effects, 330 bytes total +; Generated by the MSX Music Generator — https://github.com/jurjen74/msx-music-generator +; Play on MSXgl with: ayFX_PlayBank(id, priority); + + id name bytes frames ~time shape + 0 catpaws 26 20 0.33s noise + decay + 1 catpaws_2 26 20 0.33s noise + decay + 2 catpaws_3 26 20 0.33s noise + decay + 3 heartbeat_2 38 18 0.30s tone sweep + decay + 4 fanfare 39 27 0.45s tone + decay + 5 mapunfold 50 29 0.48s noise + decay + 6 manilaugh 70 25 0.42s tone sweep + noise + decay + 7 tadaaa_2 38 30 0.50s tone sweep + decay + +; C names array for msxgl-example/s_mysfx.c (order matches the bank): +const char* g_Names[] = { "catpaws", "catpaws_2", "catpaws_3", "heartbeat_2", "fanfare", "mapunfold", "manilaugh", "tadaaa_2" }; +#define SFX_COUNT 8 + +; Source descriptors (reproducible JSON, one per effect): +[0] catpaws: {"name":"catpaws","segments":[{"frames":2,"tone":null,"toneCurve":"lin","noise":18,"noiseCurve":"lin","vol":[6,3],"volCurve":"exp"},{"frames":3,"tone":null,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":0,"volCurve":"lin"},{"frames":2,"tone":null,"toneCurve":"lin","noise":20,"noiseCurve":"lin","vol":[5,2],"volCurve":"exp"},{"frames":3,"tone":null,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":0,"volCurve":"lin"},{"frames":2,"tone":null,"toneCurve":"lin","noise":19,"noiseCurve":"lin","vol":[7,3],"volCurve":"exp"},{"frames":3,"tone":null,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":0,"volCurve":"lin"},{"frames":2,"tone":null,"toneCurve":"lin","noise":21,"noiseCurve":"lin","vol":[5,1],"volCurve":"exp"},{"frames":3,"tone":null,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":0,"volCurve":"lin"}]} +[1] catpaws_2: {"name":"catpaws","segments":[{"frames":2,"tone":null,"toneCurve":"lin","noise":12,"noiseCurve":"lin","vol":[6,3],"volCurve":"exp"},{"frames":3,"tone":null,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":0,"volCurve":"lin"},{"frames":2,"tone":null,"toneCurve":"lin","noise":14,"noiseCurve":"lin","vol":[5,2],"volCurve":"exp"},{"frames":3,"tone":null,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":0,"volCurve":"lin"},{"frames":2,"tone":null,"toneCurve":"lin","noise":11,"noiseCurve":"lin","vol":[7,3],"volCurve":"exp"},{"frames":3,"tone":null,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":0,"volCurve":"lin"},{"frames":2,"tone":null,"toneCurve":"lin","noise":13,"noiseCurve":"lin","vol":[5,1],"volCurve":"exp"},{"frames":3,"tone":null,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":0,"volCurve":"lin"}]} +[2] catpaws_3: {"name":"catpaws","segments":[{"frames":2,"tone":null,"toneCurve":"lin","noise":8,"noiseCurve":"lin","vol":[6,3],"volCurve":"exp"},{"frames":3,"tone":null,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":0,"volCurve":"lin"},{"frames":2,"tone":null,"toneCurve":"lin","noise":10,"noiseCurve":"lin","vol":[5,2],"volCurve":"exp"},{"frames":3,"tone":null,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":0,"volCurve":"lin"},{"frames":2,"tone":null,"toneCurve":"lin","noise":9,"noiseCurve":"lin","vol":[7,3],"volCurve":"exp"},{"frames":3,"tone":null,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":0,"volCurve":"lin"},{"frames":2,"tone":null,"toneCurve":"lin","noise":11,"noiseCurve":"lin","vol":[5,1],"volCurve":"exp"},{"frames":3,"tone":null,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":0,"volCurve":"lin"}]} +[3] heartbeat_2: {"name":"heartbeat","segments":[{"frames":2,"tone":[80,55],"toneCurve":"exp","noise":null,"noiseCurve":"lin","vol":[13,15],"volCurve":"lin"},{"frames":3,"tone":[55,40],"toneCurve":"exp","noise":null,"noiseCurve":"lin","vol":[15,9],"volCurve":"exp"},{"frames":2,"tone":[110,75],"toneCurve":"exp","noise":null,"noiseCurve":"lin","vol":[12,14],"volCurve":"lin"},{"frames":4,"tone":[75,38],"toneCurve":"exp","noise":null,"noiseCurve":"lin","vol":[14,6],"volCurve":"exp"},{"frames":7,"tone":[38,38],"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":[6,0],"volCurve":"exp"}]} +[4] fanfare: {"name":"fanfare","segments":[{"frames":3,"tone":523,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":[12,14],"volCurve":"lin"},{"frames":3,"tone":659,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":14,"volCurve":"lin"},{"frames":3,"tone":784,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":14,"volCurve":"lin"},{"frames":2,"tone":1047,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":15,"volCurve":"lin"},{"frames":5,"tone":1047,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":[15,11],"volCurve":"lin"},{"frames":3,"tone":1319,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":15,"volCurve":"lin"},{"frames":8,"tone":1319,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":[15,0],"volCurve":"exp"}]} +[5] mapunfold: {"name":"mapunfold","segments":[{"frames":2,"tone":null,"toneCurve":"lin","noise":3,"noiseCurve":"lin","vol":[8,11],"volCurve":"lin"},{"frames":3,"tone":null,"toneCurve":"lin","noise":[3,6],"noiseCurve":"lin","vol":[11,6],"volCurve":"lin"},{"frames":4,"tone":null,"toneCurve":"lin","noise":5,"noiseCurve":"lin","vol":[5,9],"volCurve":"lin"},{"frames":3,"tone":null,"toneCurve":"lin","noise":[4,7],"noiseCurve":"lin","vol":[9,5],"volCurve":"lin"},{"frames":5,"tone":null,"toneCurve":"lin","noise":[6,9],"noiseCurve":"lin","vol":[6,10],"volCurve":"lin"},{"frames":4,"tone":null,"toneCurve":"lin","noise":[8,12],"noiseCurve":"lin","vol":[10,4],"volCurve":"lin"},{"frames":5,"tone":null,"toneCurve":"lin","noise":[12,18],"noiseCurve":"lin","vol":[4,1],"volCurve":"lin"},{"frames":3,"tone":null,"toneCurve":"lin","noise":18,"noiseCurve":"lin","vol":[1,0],"volCurve":"lin"}]} +[6] manilaugh: {"name":"manilaugh","segments":[{"frames":3,"tone":[280,420],"toneCurve":"exp","noise":null,"noiseCurve":"lin","vol":[10,14],"volCurve":"lin"},{"frames":2,"tone":[420,200],"toneCurve":"exp","noise":null,"noiseCurve":"lin","vol":[14,8],"volCurve":"lin"},{"frames":3,"tone":[300,460],"toneCurve":"exp","noise":null,"noiseCurve":"lin","vol":[11,15],"volCurve":"lin"},{"frames":2,"tone":[460,210],"toneCurve":"exp","noise":null,"noiseCurve":"lin","vol":[15,9],"volCurve":"lin"},{"frames":3,"tone":[320,500],"toneCurve":"exp","noise":null,"noiseCurve":"lin","vol":[12,15],"volCurve":"lin"},{"frames":2,"tone":[500,220],"toneCurve":"exp","noise":null,"noiseCurve":"lin","vol":[15,10],"volCurve":"lin"},{"frames":4,"tone":[350,600],"toneCurve":"exp","noise":null,"noiseCurve":"lin","vol":[13,15],"volCurve":"lin"},{"frames":6,"tone":[600,150],"toneCurve":"exp","noise":18,"noiseCurve":"lin","vol":[15,0],"volCurve":"exp"}]} +[7] tadaaa_2: {"name":"tadaaa","segments":[{"frames":3,"tone":523,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":[13,15],"volCurve":"lin"},{"frames":6,"tone":523,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":15,"volCurve":"lin"},{"frames":2,"tone":null,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":0,"volCurve":"lin"},{"frames":3,"tone":[523,784],"toneCurve":"exp","noise":null,"noiseCurve":"lin","vol":[13,15],"volCurve":"lin"},{"frames":10,"tone":784,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":15,"volCurve":"lin"},{"frames":6,"tone":784,"toneCurve":"lin","noise":null,"noiseCurve":"lin","vol":[15,0],"volCurve":"exp"}]} diff --git a/projects/mazegame/sfx_bank.h b/projects/mazegame/sfx_bank.h new file mode 100644 index 0000000..e117d2f --- /dev/null +++ b/projects/mazegame/sfx_bank.h @@ -0,0 +1,29 @@ +#include "msxgl.h" + +// ayFX sound-effect bank (8 effects, PSG). Lives in fixed main ROM so the +// H_TIMI music/SFX interrupt can read it without any bank switching. +// ids: 0-2 catpaws (movement), 4 fanfare (finish), 5 mapunfold (M), +// 6 manilaugh (C), 7 tadaaa (screen keypress). See sfx/sfx_bank.txt. +const u8 g_SFXBank[330] = { + 0x08, 0x0F, 0x00, 0x27, 0x00, 0x3F, 0x00, 0x57, 0x00, 0x7B, 0x00, 0xA0, 0x00, 0xD0, 0x00, 0x14, + 0x01, 0x56, 0x12, 0x13, 0x90, 0x90, 0x90, 0x55, 0x14, 0x12, 0x90, 0x90, 0x90, 0x57, 0x13, 0x13, + 0x90, 0x90, 0x90, 0x55, 0x15, 0x11, 0x90, 0x90, 0x90, 0xD0, 0x20, 0x56, 0x0C, 0x13, 0x90, 0x90, + 0x90, 0x55, 0x0E, 0x12, 0x90, 0x90, 0x90, 0x57, 0x0B, 0x13, 0x90, 0x90, 0x90, 0x55, 0x0D, 0x11, + 0x90, 0x90, 0x90, 0xD0, 0x20, 0x56, 0x08, 0x13, 0x90, 0x90, 0x90, 0x55, 0x0A, 0x12, 0x90, 0x90, + 0x90, 0x57, 0x09, 0x13, 0x90, 0x90, 0x90, 0x55, 0x0B, 0x11, 0x90, 0x90, 0x90, 0xD0, 0x20, 0xAD, + 0x76, 0x05, 0xAF, 0xF2, 0x07, 0x8F, 0xAC, 0x51, 0x09, 0xA9, 0xED, 0x0A, 0xAC, 0xF9, 0x03, 0xAE, + 0xD3, 0x05, 0x8E, 0xAB, 0x4F, 0x07, 0xA8, 0x2B, 0x09, 0xA6, 0x80, 0x0B, 0x86, 0x85, 0x84, 0x83, + 0x82, 0x81, 0x80, 0xD0, 0x20, 0xAC, 0xD6, 0x00, 0x8D, 0x8E, 0xAE, 0xAA, 0x00, 0x8E, 0x8E, 0xAE, + 0x8F, 0x00, 0x8E, 0x8E, 0xAF, 0x6B, 0x00, 0x8F, 0x8F, 0x8E, 0x8D, 0x8C, 0x8B, 0xAF, 0x55, 0x00, + 0x8F, 0x8F, 0x8F, 0x8D, 0x8B, 0x89, 0x86, 0x84, 0x82, 0x80, 0xD0, 0x20, 0x58, 0x03, 0x1B, 0x1B, + 0x59, 0x05, 0x56, 0x06, 0x55, 0x05, 0x16, 0x18, 0x19, 0x59, 0x04, 0x57, 0x06, 0x55, 0x07, 0x56, + 0x06, 0x57, 0x07, 0x58, 0x08, 0x19, 0x5A, 0x09, 0x5A, 0x08, 0x58, 0x09, 0x56, 0x0B, 0x54, 0x0C, + 0x14, 0x53, 0x0E, 0x53, 0x0F, 0x52, 0x11, 0x51, 0x12, 0x11, 0x11, 0x10, 0xD0, 0x20, 0xAA, 0x90, + 0x01, 0xAC, 0x46, 0x01, 0xAE, 0x0A, 0x01, 0x8E, 0xA8, 0x2F, 0x02, 0xAB, 0x75, 0x01, 0xAD, 0x2D, + 0x01, 0xAF, 0xF3, 0x00, 0x8F, 0xA9, 0x15, 0x02, 0xAC, 0x5E, 0x01, 0xAE, 0x18, 0x01, 0xAF, 0xE0, + 0x00, 0x8F, 0xAA, 0xFC, 0x01, 0xAD, 0x40, 0x01, 0xAE, 0x0B, 0x01, 0xAE, 0xDF, 0x00, 0xAF, 0xBA, + 0x00, 0x4F, 0x12, 0x2C, 0xF6, 0x00, 0x29, 0x45, 0x01, 0x26, 0xAC, 0x01, 0x23, 0x35, 0x02, 0x20, + 0xEA, 0x02, 0xD0, 0x20, 0xAD, 0xD6, 0x00, 0x8E, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x90, + 0x90, 0x8D, 0xAE, 0xAF, 0x00, 0xAF, 0x8F, 0x00, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, + 0x8F, 0x8F, 0x8F, 0x8C, 0x89, 0x86, 0x83, 0x80, 0xD0, 0x20, +};