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 <noreply@anthropic.com>
This commit is contained in:
epicmeme546
2026-11-10 02:02:45 -05:00
parent e10ef60fd5
commit 5f4bbd2084
6 changed files with 32 additions and 27 deletions

View File

@@ -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

View File

@@ -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).

View File

@@ -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,

View File

@@ -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.',)),

View File

@@ -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()

View File

@@ -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)