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:
+48
-10
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -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 <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
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
Binary file not shown.
@@ -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"}]}
|
||||
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user