Add ayFX sound effects; fix torus strip fills ignoring the orthogonal base

Sound effects (ayFX on the PSG, mixed over the FM music):
- 8-effect bank from sfx/sfx_bank.afb embedded as sfx_bank.h in fixed
  main ROM (interrupt-readable without bank switching). Triggers: random
  cat-paw footstep (0-2) per move, fanfare (4) on reaching the exit, map
  unfold (5) on M, laugh (6) on C, ta-daa (7) when advancing the title/
  hint/win screens. Priorities keep footsteps below UI/finish sounds.
- The H_TIMI hook now also runs ayFX_Update + PSG_Apply each frame;
  PSG switched to indirect (buffered) access with AYFX_BUFFER_DEFAULT.
- Interrupt-safety fixes this surfaced: all joystick reads go through
  ReadJoy() (DI/EI around Joystick_Read, which drives the PSG and raced
  the ISR's PSG_Apply — caused noise, phantom input, minimap-on-move),
  and the PSG mixer is muted at startup (zeroed buffer = all channels
  enabled active-low, which PSG_Apply drove as noise during the logo).

Torus scrolling fix (bug from 7c24daa, exposed by mixed-direction play):
- ScrollDraw's bulk strip fills only honoured the scroll-axis base, so
  with a non-zero orthogonal base the newly revealed edge was drawn
  rotated by 2/4/6 tiles — phantom walls/passages while collision stayed
  correct. Vertical strips are now pre-rotated by g_BaseX in staging;
  horizontal column fills split at the 256-row wrap via FillStripColumns.

CLAUDE.md updated for both (SFX table + ISR rules, both-axis fill rule).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 23:33:07 +02:00
co-authored by Claude Fable 5
parent 00b5833bb2
commit d64cb8b37c
8 changed files with 210 additions and 29 deletions
+48 -10
View File
@@ -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, 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 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 (`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 ## Build & run
@@ -70,7 +71,10 @@ All converters encode pixels as SCREEN 8 **GRB** (bits 75 = G3, 42 = R3, 1
`to_grb()`. Any new color/asset code must use the same encoding. Sprite segments (16/17) `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. 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 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 **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 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`). `~/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 - **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 installed as the `H_TIMI` hook (`BIOS_SetHookCallback`) and each VBlank calls `LVGM_Decode()`
VBlank — so the tempo is **steady regardless of how long a frame's game work takes**. (An (music) + `ayFX_Update()` + `PSG_Apply()` (SFX) — so the tempo is **steady regardless of how
earlier design decoded once per `Halt()` in the main loop; the heavy per-step VDP work in long a frame's game work takes**. (An earlier design decoded once per `Halt()` in the main
`ScrollDraw` could overrun a VBlank, so the decode rate — and the music — slowed *only while loop; the heavy per-step VDP work in `ScrollDraw` could overrun a VBlank, so the decode rate —
moving*. The ISR decouples them.) The game loops therefore contain **no** decode calls; they and the music — slowed *only while moving*. The ISR decouples them.) The game loops therefore
are plain `Halt()`/busy-waits as before the music existed. 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 - **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 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 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()` `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 and the hook install run once at the top of `main()`; `MusicVBlank` is a no-op until a track
is playing (`LVGM_IsPlaying()`). is playing (`LVGM_IsPlaying()`).
- **Config:** `LibModules` adds `psg`, `msx-music`, `vgm/lvgm_player`; `msxgl_config.h` sets - **Config:** `LibModules` adds `psg`, `msx-music`, `vgm/lvgm_player`, `ayfx/ayfx_player`;
`LVGM_USE_PSG`/`LVGM_USE_MSXMUSIC` on (rest off) and `PSG_ACCESS = PSG_DIRECT`. `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 % - 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. 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). - The boot **logo animation is intentionally silent** (no track).
### VRAM layout (off-screen caches above the visible 256×212) ### 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 - 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 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. 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 - 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 (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 progressively as columns wrap. This is the delicate part — verify long straight runs in
Binary file not shown.
+96 -13
View File
@@ -11,6 +11,8 @@
#include "psg.h" #include "psg.h"
#include "msx-music.h" #include "msx-music.h"
#include "vgm/lvgm_player.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 <stdlib.h> #include <stdlib.h>
//============================================================================= //=============================================================================
@@ -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 #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 // Sound effects (ayFX on the PSG, mixed over the FM music). Bank ids (see
// playing. Reads only from the bank-3 window, which nothing else touches. // 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) void MusicVBlank(void)
{ {
if (LVGM_IsPlaying()) if (LVGM_IsPlaying())
LVGM_Decode(); 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 // 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 ty = ya; ty <= yb; ty++) {
for (u8 tx = xa; tx <= xb; tx++) { for (u8 tx = xa; tx <= xb; tx++) {
u8 srcX = GetTileSrcX(ox + (i8)tx, oy + (i8)ty); 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; u16 sty = STAGING_Y + (u16)(ty - ya) * TILE_W;
VDP_CommandHMMM(srcX, TILE_CACHE_Y, stx, sty, TILE_W, TILE_W); VDP_CommandHMMM(srcX, TILE_CACHE_Y, stx, sty, TILE_W, TILE_W);
VDP_CommandWait(); 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 // 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 // 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 // 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 // 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 // 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 // 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) { if (dy == 1) {
// New bottom tile-row 5 → VRAM row (base+224) (off-screen slack). Staging row 0. // 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(); 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 cur = g_AO[i];
u8 w = cur - prev; u8 w = cur - prev;
if (dx == 1) { if (dx == 1) {
VDP_CommandHMMM(prev, STAGING_Y, (u8)(base + prev), 0, w, 224); FillStripColumns(prev, (u8)(base + prev), w);
} else { } else {
u8 fs = (u8)(64u - cur); 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); u8 new_vx = (u8)((i16)SPR_DRAW_X + (i16)dx * (i16)cur);
g_SprFrame = (g_SprFrame + 1) & 3; 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). // Read keyboard and joystick at the final animation frame (for chaining).
u8 row8 = Keyboard_Read(8); 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 // 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 // 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. // Returns 1 if any key (rows 0-8) or any joystick direction/button is active.
static u8 AnyInput(void) static u8 AnyInput(void)
{ {
u8 joy = Joystick_Read(JOY_PORT_1); u8 joy = ReadJoy();
if (JOY_GET_DIR(joy) != 0 || if (JOY_GET_DIR(joy) != 0 ||
IS_JOY_PRESSED(joy, JOY_INPUT_TRIGGER_A) || IS_JOY_PRESSED(joy, JOY_INPUT_TRIGGER_A) ||
IS_JOY_PRESSED(joy, JOY_INPUT_TRIGGER_B)) return 1; IS_JOY_PRESSED(joy, JOY_INPUT_TRIGGER_B)) return 1;
@@ -877,6 +944,7 @@ static void WaitKeySequence(void)
{ {
while (AnyInput()); while (AnyInput());
while (!AnyInput()); while (!AnyInput());
PlaySfx(SFX_SCREEN, SFX_PRIO_SCREEN); // ta-daa on the advancing keypress
while (AnyInput()); while (AnyInput());
} }
@@ -931,6 +999,7 @@ void TitleScreen(void)
// Wait for any key or joystick input, then release // Wait for any key or joystick input, then release
while (!AnyInput()); while (!AnyInput());
PlaySfx(SFX_SCREEN, SFX_PRIO_SCREEN); // ta-daa on the advancing keypress
while (AnyInput()); while (AnyInput());
} }
@@ -995,9 +1064,19 @@ static void ShowWinScreen(void)
void main(void) void main(void)
{ {
// Init the MSX-Music (OPLL) chip once, then install the VBlank hook that decodes // Init the MSX-Music (OPLL) chip, load the ayFX bank onto PSG channel A, then
// the current track every frame (MusicVBlank is a no-op until a track is playing). // 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(); 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); BIOS_SetHookCallback(H_TIMI, MusicVBlank);
ShowLogoAnimation(); ShowLogoAnimation();
@@ -1090,7 +1169,7 @@ void main(void)
do { do {
UpdateTimer(); UpdateTimer();
row8 = Keyboard_Read(8); 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) || } 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(row8, KEY_LEFT) || IS_KEY_PRESSED(row8, KEY_RIGHT) ||
JOY_GET_DIR(g_Joy) != 0); JOY_GET_DIR(g_Joy) != 0);
@@ -1102,7 +1181,7 @@ void main(void)
row3 = Keyboard_Read(3); row3 = Keyboard_Read(3);
row4 = Keyboard_Read(4); row4 = Keyboard_Read(4);
row5 = Keyboard_Read(5); 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) && } 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(row8, KEY_LEFT) && !IS_KEY_PRESSED(row8, KEY_RIGHT) &&
!IS_KEY_PRESSED(row3, KEY_C) && !IS_KEY_PRESSED(row4, KEY_M) && !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 // at least one of its up/left/right neighbours has an open passage to
// it; drop the player on the first such neighbour. // it; drop the player on the first such neighbour.
if (IS_KEY_PRESSED(row3, KEY_C)) { if (IS_KEY_PRESSED(row3, KEY_C)) {
PlaySfx(SFX_CHEAT, SFX_PRIO_ACTION);
u8 ey = MAZE_H - 1; u8 ey = MAZE_H - 1;
if (g_DW[g_EX][ey-1] == 0) { // open above -> step down if (g_DW[g_EX][ey-1] == 0) { // open above -> step down
g_MX = g_EX; g_MY = ey - 1; 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 // 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)) { 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); VDP_EnableDisplay(FALSE);
// Minimap is drawn at absolute VRAM (0,0); show it with zero // Minimap is drawn at absolute VRAM (0,0); show it with zero
// scroll offset regardless of the current torus base. // scroll offset regardless of the current torus base.
@@ -1141,7 +1222,7 @@ void main(void)
do { do {
Halt(); Halt();
row4 = Keyboard_Read(4); 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)); } while (IS_KEY_PRESSED(row4, KEY_M) || IS_JOY_PRESSED(g_Joy, JOY_INPUT_TRIGGER_A));
// Restore game view (DrawFull includes DrawSprite). Put the // Restore game view (DrawFull includes DrawSprite). Put the
// scroll offset back to the current torus base first. // scroll offset back to the current torus base first.
@@ -1192,10 +1273,12 @@ void main(void)
g_MX += (u8)dx; g_MX += (u8)dx;
g_MY += (u8)dy; g_MY += (u8)dy;
PlaySfx((u8)(rand() % 3), SFX_PRIO_MOVE); // random cat-paw footstep (id 0-2)
row8 = ScrollDraw(dx, dy); row8 = ScrollDraw(dx, dy);
// Win condition: reached exit cell // Win condition: reached exit cell
if (g_MY == MAZE_H-1 && g_MX == g_EX) { if (g_MY == MAZE_H-1 && g_MX == g_EX) {
PlaySfx(SFX_FINISH, SFX_PRIO_FINISH); // fanfare over the win screen
ShowWinScreen(); ShowWinScreen();
break; // back to outer loop → title screen → new maze break; // back to outer loop → title screen → new maze
} }
+8 -5
View File
@@ -49,16 +49,19 @@
#define INPUT_KB_UPDATE_MIN 0 #define INPUT_KB_UPDATE_MIN 0
#define INPUT_KB_UPDATE_MAX 8 #define INPUT_KB_UPDATE_MAX 8
// Music: screen tracks are lVGM (MSX-Music / YM2413 / OPLL). The PSG parser is // Music: screen tracks are lVGM (MSX-Music / YM2413 / OPLL); sound effects are
// kept so the player links cleanly, but the tracks carry only FM data. // ayFX on the PSG, mixed over the FM music each frame. PSG is in buffered
// PSG: direct register access (no PSG_Apply needed) — no PSG data is ever played. // (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_CHIP PSG_INTERNAL
#define PSG_ACCESS PSG_DIRECT #define PSG_ACCESS PSG_INDIRECT
#define PSG_USE_NOTES FALSE #define PSG_USE_NOTES FALSE
#define PSG_USE_EXTRA FALSE #define PSG_USE_EXTRA TRUE
#define PSG_USE_RESUME FALSE #define PSG_USE_RESUME FALSE
// MSX-Music (the chip the music actually uses). // MSX-Music (the chip the music actually uses).
#define MSXMUSIC_USE_RESUME FALSE #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). // 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_PSG TRUE // parser kept for clean linking; tracks carry no PSG data
#define LVGM_USE_MSXMUSIC TRUE // MSX-Music (OPLL) — the music format used #define LVGM_USE_MSXMUSIC TRUE // MSX-Music (OPLL) — the music format used
+1 -1
View File
@@ -2,7 +2,7 @@
ProjName = "mazegame"; ProjName = "mazegame";
ProjModules = [ ProjName ]; 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"; Machine = "2P";
Target = "ROM_KONAMI_SCC"; Target = "ROM_KONAMI_SCC";
Binary file not shown.
+28
View File
@@ -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"}]}
+29
View File
@@ -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,
};