mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
184 lines
7.8 KiB
Lua
184 lines
7.8 KiB
Lua
-- Gen 2 ChipSynth channel driver smoke against a Gold cache.
|
|
-- luajit tests/gen2_audio_test.lua
|
|
-- Also dofile'd by tests/run_tests.lua. Skips when no gold cache / audio.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local S = require("tests.harness").suite("gen2 audio")
|
|
local check, eq = S.check, S.eq
|
|
|
|
love = require("tests.love_stub")
|
|
|
|
local ChipSynth = require("src.core.ChipSynth")
|
|
|
|
local cache = os.getenv("GOLD_CACHE")
|
|
if not cache then
|
|
local home = os.getenv("HOME") or ""
|
|
cache = home .. "/Library/Application Support/LOVE/gold-dev/gold"
|
|
end
|
|
|
|
local audioPath = cache .. "/data/generated/audio.lua"
|
|
local progPath = cache .. "/assets/generated/audio/programs.bin"
|
|
local audioFile = io.open(audioPath, "r")
|
|
local progFile = io.open(progPath, "rb")
|
|
if not audioFile or not progFile then
|
|
if audioFile then audioFile:close() end
|
|
if progFile then progFile:close() end
|
|
check(true, "gold audio cache absent : SKIP")
|
|
S.finish()
|
|
return
|
|
end
|
|
audioFile:close()
|
|
progFile:close()
|
|
|
|
local audio = assert(loadfile(audioPath))()
|
|
check(audio.generation == 2, "audio.generation is 2")
|
|
if audio.runtime ~= true then
|
|
check(true, "audio.runtime still false : re-import Gold to exercise driver (SKIP synth)")
|
|
S.finish()
|
|
return
|
|
end
|
|
check(audio.bankOrder[1] == 0x07, "Gen 2 banks start at $07 (not Gen 1 $02)")
|
|
check(audio.bankOrder[3] == 0x3a, "engine/songs bank $3A present")
|
|
check(audio.songs and audio.songs.Music_TitleScreen, "Music_TitleScreen header")
|
|
check(audio.songs and audio.songs.Music_NewBarkTown, "Music_NewBarkTown header")
|
|
check(audio.waveBanks and audio.waveBanks["1"], "WaveSamples waveBanks[1]")
|
|
check(audio.drumkits and audio.drumkits.bank == 0x3a, "Drumkits in bank $3A")
|
|
eq(audio.mapSongs and audio.mapSongs.NEW_BARK_TOWN, "Music_NewBarkTown",
|
|
"New Bark mapSongs → Music_NewBarkTown")
|
|
|
|
local title = audio.songs.Music_TitleScreen
|
|
eq(title.bank, 0x3a, "TitleScreen bank")
|
|
eq(title.address, 0x77b1, "TitleScreen address (pokegold.sym)")
|
|
|
|
-- love_stub has no real PhysFS; inject programs.bin for ChipSynth.loadBanks.
|
|
local prog = assert(io.open(progPath, "rb"))
|
|
local progBytes = prog:read("*a")
|
|
prog:close()
|
|
love.filesystem.write(audio.programFile, progBytes)
|
|
|
|
local data = { audio = audio }
|
|
local ok, engine = pcall(ChipSynth.newEngine, data, title, { allowLoops = true })
|
|
check(ok, "TitleScreen engine builds" .. (ok and "" or (": " .. tostring(engine))))
|
|
if ok then
|
|
eq(engine.generation, 2, "engine.generation is 2")
|
|
eq(#engine.channels, 4, "TitleScreen has 4 channels")
|
|
-- Ch1 opens with tempo 256 (titlescreen.asm)
|
|
engine.channels[1]:nextEvent()
|
|
eq(engine.tempo, 256, "Ch1 tempo command → 256")
|
|
-- Render a short slice; must produce non-silent PCM.
|
|
local left, right = 0, 0
|
|
local peak = 0
|
|
for _ = 1, ChipSynth.SAMPLE_RATE do -- 1 second
|
|
local l, r = engine:sampleStereo()
|
|
left, right = left + l, right + r
|
|
peak = math.max(peak, math.abs(l), math.abs(r))
|
|
end
|
|
check(peak > 0.01, "TitleScreen renders audible samples (peak=" .. peak .. ")")
|
|
end
|
|
|
|
local bark = audio.songs.Music_NewBarkTown
|
|
ok, engine = pcall(ChipSynth.newEngine, data, bark, { allowLoops = true })
|
|
check(ok, "NewBarkTown engine builds" .. (ok and "" or (": " .. tostring(engine))))
|
|
if ok then
|
|
eq(#engine.channels, 3, "NewBarkTown has 3 channels")
|
|
engine.channels[1]:nextEvent()
|
|
eq(engine.tempo, 187, "NewBarkTown tempo 187")
|
|
end
|
|
|
|
-- SFX / cries (present after re-import with sfxOrder extract).
|
|
if type(audio.sfx) == "table" and audio.sfx.Sfx_CaughtMon then
|
|
check(audio.sfx.Sfx_CaughtMon.generation == 2, "Sfx_CaughtMon is Gen 2 header")
|
|
check(audio.sfxOrder and audio.sfxOrder[3] == "Sfx_CaughtMon",
|
|
"sfxOrder[3] is Sfx_CaughtMon (SFX id 2)")
|
|
local sfxOk, sfxEng = pcall(ChipSynth.newEngine, data, audio.sfx.Sfx_CaughtMon, {
|
|
sfx = true, allowLoops = false,
|
|
})
|
|
check(sfxOk, "Sfx_CaughtMon engine builds"
|
|
.. (sfxOk and "" or (": " .. tostring(sfxEng))))
|
|
else
|
|
check(true, "sfx table absent : re-import Gold for SFX coverage (SKIP)")
|
|
end
|
|
if type(audio.cries) == "table" and audio.cries.MARILL then
|
|
local cry = audio.cries.MARILL
|
|
check(cry.header and cry.header.bank, "MARILL cry has header")
|
|
check(type(cry.pitch) == "number", "MARILL cry pitch")
|
|
|
|
-- A cry is a channel of `square_note` rows, not packed music notes, and the
|
|
-- port used to parse it as the latter: the envelope byte read as a second
|
|
-- note and the frequency low byte as a note_type command that ate two more.
|
|
-- The audible result was a cry that ran until renderEffectData's five-second
|
|
-- cap. Cry_Marill_Ch5 opens `duty_cycle_pattern 0,2,0,2` then
|
|
-- `square_note 2, 8, 8, 1752`, so the first event has to come back as a tone
|
|
-- three frames long (SetNoteDuration: tempo 288 * (2+1) >> 8) at register
|
|
-- 1752 + the cry's own pitch offset.
|
|
local cryOk, cryEng = pcall(ChipSynth.newEngine, data, cry.header, {
|
|
sfx = true, allowLoops = false,
|
|
frequencyOffset = cry.pitch, cryLength = cry.length,
|
|
})
|
|
check(cryOk, "MARILL cry engine builds"
|
|
.. (cryOk and "" or (": " .. tostring(cryEng))))
|
|
if cryOk then
|
|
-- "Tempo is effectively length" (_PlayCry): the length word IS the tempo,
|
|
-- with no $80 base -- that base is Gen 1's Audio_SetSfxTempo.
|
|
eq(cryEng.channels[1].frameTicks, cry.length,
|
|
"the cry's tempo is its length word, not $80 + it")
|
|
local event = cryEng.channels[1]:nextEvent()
|
|
check(event ~= nil, "the first square_note yields an event")
|
|
if event then
|
|
eq(event.register, (1752 + cry.pitch) % 0x800,
|
|
"and it is a tone at the note's own frequency register")
|
|
eq(event.volume, 8, "with the envelope byte's volume")
|
|
eq(math.floor(event.duration * 60 + 0.5), 3,
|
|
"and SetNoteDuration's three frames")
|
|
end
|
|
-- The whole cry, which on the cart is well under a second.
|
|
local frames = 0
|
|
for _ = 1, 64 do
|
|
local next_ = cryEng.channels[1]:nextEvent()
|
|
if not next_ then break end
|
|
frames = frames + next_.duration * 60
|
|
end
|
|
check(frames < 60,
|
|
("channel 5 runs %.0f frames, under a second"):format(frames))
|
|
end
|
|
else
|
|
check(true, "cries table absent : re-import Gold for cry coverage (SKIP)")
|
|
end
|
|
|
|
-- Which sfx silence the music. On the cart sfx channel N takes hardware
|
|
-- channel N over from the music channel with the same number, so a
|
|
-- FOUR-channel sfx leaves the song nothing to play through -- and every jingle
|
|
-- is four channels. The port used to duck on a six-name list, so the phone
|
|
-- number's jingle (and the TM, the badge, the egg) played over the top.
|
|
if type(audio.sfx) == "table" and audio.sfx.Sfx_RegisterPhoneNumber then
|
|
local function channels(name)
|
|
local list = ChipSynth.effectChannels(data, audio.sfx[name])
|
|
return list and #list or 0
|
|
end
|
|
eq(channels("Sfx_RegisterPhoneNumber"), 4,
|
|
"Sfx_RegisterPhoneNumber claims all four channels")
|
|
eq(channels("Sfx_Item"), 4, "Sfx_Item claims all four")
|
|
eq(channels("Sfx_GetBadge"), 4, "Sfx_GetBadge claims all four")
|
|
-- Three-channel move sounds must NOT duck: they fire several times a second
|
|
-- inside an animation and pausing the song under each would stutter.
|
|
eq(channels("Sfx_Psychic"), 3, "Sfx_Psychic is three channels")
|
|
eq(channels("Sfx_ReadText2"), 1, "the A-press beep is one channel")
|
|
|
|
local Sound = require("src.core.Sound")
|
|
check(Sound.ducksMusic(data, "Sfx_RegisterPhoneNumber"),
|
|
"the phone-number jingle mutes the music")
|
|
check(Sound.ducksMusic(data, "Sfx_GetBadge"),
|
|
"so does the badge fanfare")
|
|
check(not Sound.ducksMusic(data, "Sfx_Psychic"),
|
|
"a three-channel move sound does not")
|
|
check(not Sound.ducksMusic(data, "Sfx_ReadText2"),
|
|
"and neither does the A-press beep")
|
|
-- Sfx_Fanfare is only three channels (5, 6 and 8) and is a jingle anyway.
|
|
check(Sound.ducksMusic(data, "Sfx_Fanfare"),
|
|
"the three-channel Sfx_Fanfare is still named as a jingle")
|
|
else
|
|
check(true, "sfx table absent, re-import Gold for duck coverage (SKIP)")
|
|
end
|
|
|
|
S.finish()
|