Skip to content

feat: Audio tab in the Insert menu with Volume and Mute in background - #1270

Open
bvtterbroot wants to merge 2 commits into
NotRexed:mainfrom
bvtterbroot:feat/audio-settings
Open

bvtterbroot wants to merge 2 commits into
NotRexed:mainfrom
bvtterbroot:feat/audio-settings

Conversation

@bvtterbroot

Copy link
Copy Markdown

What

The Insert menu gets a fourth tab, Audio, between Graphics and Gameplay, with two rows:

  • Volume, 0 to 100% in steps of 5 (default 100%). The mixer (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.
  • Mute in background (default Off): silent while another window has the focus. An overlay in front (a tool or no-activate window, as host/nn_hid.c's game_in_front treats 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 = 27 and HOS_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 in hookshot_options.ini (volume=, mute_in_background=).

menu_hit::tab_slot_w now 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

  • Machine: Windows 11, AMD Radeon RX 7900 XTX (driver 32.0.32015.7029), Ryzen 7 7800X3D.
  • In game: Kokiri Forest and Link's house. Volume from 100% to 0% and back: the level follows with no clicks, 0% is silent. Mute in background On: alt-tab to another window silences the game within about 0.1 s, switching back brings the sound back. Frame rate unchanged (the change is in the audio thread only).
  • Game-free: new tests/test_audio_volume.py compiles audio_apply_gain from 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.cpp gains checks for a four-tab strip. tests/run_all.py shows no new failures on Windows: the 3 that fail locally fail the same way on main.
  • readme: the options table and a note under it.

🤖 Generated with Claude Code

@hckrman101 hckrman101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_front in 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 with game_in_front (or copying its last_normal_was_us state).
  • 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.

Comment thread host/nn_audio.c Outdated
if (!w) return 0;
GetWindowThreadProcessId(w, &pid);
if (pid == GetCurrentProcessId()) return 1;
return (GetWindowLongPtrA(w, GWL_EXSTYLE) & (WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE)) != 0;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread gpu/vk/vk_menu.h Outdated
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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

bvtterbroot and others added 2 commits November 9, 2026 21:26
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>
@bvtterbroot

Copy link
Copy Markdown
Author

Thanks for the careful review. All points are in 5976ed6 (the branch is also rebased on current main):

  • Overlays: the foreground check now keeps its own last_normal_was_us, as game_in_front does: a tool or no-activate window counts as the game's only while the last normal window in front was the game's. The mixer thread keeps its own copy of the state rather than sharing game_in_front's statics across threads. The decision is a small pure function (audio_front_decide), and tests/test_audio_volume.py checks the toast-behind-the-game case.
  • Volume curve: now squared (50% is -12 dB). The readme says so.
  • Stale comments: fixed (27, 28).
  • Tab widths: measured with GDI (Segoe UI, 13 px bold at scale 1): GAMEPLAY is 69 px and CONTROLS 68 px. In 77 px slots, GAMEPLAY's highlight came within 1.5 px of CONTROLS' title. kTabSide is now 70 (the [Q / LB] / [E / RB] hints are 49 / 47 px from 16 in), so the slots are 85 px with ~14 px between the highlight and the next title. test_menu_hit.cpp has the new edges. Checked in game at scale 2 (1440p).
  • Audio output in the Insert menu: Auto, Stereo or 5.1 #1240: agreed, its Audio output row belongs in this tab. Happy to move it if Audio output in the Insert menu: Auto, Stereo or 5.1 #1240 lands first, or @janvitos can move it if this one does.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants