Skip to content

About

Audio-reactive haptics and adaptive triggers for the DualSense controller on Linux, over Bluetooth or USB

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Repository files navigation

DualSense Haptics

Audio-reactive haptics and adaptive triggers for the Sony DualSense controller on Linux, over Bluetooth or USB.

Windows has DualSenseX, which turns your system audio into rumble on the DualSense's motors. There's no DSX for Linux — this is that, built from scratch on top of the kernel's own force-feedback API, with a full GUI on top: presets, per-user profiles, per-app audio binding, adaptive trigger effects, per-button haptics, a full LED preset system, a system tray icon with battery %, autostart, light/dark/system theming, and 9 languages.

DualSense Haptics - home screen

How it works

Over USB, the DualSense exposes itself as a 4-channel USB Audio Class device: front-left/front-right are its tiny internal speaker, and rear-left/rear-right are literally the two haptic motors, wired up as ordinary audio outputs. That's the exact mechanism DSX uses on Windows and the PS5 itself uses internally — real PCM waveforms played straight onto the actuators, not a synthesized effect. This app detects that device and, by default, streams your live system audio (gain-staged and band-limited to what the motors reproduce well) directly onto it — literal audio-to-haptics, independently per motor. It can be tuned or turned off under Experimental Features → Direct Audio (USB).

Over Bluetooth, that USB Audio interface doesn't exist, so by default the app falls back to the same approach it always used: capturing your system's default audio output, splitting it into a bass band and a treble band, and driving the controller's two rumble motors (FF_RUMBLE) with an envelope follower per band, with noise-gating so a constant background hum doesn't drown out transients like footsteps or gunshots. It's not literal audio, but it tracks impacts, bass, and voice noticeably better than a flat "vibrate on any sound" approach.

There's also an experimental, opt-in literal-audio path over Bluetooth, using a built-in Python reimplementation (saxense_algo.py) of the DualSense's Bluetooth HID haptics protocol, independently reverse-engineered by egormanga/SAxense (much lower fidelity than the USB path — 8-bit, 3kHz combined — but still real PCM, not a synthesized envelope, and the same per-motor precision holds up in practice). It's off by default, needs nothing installed separately — see Credits.

Adaptive trigger effects (L2/R2 resistance profiles) are applied via dualsensectl, since they're one-shot HID reports rather than something worth reimplementing here.

Over Bluetooth, Steam grabs raw HID control of the controller for any game with native adaptive-trigger support and keeps writing to it for as long as Steam runs — this app's own rumble still gets sent, but gets silently overwritten on the wire, so nothing reaches the motors. Trigger effects kept working in desktop testing with a regular DualSense, but that isn't guaranteed everywhere: HID has no notion of ownership, so whichever output report reaches the controller last wins, and where Steam Input is fully in control (e.g. Steam Deck Gaming Mode) it can overwrite an effect too — see the Steam Deck section. Trigger + Vibration Mix (opt-in, off by default — under Experimental Features on desktop, or its own toggle in the Decky plugin's QAM panel; see Steam Deck / SteamOS) fixes this: it clones the controller via /dev/uhid, hides the real device from everyone else, and merges its own audio-reactive rumble into whatever Steam separately writes for triggers/lightbar before forwarding it to the real hardware — so both work at once instead of one silently blocking the other. On desktop, a dedicated udev rule keeps the real device locked to root the instant it (re)appears on every reconnect, not just once at startup, so Steam can never win a race to grab it back — even a fast one — and only ever sees the clone; it needs that udev rule and small setcap'd helper the Arch package installs automatically (see Installation), without which it falls back to the same "detect and report" behavior described in Limitations. The Decky plugin's own copy runs as root already, so it needs none of that udev-rule locking to keep the real device out of Steam's hands on future reconnects — the one thing it still does is a one-time forced unbind/rebind of the real device the moment the feature turns on, to evict Steam if it already had the controller open from before.

LED Indication (its own page on desktop, off by default) drives the lightbar and 5 player-indicator LEDs with a choice of presets: static color, breathing, rainbow, a running wave, heartbeat, battery-level, a custom color sequence you define yourself, and Immersive — the original audio-reactive mode, turning the lightbar and player LEDs into a live bass/mid/treble meter with configurable colors per band, blended by how loud each band is right now, with the 5 player LEDs working as a plain volume bar. The Decky plugin still only exposes Immersive for now, as its own toggle in the QAM panel. On the Decky plugin (and on desktop whenever Trigger + Vibration Mix is off) it drives the controller's lightbar/player LEDs directly through the kernel's own LED class devices, no proxy needed; with Trigger + Vibration Mix on, it rides along inside that same proxy session instead. Either way it's extra Bluetooth traffic on top of everything else, so a congested BT channel can make it flaky too — see Limitations.

Features

  • Direct audio-to-haptics over USB — live system audio streamed as literal PCM straight onto the two motors, the same mechanism DSX and the PS5 itself use. Falls back to the band-split envelope approach below over Bluetooth, where that hardware path doesn't exist — plus an experimental, opt-in literal-audio path over Bluetooth too, at much lower fidelity (see How it works and Credits).
  • Audio-to-haptics (Bluetooth / fallback) — band-split bass/treble envelope followers with adjustable attack/release, sensitivity, contrast (gamma), and background noise suppression, independently for each motor.
  • 5 built-in presets (Balanced, Cinema, Music, Voice & Podcasts, Maximum Sensitivity) plus your own saved profiles, reorderable by drag-and-drop under Profiles.
  • Per-app audio binding — by default the global profile captures all system audio, but you can bind specific apps so only their sound drives the haptics; if two bound apps are running at once it automatically falls back to the global profile rather than guessing which one you meant.
  • Adaptive triggers — 7 resistance presets (soft resistance, hard wall, weapon trigger, bow, machine gun, ratchet, gallop), set independently per trigger (L2/R2), plus a custom effect builder to dial in the raw dualsensectl parameters (mode, position, strength, frequency, etc.) by hand and experiment beyond the presets. If a game is already driving the triggers itself, the desktop app detects that the device is held open elsewhere and skips automatically re-applying its own effect on reconnect, so it won't fight the game — or, on Bluetooth, turn on Trigger + Vibration Mix (see How it works) so it doesn't need to skip anything in the first place. The Decky plugin never re-applies trigger presets automatically: pick the preset again after the controller reconnects.
  • Per-button haptics — pick any face button, bumper, trigger click, stick click, or the D-pad to buzz lightly while held, mixed with the audio vibration, at its own strength, from the motor on that side of the pad.
  • LED Indication — a full preset system for the lightbar and 5 player-indicator LEDs: static color, breathing, rainbow, wave, heartbeat, battery-level, a custom color sequence, and Immersive, the original mode that pulses and color-shifts with bass/mid/treble in real time (see How it works); each preset has its own color and timing controls. The Decky plugin currently ships Immersive only.
  • Works over USB or Bluetooth, with a badge on the home screen showing which one is active.
  • System tray icon with live connection status and battery percentage.
  • Autostart on login.
  • Light / dark / system theme, and 9 languages (English, Russian, Chinese, Spanish, German, French, Japanese, Portuguese, Korean) — both switchable live from Settings, no restart needed.
  • Steam Deck / SteamOS: a Decky Loader plugin (see deck-plugin/) puts the essentials in the Quick Access Menu — no need to leave Game Mode.

Screenshots

Presets Presets Adaptive Triggers Adaptive Triggers
Button Vibration Button Vibration Vibration Settings Vibration Settings
LED Indication LED Indication

Requirements

  • Linux with the kernel's hid-playstation driver (mainline since Linux 5.16; handles both USB and Bluetooth and exposes the controller through the standard joystick force-feedback API — no special setup needed beyond having a reasonably current kernel).
  • Python 3.10+
  • PySide6 (Qt6 bindings)
  • python-evdev
  • PulseAudio or PipeWire with pipewire-pulse (needs parec on PATH)
  • dualsensectl — only required for adaptive trigger effects; everything else works without it
  • A udev rule and small setcap'd helper — optional, only needed for Trigger + Vibration Mix (off by default); the Arch package installs both automatically, see Installation
  • Your user needs read/write access to the controller's evdev/hidraw devices (normally granted via the input/plugdev group or a udev rule that ships with hid-playstation-aware distros; if in doubt, check ls -l /dev/input/event* and /dev/hidraw*)

Tested with the regular DualSense and DualSense Edge, over both USB and Bluetooth.

Installation

Easiest: the graphical setup wizard

One wizard installs anything you want - the app itself, the Steam Deck plugin, and the two optional extras below - with a few clicks and your admin password when needed:

  • No clone needed: download dualsense-haptics-bootstrap.desktop on its own and double-click it in a file manager - it fetches the setup wizard itself and takes it from there. No git clone, no terminal.
  • If you already cloned the repo: run packaging/dualsense-haptics-setup.sh, or double-click packaging/dualsense-haptics-setup.desktop.

It shows a checklist of what to set up, then handles the compiling/downloading/installing itself (needs zenity, already installed on most desktops). On Arch it builds and installs the real package; on other distros it sets up a source checkout under ~/.local/share/dualsense-haptics plus an app-menu launcher. You can re-run it any time to add more later.

The sections below cover the same steps by hand, if you'd rather not run someone else's script, or don't have zenity.

Arch Linux / pacman

A PKGBUILD is included under packaging/:

git clone https://github.com/sendement/dualsense-haptics.git
cd dualsense-haptics/packaging
makepkg -si

dualsensectl is AUR-only, so makepkg -si won't fetch it automatically — install it first with an AUR helper:

paru -S dualsensectl   # or: yay -S dualsensectl

Other distributions

No distro packaging beyond the Arch one exists yet — run it from source:

git clone https://github.com/sendement/dualsense-haptics.git
cd dualsense-haptics

# Debian/Ubuntu-style dependency names, adjust for your distro:
sudo apt install python3 python3-pyside6.qtwidgets python3-evdev pipewire-pulse

python3 main.py

Build dualsensectl from source if your distro doesn't package it — it's a small C program with a Makefile:

git clone https://github.com/nowrep/dualsensectl.git
cd dualsensectl && make && sudo make install

Optional: Trigger + Vibration Mix

Not required for anything else in the app - only for Experimental Features → Trigger + Vibration Mix. The Arch package's .install hook already sets this up automatically, and the setup wizard above does the same for other distros - it's bundled into installing the app itself there, not a separate step. To do it by hand instead:

sudo groupadd -r dualsense-haptics
sudo usermod -aG dualsense-haptics "$USER"    # log out and back in after this
sudo install -Dm755 packaging/src/dualsense-hidlock.c /tmp/dualsense-hidlock.c
gcc -O2 -o /usr/lib/dualsense-haptics/dualsense-hidlock /tmp/dualsense-hidlock.c
sudo setcap 'cap_fowner,cap_dac_override+ep' /usr/lib/dualsense-haptics/dualsense-hidlock
sudo install -Dm644 packaging/71-dualsense-haptics-uhid.rules \
    /usr/lib/udev/rules.d/71-dualsense-haptics-uhid.rules
sudo install -Dm644 packaging/72-dualsense-haptics-proxy-lock.rules \
    /usr/lib/udev/rules.d/72-dualsense-haptics-proxy-lock.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --subsystem-match=leds

Steam Deck / SteamOS (Decky Loader plugin)

A trimmed-down version lives under deck-plugin/ as a Decky Loader plugin - a Quick Access Menu panel instead of a window, for using it without leaving Game Mode. It shares the same engine as the desktop app (same haptics_engine.py/config.py/presets.py, vendored unchanged) and reads the same kind of config, just through a much smaller set of controls: an on/off toggle, connection/battery status, presets, saved profiles (created on desktop, selectable here), adaptive trigger presets, the Direct Audio USB/Bluetooth toggles, and its own Trigger + Vibration Mix toggle (off by default, same mechanism as desktop - see How it works). Per-button haptics and the custom trigger builder are still desktop-only, deliberately left out to keep the QAM panel to a handful of widgets.

Trigger + Vibration Mix itself was pulled from the Decky plugin early on after live testing in Big Picture/gamescope kept showing a duplicate controller icon with doubled inputs on reconnect, traced to Steam's own controller detection rebuilding its view of the cloned device on every single reconnect rather than recognizing it as the same one it already knew about. Fixed since by keeping the clone itself alive across real-device reconnects - only the real-device side gets torn down and relocked, so Steam only ever sees the clone bind once per session - the same fix the desktop app's own copy of this feature already needed for an unrelated reason (a race to grab the real device back from Steam on every reconnect).

Adaptive triggers on the Deck. A trigger preset is one dualsensectl call (a single HID output report) sent when you pick it in the panel; the plugin does not re-apply it after a reconnect (the controller drops the effect when it reconnects), so pick it again. "Overridden by Steam" in the panel only means another process (Steam, always, in Gaming Mode) holds the controller open - nothing is blocked, but with Steam Input active Steam's own output reports can overwrite the effect, more so over Bluetooth than USB, so USB is the reliable choice there. A command counts as failed if dualsensectl exits non-zero or logs a hid_write error (it can still exit 0 in that case); to check by hand, run dualsensectl trigger left feedback 0 8; echo $? in a terminal and look for hid_write in the output - none, but the effect vanishes, points to Steam overwriting it (try turning off Steam Input for the controller).

Requires Decky Loader already installed. Easiest: pick "Steam Deck / Decky Loader plugin" in the setup wizard above. By hand instead:

git clone https://github.com/sendement/dualsense-haptics.git
cd dualsense-haptics/deck-plugin
./install.sh

install.sh copies a prebuilt frontend (dist/, committed in this repo so a Node toolchain isn't needed on the Deck itself) into ~/homebrew/plugins/ and restarts the plugin_loader service. To rebuild the frontend yourself after editing src/index.tsx: npm install && npm run build, then re-run install.sh.

Usage

Launch it (dualsense-haptics if installed via the package, or python3 main.py from source) and it opens on the Home page, showing connection status, active profile, trigger state, and battery. Pick a preset under Presets, tune things further under Vibration Settings, and save your own combination as a profile under Profiles (drag to reorder). Adaptive triggers, per-button vibration, per-app audio binding, and LED presets each live on their own page. Theme and language are under Settings.

Closing the window minimizes it to the tray rather than quitting — use the tray icon's context menu to reopen, toggle vibration, or quit. Check Autostart in the sidebar to have it launch minimized to tray on login (pass --tray manually to start minimized without going through autostart).

Limitations

  • Over Bluetooth, by default haptic quality depends on the DSP tuning in Vibration Settings rather than a 1:1 waveform — see How it works for why, and for the experimental opt-in path that gets literal (if lower-fidelity) audio over Bluetooth too.
  • Vibration/button-haptics needs the controller to expose a force-feedback evdev interface, which requires hid-playstation; very old kernels won't have it.
  • Adaptive triggers depend on dualsensectl being installed; without it, everything else still works.
  • Built and tested on Arch/Hyprland; should work anywhere with a recent kernel and PipeWire/PulseAudio, but other desktop environments and distros haven't been extensively tested.
  • Everything here rides over Bluetooth's own radio link, which has no guaranteed bandwidth. A busy 2.4GHz channel — Wi-Fi and Bluetooth sharing the same radio (common on a Steam Deck), other Bluetooth devices nearby, or physical distance/obstructions — can show up as crackling on the experimental SAxense audio path (especially on higher-pitched sounds, which need more of that limited bandwidth to come through cleanly), or even the controller dropping its connection outright. None of this is something the app can fix in software; if you see it, try turning off Wi-Fi or moving closer to confirm it's the radio and not a bug.
  • Occasionally the controller and your PC's Bluetooth adapter can get out of sync about the connection state - the app (correctly) sees the controller as gone, but your desktop's own Bluetooth indicator/notifications may keep showing it as connected for a while, or not notice at all. If reconnecting normally doesn't help, turn the controller fully off (hold the PS button a few seconds until it powers down) and back on - that forces a clean reconnect. This is a Bluetooth stack/radio quirk between the OS and the controller, not something this app causes or can detect/fix from the outside.

Credits

  • dualsensectl (nowrep) — used for adaptive trigger effects.
  • SAxense (egormanga/Sdore, apps.sdore.me/SAxense) — the Bluetooth haptics-over-audio protocol used by the experimental direct audio mode is their independent reverse-engineering research; see their repo for details. saxense_algo.py is a Python port of the wire-protocol part of their (MPL-2.0) C source and stays under MPL 2.0 itself (the rest of this project is MIT — see LICENSE); nothing else from their project is included.

License

MIT

About

Audio-reactive haptics and adaptive triggers for the DualSense controller on Linux, over Bluetooth or USB

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages