Rumble strength in the Insert menu, and HD rumble to the heavy and light motors by frequency #1169

Merged
hckrman101 merged 2 commits from rumble-strength into main 2026-11-09 16:57:05 +00:00
hckrman101 commented 2026-11-09 16:06:22 +00:00 (Migrated from github.com)

Adds a Rumble strength row to the Insert menu's Controls tab and sends the game's HD rumble to a controller's heavy and light motors by frequency instead of by band.

Rumble strength

Off, 25% to 150% in steps of 25, default 100%. It is saved in hookshot_options.ini as rumble_strength, like the other rows, and reaches the host as option 9 (rx_option(9, 100)). It stops at its ends like Mouse sensitivity: Left at Off, Right (or Enter) at 150%, a click on either half of the value and the mouse wheel never wrap round from Off to the strongest setting or back. A hand-edited ini value is put on the row's steps when it is loaded (clamped to 0-150, then the nearest 25). Like the other controls rows, changing it does not set the quality preset back to Custom.

host/pad_sdl.c scales the motors' 0..65535 values in the poll, just before rumble_due, so a change applies at once: Off is a change to zero, and the poll sends the stop to a rumble that is already playing. Up to 100% each motor is scaled on its own (100% sends the values unchanged). Above 100%, when the stronger motor would pass 0xFFFF, both are scaled by the same factor so that the stronger just reaches the top, which keeps the heavy/light balance the frequency split chose (heavy 50000 / light 30000 at 150% gives 65535 / 39321, not 65535 / 45000).

Frequency-weighted motors

A Switch HD rumble value is two bands, each with an amplitude and a frequency, and the bands overlap (low about 41-626 Hz, high about 82-1253 Hz, 160 Hz and 320 Hz by default). Until now the low band always drove the heavy motor and the high band the light one, with frequency only lowering the strength. rumble_map in host/nn_hid.c (a pure function) keeps each band's strength exactly as before (rumble_motor, the same boost and per-band falloff), then splits it between the motors by its frequency (rumble_heavy_share): all heavy up to 160 Hz, all light from 200 Hz, and a crossfade on a log scale between them (log2(1.25), about a third of an octave), at constant power (square roots of the shares) so a band in the crossfade is not felt weaker. Each motor takes the stronger of the two bands' parts.

At the defaults (160/320 Hz) and at what OoT sends in play (low about 100-130 Hz, high about 250-300 Hz, from hid: vibration logs) every band lands on the same motor at the same strength as before, so nothing changes in feel today. The crossfade ends at 200 Hz to leave room below OoT's high band, and the default low band (160 Hz) is still all heavy. What changes: high-band content played below 200 Hz now reaches the heavy motor (all of it at 160 Hz and below), and low-band content played above 160 Hz reaches the light one.

Left and right

Not changed. OoT sends the same values to both vibration devices (handles 00000003 and 00010003 log the same numbers), and SDL exposes a gamepad's low and high motors, not left and right devices, so keeping the sides apart would not change what any pad plays. They are still merged by the stronger of the two.

Tests

tests/test_rumble_mapping.py compiles rumble_motor, rumble_heavy_share, rumble_map and rumble_strength from the sources and checks: at 100% the output matches the old mapping for a grid of amplitudes and default / OoT frequencies, Off gives 0, the strength grows with the percentage and the value, is unchanged per motor up to 100% and above it keeps the motors' ratio with the stronger at 0xFFFF, the share follows the documented curve (1 up to 160 Hz, 0 from 200 Hz, a half at their geometric middle), a band of either kind at 170-199 Hz is split as documented at constant power, OoT's high band range reaches only the light motor, a low-played high band reaches only the heavy motor and a high-played low band only the light one.

tests/test_rumble_menu.py compiles the menu's set_row, the wheel loop, ini_key and the ini snap from gpu/vk/vk_menu.cpp with stubs and checks that the row stops at Off and 150% for the arrows, Enter and clicks and for several wheel notches at once, keeps the quality preset, is saved as rumble_strength and that ini values are snapped to the steps. It also checks that option 9 is opts.rumble in vk_replay.cpp's option table, that no id there is used twice or collides with the cheat options or items, and that only host/pad_sdl.c reads option 9. Both are picked up by tests/run_all.py.

Not tested with a controller: how the crossfade feels on real motors.

Adds a **Rumble strength** row to the Insert menu's Controls tab and sends the game's HD rumble to a controller's heavy and light motors by frequency instead of by band. ## Rumble strength Off, 25% to 150% in steps of 25, default 100%. It is saved in `hookshot_options.ini` as `rumble_strength`, like the other rows, and reaches the host as option 9 (`rx_option(9, 100)`). It stops at its ends like Mouse sensitivity: Left at Off, Right (or Enter) at 150%, a click on either half of the value and the mouse wheel never wrap round from Off to the strongest setting or back. A hand-edited ini value is put on the row's steps when it is loaded (clamped to 0-150, then the nearest 25). Like the other controls rows, changing it does not set the quality preset back to Custom. `host/pad_sdl.c` scales the motors' 0..65535 values in the poll, just before `rumble_due`, so a change applies at once: Off is a change to zero, and the poll sends the stop to a rumble that is already playing. Up to 100% each motor is scaled on its own (100% sends the values unchanged). Above 100%, when the stronger motor would pass 0xFFFF, both are scaled by the same factor so that the stronger just reaches the top, which keeps the heavy/light balance the frequency split chose (heavy 50000 / light 30000 at 150% gives 65535 / 39321, not 65535 / 45000). ## Frequency-weighted motors A Switch HD rumble value is two bands, each with an amplitude and a frequency, and the bands overlap (low about 41-626 Hz, high about 82-1253 Hz, 160 Hz and 320 Hz by default). Until now the low band always drove the heavy motor and the high band the light one, with frequency only lowering the strength. `rumble_map` in `host/nn_hid.c` (a pure function) keeps each band's strength exactly as before (`rumble_motor`, the same boost and per-band falloff), then splits it between the motors by its frequency (`rumble_heavy_share`): all heavy up to 160 Hz, all light from 200 Hz, and a crossfade on a log scale between them (log2(1.25), about a third of an octave), at constant power (square roots of the shares) so a band in the crossfade is not felt weaker. Each motor takes the stronger of the two bands' parts. At the defaults (160/320 Hz) and at what OoT sends in play (low about 100-130 Hz, high about 250-300 Hz, from `hid: vibration` logs) every band lands on the same motor at the same strength as before, so nothing changes in feel today. The crossfade ends at 200 Hz to leave room below OoT's high band, and the default low band (160 Hz) is still all heavy. What changes: high-band content played below 200 Hz now reaches the heavy motor (all of it at 160 Hz and below), and low-band content played above 160 Hz reaches the light one. ## Left and right Not changed. OoT sends the same values to both vibration devices (handles `00000003` and `00010003` log the same numbers), and SDL exposes a gamepad's low and high motors, not left and right devices, so keeping the sides apart would not change what any pad plays. They are still merged by the stronger of the two. ## Tests `tests/test_rumble_mapping.py` compiles `rumble_motor`, `rumble_heavy_share`, `rumble_map` and `rumble_strength` from the sources and checks: at 100% the output matches the old mapping for a grid of amplitudes and default / OoT frequencies, Off gives 0, the strength grows with the percentage and the value, is unchanged per motor up to 100% and above it keeps the motors' ratio with the stronger at 0xFFFF, the share follows the documented curve (1 up to 160 Hz, 0 from 200 Hz, a half at their geometric middle), a band of either kind at 170-199 Hz is split as documented at constant power, OoT's high band range reaches only the light motor, a low-played high band reaches only the heavy motor and a high-played low band only the light one. `tests/test_rumble_menu.py` compiles the menu's `set_row`, the wheel loop, `ini_key` and the ini snap from `gpu/vk/vk_menu.cpp` with stubs and checks that the row stops at Off and 150% for the arrows, Enter and clicks and for several wheel notches at once, keeps the quality preset, is saved as `rumble_strength` and that ini values are snapped to the steps. It also checks that option 9 is `opts.rumble` in `vk_replay.cpp`'s option table, that no id there is used twice or collides with the cheat options or items, and that only `host/pad_sdl.c` reads option 9. Both are picked up by `tests/run_all.py`. Not tested with a controller: how the crossfade feels on real motors.
Commenting is not possible because the repository is archived.
No description provided.