feat(menu): Volume slider for the game's sound #1737

Closed
getzabelz wants to merge 2 commits from feat/audio-volume into main
getzabelz commented 2026-11-10 15:00:17 +00:00 (Migrated from github.com)

What

A Volume row on the Insert menu's Gameplay tab (above Audio output): a slider from Muted to 100% in steps of 5, 100% by default (the game's own level). It applies at once and is kept in hookshot_options.ini as volume=. Help line: "Master volume level."

The Insert menu's Gameplay tab with the new Volume row at 35%

How

  • host/nn_audio.c: au_volume_apply scales the final mix in render_frame, after the limiter and before the samples become int16, with rx_option(HOS_OPT_VOLUME, 100) read once per 5 ms frame.
    • The gain is the square of the share (50% = -12 dB, 25% = -24 dB), so each step of the slider sounds about as large as the last.
    • It ramps from the last frame's gain to the new one over the frame, like the game's own volume changes, so dragging the slider does not click.
    • At 100% the samples are left untouched (early return).
  • gpu/vk/vk_api.h: HOS_OPT_VOLUME (44), after HOS_OPT_DISCORD.
  • gpu/vk/vk_replay.cpp: the option table answers it (clamped 0..100). An id the table does not answer reads as 0, which would silence the game, so the test checks for this line.
  • gpu/vk/vk_menu.{h,cpp}: opts.volume, the row (preset_exempt, not a restart row), tx_volume, and the slider / stop-at-ends lists.
  • readme.md: the options table and a line in the option notes.

Tests

  • New tests/test_audio_volume.py (registered in run_all.py with --cc gcc): checks the row, the default, the slider flags, the enum and the renderer's answer, and that the call sits after the limiter. It also compiles the real au_volume_gain / au_volume_apply and checks 100% = unchanged, 0% = silence, 50% = 0.25, monotonic steps, and the ramp up and down without overshoot.
  • Full run_all.py on Windows: everything passes except nup_paths, builder_dlss_update, fpx_sticky and precompile_compute_guard, which fail the same way on a clean main here (local environment).
  • Built with the v2.0.13 builder and played in game: the slider changes the volume at once, Muted is silent, 100% sounds as before, and the value persists across restarts.
## What A **Volume** row on the Insert menu's **Gameplay** tab (above Audio output): a slider from **Muted** to **100%** in steps of 5, 100% by default (the game's own level). It applies at once and is kept in `hookshot_options.ini` as `volume=`. Help line: "Master volume level." ![The Insert menu's Gameplay tab with the new Volume row at 35%](https://i.imgur.com/jLCqSpr.png) ## How - `host/nn_audio.c`: `au_volume_apply` scales the final mix in `render_frame`, after the limiter and before the samples become int16, with `rx_option(HOS_OPT_VOLUME, 100)` read once per 5 ms frame. - The gain is the square of the share (50% = -12 dB, 25% = -24 dB), so each step of the slider sounds about as large as the last. - It ramps from the last frame's gain to the new one over the frame, like the game's own volume changes, so dragging the slider does not click. - At 100% the samples are left untouched (early return). - `gpu/vk/vk_api.h`: `HOS_OPT_VOLUME` (44), after `HOS_OPT_DISCORD`. - `gpu/vk/vk_replay.cpp`: the option table answers it (clamped 0..100). An id the table does not answer reads as 0, which would silence the game, so the test checks for this line. - `gpu/vk/vk_menu.{h,cpp}`: `opts.volume`, the row (`preset_exempt`, not a restart row), `tx_volume`, and the slider / stop-at-ends lists. - `readme.md`: the options table and a line in the option notes. ## Tests - New `tests/test_audio_volume.py` (registered in `run_all.py` with `--cc gcc`): checks the row, the default, the slider flags, the enum and the renderer's answer, and that the call sits after the limiter. It also compiles the real `au_volume_gain` / `au_volume_apply` and checks 100% = unchanged, 0% = silence, 50% = 0.25, monotonic steps, and the ramp up and down without overshoot. - Full `run_all.py` on Windows: everything passes except `nup_paths`, `builder_dlss_update`, `fpx_sticky` and `precompile_compute_guard`, which fail the same way on a clean `main` here (local environment). - Built with the v2.0.13 builder and played in game: the slider changes the volume at once, Muted is silent, 100% sounds as before, and the value persists across restarts.
hckrman101 (Migrated from github.com) approved these changes 2026-11-10 16:41:20 +00:00
hckrman101 (Migrated from github.com) left a comment

Looks good. Checked the row (generic ini_key save/load picks up volume=), HOS_OPT_VOLUME 44 (no id collision), the renderer's answer in vk_replay.cpp, and the mix: applied after the limiter, ramped over the frame, early return at 100%. rx_option is already declared the same way in other host files, and the test is registered in run_all.py.

Nit: the help line "Master volume level." is a bit bare compared to the other rows' help text, but fine.

Looks good. Checked the row (generic ini_key save/load picks up `volume=`), HOS_OPT_VOLUME 44 (no id collision), the renderer's answer in vk_replay.cpp, and the mix: applied after the limiter, ramped over the frame, early return at 100%. rx_option is already declared the same way in other host files, and the test is registered in run_all.py. Nit: the help line "Master volume level." is a bit bare compared to the other rows' help text, but fine.
hckrman101 commented 2026-11-10 17:09:41 +00:00 (Migrated from github.com)

BTW there's a new menu implementation

BTW there's a new menu implementation
hckrman101 commented 2026-11-10 17:50:27 +00:00 (Migrated from github.com)

Heads-up: this conflicts with main now (a lot landed today). Could you merge main into the branch (or rebase) and resolve the conflicts? Thanks!

Heads-up: this conflicts with main now (a lot landed today). Could you merge main into the branch (or rebase) and resolve the conflicts? Thanks!
getzabelz commented 2026-11-10 18:33:03 +00:00 (Migrated from github.com)

Closing this: #1270 (merged today) already adds a Volume slider in the new Audio tab, with the same curve (the square of the share) and ramp, plus Mute in background. It also saves as volume=, so players keep their setting. Thanks for the review!

Closing this: #1270 (merged today) already adds a Volume slider in the new Audio tab, with the same curve (the square of the share) and ramp, plus Mute in background. It also saves as `volume=`, so players keep their setting. Thanks for the review!
Commenting is not possible because the repository is archived.
No description provided.