Files
gen1recomp/tests/game3_se_length_test.lua
2026-09-23 16:34:15 -04:00

136 lines
4.6 KiB
Lua

#!/usr/bin/env luajit
-- pokefirered/src/battle_anim_special.c:1200
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local function find_pack_root()
local root = require("tests.game3_cache").root("audio/index.lua")
return root and (root .. "/audio")
end
local root = find_pack_root()
if not root then
print("[skip] no imported FireRed audio pack; nothing to measure")
os.exit(0)
end
print("[test] pack at " .. root)
local cache = { read = function(_, rel)
local f = io.open(rel, "rb")
if not f then return nil end
local d = f:read("*a")
f:close()
return d
end }
local Player = require("src.core.game3.m4a_player")
local Mix = require("src.core.game3.m4a_mix")
local Audio = require("src.core.game3.audio")
local pack, err = Player.loadPack(cache, root)
if not pack then
print("[skip] loadPack failed: " .. tostring(err))
os.exit(0)
end
local function bake_seconds(id, maxSec)
local slot = { voices = {} }
if not Player.start(pack, cache, slot, id, { forceSeq = true }) then return nil end
local L = Player.bakeSlot(slot, { raw = true, maxSec = maxSec, stopOnGoto = false })
return #L / Mix.SAMPLE_RATE
end
-- pokefirered/src/m4a_1.s:751
local longest, longestId = 0, nil
for id = 0, 400 do
local info = Player.songInfo(pack, id)
if info and info.kind == "se" and not info.hasGoto then
local s = bake_seconds(id, 60)
if s and s > longest then longest, longestId = s, id end
end
end
check(longest > 0, "measured the pack's one-shot SE cues")
check((tonumber(Audio.SE_ONESHOT_MAX_SEC) or 0) > longest,
string.format("SE_ONESHOT_MAX_SEC %.1fs clears the longest one-shot (id %s, %.3fs)",
Audio.SE_ONESHOT_MAX_SEC or -1, tostring(longestId), longest))
check(Audio.SE_LOOP_MAX_SEC == 2.5, "looping cues keep pret's 2.5s slice")
Audio._ready = true
Audio._pack = pack
Audio._cache = cache
local realBake = Player.bakeSlot
local function production_max(id)
local seen = nil
Player.bakeSlot = function(slot, opts) seen = opts and opts.maxSec; return { 0 }, { 0 } end
Audio._seRawClear()
pcall(Audio.playSe, id)
Player.bakeSlot = realBake
Audio._seRawClear()
return seen
end
local prodMax = production_max(319)
check(type(prodMax) == "number", "Audio.playSe passes a numeric maxSec (" .. tostring(prodMax) .. ")")
-- pokefirered/src/battle_anim_special.c:1200
local SE = require("src.core.game3.se_ids")
check(production_max(SE.SE_EXP) == Audio.SE_LOOP_MAX_SEC,
"SE_EXP keeps the 2.5s slice (" .. tostring(production_max(SE.SE_EXP)) .. ")")
check(production_max(SE.SE_LOW_HEALTH) == Audio.SE_LOOP_MAX_SEC,
"SE_LOW_HEALTH keeps the 2.5s slice (" .. tostring(production_max(SE.SE_LOW_HEALTH)) .. ")")
-- pokefirered/include/constants/songs.h:89, :102, :284
local CUES = { 319, 165, 204, 227, 249, 276, 85, 98 }
for _, id in ipairs(CUES) do
local natural = bake_seconds(id, 60)
local idMax = production_max(id)
local production = idMax and bake_seconds(id, idMax)
if not natural then
print("[skip] id " .. id .. " not in this pack")
else
check(production and math.abs(production - natural) <= 1 / 60,
string.format("id %d bakes in full (%.3fs production vs %.3fs natural)",
id, production or -1, natural))
end
end
local bakes = 0
Player.bakeSlot = function(slot, opts) bakes = bakes + 1; return realBake(slot, opts) end
Audio._seRawClear()
for _ = 1, 3 do pcall(Audio.playSe, 319) end
check(bakes == 1, "MUS_CAUGHT_INTRO bakes once across three plays (" .. bakes .. ")")
local entry = Audio._seRaw[319]
check(entry and entry.frames / Mix.SAMPLE_RATE >= 4.5,
string.format("memoized buffer is the full cue (%.3fs)", entry and entry.frames / Mix.SAMPLE_RATE or -1))
bakes = 0
pcall(Audio.playSe, 319, { maxSec = 1 })
check(bakes == 1 and Audio._seRaw[319] == entry, "an explicit maxSec bypasses the memo")
local budget = Audio.SE_RAW_MAX_FRAMES
Audio.SE_RAW_MAX_FRAMES = entry and entry.frames + 1 or budget
pcall(Audio.playSe, 5)
check(Audio._seRaw[5] ~= nil and Audio._seRaw[319] == nil
and Audio._seRawFrames <= Audio.SE_RAW_MAX_FRAMES,
"memo evicts least-recent cue at the frame budget (" .. tostring(Audio._seRawFrames) .. ")")
Audio.SE_RAW_MAX_FRAMES = budget
Audio._seRawClear()
Player.bakeSlot = realBake
local caught = bake_seconds(319, prodMax)
check(caught and caught >= 4.5,
string.format("MUS_CAUGHT_INTRO is at least 4.5s (%.3fs)", caught or -1))
if failed > 0 then
print(string.format("\n%d check(s) failed", failed))
os.exit(1)
end
print("\nall SE length checks passed")