Repository navigation
feat: Audio tab in the Insert menu with Volume and Mute in background - #1270
bvtterbroot wants to merge 2 commits into
Conversation
hckrman101
left a comment
There was a problem hiding this comment.
Nice, tidy change. Option ids 27/28 are free on main and in the open PRs, no API version bump is needed (no struct change), and the 4-tab hit-testing, Q/E cycling (all through TAB_COUNT) and the rows' flags (slider, stops_at_ends, preset_exempt) are consistent. Ini keys come out as volume and mute_in_background, and an old ini without them keeps the 100 / Off defaults. The gain ramp is correct (0..1 so nothing clips, same gain on every channel, applied after the mix in stereo and 5.1), and the tests are registered with their compiler.
One real thing, plus nits:
- The foreground check treats any tool or no-activate window as the game's, with no memory.
game_in_frontin nn_hid.c only does that while the last normal foreground window was ours. Here, with the game in the background, a notification toast or a tray flyout takes the foreground, the check says "front", and the sound comes back until it goes away. Worth sharing that logic withgame_in_front(or copying itslast_normal_was_usstate). - Heads-up on #1240 (Audio output Auto/Stereo/5.1 in Gameplay): it touches the same files (vk_menu.cpp, nn_audio.c, readme, run_all.py) and that row belongs in this new Audio tab, so whichever lands second needs a rebase, and the row should probably move here.
- The volume is linear, so 50% is -6 dB and the bottom half of the slider is nearly all very quiet. A squared curve (gain = v*v) usually feels more even. Fine to leave if you prefer.
- Stale comments (inline).
- Not a blocker: the tab slots are now (w - 170) / 4, so please eyeball that GAMEPLAY / CONTROLS still fit at the bold 13 px size at scale 1 and in a narrow window.
| if (!w) return 0; | ||
| GetWindowThreadProcessId(w, &pid); | ||
| if (pid == GetCurrentProcessId()) return 1; | ||
| return (GetWindowLongPtrA(w, GWL_EXSTYLE) & (WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE)) != 0; |
There was a problem hiding this comment.
Any TOOLWINDOW / NOACTIVATE foreground counts as ours, whatever was in front before. nn_hid.c's game_in_front only allows that while the last normal foreground window was the game's (last_normal_was_us), so with the game in the background a toast or tray flyout would unmute it. Same state would fix it here.
| int preset = 0; // quality preset: 0 Custom, 1 Minimum (4GB), 2 Low (6GB), 3 Balanced (8GB), 4 High (12GB), 5 Ultra (16GB+) | ||
| int vram_mb = 0; // dynamic texture cache budget limit in MB | ||
| int mirror_mb = 0; // staging mirror limit in MB | ||
| int volume = 100; // (hookshot) the game's sound in percent, 0 to 100 (host/nn_audio.c, option 18) |
There was a problem hiding this comment.
Nit: the ids are 27 and 28 (HOS_OPT_VOLUME / HOS_OPT_MUTE_BACKGROUND), not 18 and 19 (those are the gyro's). Same on the next line.
The Insert menu gets a fourth tab, Audio, between Graphics and Gameplay: - Volume, 0 to 100% in steps of 5: the mixer scales each frame before the output, ramping a change over one frame so it does not click. - Mute in background: silent while another window has the focus. An overlay (a tool or no-activate window, as host/nn_hid.c treats Steam's or Discord's) still counts as the game's window. The host reads both through the renderer's option table (18 and 19), every 100 ms. Out of the box nothing changes: 100% and off. Without the Vulkan renderer's menu the game plays at full volume. The tab strip's slot width now follows the tab count (vk_menu_hit.h). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt, a squared volume curve - Mute in background: a tool or no-activate window in front counts as the game's only while the last normal window in front was the game's, as host/nn_hid.c's game_in_front decides it (the mixer keeps its own state). A toast or a tray flyout no longer brings the sound back while the game is behind another window. - Volume follows a squared curve: 50% is a quarter (-12 dB), so the bottom half of the slider is no longer nearly all very quiet. - The tab strip's ends are 70 wide (their hints are ~50 from 16 in), so the four tabs get slots of 85 at scale 1: GAMEPLAY's highlight (13 px bold) left CONTROLS' title 1.5 px at 77. - vk_menu.h's comments name the right option ids (27, 28). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8c0cef4 to
5976ed6
Compare
|
Thanks for the careful review. All points are in 5976ed6 (the branch is also rebased on current main):
|
What
The Insert menu gets a fourth tab, Audio, between Graphics and Gameplay, with two rows:
host/nn_audio.c) scales each frame before the output, on every output channel (stereo or Mix for 5.1 when the default output has 5.1 speakers #1182's 5.1). A change ramps over one 5 ms frame, so it does not click. Gains stay at or below 1, so nothing clips. At 100% the frame is left alone.host/nn_hid.c'sgame_in_fronttreats Steam's or Discord's) still counts as the game's window.The host reads both through the renderer's option table every 100 ms, as new ids
HOS_OPT_VOLUME = 27andHOS_OPT_MUTE_BACKGROUND = 28(gpu/vk/vk_api.h; 18 to 22 are the motion rows, 24 to 26 DLSS). Without the Vulkan renderer's menu the game plays at full volume. Both are saved inhookshot_options.ini(volume=,mute_in_background=).menu_hit::tab_slot_wnow takes the tab count instead of assuming 3, so the tab strip and its clicks fit four tabs.Why
There was no way to set the game's volume apart from the Windows mixer, and no way to silence it while alt-tabbed. Both are common requests for a PC game.
#1240's Audio output row (Auto, Stereo, 5.1) would fit in this tab too. Whichever of the two lands second can move it here.
How it was tested
tests/test_audio_volume.pycompilesaudio_apply_gainfrom the source and checks the ramp, the end gain, no clipping and equal gain on 2 and 6 channels. It also checks the option ids against the renderer's table and the menu's rows and defaults.tests/test_menu_hit.cppgains checks for a four-tab strip.tests/run_all.pyshows no new failures on Windows: the 3 that fail locally fail the same way onmain.🤖 Generated with Claude Code