diff --git a/hosts/yarn/default.nix b/hosts/yarn/default.nix index 40daac1..1ff8802 100644 --- a/hosts/yarn/default.nix +++ b/hosts/yarn/default.nix @@ -16,6 +16,7 @@ ./lact.nix ./vr.nix ./forza-trigger + ./dualsense-attenuation.nix inputs.impermanence.nixosModules.impermanence ]; @@ -173,4 +174,9 @@ # PS5 DualSense adaptive triggers in Forza Horizon 4 / 5. services.forzaTrigger.enable = true; + + # PS5 DualSense LRA whine fix: cap firmware-level haptic amplitude so the + # voice-coil actuators never hit their audible-buzz regime under heavy + # rumble. Independent of the forza-trigger adaptive trigger path. + services.dualsenseAttenuation.enable = true; } diff --git a/hosts/yarn/dualsense-attenuation.nix b/hosts/yarn/dualsense-attenuation.nix new file mode 100644 index 0000000..acffe13 --- /dev/null +++ b/hosts/yarn/dualsense-attenuation.nix @@ -0,0 +1,93 @@ +# DualSense rumble/trigger firmware-level attenuation. +# +# The PS5 controller's haptic actuators are linear resonant actuators (LRAs) — +# voice coils, not ERM rotors. When driven near max amplitude they emit an +# audible high-pitched whine in addition to the intended rumble. This is a +# hardware property of the actuator, not a kernel or Steam Input bug, and +# can't be fixed in any rumble path that talks to the LRAs at full strength. +# +# Sony exposes a firmware knob ("rumble emulation amplitude modifier", in +# DS_OUTPUT report 0x05/0x31, gated by valid_flag2 bit RUMBLE_EMULATION_SELECT) +# that clamps haptic / trigger amplitude before the actuator is driven. This +# module sends that report once on plug-in via dualsensectl. The setting +# lives in controller RAM until the next power-cycle (USB unplug or BT +# disconnect-with-power-off), so re-applying on every udev `add` covers +# every reconnect. +# +# Coexists with Steam Input's rumble path: Steam writes rumble in the same +# output report, but only sets COMPATIBLE_VIBRATION / HAPTICS_SELECT in +# valid_flag0 — not RUMBLE_EMULATION_SELECT — so its writes don't reset the +# attenuation. Verified live with Steam (gamepadui) and forza-trigger both +# holding hidraw concurrently. +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.dualsenseAttenuation; + + applyScript = pkgs.writeShellScript "dualsense-apply-attenuation" '' + set -eu + # Retry briefly: the hidraw node exists by udev `add`, but the device + # may still be settling its initial HID descriptor exchange. dualsensectl + # needs to read the firmware report before it can write attenuation. + for _ in 1 2 3 4 5 6 7 8 9 10; do + if ${pkgs.dualsensectl}/bin/dualsensectl -l 2>/dev/null | grep -q ":"; then + exec ${pkgs.dualsensectl}/bin/dualsensectl attenuation \ + ${toString cfg.rumble} ${toString cfg.trigger} + fi + sleep 0.2 + done + echo "no DualSense detected; nothing to attenuate" >&2 + exit 0 + ''; +in +{ + options.services.dualsenseAttenuation = { + enable = lib.mkEnableOption "Apply DualSense haptic attenuation on plug-in"; + + rumble = lib.mkOption { + type = lib.types.ints.between 0 7; + default = 3; + description = '' + Rumble (LRA) attenuation, 0-7. Higher = weaker. 3 keeps the LRAs + below their audible-whine threshold while leaving rumble clearly + perceptible. 0 disables attenuation (full strength). + ''; + }; + + trigger = lib.mkOption { + type = lib.types.ints.between 0 7; + default = 0; + description = '' + Adaptive trigger attenuation, 0-7. Higher = weaker. 0 leaves + trigger feedback untouched — forza-trigger drives the triggers + with bounded amplitudes already, so the trigger LRAs don't + normally hit the audible regime. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + services.udev.extraRules = '' + # DualSense (USB) + ACTION=="add", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="0ce6", TAG+="systemd", ENV{SYSTEMD_WANTS}+="dualsense-attenuation.service" + # DualSense Edge (USB) + ACTION=="add", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="0df2", TAG+="systemd", ENV{SYSTEMD_WANTS}+="dualsense-attenuation.service" + # DualSense (Bluetooth) + ACTION=="add", SUBSYSTEM=="hidraw", KERNELS=="*054C:0CE6*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="dualsense-attenuation.service" + # DualSense Edge (Bluetooth) + ACTION=="add", SUBSYSTEM=="hidraw", KERNELS=="*054C:0DF2*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="dualsense-attenuation.service" + ''; + + systemd.services.dualsense-attenuation = { + description = "Apply DualSense rumble/trigger attenuation in controller firmware"; + serviceConfig = { + Type = "oneshot"; + ExecStart = applyScript; + }; + }; + }; +}