Files
gen1recomp/tests/gen2_sound_alias_test.lua
T
bryanthaboi ae6cac89e1 G2 support
2026-08-11 11:53:30 -04:00

162 lines
6.4 KiB
Lua

-- Gen 2 sfx name resolution and the fanfare duck rule, ROM-free.
-- luajit tests/gen2_sound_alias_test.lua
-- Also dofile'd by tests/run_tests.lua. Fixtures only: no Gold cache, no
-- audio device. Covers the two contracts a Gold session leans on:
-- * a shared UI module naming a pokered sfx ("Press_AB") reaches the Gen 2
-- label through Sound.resolve, and the play/stop/isPlaying trio agree on
-- the key the source ended up cached under;
-- * a header read that could not run yet is retried, never remembered as
-- "this sfx claims no channels" (which would leave a four-channel jingle
-- playing over the map music for the rest of the session).
package.path = "./?.lua;./?/init.lua;" .. package.path
local S = require("tests.harness").suite("gen2 sound alias")
local check, eq = S.check, S.eq
if not _G.love then _G.love = require("tests.love_stub") end
-- Minimal audio device: enough for the file-def branch of Sound.play.
local playing = {}
local Source = {}
Source.__index = Source
function Source:play() playing[self] = true end
function Source:stop() playing[self] = nil end
function Source:isPlaying() return playing[self] == true end
function Source:setVolume(v) self.volume = v end
function Source:setPitch(p) self.pitch = p end
local built = 0
local savedAudio = love.audio
love.audio = {
newSource = function(file)
built = built + 1
return setmetatable({ file = file }, Source)
end,
}
local Sound = require("src.core.Sound")
-- === resolution ===
-- A Gen 1 cache answers to the name the shared UI already uses, so the alias
-- table must stay out of the way there.
local gen1 = { audio = { fanfares = {}, sfx = { Press_AB = "sfx/press_ab.wav" } } }
eq(Sound.resolve(gen1, "Press_AB"), "Press_AB",
"a name the cache already has resolves to itself")
-- A Gold cache is keyed by pokegold's labels, so the same call has to hop.
local gold = {
audio = { fanfares = {}, sfx = { Sfx_ReadText2 = "sfx/readtext2.wav" } },
}
eq(Sound.resolve(gold, "Press_AB"), "Sfx_ReadText2",
"the A-press beep hops to the Gen 2 label")
eq(Sound.resolve(gold, "Sfx_ReadText2"), "Sfx_ReadText2",
"a Gen 2 name is passed through untouched")
eq(Sound.resolve(gold, "Nothing_Named_This"), "Nothing_Named_This",
"an unknown name is returned as-is")
-- Only names a Gold-reachable shared module actually plays belong in the
-- table; a row nothing calls is a wrong mapping waiting to be believed.
for name, target in pairs(Sound.GEN2_ALIASES) do
eq(type(target), "string", ("alias %s names an sfx"):format(name))
eq(target:sub(1, 4), "Sfx_",
("alias %s targets a pokegold label"):format(name))
end
eq(Sound.GEN2_ALIASES.Press_AB, "Sfx_ReadText2",
"Press_AB is the alias the shared TextBox / ChoiceBox need")
-- === play / stop / isPlaying agree on the resolved key ===
local src = Sound.play(gold, "Press_AB")
check(src ~= nil, "the aliased sfx plays")
eq(built, 1, "one source built")
check(Sound.isPlaying("Press_AB"),
"isPlaying finds the source under the raw name the caller used")
Sound.stop("Press_AB")
check(not Sound.isPlaying("Press_AB"),
"stop reaches the same source (the elevator pattern: play X then stop X)")
check(not Sound.isPlaying("Never_Played"), "an unknown name reads as silent")
Sound.stop("Never_Played") -- must not raise
-- The Gen 1 side of the same call keeps its own key.
local g1src = Sound.play(gen1, "Press_AB")
check(g1src ~= nil and g1src ~= src, "a Gen 1 cache builds its own source")
check(Sound.isPlaying("Press_AB"), "and is found under its own name")
Sound.stop("Press_AB")
-- === the Gen 2 sfx priority gate (home/audio.asm PlaySFX) ===
-- Ids run highest priority first (constants/sfx_constants.asm), so a LOWER id
-- still sounding makes PlaySFX drop the request (`cp e / jr c, .done`), and an
-- id at or below wCurSFX takes the channels over because _PlaySFX zeroes
-- ch5-ch8 before it loads the new header. Sfx never layer on this path.
local gate = {
audio = {
fanfares = {},
sfxOrder = { "Sfx_Dummy", "Sfx_Tackle", "Sfx_Elevator" },
sfx = {
Sfx_Tackle = { file = "sfx/tackle.wav", generation = 2 },
Sfx_Elevator = { file = "sfx/elevator.wav", generation = 2 },
},
},
}
local tackle = Sound.play(gate, "Sfx_Tackle")
check(tackle ~= nil, "the tackle sounds")
check(Sound.play(gate, "Sfx_Elevator") == nil,
"SproutTower3FRivalScene: the elevator rumble is dropped, not layered")
check(tackle:isPlaying(), "and the tackle is left alone")
tackle:stop() -- the sound ends; wCurSFX only bites while a channel is on
local elevator = Sound.play(gate, "Sfx_Elevator")
check(elevator ~= nil, "with the channels free the same sfx does play")
check(Sound.play(gate, "Sfx_Tackle") ~= nil,
"a higher-priority id is not dropped")
check(not elevator:isPlaying(), "_PlaySFX cut the sound that held the channels")
-- Battle animations reach PlayStereoSFX instead, which has no gate and never
-- writes wCurSFX (engine/battle_anims/anim_commands.asm anim_sound).
check(Sound.playStereo(gate, "Sfx_Elevator") ~= nil,
"an animation sound plays over whatever is sounding")
check(tackle:isPlaying(), "and does not stop it either")
check(Sound.play(gate, "Sfx_Elevator") == nil,
"the animation sound did not become the priority to beat")
tackle:stop()
elevator:stop()
-- === a header that could not be read yet is retried ===
local ChipSynth = require("src.core.ChipSynth")
local realChannels = ChipSynth.effectChannels
local calls = 0
ChipSynth.effectChannels = function()
calls = calls + 1
if calls == 1 then error("program banks not readable yet") end
if calls == 2 then return nil end -- effectChannels' own "not knowable"
return { 5, 6, 7, 8 }
end
local jingle = {
audio = { fanfares = {},
sfx = { Sfx_TestJingle = { address = 0x4000, generation = 2 } } },
}
check(not Sound.ducksMusic(jingle, "Sfx_TestJingle"),
"a header read that raised does not duck")
eq(calls, 1, "one attempt so far")
check(not Sound.ducksMusic(jingle, "Sfx_TestJingle"),
"nor does one that answered nil")
eq(calls, 2, "the failure was retried, not remembered as zero channels")
check(Sound.ducksMusic(jingle, "Sfx_TestJingle"),
"the read that worked ducks the music: four channels leave the song none")
eq(calls, 3, "three attempts")
check(Sound.ducksMusic(jingle, "Sfx_TestJingle"),
"still ducks")
eq(calls, 3, "and THAT answer is cached, so the header is read once")
ChipSynth.effectChannels = realChannels
Sound.invalidate()
love.audio = savedAudio
S.finish()