mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
162 lines
6.4 KiB
Lua
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()
|