Skip to content

feat: DualSense audio haptics, Switch Pro HD rumble and full-rate PCM haptic relay - #238

Open
ripps818 wants to merge 29 commits into
safijari:mainfrom
ripps818:dualsense-haptic
Open

ripps818 wants to merge 29 commits into
safijari:mainfrom
ripps818:dualsense-haptic

Conversation

@ripps818

@ripps818 ripps818 commented Aug 20, 2026 •

Copy link
Copy Markdown

Summary

Adds haptics that use the Steam Controller 2's actuators directly instead of motor-style rumble:

  • DualSense mode: the puck presents a real pad's 4-channel USB audio function and plays the game's haptic channels on the controller. By default it streams them as PCM to the grip actuators, as a real DualSense does.
  • Switch Pro mode: renders the console's HD rumble. Both bands play on the grips, and the high band plays as tones on the trackpads.
  • Steam mode: relays the controller's PCM haptic streams (OUTPUT 0x87-0x89), so audio-to-haptics apps work through the puck.

Changes

Eight commits, each builds on its own:

  1. UAC1 4-channel audio haptics (mode_ps5_audio.*). 48 kHz 4-channel output plus a silent 2-channel mic, which PipeWire's stock DualSense profile requires. Channels 3 and 4 drive the actuators in one of four styles, set from the panel:

    • wave (default): the waveform itself, low-passed at 300 Hz and streamed to the grips as 4 kHz u-law PCM.
    • tone: 0x83 tones per actuator.
    • split: below 80 Hz as rumble, the rest as tones.
    • rumble: 0x80, merged with the game's normal rumble.
  2. DualSense identity:

    • byte-for-byte report descriptor, pairing report tail, bcdDevice 1.00, no serial string in the single-HID mode;
    • feature reports answered at their declared sizes with a real pad's values, or stalled, never a short reply;
    • product string "DualSense Wireless Controller", as a real pad reports it.
  3. PS trackpad click fix: the click pulse now buzzes the pad that was clicked. It always used side 1, which is the right pad.

  4. Steam mode relay of OUTPUT 0x87-0x89. These are raw haptic sample streams, not settings reports, and the real puck forwards them. Changes:

    • relay payload raised to 63 bytes for haptic reports (feature commands stay at 60);
    • a second relay per poll cycle while the ring is backed up (an 8 kHz stream needs ~258 frames/s);
    • one same-PID resend of an unanswered PCM frame.
  5. PCM stream + wave style + grip limiter:

    • shared hapticPcmStart / hapticPcmSend / hapticUlaw;
    • short first frames, so playback starts after 24 ms instead of 31 ms;
    • a soft-limit knee (default 85%) rounds off grip peaks instead of clipping them into pops.
  6. Switch Pro HD rumble. Decoder:

    • tracks band frequencies;
    • maps amplitude code 100 to full scale, matching the public amplitude table and SDL's encoder (it used to play every level at 0.56x);
    • SPI 0x6012 reports device type 3, which Eden's direct Pro Controller driver needs.

    Renderer (in the main loop, not the USB callback):

    • grips: low + high band per side, summed and phase-continuous, as PCM;
    • trackpads: the high band as 0x83 tones, capped at -15 dB between 250 and 300 Hz;
    • silence stops it after a 500 ms hang.

    The decode and renderer are adapted from @Froggerdog's HD Rumble Emulation in Add optional Switch Pro profiles, trackpad D-pad and HD Rumble Emulation #303, without its profiles, trackpad D-pad and shortcut changes.

  7. Panel: wave in the DualSense style button. A grip limiter select and an HD trackpad strength select in the Rumble card, hidden on older firmware.

  8. Docs:

    • docs/DUALSENSE_HAPTICS.md: setup, Proton notes, hardware measurements;
    • PROTOCOL.md: OUTPUT 0x86-0x89 table, actuator routing, measured PCM behaviour, HD rumble (§9.5), new fields;
    • README and TESTING_GUIDE.

Update

Four things were added on top of the eight commits, after testing and the reports in the comments:

  1. No click after HD rumble, and fewer dropped PCM frames. Stop messages now go only to actuators that may still be playing, since a -128 dB cut or 0x86 op 1 sent to an idle one plays as a click of its own. An unanswered 0x86-0x89 frame goes back to the front of the ring for up to two more poll cycles. This path is shared, so DualSense wave and Steam-mode 0x87-0x89 relay get the retry too.
  2. Per-type haptic style and grip limiter for the Switch and the DualSense (fields 120-127). The Switch gets the four styles for its grips (wave, tone, split, rumble); both types start on wave. Fields 88 and 115 still work for older panels.
  3. Per-type rumble strength (fields 108-111; 22 still edits the active type). The Switch starts at 100%, where its wave stops getting stronger; the other types start at 200%.
  4. HD Rumble toggle for the Switch (field 128). Off plays plain motor rumble with its own strength (200%).

In the panel, each controller's tab now has Rumble strength, and the Switch and DS5 tabs have Haptics style and Grip limiter; the Switch tab also has HD Rumble and HD trackpad strength. The Rumble card keeps Style and Test rumble.

The status blob grows to p[202..214] (limiter and style per type, HD Rumble, strength per type). A config from before these starts every type from the old shared limiter, style and strength.

New tests: tests/type_haptics/run.py, and more cases in tests/hd_rumble/run.py. Hardware: Mario Kart in Eden feels fine.

Update 2: upstream merge and haptics fixes

Merged current main (no conflicts left) and brought over the fixes from testing in the comments (Jefemy's reports on HD rumble position and strength):

  1. Wave frames are delivered once. The controller answers a PCM frame in its ESB ACK up to ~1 ms later, so the old 400 us window cut most replies off and the resends played twice. The puck now waits up to 1200 us, counts an empty ACK as delivered, and turns the radio around to RX in hardware, since in DualSense mode the USB audio work preempted a software RXEN and 83% of replies were missed (now 1%).
  2. 0x86 goes out once per stream. Re-sending it every second made the grips stutter once a second.
  3. Switch HD bands restart in step on each effect, so the same effect no longer swells or fades depending on where the bands' phases were.
  4. DualSense wave ticks and underruns: an empty ACK after a PCM frame no longer counts as a dropped poll, which skipped a third of relay turns; and the ISO OUT endpoint is armed with a second buffer before processing, because the nRF driver drops a packet when none is armed.
  5. USB audio survives settings saves. A flash write mid-stream wedged the ISO OUT endpoint until a bus reset. Saves now run from the loop task (also off the usbd task's 800-byte stack) and wait while the audio interface is open; a watchdog re-arms a stuck endpoint.
  6. Defaults: grip limiter 85% (IMU: no measurable strength lost, 70 cost 3-5%); the Switch starts on wave; the Switch wave is capped at 100% grip strength and the DualSense wave at 250% gain, past which the grips grind.
  7. Soft boost (fields 144-147, blob p[215..218]): an upward compressor that raises quiet wave passages such as footsteps, Off / 1.5x (default) / 2x / 3x / 4x.
  8. "Test haptic rumble" (op 0x36): a tone panned across the grips, in every mode. Op 0x35 reads PCM stream counters (debug).

Behaviour changes worth a look

  • Switch Pro renders HD rumble by default. The Switch tab's HD Rumble toggle (field 128) turns it off, which plays the console's rumble as plain motor rumble. The rumble style (field 39) applies in every other mode, and in Switch mode with HD Rumble off.
  • Unanswered PS5 feature reports now stall, as on a real pad. That includes 0x03/0x08, which SDL probes for capabilities.
  • Steam mode now forwards 0x87-0x89. Steam's own traffic uses them only for haptic streams.

Protocol / config

  • Status blob v23 (upstream is at 22, with the RF recovery marker at p[196]) appends p[197..201]: audio gain/2, audio haptics on/off, audio style, grip limiter knee, HD trackpad strength/2.

  • WebUSB fields:

    • 30 audio gain (percent/2, 0 = auto)
    • 31 audio haptics on/off
    • 88 audio style (0 rumble, 1 tone, 2 split, 3 wave)
    • 115 grip limiter knee (50-100, 100 = off)
    • 141 HD trackpad strength (percent/2)

    None of these collide with Add optional Switch Pro profiles, trackpad D-pad and HD Rumble Emulation #303. 141 is Add optional Switch Pro profiles, trackpad D-pad and HD Rumble Emulation #303's own field for the same setting.

  • Later fields: 108-111, 120-127, 128 and 144-147, blob p[202..218] (see the updates above).

  • cfg.bin: new fields are appended to the tail, after upstream's ext[] block, so existing files keep loading. A file from an earlier build of this PR loses its saved haptics settings once and starts on the defaults.

Related PRs

Testing

  • make check (clang-format 18) passes; every commit builds.
  • python3 tests/hd_rumble/run.py: HD decode, rendering, stop, transition, pan test and soft boost tests, built with ASan/UBSan and -Werror; python3 tests/type_haptics/run.py: per-type defaults and config loading.
  • Hardware, this branch:
    • DualSense audio haptics (wave) in Control Resonant (GE-Proton, Steam Input off);
    • Switch Pro HD rumble in Super Mario Odyssey on Eden with the Direct Pro Controller driver.
  • Hardware, earlier versions of this PR: Stellar Blade, Hi-Fi Rush and FFXIV. Per-game status and known issues are in docs/DUALSENSE_HAPTICS.md.

Linux setup

No WirePlumber config is required. Set the DualSense Direct sink's haptic channels to 100% once:

pactl set-sink-volume alsa_output.usb-Sony_Interactive_Entertainment_DualSense_Wireless_Controller-00.Direct__Direct__sink 40% 40% 100% 100%

make install-wireplumber installs an optional hook that does this automatically. libScePad games (Sony PC ports) need GE-Proton 11-6+ and Steam Input off.

AI Disclosure

Note

This feature was heavily developed with the assistance of LLMs/AI. Everything has been reviewed, built, and hardware-verified.

🤖 Generated with Claude Code

@ripps818 ripps818 changed the title feat(ps5): add USB UAC1 4-channel audio haptics support feat(ps5): add USB UAC1 4-channel audio haptics support Aug 20, 2026
@ga2mer

ga2mer commented Aug 21, 2026

Copy link
Copy Markdown

Why it uses regular rumble instead of audio-driven way? I think you could feed my code to your LLM Agent so it can come up with a better solution

@ripps818
ripps818 marked this pull request as draft August 22, 2026 15:56
@ripps818

Copy link
Copy Markdown
Author

I've been working on this in my personal branch for a while. But my lack of knowledge on this is keeping me from getting it working correctly. It seems everytime a get one feature working, another stops working. If I get pcm haptics working, gyro and legacy rumble disappear. Something tells me this has do with hid-playstation feature flags, but I need to do some actual research now. I changed this to a draft until I figure it out.

@ripps818
ripps818 force-pushed the dualsense-haptic branch 2 times, most recently from 73fc73b to 0b734a8 Compare August 23, 2026 02:34
@ripps818

Copy link
Copy Markdown
Author

Okay, I refactored it a bit. If anybody wants to tear it apart, feel free.

ripps818 and others added 8 commits October 7, 2026 18:45
Present a 4-channel 48 kHz UAC1 audio function in the DualSense modes
and turn the haptic channels (3-4) into controller haptics over RF.

USB identity, so PipeWire picks the Direct profile and libScePad
accepts the pad:
- audio function on interfaces 0-2 with a silent 2ch mic, gamepad on 3
- byte-for-byte 273-byte report descriptor, pairing report tail,
  bcdDevice 1.00
- no serial string in MODE_PS5_GAME (wrap of tud_descriptor_device_cb)
- static firmware-info payload (0x20); NULL output buffers ignored

Audio haptics:
- level from 20 ms RMS with an instant-rise/halving-fall envelope and
  auto gain; square-root drive
- tone style: per-actuator 0x83 tones; frequency from zero crossings
- split style: below 80 Hz as 0x80 rumble, the rest as tones
- rumble style: 0x80 rumble merged with legacy host rumble
- panel toggle, style button and gain slider (WebUSB fields 30/31/88)

WirePlumber: an optional hook starts only the haptic channels of a
first-seen Direct route at 100% (`make install-wireplumber`).

docs/DUALSENSE_HAPTICS.md covers setup, Proton launch options and the
hardware measurements behind the design.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A real USB DualSense either answers a feature GET in full or stalls it;
it never sends a short reply. Answer 0x05, 0x09, 0x20, 0x22, 0x81,
0x83, 0xE0, 0xF1 (63 B), 0x85, 0xF2 and 0xF5 at the sizes the report
descriptor declares, with a real pad's values where they are known,
and stall everything else (0x03/0x08 capability, 0x0A, 0x21, ...).

Product string "DualSense Wireless Controller", as a real USB DualSense
reports it ("Wireless Controller" alone is the DS4's).

Input report: fill the bytes a real pad sends where we have no source
(sensor temperature, two constant bytes, the second timestamp).

UAC1 function: use a real pad's terminal/unit IDs and associations
(mic path IT 4 -> FU 5 -> OT 6), master mute + volume only, adaptive
sync on the OUT endpoint, no sampling-frequency control, and its max
packet sizes (392/196, mic packets 192). Volume ranges match what Linux
shows for a real pad.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OUTPUT 0x82/0x83 select actuators as trackpads 0 left, 1 right, 2 both
and grips 3 left, 4 right, 5 both (0x81 swaps 0 and 1). The PS-mode pad
click pulse always sent 1, so every click buzzed the right pad.

Add HSIDE_* constants and hsidePads(); psPadClickEdge() now takes the
click mask and buzzes the pad(s) clicked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The controller's OUTPUT handlers for 0x87/0x88/0x89 are raw haptic
sample streams (0x87 [target][samples] to the trackpad or grip
actuators, 0x88 a stereo grip stream, 0x89 a length-prefixed 0x87).
OpenPuck relayed only 0x80-0x86, believing these were settings reports,
while the real puck forwards every OUTPUT report up to 64 bytes, so
audio-to-haptics apps produced no feel through it.

- Relay 0x80-0x89 and raise the haptic relay payload to 63 bytes so a
  full stream report fits (frame 67 B, under the 96 B MAXLEN).
  Feature-0x01 command relays keep their 60-byte cap (RELAY_CMD_MAXP).
- An 8 kHz stereo stream needs ~258 frames/s, above the 250 Hz poll
  cycle: flush a second queued relay in the same cycle while the ring
  holds a backlog.
- Resend an unanswered 0x86-0x89 frame once with the same PID (ESB
  dedup drops it if the first copy landed).
- Count ring evictions; the serial stat line shows relay=/s and drop=.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the PCM haptic stream (OUTPUT 0x86 format, 0x88 stereo frames of
31 u-law samples per side at 4 kHz, measured on the controller's IMU)
and a DualSense "wave" audio-haptic style that uses it: the haptic
channels low-passed at 300 Hz (two Butterworth sections, also the
anti-alias), decimated to 4 kHz and streamed to the grip actuators.

- The format is re-sent every second and the stream stays up for
  300 ms after the gate closes, so short pauses don't pay the
  controller's ~40 ms pre-buffer again. The first frames are sized so
  playback starts after 24 ms instead of 31 ms.
- Wave plays the channels as sent (no auto gain); 100% is 40% of full
  scale. It is the new default style, also for a cfg.bin saved by an
  earlier build of this branch (whose stored style was the old tone
  default, not a choice).
- Grip limiter: PCM peaks above a knee (default 70% of full scale)
  round off instead of clipping, which plays as a pop. 100 = off.
  Persisted; WebUSB field 115, status blob p[199].
- WebUSB field 88 accepts wave (3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In Switch Pro mode the console streams HD rumble: two bands per side,
each with its own amplitude and frequency. OpenPuck used to keep only
one amplitude per side for the 0x80 rumble. Now it renders the bands:

- each side's low + high band, summed and phase-continuous, as a 4 kHz
  PCM stream on that side's grip (where a Pro Controller's actuators
  are). The stream stays up through 500 ms gaps so new effects don't
  pay the controller's pre-buffer again.
- each side's high band as an 0x83 tone on that side's trackpad, held
  under a quiet ceiling around 250-300 Hz where the pads ring loudly.
  Tones are refreshed every 50 ms, re-sent at once on a hit, and cut
  with -128 dB when the band goes quiet.

The decoder now tracks band frequencies (absolute 7-bit and relative
5-bit codes, around 160/320 Hz) and maps amplitude code 100, not 127,
to full scale as the public amplitude table and SDL's encoder do: every
level used to play at 0.56x. SPI 0x6012 reports device type 3 (Pro
Controller), which Eden's direct driver needs to accept input.

Switch Pro always renders HD rumble; the rumble style applies to the
other modes. Grip strength follows the rumble strength; trackpad
strength is a separate setting (persisted, WebUSB field 141, the same
field upstream PR safijari#303 uses; status blob p[200]). The relay scrub now
drops queued 0x83 tones on reconnect/stop and keeps their -128 dB cuts.

The band decoder and renderer are adapted from Froggerdog's HD Rumble
Emulation in upstream PR safijari#303 (without its profiles, trackpad D-pad and
shortcut feedback). tests/hd_rumble/run.py covers decode, rendering,
stop and transitions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- DualSense tab: the haptics style button cycles rumble, tone, split
  and wave (the firmware default).
- Rumble card, on firmware with protocol v22: a grip limiter select
  (field 115; 50-90%, 70% default, or off) and an HD trackpad strength
  select (field 141; off = grips only, 100% default), with a note on
  how Switch Pro renders HD rumble. Older firmware hides them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- DUALSENSE_HAPTICS.md: wave style (300 Hz low-pass, PCM to the grips),
  grip limiter, feature reports answered like a real pad, protocol v22
  fields and blob bytes, and the FFXIV prefix repair on Wine 11.
- PROTOCOL.md: 63-byte haptic relay and backlog flush, OUTPUT
  0x86-0x89 table, actuator routing, measured PCM stream behaviour,
  Switch Pro HD rumble (section 9.5), fields 30/31/88/115/141 and the
  v22 status blob tail.
- README: HD rumble and grip limiter, DualSense setup on Linux/Proton,
  the Steam-mode 0x87-0x89 relay.
- TESTING_GUIDE: HD rumble, grip limiter, audio styles, PCM relay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ripps818 ripps818 changed the title feat(ps5): add USB UAC1 4-channel audio haptics support feat: DualSense audio haptics, Switch Pro HD rumble and full-rate PCM haptic relay Oct 8, 2026
@ripps818
ripps818 marked this pull request as ready for review October 8, 2026 01:21
@ripps818

ripps818 commented Oct 8, 2026

Copy link
Copy Markdown
Author

Yet another refactor. Updated the protocol to improve a bunch of haptic related protocols, improved dualsense audio haptics, and added switch pro hd rumble.

@kohupallintrax

Copy link
Copy Markdown

Working well for me in Arknights Endfield and Infinity Nikki in Dualsense mode. Touchpad haptics for touchpad "click" feels a bit stiffer than the typical steam controller experience.

@ripps818

ripps818 commented Oct 9, 2026

Copy link
Copy Markdown
Author

Try disabling audio haptics, and tell me if it still feels "stiff" I suspect it might latency from the audio stream queue.

- Add relayEnqueueFront for priority messages
- Bypass queued PCM frames for click events
- Replace click button with dropdown for haptic style
@Jefemy

Jefemy commented Oct 11, 2026 •

Copy link
Copy Markdown

Switch rumble feels much better, however it seems to rumble twice. It will fall off and then come back a bit at the end. Doesn't happen on #303. It also feels like it is 'dropping' the rumble a bit. Like a packet is being lost? On the Mario Kart World character select panning across each character doesn't feel consistent like on a real Pro controller. Might be a symptom of some interference with the rumble not ending properly.

@ripps818

Copy link
Copy Markdown
Author

@Jefemy
Have you tried changing the grip limiter? I experienced harsh peaking with dualsense pcm haptics, and I adapted that system into the switch hd rumble. It's set to curve off peaks at 70%, but I left a slider to customize it. Move it to 90 or 100 (effectively disabling the limiter) and tell me if it still feels dropped off.

There very well could be dropped packets, so I'll look into it.

@Jefemy

Jefemy commented Oct 11, 2026

Copy link
Copy Markdown

@Jefemy Have you tried changing the grip limiter? I experienced harsh peaking with dualsense pcm haptics, and I adapted that system into the switch hd rumble. It's set to curve off peaks at 70%, but I left a slider to customize it. Move it to 90 or 100 (effectively disabling the limiter) and tell me if it still feels dropped off.

There very well could be dropped packets, so I'll look into it.

Just tried messing around with all the settings on the panel using the built in web panel test rumble. On everything I tested there is a slight "click" after the rumble should end. Its power doesn't seem to be affected by any of the settings.

@ripps818

Copy link
Copy Markdown
Author

are you using the docs/index.html in the pr, or the github.io config panel? Because the github.io one won't show my added options unless this PR gets merged.

@Jefemy

Jefemy commented Oct 11, 2026

Copy link
Copy Markdown

I am using the panel and uf2 downloaded from the main branch of your repo. I also first tested it using a uf2 compiled from the branch pulled here.

@ripps818

Copy link
Copy Markdown
Author

Oh, if your using the main in my ripps818/openpuck repo, it has tons of other stuff. You should make a new issue on my repo, not here. Because it might not be affecting this PR.

@Jefemy

Jefemy commented Oct 11, 2026 •

Copy link
Copy Markdown

I only used the main branch to make sure it wasn't fixed by another commit. Both issues are still in this PR. I will make an issue on your repo to continue this though.

EDIT: Switch HD Rumble works great now. Have been using it for a few weeks now

ripps818 and others added 20 commits October 11, 2026 22:50
When an HD rumble went silent, the grip stream ran 500 ms of silence and
then hdStop sent two -128 dB pad cuts and a 0x86 op 1. Sent to idle
actuators, these play as a click of their own, at a strength no rumble
setting changes. hdStop now cuts the pads only while a tone is on, and
trims the grip stream only if it played within the last 150 ms. A
silent stream just stops sending frames, like the DualSense wave
stream.

A PCM stream keeps a fixed fill once it starts, so a frame lost on a
bad channel takes its samples out of that cushion for good, and two or
three losses run the stream dry. A 0x86-0x89 frame still unanswered
after its same-PID resend now goes back to the front of the ring for up
to two later cycles. The same cycle's backlog flush is skipped after a
requeue.

Fixes #14

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 32a8ff2)
The Switch and the DualSense each keep their own haptic style and grip
limiter knee, since different systems may suit different ones. The limiter
was one global shared by both, and the Switch had no style at all.

Switch HD rumble now has the DualSense's four styles for its grips: wave
(the streamed waveform, as before and still the default), tone (each
side's stronger band as a grip tone), split (low band as 0x80 rumble, high
band as a grip tone) and rumble (the stronger band as 0x80). The pads
always play the high band as tones. Changing style mid-effect stops the
old style's vibration.

Protocol 22 (not bumped): fields 120-123 (limiter) and 124-127 (style) by
type, blob p[201..208]. Fields 115 and 88 still work for older panels, and
a config from before the per-type fields starts both types from the legacy
shared values. The panel falls back to 115 / 88 on older firmware.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…100%

The wave style's two full-scale bands made the Switch's grips rough at the
shared 200% strength, so the Switch now starts on the tone style at 100%
and every other type keeps wave at 200%. Grip strength is kept per type:
field 22 edits the active type's, and a config from before the per-type
values seeds every type with the old shared value unless it was still the
default.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Off plays the console's rumble as plain 0x80 motor rumble (each side's
peak, relayed on change), shaped by the host-rumble style and strength
like every other mode. Protocol 22 (not bumped): field 128, blob
p[209]. The panel shows the toggle on the Switch tab and greys the haptics
style and grip limiter while it is off.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Plain motor rumble needs the 200% the other types start with, where the HD
tones want the Switch's 100%, so turning HD rumble off would have made the
rumble weak. The Switch now keeps a second strength for HD rumble off
(200% by default); the Strength setting edits whichever applies, and
field 128 switches between them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Each type already kept its own grip strength, but the Rumble card's one
Strength select only edited the active type's. Every controller tab now
has a Rumble strength row (the Switch's follows its HD Rumble setting) and
the Switch tab has the HD trackpad strength. Protocol 22 (not bumped):
fields 108-111 edit a type's strength, blob p[210..213] read them; field 22
still edits the active type's, and older firmware keeps working on the
active tab.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Upstream took protocol v22 and blob byte p[196] for the RF recovery
marker, and appended a 10-byte ext[] block to cfg.bin. This branch's
fields move behind both:

- status blob: protocol 23, audio haptics and per-type haptics bytes
  shift from p[196..213] to p[197..214] (WB_PAYLEN 213); panel and
  docs follow
- cfg.bin: the haptics tail sits after ext[], so builds of this PR
  drop their saved haptics settings to defaults once
- Makefile wraps the device descriptor alongside upstream's string
  descriptor and control-transfer wraps
- DualSense feature reports keep this branch's real-pad answers
  (0x03 stalls, as a real pad does); upstream's identical firmware
  info table is kept once

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wave style sums each grip's low and high band as two sine
oscillators that kept running through the 500 ms hang between effects.
Each new effect so started with a random offset between the bands, and
with close band frequencies the sum beats: the same rumble swelled on
one play and faded on the next. Reset a side's two phases when it starts
sounding after silence, where the level is zero so the jump can't click.

Refs #17

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The grip PCM stream played rough on both: most frames were resent and
many played twice. The controller answers each frame in its ESB ACK,
either empty within ~200 us or packed with queued reports up to 127
bytes, ending ~800-1000 us after the frame. A 400 us window cut most
replies off, empty ACKs did not count, and a retry on a later cycle
carries a new PID, so the controller played the duplicate.

- Wait up to 1200 us after a PCM frame and count an empty ACK as
  delivered.
- Turn the radio from TX to RX in hardware (DISABLED->RXEN). In
  DualSense mode the USB audio processing preempted the loop between
  TX end and a software RXEN, and 83% of frames got no reply; now 1%,
  and polls are back at 250/s.
- Send 0x86 once per stream, on both paths: re-sending it every second
  made the grips stutter once a second. A controller whose link drops
  gets a fresh stream when it returns.
- Start a Switch HD stream 62 samples ahead, so an effect starts
  ~15.5 ms sooner.
- Limiter knee 85 for both types (IMU: no measurable strength lost; 70
  cost 3-5%, and blind A/B tests preferred 85). Cap the Switch wave at
  100% grip strength and the DualSense wave at 250% gain: past full
  scale the grips flatten, clip, and grind.
- Debug op 0x35 (0xB1 frame): PCM stream counters, reply delays and
  outcomes, the controller's 0x44 events, and DualSense audio state.
- Op 0x36: a Switch HD pan test, a tone panned left-right-left.

Refs #17

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 0x35 / 0xB1 grip PCM counters grow from 134 to 198 bytes:

- wait in the relay ring before each 0x88 frame's first send, and the
  gap between frames made for one controller (ms histograms)
- "ran dry" events sorted into end of stream, mid-stream, and
  mid-stream within 500 ms of the start
- each frame's first send against a steady sample clock from the
  stream's first frame: earliest, latest, last, and the value at the
  last mid-stream dry
- other relays sent while a stream runs, relay turns skipped after a
  missed poll, and the poll count

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two faults, found with the new send-timing counters and a blind 5-play
test (ticks felt matched dropped packets and underruns one for one):

- The poll loop skipped the next relay turn whenever a poll got no
  data. After a PCM frame the controller has often just sent its input
  in that frame's ACK, so the poll's empty ACK was taken for a drop:
  ~90 skipped turns/s, frames held a 4 ms cycle, send times spread
  ~14 ms against the controller's ~15.5 ms fill, and streams ran dry
  mid-play. An empty ACK with a good CRC now counts as a reply. Skips
  0-1 per play, no mid-stream underruns. Applies to every mode.

- The nRF5x USB driver drops an ISO OUT packet without an error when no
  transfer is armed at its SOF. The audio callback re-armed only after
  its DSP, so a slow packet lost the next one: 1 ms cut out of the
  wave, felt as a tick. It now arms a second buffer first; every packet
  arrives (host and puck counts match).

PROTOCOL.md also records the DualSense limiter A/B: 85 and 100 felt
alike, 70 weaker; 85 stays the default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pan test (op 0x36, "Test haptic rumble") fed the Switch HD path, so
it did nothing outside Switch Pro mode with HD rumble. Elsewhere it now
runs the wave generator itself and streams the same equal-power pan to
the grips as PCM, then plays silence through the hang and lets the
stream run dry, so it ends without the stop click. A controller already
streaming DualSense audio is skipped: two writers on one stream garble
both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tone was the Switch default only because the wave style's two
full-scale bands felt rough on it. The wave fixes, the 100% grip
strength cap and the limiter knee of 85 fixed that, and wave plays both
HD bands at their own frequencies, left and right apart, like the
DualSense already does. Saved styles are kept; fresh setups and Reset
to defaults get wave.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A flash save while the DualSense audio stream played wedged the ISO OUT
endpoint: the driver kept a transfer pending, refused every re-arm, and
dropped every packet until a bus reset. In a game, switching the style
Wave -> Rumble -> Wave in the panel left Wave silent for good (measured:
alt 1, 1000 packets/s sent, none received; a USB reset brought it back).
The same stall is why the controller's input reports stopped after a
settings change mid-game.

- While the audio interface is at alt 1, a setting applies at once and
  its save waits until the stream stops; every WebUSB save goes through
  saveCfgSoon. A mode switch or
  firmware-update reboot writes a held save first.
- A watchdog re-arms the endpoint when alt 1 goes 50 ms without a
  packet (the host sends one every frame), for any other flash write
  that lands mid-stream. Counted in the 0xB1 frame (200 bytes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The grips play down to ~0.5% of full scale in proportion (IMU), but a
hand barely feels a few percent, so soft footsteps vanish and a fading
effect seems to stop dead. Soft boost is an upward compressor on the
grip PCM stream, Switch and DualSense: below 20% of full scale the
level rises 2:1 toward it, capped at 1.5x / 2x / 3x / 4x; louder
passages pass unchanged to the limiter. One envelope for both grips
keeps the balance, and below 0.3% the boost fades out so a quiet
track's noise stays quiet.

Per type, fields 144-147, blob p[215..218], saved in cfg.bin. The
default is 1.5x: in a blind A/B the fade felt continuous with it and
cut out with it off, and 2x lifted some effects too far.

Also brings the limiter and haptic-style defaults in the docs up to
date (85%, wave).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel showed 24 words free on the usbd task (800 bytes, fixed in
the core) in DualSense mode, against 105-122 in the other modes. Only
DualSense runs USB audio there:

- Every 1 ms audio packet copied its 63-byte 0x88 frame onto the stack
  before queueing it. Producers now fill the frame in place
  (hapticPcmSend 104 -> 40 bytes of stack).
- WebUSB settings saves ran LittleFS on that stack. saveCfgSoon now
  only marks the save; cfgSaveTask writes it from the loop task.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The band split, its energies and the zero crossings only feed the
tone, split and rumble styles, yet ran on every 48 kHz sample in Wave
too: about a third of the per-sample work in the usbd task. Output is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- "Test haptic rumble" in the Rumble card plays the grip pan test
  (op 0x36): the Switch tab's haptics style in Switch Pro mode, Wave
  elsewhere.
- Soft boost (Off / Gentle 1.5x / Low 2x / Medium 3x / High 4x) on the
  Switch and DS5 tabs, fields 144 + type; hidden on firmware without
  it.
- The grip limiter offers 85% (the new default) and 95%; both style
  dropdowns mark Wave as the default.
- Notes say where the Wave style stops getting stronger: 100% grip
  strength on the Switch, 250% gain on the DualSense. In Wave the
  DualSense gain's Auto reads "Auto (100%)", since Wave skips the auto
  gain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Played one Black Flag clip to the puck and a real DualSense together and
read both controllers' accelerometers. Mid-level effects matched at 0.5x
and the clip's hits reached the grips' ceiling at 2x, so:

- the DualSense Strength runs 0-100% of each style's strongest (protocol
  24, was percent/2 of 10-500%); in Wave 100% is 2x the track and Auto
  25% (0.5x); a gain saved on the old scale loads as Auto
- a Wave expander raises the gain with the track's own level
- the Wave gate drops to 16 so quiet menu effects play
- Soft boost starts off on the DualSense; the Grip limiter stays 85

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants