initial commit

This commit is contained in:
bryanthaboi
2026-07-17 20:30:02 -04:00
commit a5d2e77e7d
298 changed files with 100561 additions and 0 deletions
+222
View File
@@ -0,0 +1,222 @@
-- Sound effects and cries synthesized from compact ROM channel programs or
-- loaded from legacy static audio definitions. Sources are cached; headless
-- use is a safe no-op.
local Sound = {}
local cache = {}
local enabled = true
-- port addition: 0-7 SFX volume from save.options.sfxVol (OptionsMenu),
-- scaling the 0.8 base every source gets
local BASE_VOLUME = 0.8
local volumeScale = 1
-- Fanfares occupy the music's tone channels on the Game Boy: their sfx
-- headers claim channels 5-7 (= hardware channels 1-3), silencing the
-- song until they finish (audio/headers/sfxheaders*.asm; the game also
-- blocks on them via PlaySoundWaitForCurrent/WaitForSoundToFinish).
-- The Poké Flute even issues SFX_STOP_ALL_MUSIC first
-- (engine/items/item_effects.asm). Music.lua pauses the current song
-- while one of these plays and resumes it afterwards. Ordinary short
-- SFX (menu beeps, hits, cries) stay overlaid.
local FANFARES = {
Level_Up = true,
Caught_Mon = true,
Get_Item1 = true,
Get_Item2 = true,
Get_Key_Item = true,
Pokedex_Rating = true,
Dex_Page_Added = true,
Pokeflute = true,
}
local function playPath(data, key, path, pitch, tempo)
if not enabled or not love.audio or not path then return nil end
local src = cache[key]
if not src then
local ok, s
if data.audio and data.audio.runtime and type(path) == "table" then
ok, s = pcall(
require("src.core.ChipAudio").newSfx,
data, key:match("^([^@]+)") or key, pitch, tempo, path)
else
ok, s = pcall(love.audio.newSource, path, "static")
end
if not ok or not s then
enabled = false
return nil
end
s:setVolume(BASE_VOLUME * volumeScale)
cache[key] = s
src = s
end
src:stop()
src:play()
return src
end
function Sound.play(data, name)
local sfx = data.audio and data.audio.sfx
local src = playPath(data, name, sfx and sfx[name])
if src and FANFARES[name] then
require("src.core.Music").duckForFanfare(src)
end
end
-- Play a move's sound with its MoveSoundTable pitch/tempo modifiers
-- (data/moves/sfx.asm; GetMoveSound loads them into wFrequencyModifier/
-- wTempoModifier and the battle sound engine applies them to every
-- battle SFX -- audio/engine_2.asm Audio2_ApplyFrequencyModifier/
-- Audio2_SetSfxTempo). The extractor pre-synthesizes one WAV per
-- distinct (sfx, pitch, tempo) as "<name>@<pitch><tempo>" keys in the
-- sfx table; older audio.lua builds without the variants fall back to
-- the unmodified sound.
-- anim: a moves.lua anim table { sound, pitch, tempo }.
function Sound.playMove(data, anim)
if not anim or not anim.sound then return end
local sfx = data.audio and data.audio.sfx
if not sfx then return end
local name = anim.sound
local pitch, tempo = anim.pitch or 0, anim.tempo or 0x80
if data.audio.runtime and sfx[name] then
playPath(data, ("%s@%02x%02x"):format(name, pitch, tempo),
sfx[name], pitch, tempo)
return
end
if pitch ~= 0 or tempo ~= 0x80 then
local key = ("%s@%02x%02x"):format(name, pitch, tempo)
if sfx[key] then
playPath(data, key, sfx[key])
return
end
end
playPath(data, name, sfx[name])
end
function Sound.playCry(data, species)
local cries = data.audio and data.audio.cries
-- returns the source (nil headless) so callers that block on the cry
-- like the original's PlayCry -> WaitForSoundToFinish can poll it
local definition = cries and cries[species]
if data.audio and data.audio.runtime and definition then
local key = "cry:" .. tostring(species)
local src = cache[key]
if not src then
local ok, generated = pcall(
require("src.core.ChipAudio").newCry, data, species)
if not ok or not generated then return nil end
generated:setVolume(BASE_VOLUME * volumeScale)
cache[key] = generated
src = generated
end
src:stop()
src:play()
return src
end
return playPath(data, "cry:" .. tostring(species), definition)
end
-- GROWL/ROAR are the only two moves that play a cry (IsCryMove checks
-- wAnimationID); GetMoveSound still adds their own MoveSoundTable pitch/
-- tempo bytes on top of the cry's species modifiers before the tempo
-- register is set (Audio2_SetSfxTempo: tempo9bit = wTempoModifier+$80).
-- $80 is the table's "no extra shift" tempo byte (every other move's
-- entry defaults to it), so the two moves' own bytes -- Growl's $c0,
-- Roar's $40 -- are the *extra* shift on top of whatever the species'
-- cry already sounds like. The generated cry source already includes the
-- species' pitch/tempo, so layer the move's extra shift on with
-- Source:setPitch (pitch mod is left unmodeled: both moves set it $00).
function Sound.playMoveCry(data, species, tempoMod)
local src = Sound.playCry(data, species)
if src and tempoMod and tempoMod ~= 0x80 then
pcall(src.setPitch, src, 256 / (128 + tempoMod))
end
return src
end
-- is a previously played one-shot still sounding? (ShakeElevator's
-- .musicLoop polls wChannelSoundIDs+CHAN5 until SFX_SAFARI_ZONE_PA
-- ends.) Headless / never-played names read as silent.
function Sound.isPlaying(name)
local src = cache[name]
if not src then return false end
local ok, playing = pcall(src.isPlaying, src)
return ok and playing or false
end
-- cut a one-shot short (the SFX_STOP_ALL_MUSIC beats around the
-- elevator shake stop the last collision thud mid-ring)
function Sound.stop(name)
local src = cache[name]
if src then pcall(src.stop, src) end
end
-- Looping sources (the low-health alarm): started/stopped by game
-- states. ChipAudio generates the two-tone siren used by runtime imports;
-- legacy data can still provide a looping static source.
local loopCache = {}
local looping = {}
function Sound.startLoop(data, name)
if looping[name] then return end
local sfx = data.audio and data.audio.sfx
local path = sfx and sfx[name]
local runtimeAlarm = data.audio and data.audio.runtime
and name == "Low_Health_Alarm"
if not enabled or not love.audio or (not path and not runtimeAlarm) then
return
end
local src = loopCache[name]
if not src then
local ok, s
if data.audio.runtime and name == "Low_Health_Alarm" then
ok, s = pcall(require("src.core.ChipAudio").newLowHealthAlarm)
elseif data.audio.runtime and type(path) == "table" then
ok, s = pcall(
require("src.core.ChipAudio").newSfx, data, name)
else
ok, s = pcall(love.audio.newSource, path, "static")
end
if not ok then return end
s:setLooping(true)
s:setVolume(BASE_VOLUME * volumeScale)
loopCache[name] = s
src = s
end
src:play()
looping[name] = src
end
function Sound.stopLoop(name)
local src = looping[name]
if src then
pcall(src.stop, src)
looping[name] = nil
end
end
-- is a looping source currently sounding? (drivers assert on this)
function Sound.isLooping(name)
return looping[name] ~= nil
end
-- 0-7 SFX volume level (0 mutes); cached sources (menu beeps, cries,
-- the low-health alarm loop) update immediately so the change is heard
-- on the next play
function Sound.setVolumeLevel(level)
volumeScale = math.max(0, math.min(7, level or 7)) / 7
for _, src in pairs(cache) do
pcall(src.setVolume, src, BASE_VOLUME * volumeScale)
end
for _, src in pairs(loopCache) do
pcall(src.setVolume, src, BASE_VOLUME * volumeScale)
end
end
-- re-apply persisted audio options (Game calls this on boot and after
-- loading a save)
function Sound.applyOptions(opts)
Sound.setVolumeLevel(opts and opts.sfxVol or 7)
end
return Sound