From 0af69e2df6960c847ee73a9b57fbd3f76a0a4476 Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Thu, 30 Jul 2026 11:20:21 -0400 Subject: [PATCH] pikachu volume --- docs/new-features.md | 5 +++ src/core/SaveData.lua | 3 ++ src/core/Sound.lua | 57 ++++++++++++++++++++++++++------ src/save_convert/SaveConvert.lua | 3 +- src/ui/OptionsMenu.lua | 27 ++++++++++++++- 5 files changed, 83 insertions(+), 12 deletions(-) diff --git a/docs/new-features.md b/docs/new-features.md index 87d5c781..42d72087 100644 --- a/docs/new-features.md +++ b/docs/new-features.md @@ -193,6 +193,11 @@ flushes the live options. Old saves that still embed an `options` table are migrated once into `options.lua` on load. - Music / SFX volume +- PIKACHU VOL (0-7, Yellow only): trims Pikachu's PCM voice clips under the + SFX level, so the follower's constant chatter, the title-screen cry and + every in-battle "Pika!" can be pulled down (or muted at 0) without + quieting the rest of the sound effects. The row is hidden on Red/Blue, + which have no voice clips. - Music Filter - OG GLITCHES on / off (Gen 1 quirks vs. modern-clean battle rules) - BATTLE LAYOUT (OG / WIDE); see "Widescreen battle layout" below diff --git a/src/core/SaveData.lua b/src/core/SaveData.lua index e6e0a5a3..c597c361 100644 --- a/src/core/SaveData.lua +++ b/src/core/SaveData.lua @@ -223,6 +223,9 @@ function SaveData.defaultOptions() -- 0-7 like the GB's NR50 master volume musicVol = 7, sfxVol = 7, + -- Yellow only: 0-7 trim for Pikachu's PCM voice clips on top of sfxVol + -- (the PIKACHU VOL row appears only on Yellow; see Sound.lua) + pikaVol = 7, musicFilter = 0, -- logic fast-forward multiplier; audio is unaffected (GameSpeed.lua) speed = 1, diff --git a/src/core/Sound.lua b/src/core/Sound.lua index 9fdf51f3..d1bd9d05 100644 --- a/src/core/Sound.lua +++ b/src/core/Sound.lua @@ -16,6 +16,30 @@ local cache = {} -- scaling the 0.8 base every source gets local BASE_VOLUME = 0.8 local volumeScale = 1 +-- port addition (Yellow only): 0-7 trim from save.options.pikaVol on top of +-- the SFX level, for Pikachu's voice clips alone. Yellow voices every +-- Pikachu cry with PCM samples that are far louder and far more frequent +-- than the chip cries around them (the follower alone talks on every +-- interaction), so this is the one source players want to pull down +-- without muting the rest of the SFX bus. 7 = untouched, 0 = silent. +local pikaScale = 1 + +-- cache keys whose volume the Pikachu trim applies to: the PCM clips, plus +-- the chip PIKACHU cry that a Yellow cache without extracted clips falls +-- back to (playCry). Red/Blue never reach the second branch, so a shared +-- options.lua carrying a low pikaVol cannot quiet their Pikachu. +local function isPikaKey(key) + if type(key) ~= "string" then return false end + if key:sub(1, 8) == "pikacry:" then return true end + return key == "cry:PIKACHU" + and require("src.core.GameVersion").isYellow() +end + +local function volumeFor(key) + local scale = volumeScale + if isPikaKey(key) then scale = scale * pikaScale end + return BASE_VOLUME * scale +end -- Fanfares occupy the music's tone channels on the Game Boy: their sfx -- headers claim channels 5-7 (= hardware channels 1-3), silencing the @@ -91,7 +115,7 @@ local function playPath(data, key, def, pitch, tempo) reportBadDef("sfx", key, owner(data, "sfx", key), err) return nil end - s:setVolume(BASE_VOLUME * volumeScale) + s:setVolume(volumeFor(key)) cache[key] = s src = s end @@ -215,7 +239,7 @@ function Sound.playPikaCry(data, n) cache[key] = false return nil end - s:setVolume(BASE_VOLUME * volumeScale) + s:setVolume(volumeFor(key)) cache[key] = s src = s end @@ -249,7 +273,7 @@ function Sound.playCry(data, species) owner(data, "cries", species), err) return nil end - s:setVolume(BASE_VOLUME * volumeScale) + s:setVolume(volumeFor(key)) cache[key] = s src = s end @@ -325,7 +349,7 @@ function Sound.startLoop(data, name) return end s:setLooping(true) - s:setVolume(BASE_VOLUME * volumeScale) + s:setVolume(volumeFor(name)) loopCache[name] = s src = s end @@ -349,14 +373,26 @@ 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 +local function reapplyVolumes() + for key, src in pairs(cache) do + if src then pcall(src.setVolume, src, volumeFor(key)) end + end + for key, src in pairs(loopCache) do + if src then pcall(src.setVolume, src, volumeFor(key)) end + end +end + function Sound.setVolumeLevel(level) volumeScale = math.max(0, math.min(7, level or 7)) / 7 - for _, src in pairs(cache) do - if src then pcall(src.setVolume, src, BASE_VOLUME * volumeScale) end - end - for _, src in pairs(loopCache) do - if src then pcall(src.setVolume, src, BASE_VOLUME * volumeScale) end - end + reapplyVolumes() +end + +-- 0-7 Pikachu-voice trim on top of the SFX level (7 = no trim, 0 mutes the +-- clips while the rest of the SFX bus keeps its level). Yellow only: on +-- Red/Blue no cached key answers isPikaKey, so this is inert there. +function Sound.setPikaVolumeLevel(level) + pikaScale = math.max(0, math.min(7, level or 7)) / 7 + reapplyVolumes() end -- hot reload / jukebox A-B: drop one key's sources (its pitch-tempo @@ -390,6 +426,7 @@ Assets.register(Sound.invalidate) -- loading a save) function Sound.applyOptions(opts) Sound.setVolumeLevel(opts and opts.sfxVol or 7) + Sound.setPikaVolumeLevel(opts and opts.pikaVol or 7) end return Sound diff --git a/src/save_convert/SaveConvert.lua b/src/save_convert/SaveConvert.lua index c6f3a5f1..ef2547e3 100644 --- a/src/save_convert/SaveConvert.lua +++ b/src/save_convert/SaveConvert.lua @@ -153,7 +153,8 @@ local function defaultsSave() options = { textSpeed = 3, animations = true, battleStyle = "shift", battleLayout = "og", - ruleset = "gen1_faithful", musicVol = 7, sfxVol = 7, musicFilter = 0, + ruleset = "gen1_faithful", musicVol = 7, sfxVol = 7, pikaVol = 7, + musicFilter = 0, speed = 1, colors = "gbc", tilt = 0, gbcfx = 0, videoMode = "windowed", mods = {}, }, diff --git a/src/ui/OptionsMenu.lua b/src/ui/OptionsMenu.lua index 94e064d1..5e7fb6bc 100644 --- a/src/ui/OptionsMenu.lua +++ b/src/ui/OptionsMenu.lua @@ -2,7 +2,8 @@ -- (engine/menus/main_menu.asm DisplayOptionMenu), the battle ruleset -- (cycles the merged rulesets registry; gen1_faithful keeps the original -- quirks), plus the port's audio rows and display rows: music/SFX --- volume (0-7), music low-pass filter (OFF/1X/2X/3X), COLORS / TILT / +-- volume (0-7), PIKACHU VOL (0-7, Yellow only: trims the PCM voice clips +-- under SFX VOL), music low-pass filter (OFF/1X/2X/3X), COLORS / TILT / -- GBC FX / ZOOM / VOID FILL / VIDEO MODE, and the MODS row that opens -- the mod manager. -- Rows are descriptors fed through the ui.options.rows hook, so mods can @@ -16,6 +17,7 @@ local GBCFX = require("src.render.GBCFX") local Zoom = require("src.render.Zoom") local TileRenderer = require("src.render.TileRenderer") local GameSpeed = require("src.core.GameSpeed") +local GameVersion = require("src.core.GameVersion") local VideoMode = require("src.core.VideoMode") local FrameCap = require("src.core.FrameCap") local Logger = require("src.core.Logger") @@ -178,6 +180,18 @@ local function buildRows(game) require("src.core.Sound").setVolumeLevel(o.sfxVol) return true end }, + -- Yellow only (filtered out below on Red/Blue): trims Pikachu's voice + -- clips under the SFX level, since Yellow plays them constantly (every + -- follower interaction, the title screen, the dex) and they are mixed + -- much hotter than the chip cries. + { id = "pikaVol", label = Strings("PIKACHU VOL"), + value = function(g) return volLabel(g.save.options.pikaVol) end, + step = function(g, dir) + local o = g.save.options + o.pikaVol = stepVolume(o.pikaVol, dir) + require("src.core.Sound").setPikaVolumeLevel(o.pikaVol) + return true + end }, { id = "musicFilter", label = Strings("MUSIC FILTER"), value = function(g) return FILTERS[(g.save.options.musicFilter or 0) + 1] @@ -317,6 +331,17 @@ local function buildRows(game) end rows = filtered end + -- PIKACHU VOL only means something where the voice clips exist: Yellow + -- (data.audio.pikaCries is the clip count the importer wrote). Red/Blue + -- keep the row list they always had. + local audio = game and game.data and game.data.audio + if not (GameVersion.isYellow() and audio and audio.pikaCries) then + local filtered = {} + for _, row in ipairs(rows) do + if row.id ~= "pikaVol" then filtered[#filtered + 1] = row end + end + rows = filtered + end -- A mod's render pipelines are display modes like TILT, so their rows sit -- with it rather than at the end of the list where a mod's own -- ui.options.rows additions land. Nothing registered means nothing