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.
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.
- 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
dualsensectlparameters (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.
Presets |
Adaptive Triggers |
Button Vibration |
Vibration Settings |
LED Indication |
- Linux with the kernel's
hid-playstationdriver (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(needspareconPATH) 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/hidrawdevices (normally granted via theinput/plugdevgroup or a udev rule that ships withhid-playstation-aware distros; if in doubt, checkls -l /dev/input/event*and/dev/hidraw*)
Tested with the regular DualSense and DualSense Edge, over both USB and Bluetooth.
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.desktopon 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-clickpackaging/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.
A PKGBUILD is included under packaging/:
git clone https://github.com/sendement/dualsense-haptics.git
cd dualsense-haptics/packaging
makepkg -sidualsensectl is AUR-only, so makepkg -si won't fetch it automatically —
install it first with an AUR helper:
paru -S dualsensectl # or: yay -S dualsensectlNo 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.pyBuild 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 installNot 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=ledsA 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.shinstall.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.
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).
- 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
evdevinterface, which requireshid-playstation; very old kernels won't have it. - Adaptive triggers depend on
dualsensectlbeing 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.
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.pyis 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.





