Files
punktfunk/docs-site/content/docs/controller-audio.md
T
enricobuehler 23edf4e702
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Failing after 4m11s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Failing after 4m24s
ci / rust-arm64 (pull_request) Successful in 2m50s
android / android (pull_request) Successful in 6m29s
ci / web (pull_request) Successful in 1m53s
ci / docs-site (pull_request) Failing after 45s
ci / rust (pull_request) Successful in 7m2s
ci / bun-nix (pull_request) Successful in 23s
fix(client): the pad's speaker shares a channel with its headphone jack, and powers up on the jack
Field result from the Deck: haptics FELT, speaker inaudible — with the routing
already proven correct. Capturing the sink's own monitor while the client renders
shows the speaker pair carrying full-scale signal:

  --coils    ch0 0.0000  ch1 0.0000  ch2 0.5000  ch3 0.5000
  --speaker  ch0 0.5000  ch1 0.5000  ch2 0.0000  ch3 0.0000

so nothing was lost on the way to the pad. The loss is inside it. Channel 1 of the
DualSense's audio function is the headphone jack's RIGHT channel *and* the built-in
mono speaker — #259 reads the same thing out of the UCM from the host side ("ch1 is
the built-in mono speaker") — and which of the two physically sounds is chosen by
`ucAudioEnableBits`, report byte 8. A pad powers up pointing at the jack, so with
nothing plugged in the speaker pair goes nowhere. The coils are channels 2/3 and are
NOT affected by that select, which is exactly why haptics worked the instant the
samples were routed right and the speaker did not.

We only ever wrote those bytes when a host forwarded a game's `AudioCtl`, so a title
that manages no audio settings of its own — and every standalone test — got silence.
A tier-A slot with the speaker capability now sends a default speaker-enable packet
beside the audio-haptics packet it already sends. A later `AudioCtl` still overrides
it verbatim, so a game driving its own volume still wins.

⚠ The path byte is EMPIRICAL, not documented: SDL's vendored SDL_hidapi_ps5.c pins
the struct layout but never writes these fields. Measured on 054c:0ce6 using the
pad's own microphone as the detector (Goertzel at the test tone): 0x20 loudest at
~5x the noise floor, 0x30 also sounds, 0x10 silent. That is thin evidence for a
constant, so both it and the volume are field levers —
PUNKTFUNK_PAD_SPEAKER_PATH / PUNKTFUNK_PAD_SPEAKER_VOLUME, hex or decimal — and an
on-glass confirmation of which value a human actually hears is still owed.

The test pins what must not regress: the two validity bits are set, volume and path
land at the same offsets the AudioCtl fold uses, every other byte stays zero, and
`ucEnableBits1` bits 0/1 stay CLEAR — asserting either would enable rumble emulation
and disable audio haptics, muting the coils to make the speaker audible.

Gates: clippy -D warnings over the four client packages, build, 207 tests (204 in
pf-client-core), fmt — on top of current main.
2026-08-16 00:53:18 +02:00

9.7 KiB
Raw Blame History

title, description
title description
Controller speaker and haptics DualSense voice-coil haptics and the pad's built-in speaker, streamed from the host to the controller in your hands — what to enable, and what "set it to Pro Audio" means on a Linux host.

A DualSense is partly an audio device. Its little speaker and its two voice-coil motors — the actuators that make a PS5 pad feel like sand, rain or a bowstring instead of a buzzing phone — are all driven by a four-channel audio stream, not by rumble commands. Games that support them write PCM into "the controller's audio device".

Punktfunk gives that device to the game on the host, captures what the game writes, and streams it to the controller physically in your hands, on its own low-latency plane. Channels 12 are the pad's speaker, channels 34 are the voice coils.

What you need

  • A DualSense or DualSense Edge plugged in over USB on the client. Bluetooth pads expose no audio interface at all, so they fall back to ordinary rumble — this is a limit of the controller, not of Punktfunk.
  • On the client, Controller haptics is on by default. So is Controller speaker on the Linux and Windows apps — turn it off in client settings if you would rather all game audio came out of your speakers. On Android the speaker is opt-in.
  • On a Linux host, a game that speaks DualSense — which in practice means running it under GE-Proton 11-5 or newer. Stock Proton does not route controller audio.
  • On the host, controller audio is on by default (PUNKTFUNK_PAD_AUDIO).

Nothing is sent while the pad is quiet, so leaving it on costs nothing.

"Set the controller audio to Pro Audio" — you don't have to

If you have looked into DualSense haptics on Linux before, you have probably run into this advice: plug the pad into the Linux box, open your sound settings, find DualSense wireless controller (PS5), and switch its Profile to Pro Audio. That advice is real and it is correct — for a pad plugged directly into the host.

The reason is channel layout. A pad's other profiles present it as a mono speaker, a stereo headphone jack, or a positioned four-channel "surround" device. Games write their haptics as four unpositioned channels, so on any of those profiles the audio system helpfully re-mixes them into the speaker pair and the voice-coil channels are folded away. You feel nothing. Pro Audio is the one profile that hands the four channels through untouched, in order.

Punktfunk's controller audio device is already in that shape. It is created as four raw channels with no re-mixing, which is exactly what Pro Audio produces — so there is nothing to switch, and no switch to make.

That is also why it looks different in your sound settings. A real pad is a USB sound card, so it gets a Profile dropdown; Punktfunk's is a software device, so it has no card and no dropdown. Seeing Wireless Controller with a volume slider and no profile selector is what a correctly minted controller-audio device looks like. It is not a sign that something is missing.

Checking it is working

On the host, one line per pad is logged when the device is created:

pad-audio sink minted (Pro Audio shape: 4ch AUX0..AUX3, ch0/1 speaker, ch2/3 coils)

and, once a client that can render it connects:

pad audio streaming (0xD1, Opus 48 kHz, silence-gated)

When a game actually starts driving the actuators, the pad's own driver reports it:

DS5 title asserted haptics-select (audio haptics) pad=0

That last line is the one that matters: it means a title recognised the controller as an audio device and switched the pad out of plain rumble. If you see it and still feel nothing, the problem is downstream — on the client or the pad. If you never see it, the game never found the device.

You can also look at the device directly:

pactl list sinks | grep -A25 Speaker__sink

The line to check is audio.position = "AUX0,AUX1,AUX2,AUX3" — four unpositioned channels is the layout that reaches the voice coils. Anything positioned (FL,FR,RL,RR) would not.

If a game does not find it

Games identify the controller's audio device by name and by USB ids, and different titles check different things. GE-Proton has several routes to the pad, and a couple of them are opt-in per game. Add these as launch options if a title is not cooperating:

PROTON_DUALSENSE_HAPTICS_PREFER_NON_EVENT=1 %command%

This forces GE onto its most direct route — it opens Punktfunk's controller-audio device by name and writes the four channels straight into it, with no re-mixing anywhere in between. It is the first thing to try.

Some titles additionally want:

PROTON_SONY_WINDOWS_DEVICE_NAMES=1 PROTON_KEEP_SONY_AUDIO_ENDPOINT_VISIBLE=1 %command%

and Death Stranding Director's Cut has its own:

PROTON_DUALSENSE_SPLIT_AUDIO=1 %command%

To see which route GE took, launch the game with WINEDEBUG=+pulse and look for a line beginning Routing DualSense. It names the device it chose and how it opened it.

On a Linux client, the pad's own profile matters too

Everything above is about the host, where the controller-audio device is one Punktfunk mints. On a Linux client the pad is real, and the same channel-layout problem shows up from the other side: the voice coils are physically channels 3 and 4 of the controller's USB sound card, and a controller almost never presents as a four-channel device on its own. Depending on your distribution it appears as a stereo output, or as a mono Speaker plus a stereo Headphones pair. Playing into any of those puts the haptics in the headphone jack and folds the coil channels away — audio that looks perfectly healthy, felt as nothing at all.

Punktfunk handles this for you. When it needs the coils and the pad is not already presenting four channels, it switches the controller's card to Pro Audio for the length of the session and puts your setting back afterwards. You will see the profile change in your sound settings while you are streaming; that is expected. It is never saved as the card's remembered profile.

If you would rather manage the card yourself, set PUNKTFUNK_PAD_AUDIO_PROFILE=0 on the client. Then Punktfunk uses a four-channel profile if you have already selected one and logs what it needs if you have not.

Most systems never reach the switch at all. Where your distribution ships a recent alsa-ucm-conf — Bazzite and SteamOS among them — a DualSense already exposes its four channels behind its split speaker and headphone outputs, and Punktfunk finds them there. The switch is the fallback for systems that only offer the older stereo profile. If you run the client as a Flatpak, your audio manager may not let a sandboxed app change a card's profile; if the log says so, switch the controller to Pro Audio yourself, which is the same fix.

Checking the client side without a host

The client can test the whole path on its own — no host, no game, no pairing. Plug in the DualSense and run:

punktfunk-session --pad-audio-test

It prints every DualSense object it can see in your audio graph, says which one it chose, and then plays a tone into the voice coils for three seconds. If the pad buzzes, the client side is working and any remaining silence is coming from the host or the game. Add --speaker to test the pad's speaker instead, and --seconds N for a longer run.

On the Steam Deck and other flatpak installs, run it inside the sandbox:

flatpak run --command=punktfunk-session io.unom.Punktfunk --pad-audio-test

Why the speaker needs more than routing

The controller's speaker and its headphone jack share a channel. Channel 1 of the pad's audio device is the headphone jack's right channel and the built-in speaker, and the controller decides which one actually sounds. It powers up pointing at the jack — so with nothing plugged in, a perfectly routed speaker stream is heard by nobody.

Punktfunk points the pad at its own speaker when Controller speaker is on. The voice coils are different channels and are not affected by that choice, which is why haptics work as soon as the audio is routed correctly and the speaker needs this extra step. A game that drives the pad's audio settings itself still overrides it. If your pad's speaker stays quiet, PUNKTFUNK_PAD_SPEAKER_PATH and PUNKTFUNK_PAD_SPEAKER_VOLUME let you bisect it without a rebuild.

Known limits

  • Bluetooth client pads get rumble, not haptics. No audio interface exists over BT.
  • Titles that match the controller by container ID — a Windows notion of "these devices are the same physical thing" — will not recognise the pairing on a Linux host, because the virtual pad has no USB device behind it to derive one from. Titles that match by name or by USB ids are unaffected, which is most of them.
  • A pad plugged into the host itself can steal the audio. If a real DualSense is connected to the host while you are streaming to a different one, some titles will find the local pad's sound card first. Unplug it, or stream from a host that has no pad attached.
  • The Pro Audio switch on a Linux client renames the pad's microphone too. Switching a sound card's profile re-creates all of its inputs and outputs, so if you had picked the DualSense's own microphone as your mic, that session falls back to your default one. Pick a different microphone, or set PUNKTFUNK_PAD_AUDIO_PROFILE=0 and select a four-channel profile on the card yourself.
  • A client killed mid-stream leaves the pad on Pro Audio. The profile is restored when a session ends normally and is never written to your saved settings, so anything that reloads the card — unplugging it, logging out, a reboot — brings your own profile back.