From 5f4bbd208477d2e8e2d016e842ee5c32fc0ffc26 Mon Sep 17 00:00:00 2001 From: epicmeme546 <178368684+epicmeme546@users.noreply.github.com> Date: Tue, 10 Nov 2026 02:02:45 -0500 Subject: [PATCH] Play the launcher's sounds at Low by default. A new launcher.json starts with sounds at low instead of off, and a value the file cannot hold falls back to low too (launcher/ui/sound.py DEFAULT_LEVEL). The player can still pick Off, Medium or High in Customize Launcher. The sounds are still only the game's select and back sounds, read from the player's own game. Co-Authored-By: Claude Opus 5.5 --- docs/launcher.md | 4 ++-- docs/releases/v0.6.2.md | 2 +- launcher/settings.py | 2 +- launcher/ui/app.py | 7 ++++--- launcher/ui/sound.py | 17 +++++++++-------- tests/launcher_test.py | 27 +++++++++++++++------------ 6 files changed, 32 insertions(+), 27 deletions(-) diff --git a/docs/launcher.md b/docs/launcher.md index 33c04bd1..fc616819 100644 --- a/docs/launcher.md +++ b/docs/launcher.md @@ -47,7 +47,7 @@ Customize Launcher (on the home screen) makes the look the player's own: - Animations (on by default): the cursor glides from row to row, a new screen slides into place, sparks drift up over the backdrop and Navi bobs in the title band with a pulsing halo (`App.animate`). They move by the time that passed, stand still while the window is hidden or minimised, and Off stops them all. -- Sounds (off by default; Low, Medium or High): two of the game's own interface sounds, the decide and cancel sounds +- Sounds (Low by default; Off, Medium or High): two of the game's own interface sounds, the decide and cancel sounds of its menus, when a row is chosen or a choice changes and when the player goes back. The launcher decodes them from the player's built game or dump into `data/cache/sounds` (`gamefiles.extract_sounds`: `Sound/Resource/Stream/SE_Sys_Do00.bwav` and `SE_Sys_Cancel00.bwav`, the files `runtime/menu_sound.c` plays in the @@ -377,7 +377,7 @@ pruning, unsafe archives, the background update (a release staged beside the run `staged.json` marker, a staged release installed after a restart, no install while the game runs, the install once it exited, nothing by itself with Install Updates Automatically off, a failed staging tried again after the next check), the font decoding on a system font wrapped like the game's, the look (every colour scheme complete, the -background picture cut to the window's shape and kept, the sound effects as WAVs and off by default), `make_game.py` +background picture cut to the window's shape and kept, the sound effects as WAVs and low by default), `make_game.py` on a synthetic `.nup` (events, skipped steps, `--rebuild` without the dump, step keys, the shaders key following the translator the build uses), the RomFS step beside the lift and compile diff --git a/docs/releases/v0.6.2.md b/docs/releases/v0.6.2.md index 153ca7ef..b5f201a9 100644 --- a/docs/releases/v0.6.2.md +++ b/docs/releases/v0.6.2.md @@ -9,7 +9,7 @@ Draft notes for the release after 0.6.1. The version number is not decided yet. it takes effect at the next start. - Customize Launcher (on the launcher's home screen): six colour schemes, a background picture of your own with adjustable dimming, animations (a gliding cursor, sliding screens, drifting sparks) and sound effects: the game's - own select and back sounds, read from your game. The sounds are off until you turn them on. + own select and back sounds, read from your game, quiet (Low) unless you pick another loudness or turn them off. - Three more settings take effect without a restart: HDR between off and the tone mapper (only the game's native HDR still waits for one), the camera's stick sensitivity, and the speakers (stereo or 5.1). diff --git a/launcher/settings.py b/launcher/settings.py index 0418892d..c654e21b 100644 --- a/launcher/settings.py +++ b/launcher/settings.py @@ -19,7 +19,7 @@ DEFAULTS = { 'background': '', # the picture the player chose as the background; its fitted copy is data/background.npy 'background_dim': 40, # how much that picture is darkened, in percent 'animations': True, # the cursor glides, screens slide in, sparks drift, Navi bobs - 'sounds': 'off', # the chimes: off (until the player turns them on), low, medium or high + 'sounds': 'low', # the game's select and back sounds: off, low (sound.py DEFAULT_LEVEL), medium, high 'feed_repo': '', # owner/name overriding feed.json, for testing a fork's releases 'github_token': '', # only for a private release repository; stored as plain text 'last_check': 0.0, diff --git a/launcher/ui/app.py b/launcher/ui/app.py index a1ecb8ff..6b73ce8e 100644 --- a/launcher/ui/app.py +++ b/launcher/ui/app.py @@ -918,7 +918,7 @@ class App: def sounds(d): keys = [k for k, _ in SOUND_LEVELS] now = self.settings.get('sounds') - at = keys.index(now) if now in keys else 0 + at = keys.index(now if now in keys else sound.DEFAULT_LEVEL) self.settings['sounds'] = keys[max(0, min(len(keys) - 1, at + d))] self.settings.save() self.sounds.set_level(self.settings['sounds']) @@ -946,9 +946,10 @@ class App: Row('divider'), Row('toggle', 'Animations', on=animations, get=lambda: bool(self.settings.get('animations')), desc=('A gliding cursor, sliding screens and drifting sparks.',)), - Row('enum', 'Sounds', get=lambda: dict(SOUND_LEVELS).get(self.settings.get('sounds'), 'Off'), + Row('enum', 'Sounds', get=lambda: dict(SOUND_LEVELS).get(self.settings.get('sounds'), + dict(SOUND_LEVELS)[sound.DEFAULT_LEVEL]), left=lambda: sounds(-1), right=lambda: sounds(1), - desc=('The game\'s select and back sounds. Off by default.',)), + desc=('The game\'s select and back sounds. Low by default.',)), Row('divider'), Row('action', 'Back', on=lambda: self.show('home', self.home_row('Customize Launcher')), height=40, desc=('Back to the main menu.',)), diff --git a/launcher/ui/sound.py b/launcher/ui/sound.py index f49b7d52..025fff88 100644 --- a/launcher/ui/sound.py +++ b/launcher/ui/sound.py @@ -3,11 +3,11 @@ going back, read from the player's game (launcher/gamefiles.py extract_sounds de runtime/menu_sound.c plays them in the in-game menu). No sound ships with the launcher and none is made here: until the player's game has been read, and for a sound it does not hold, the launcher is silent. -They are off until the player picks a loudness in Customize Launcher (the Sounds row). An effect is the game's sound -as a 16-bit WAV in memory, scaled to the loudness. Windows plays it with PlaySound from that memory; elsewhere the WAV -goes to a file in the cache folder and the first of pw-play, paplay and aplay found plays it. One effect sounds at a -time: a new one cuts the last. Moving the cursor makes no sound, as it would cut a choice's sound short every time -the pointer lands on a row of the next screen. +They play at Low until the player picks another loudness, or Off, in Customize Launcher (the Sounds row). An effect +is the game's sound as a 16-bit WAV in memory, scaled to the loudness. Windows plays it with PlaySound from that +memory; elsewhere the WAV goes to a file in the cache folder and the first of pw-play, paplay and aplay found plays +it. One effect sounds at a time: a new one cuts the last. Moving the cursor makes no sound, as it would cut a +choice's sound short every time the pointer lands on a row of the next screen. select a row was chosen, a switch or a choice changed, Play, the end of a build the game's decide sound back back to the screen before, something could not be done the game's cancel sound @@ -21,6 +21,7 @@ import subprocess import numpy as np LEVELS = {'off': 0.0, 'low': 0.18, 'medium': 0.4, 'high': 0.75} # the Sounds choice: the loudest sample's share of full scale +DEFAULT_LEVEL = 'low' # a new launcher.json's (launcher/settings.py DEFAULTS), also for a value it cannot hold EFFECTS = ('select', 'back') # the names play takes: the game's sounds of gamefiles.SOUNDS @@ -50,10 +51,10 @@ class Sounds: """Plays the effects at the level of the Sounds choice. Every failure is silent: a PC without a sound device, or without a player program, just has a quiet launcher.""" - def __init__(self, cache_dir, level='off', log=None): + def __init__(self, cache_dir, level=DEFAULT_LEVEL, log=None): self.cache = cache_dir self.log = log or (lambda line: None) - self.level = None # set_level below: 'off' unless the player chose a loudness + self.level = None # set_level below: DEFAULT_LEVEL unless the player chose another self.game = {} # the game's sounds decoded from the player's files: name -> (rate, channels, samples) self.wavs = {} # name -> WAV bytes at the current level (Windows plays from this memory: it must stay) self.files = {} # name -> the WAV file elsewhere @@ -88,7 +89,7 @@ class Sounds: return bool(game) def set_level(self, level): - level = level if level in LEVELS else 'off' + level = level if level in LEVELS else DEFAULT_LEVEL if level == self.level: return self.stop() diff --git a/tests/launcher_test.py b/tests/launcher_test.py index db895a39..6378f9ef 100644 --- a/tests/launcher_test.py +++ b/tests/launcher_test.py @@ -18,7 +18,7 @@ Run: python tests/launcher_test.py (Python 3.14 for the zstd tests; the window a source checkout's build/xlate/), the prompts step with a stand-in prompt tool, the object cache of the compile step with the system's C compiler, and its windres call under folders with spaces (a script in windres's place) - jobs: an install driven by a fake make_game.py, with and without the prompts row -- the look: the colour schemes, the background picture's fitting and kept copy, the sound effects (off by default, +- the look: the colour schemes, the background picture's fitting and kept copy, the sound effects (low by default, only the game's own select and back sounds, decoded from a made-up .bwav) - the window: --selftest draws every screen (skipped without a display) """ @@ -2599,9 +2599,10 @@ class LookTest(Tmp): self.assertAlmostEqual(peak / 32767, sound.LEVELS['high'], delta=0.01) self.assertFalse(sound.Sounds(self.tmp / 'empty', 'high').game) - def test_only_the_games_sounds_play_and_only_once_turned_on(self): + def test_only_the_games_sounds_play_at_low_unless_set(self): sound = self.sound - self.assertEqual(settings.DEFAULTS['sounds'], 'off') + self.assertEqual(settings.DEFAULTS['sounds'], 'low') + self.assertEqual(sound.DEFAULT_LEVEL, 'low') self.assertEqual(set(gamefiles.SOUNDS), set(sound.EFFECTS)) empty = self.tmp / 'empty' self.game_sounds() @@ -2616,27 +2617,29 @@ class LookTest(Tmp): self.assertTrue(quiet.load_game()) # the game's sounds, once decoded quiet.play('select') self.assertEqual(popen.call_count, 1) - s = sound.Sounds(self.layout.cache) # no level given, as a new launcher.json has it + s = sound.Sounds(self.layout.cache) # no level given, as a new launcher.json has it: low + self.assertEqual(s.level, 'low') s.play('select') - s.set_level('nonsense') # a level launcher.json cannot hold: off + self.assertEqual(popen.call_count, 2) + s.set_level('off') # the player turned them off s.play('select') - self.assertEqual(popen.call_count, 1) - s.set_level('low') + self.assertEqual(popen.call_count, 2) + s.set_level('nonsense') # a level launcher.json cannot hold: the default + self.assertEqual(s.level, 'low') s.play('back') s.play('move') # the cursor has no sound s.play('no such effect') - self.assertEqual(popen.call_count, 2) + self.assertEqual(popen.call_count, 3) with open(popen.call_args[0][0][1], 'rb') as f: self.assertEqual(f.read(4), b'RIFF') - s.set_level('off') - s.play('select') - self.assertEqual(popen.call_count, 2) + peak = max(abs(v) for v in struct.unpack_from('<8h', s.wav('back'), 44)) + self.assertAlmostEqual(peak / 32767, sound.LEVELS['low'], delta=0.01) def test_the_look_settings_survive_and_fall_back(self): path = self.tmp / 'launcher.json' s = settings.Settings(path) self.assertEqual((s['theme'], s['background'], s['background_dim'], s['animations'], s['sounds']), - ('kokiri', '', 40, True, 'off')) + ('kokiri', '', 40, True, 'low')) s['theme'], s['sounds'], s['background_dim'] = 'shadow', 'high', 60 s.save() t = settings.Settings(path)