From d64cb8b37ce4b779bed1f388df1421bb4a30b825 Mon Sep 17 00:00:00 2001 From: Jurjen Ladenius Date: Sat, 4 Jul 2026 23:33:07 +0200 Subject: [PATCH] Add ayFX sound effects; fix torus strip fills ignoring the orthogonal base MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- projects/mazegame/CLAUDE.md | 58 ++++++++++--- projects/mazegame/emul/rom/mazegame.rom | Bin 262144 -> 262144 bytes projects/mazegame/mazegame.c | 109 +++++++++++++++++++++--- projects/mazegame/msxgl_config.h | 13 +-- projects/mazegame/project_config.js | 2 +- projects/mazegame/sfx/sfx_bank.afb | Bin 0 -> 330 bytes projects/mazegame/sfx/sfx_bank.txt | 28 ++++++ projects/mazegame/sfx_bank.h | 29 +++++++ 8 files changed, 210 insertions(+), 29 deletions(-) create mode 100644 projects/mazegame/sfx/sfx_bank.afb create mode 100644 projects/mazegame/sfx/sfx_bank.txt create mode 100644 projects/mazegame/sfx_bank.h 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 0165761229e31be82d37c778beae24244a4fce52..b125acc3340fefd9e54325cc2830a981216a25a2 100644 GIT binary patch delta 7589 zcmbt34OkRclHEN&4F7-rp@7|k;D|2ZU`8FWMU5c-FhL@q;YU{iiG~poh6yr*n&29Z zqMcy$c8xK+`7?3inhTfgw-L4%lgV7?Fs`%Nc&{VwLM}0wL{qX+|J`BXPh}r#iB_$m z!>-5w4~hY4GU25iLSr?bQKt$grW1>ZLZXp4MRXJ6$bzWo*4EawJht3!R-k2Hq zT61vAv(N0^v%9&Qe%nbrf9UL6#GylH55vc{l{kF((DMfm*wzAY`1u3-Tb|hiY(=su zQ`RX8lxr0eCdI6i7b@2V6etS=ic~S{1J|pH0t7ZoVIa5tL`KKPm ze1Mxkl)*sGDAQ5{OSo5M8Prpcay_ysGd3){**sLejMcw*eJg;sYL~GjAD`9KM;erb zW2wg2a84t)0ftMIvlKMt^2FfytcG*6LSp1zl<$f=Q^B9fvZ1!&%5ApZuHMSO+6K~5 zwHsAi`>62>J+xKO}3=VN3nAb%od93Z%u zfF;zaRrJx!Zx2OBBmd?T={3tsZ3g;RQ|DF4Uq(B4JUAV z!^8h+HD?H&qoWOyKjHPY(ShcL5ER#VKw^jTB7@A}QVKQ}uoz*f60(+YAB57>lE=8K zp$QcA*wL`@)cXc*Vi-OCeFKgfiwAhxH33NH+Q7eS;8uk_KD7u_l871P1|o26eC8GA zB+-jA2crPXKyY0Mo1!{Xfen;y;I4-y`l2NyJXs;d9Je5RF5q~GAXB)Gipb+^;S;Sh z@}5SuZe;SnrlR*pXdD6PeK+VU=lMKTI)VyZH)h;uL~ZV;(Miws^?8sXp2|bH!;Zy1 zCY!|QMg<-(;|MUiuJ6T+FX!3NNB7amq3cI+_e35`0W+(Fk7F4`U~aTmjLeI``Gn8o ziXyhF|F{PGfN2Qnh6$w%IfjyuV|YTuAo~ew8A7J-**xyshy~PvHQe;bIBNxPkATM{ zsk{#3@|nf!FzPkfsmEj-7c<~-&3E0$%dT_lr#bgs1QQJC6TQG7*kBV`L>7cOzwG4C#=d+P%7<|{=u8C*}4-fCVR;JfE_a+hiiTo`|1S-8dfw<7WW(NKP z{~tj+T~$d|z~6s>kEF?%8>o6XiBK1a3_+@kL}tAR7BVHkE}Q!Ei$#Ve)TN?&gUDwu`X9xTtayj;M3TE;Pxeijdq+v2>OpL`dlMvUNYnH$T`w}le$ikQ zY0Ccm4I;Tov{4Vt!Cpgc_sU)_MAi3_h%4BxmwF|g2eFPle#2VtJf!k1+{f~K!a(1@ zbHUp_gSt^2n9?mcz=vJ!8l`@&li#@(HXWI617;yhYDgPcw_THgEVnOSgF_k82;Nz* z$pE&l7yFkMi%BHxDc;$!+X_>|rt)L~$jYGSKO`bkvO0+PrxJWolIwObM;^pVt_ydq zra?nU2pp&b85tQ>;KKvytjq}K82G_vq#N0cXYKGNnf6bs3UQ7x4P%9TivwqnvaeJ1 z`TU}KRzxix1H1S$B0H$73{|*Nh_y=9j5IPPVyLs};^s>I9A`$KOgqQf+l2}SQEB18 zm0qYeM%c|l(CW?}v~wf~iVCop+|2*3z;*<++%~ckg$q>@as^E9l+#N=DP+12G3mGG z7-LA@ai6;u72_>f_)Vbny>?9w9xJ1lAj|Mi$ByoW&$I4x8^%ou^AY&wfa8vjXRL#8C>9LF zZ@z*ocZI~2J*W<6l6FlXcrv&?A5{dE7>1;*2&){y=GKisEI?8aN^lwFWksOd-og}> zP+W`ZJi>R2wOXg-hH;XqAd!t^@}))4ba-WH!8>D~DU#Bim)ban%B`L7N5$oJkwfda z_=(2JzpUsC;a^!7MnK)5NK@2@@PAl`XI!+7dwt@P%-6Q05NrT!3ri2Ar+medf>;G* zW6A{y5(dgy3VJS91$dC*j7SUZiX;fLAjw_2o|`d=@ttypNmG4L?I`Z}D0E>I>K%oG zWBg-h$1nj>Wyv&ip)r0rO{35&qnsa)Lj5t!JZTIrT73#Ll%kjEEfx~jF*1O@9)-S@ zAWZGY5SKMsz3TgBBU}6#QEBNyrTFN?Ym)PaifwLes0{v{;t+l(^xCA}?~1+aA72Fp zK;)O}VoDPVx_oxHSj@dQd7<@8g&npJl0#})LuzARIIVJQ)W{uGw2UR~VFar>GB2zO zwt1EYBSQF$5}RG1OWNrn{M8Jlkgyefi@hlOgKdB~*5;joyr(64v#Zq3eUyh2!QYV8{n|rflh~bhXVi!2Vlx_a9oH@ zwm!czMa3$FqdPBkGB1=tl^vP@EW&95#__3TtePu(*^%m)(%6*}3|yVeZ!uFaFaZy+ zJYE(Ap(~Sm{HC&$aM*~WJDCqKS71UfF!k_Hm$7kR1|hFRI+i!CUc7!;RdwYo+HB;u z#4WMTF)}Y?3T!p=x~P-cw7(IBPj(6!b&=0zGB3M?W3@uydg1jF=Hy5<#Q%kIp&Q>N znMdFfx%L0FU1K5<*PJPCHU!o9e`8ZkiL$DG#G{ z>Y!Zq^wEZP4UHV0dd+0`R%ph-m+P21-m2Iv=u4%f+6yt+13A_4l;%4}o#wwB<(j`a znl#t+S|>!Rp&``_k@~rx8&YtJ3}UoAMJ^>|SBgSv-F`9NS$Fl3*>KC9D@{FHntCRD zEt(id90mb9MA|YW61X zBt6?Ydy_c>tiU8Xb+KRuyi|T*BXWQ=-$*lzQoyMHAv*}!kBzZiy|f6w=8mNECJ{z_IQ?V`M%8d<(X8<2$Dp*>W)xIw>LuH7jZBR7LA@b- zvBJ-fPdlhO!A_F$Su~thE)QR{S+bgh%aw4X7gyosE5NuBAD_jGE2ZUtBT+TfX(PhH zqIS5s`27GZ1l2E8R!VKUlm8V|Irv3dSlMXp0OC@>eHg*Ln(dO)Tc!4X_0xAo0>2=+EJySC-(hlXml*< z3p;1D@s28PRYH>WCIn)`#mww9G65gj0Tt=^WgtZO_i{Ldf4)_Sd@_=#$LGSlt&v0n z0DQ+*2$-?tAwT&CFZuT6ElW&v-^MRvdCZmbXz6Rf4h+1 zP|5MrwkSWUw1Zr3cH(Rbn$3xsagT55f0fVK;-A=lV|t#d9lI+ZLj@g}JWW1C@|6;3#@ihn{XjI3-d?%`SQN_Y$nOORbST*ZBw zluRA3;s%qRnCPgIZoF5I+oen7;8Y`Ay|$CCyb&I%A6T7H=^|o zB7Om^skDzpKZK2r_OU;}XG`qS(a~^a#)3vE+|OoCR@|(xuUx{V&YWPa+5pE5bjHld z$cSEv-AgklyY-KYPWe1a`NbA9fA}eNqiA29o!ltG>8xz{h^5k)2K{T|2P`>n5+rl& zYhwQsjiNL$uF_)EO`^??KR~eqWBWb*Z^S-Qqk^N_`Up+wWXVlpZi1sqqXPDX{(P(G zsL_O465KQ`PkuxEM!Q>t;aKz@@e_xcR_b?)+BQJ6x?4X951()q1Z72p3UZ_Ngy_iY zP`@DB9YNt<%yH3?)1f{pVKB!_qGNG~x>d9r^vA^0Dd)7uMExEy*(%yu4=9L5*&^g1 z$PVA*q8$c!DR%yjk$Y)otYR*tQ6u-k%%s@&YCDhMhv65EXwC?I8O?F=8;mR|Wf<<2 znbCm@Fe|+E#d5!&sm}MafB%5}Lq9u{X%rULYu^&}Z;2UFfy$g1i;0OGv;s+Zuh#ML zK)SE*C=(d9F)mYZzBF6&nKQ@^AVgkYZ+x8gP(2A|! zbZ__m`_JvFnz=RBjWzpy|NK)n@M+r{U`pml4LrM+GB4pKS4lK*-F_Bx2vmq^+b==Q z|AWK}g?;3anixN`pHCWO4#D*#=I)X6i8ZjAi!y_cBg^V%jVBt9bT`OHSz~#_L#z^q zpS?lulNrm`)HL*2QwY!+Ob{rO$%qI#Z9A6E^M$;oqyVX%CA1EBA`UPpl@611~u_lrIy`RO~D{V>v|eNNQx7sJ!SB>HoHI`q`m zeGpm)MCN(mV{*5v4~fEaJMbjUFp1Y2;pGo-pmPHD2xueaL=4Op;F|`I$3KJhSz(Bf zT*oiij(-&E)*letH45f{_=p`iv3I$z(b1AK9T0cJAUxg*BJJ)U=pwWG2ThdwM@_8T zrAczSHM2rA>Aa?PjAFhX#hk!uLeDq9AKHg;Ftfw(>6RVI`$Uk8NwA{Rb$-@2c2xHL zhmj4xe5&d4E4blKr_PKju3hc-RtEoeVXfu9N==|%t>ffrcdfzYH|%ilxnVC`&9sUW zh?p9?0Ra4yfZa&8ijX4%YZG<=vQ4b9r$SD^oG*Df)&By?g83B?0C!9*SQ<})&+c?2 zM2s}1o(>76$@8XBl1k18*F8+KdPQ0K!b3tel;dj?Tu04LSvR0T1sxH>&#HrL)VC;` z4Y+*j>gm3YcR>YEFzglE{F!rB~mO^JnY0oO;%Si=YytaIT*J zs$Td2f4QG&6PU2_AG@=U!A6m1PnVn5mtc6V5r;Nn4CnwIUh*`)<7#u|b`E>W*SKW=6IGLpn z_1?SpzWeUGzjxnjZx3s44?CGCd9^d?$vg$oa{kic?C=u;Ro67~!8Cum{4))4QX{`$ z)8YRY1i!B)8*H+`2qmr2W$W+u@~7*2D8+?Fiuj=wZR7bZeaGA=n zT&;3&!Rs&`z!ngdFp>RSrXf$QWu@{gGI$-kRvw#7tiRPh6|$buow_*>x}%Zn84364 z`nDm7UP0K`(T0}uw9E<+_C+~ELenc>I`7lYA>rw+{9W z)_r3g%Rmh-RO{x+YW7t{ED6nfiqgkZYxt-dG%%3y9U4RwL7+@MGRqt{RP&hZu?!Pm zoXaUUV2BbRRkIK_(~prqgXgkG{j%l_t?L=+rstb@Y6HvptstQp_ghjqv`*h|>ZapR zgS!<_mE9_Zgb3r*d2tJf*5k+XLI~*V)Q;m>tSShZ-D@y@A%-Q0uwZ~WGBHpIz=ycZ z2LNmG&mpt(*iZc9^DTLxwV&tN+f-4E;?&JoJ;T=C#2bSd{QJ2*BF9cWlX=o$H(g-- zI&Yo_lKb|y3z+;9+&3jEMOrp7V0CoyhW?)3YhxLEj(`gKd**Yu^JLt&dFWOj1*HnGL?>T-U$0Uf-9nnnkZ`S$B{$WhcGPn5u>ez}2b z+l(sippsE^@Gfe)2bRv{&707{G1N4Uv@QafU5;T$$^W zs0SqKq{B?8LxCkSv|yRa3w}Z-H^}5hU4Q#RzlBA?%gBE(U;{#4UUa!2aUp7$7zBam zh~efA6|mNjmD&qiM1R8j>rw@n)<96)-~fmn$^#5EflJ95B|tI2yq6zZ$Hs)J$eV@i z6QK#@JB4gjXcSpe$QnacQ6+^qY|L&T>ZbjW)>*|B6|(P#KE3$<7QiGD$&xA}U|W3l zHR=p8h;s)e2h~7yEe?zIC#$iBu5Mv-!j^i&r9Nz#Tnsw)=V6ZnjDryRbC{O=K+lGT zM_X+ABj}(D8Qox2^t%}q4gvH#7YHolb{C_Ht<$oMy=h}|6V1i5@Gdpsh}q5$d{ zaG6AZkH^OiE@E7L0LureF-7BKJ+v-uHnp7YQnyLgwktuZ&Nj4fm0G z^5p6!aMB4w3+fo}<%>5|_Ozge`=<+xaT;l72ykX~i9o3&>g@vV zmkHEP5MkPwV1;BNQClhiNvNv?YPX;z60}!%(G9tRV&I#l%6&Au?7zqE(cHYYD|FeZ zPGr32wP2wWh8AQT(+34Xp1+rsm9-aCJC?yn4Uk@ucDx#PP>m(H;Bo!#OJ6>XJ>NKv zh4XHD0%^sh;PC$neoQ8$N&XZH9Yu8JbPp^}WK zAuzCG{saT|PtlZR3`|mTFTVO3D1z_ZG=L6+AAo299s_aTY=hcHG!;%WqjNUUFr7&7 zAB#l|w&euY!|2ng{`@Bfbl9C5%Kz5TrqMXW;EEH?2+3h4o70p)b3x2nqZ%ay-S!eO zg?O;~Iav1C!vB`PTN-}3l-(R-Saf%HPY`#wG=zY|Se`C#4&u5>@uHEX?A4f@Y-dd> z!T5p8FsiYP)UO!{zp4&uvDE((dwjuehJ=xm@dr5YaD=4?_l6UMi5K}sZ)dkHqP!!QWp-dnS<-r3&pY%e^P@)Jul>Ja;RtZz>FEVO+N^w+a^KGwI+-PvA@l3Fct!nL4R zWQvpzQhzApCrc}q1ZnFFGjP{;G>0}fFX683nB57hjbBkvvuk)TJd5CKcI6NB;*R@3 z57ZFx1ZsqmGjMf8yyRMHAY>N4YFBH2UphnuY&LVt`c~z!QieEDH`wdN-#@^96rV&^ z?PC8DA49@uB7T|W)`3)iM$X?o(BDJtEQhK!H~}W`?-2bx1Kh+;M$J~fWlyumw)UpZ z1F9bC2qu~bL_h;1%JM)6T&XzXmY1i7LO9$#Kpn$W0SSXZG{EJQGjX5MbXK-@v0%zIg|AW~KoGeZg-20KSG%tKj~n`+wPG zPQ2l2E!D-u;E>d49h%1-4;hWhaA+k~I4LA-vuZxMr;>eHwHDu-K36RPN9-Ah&jy!9 zwKI@R`ra7qm8gi`!9>Y78TEBg{{gz{0s8bK(CrVmv2mO6Ol#<@JSY&r^prdq}Xt+wJ8IGj2K`!uvjo52dr&_vI2Uxp0iM(FL-czrzT(2@^fi|e6 z4y_W@fw#<^s{}6I35WhpI^e%8oTIvCQQ|ERw!4-7s}ojq+KF2F<^vW`vw6w{DBBVV z1OKt5=3yIn*T*|hF}zU^g;2eLKUdT&Dx)nlecV$=={@yRpVVN2l4(xG#J6nn^1x>W zTuku3H>l(b+}y2aM3sq_O0p@g63xe^^%}%w^iyg zg`scZ(Sx*qYtM)8*9JH@)pfen!q6RktNqELkn;vBr>IQx53mhMOWClbNb*oMyEG|d{>MP{q0=mT_Y#MR zK3v0fSF`m=dlXSMHvDArRniI)nqWFx^Ipx!FSxTc)~jzauT5Wl(>9zBC)kjmfr-Uu zk{=Oc&$7q){+UHs3=6GZ__CmtfOcLMx>fRPyrZTcGFJ>JhW!J**wRpYjX%D-pTq61 zc4NQ%R!nj}2($3VApZxTYja%w2UUxwPU&xTC>SF8mV^#r+F_mr4yjdj+=sPzDGQ^- zBix-3;kY*~_H6u6EqgF|8F{;weJgoKw7O2b1zvyC78)8lbYpz60j{I4hwxg-RnjxVm<$B>{8wrQCrE^H4RW#fKuPvAUVxLCO1ZHSR#ik@Xl=7Hjxg+|ev zfLA0dT~3pLdTjYqxH-0b32xq9o`IXcEKkOH)GtMcn{_FfxG|@!z|HxT6x@8Bl1}E% zj-Gr>lMmN=e0|+Avcn)id7GRZGaRXF5%BBSpQ>$h!9RdTMtaF-;p~4twlK>x>7a)f z=0=4PgSsOk!`y0He$L2_Mi*LYN#y)m?TH@-c13jJiMR9x|S~;V-idH(go_&nup$qDq ziKp>I$P!9+XPUa$H@T>Oc5-aJcQW<%KK_r5npXtfD?*l7j#1NdAu-|O7C`Y)2kf7X zWq1d%2iWE5N=s)wl+|bIm$eCnrlH%kpW_N0%KaIS6Vz=&gNc(hLh+~9Jtr7&Km7Z8 zlOOyIg@S@(%zFVuNThOw!rwn2ATThvz9er8=CyXa{Rz*Hna6&)fS_Xb$3U4`yYIUd z`#rxk`}gjDW54C%{_nj%-;ym{XFVK$dTMk(yxvt%lej5_w`TT=kHi!Ng;8gHKh(He z_hVWpWWz`I$M}eSbmAma5H6!I^?;1)*blqulj^+`8wz}+@kDFql@{47sgnEP2c#l} zkGw_Z<*DSp00}Iq1W3(xu2^ho6>xDaOII#y6+m!i;#eh9f0-y4|ksTUyCz}rtZ?GF#<<0Mc(p+L0>eR3lNV!~y@I5220p~FE0 zv*wuKm4rLdKplgbR?TyQ?wAm2Ne>YTp7RmFNNo|Un&$=TMWCas_393RA8x>lw8J7E zAA%?Y7>JyJ%?@q23{qkV?dM$v&&Ln?Ll&6Ahc+$pc72WefH!Y{07gpJT5?sh~ z+jT<~DZ0$_!ZDcTWngpNR&6)AZqpI2JG9c}q!S%3IwgqC;Qq-lM=*r)#N+bLJs#S3{ zjrJ6KT%2mMZAg0j_<4QH)pOG(n@vB|e(5)jtZS8NwW<-^3?7_Yx!9&4ZlH0nw*@2< zK_&pnHgcaeGH!fcmK*rACa%1Re;j{Vt2RMdv+K^4ynlf*|DQ`+Y|+D4JYAcFAlixU XJI_jA4}LDYBU{puP5vrddg1>7Kq //============================================================================= @@ -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 0000000000000000000000000000000000000000..cd50c7882a76fdaea652daa591ab6a802aa76b07 GIT binary patch literal 330 zcmd~;*fkA{ZOh|abgbAS{LO?QH7|a$G1hOwEgzuCYv68cZE0?5Y`CDX z<{Cq9-?~){eSPctfyDZ3hW`G(-k$FDp+H7|Z+B-~OA}C81hcesBx@L3D65!+WF$*C zTPS-NTR3|Jhh!9I6h{PS7QznI1 iOcxZ^USsG3x&s6zOz2&=o?(4I$Xzhd)7jPxbR7U~pLsk0 literal 0 HcmV?d00001 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, +};