The virtual pad wore one AUX node where a real DualSense shows a three-node split
ci / rust-arm64 (pull_request) Successful in 1m51s
ci / web (pull_request) Successful in 1m47s
ci / docs-site (pull_request) Successful in 1m54s
android / android (pull_request) Successful in 5m49s
ci / bun-nix (pull_request) Successful in 27s
ci / rust (pull_request) Successful in 6m10s
ci / rust-arm64 (pull_request) Successful in 1m51s
ci / web (pull_request) Successful in 1m47s
ci / docs-site (pull_request) Successful in 1m54s
android / android (pull_request) Successful in 5m49s
ci / bun-nix (pull_request) Successful in 27s
ci / rust (pull_request) Successful in 6m10s
A game that renders DS5 haptics writes a POSITIONED FL/FR/RL/RR quad, because that is the only public 4-channel surface a physically connected pad publishes. We minted a single AUX0..AUX3 node wearing the mono sink's `Speaker__sink` name, so that write was position-remixed on arrival and the coil pair folded away — measured on .181, `peak_speaker=0.2441` with `peak_coils=0.0000`. The haptics were discarded silently: nothing errored, nothing logged, the sink looked healthy. Measured a real DS5 (054c:0ce6, USB) on a SteamOS 3.7 Deck running alsa-ucm-conf 1.2.14-2.4 and minted what it actually presents — a card's worth of nodes, not one: alsa_output.hw_punktfunkpad<N>_0 Audio/Sink/Internal 4ch AUX0..AUX3 …-<NN>.HiFi__SpeakerHaptic__sink Audio/Sink 4ch FL FR RL RR …-<NN>.HiFi__Speaker__sink Audio/Sink 1ch MONO with the public pair naming the hidden parent in `api.alsa.split.name` (GE-Proton's `pipewire:NODE=` haptic leg) and the parent naming itself, exactly as the specimen does. Everything written to any of the three is summed onto one hardware quad, which a real pad gets free from ALSA SplitPCM and we have to do by hand — GE drives the haptic leg and the controller-effect leg AT ONCE by design, so emitting each node's buffers straight into the chunk channel would interleave them and gap both halves. The UCM also settles two things this file had wrong: - The four hardware channels: `Headphones` takes Channel0 0/Channel1 1, `Speaker` takes Channel0 1, both haptic devices take Channel2 2/Channel3 3. So ch1 is the built-in mono speaker, and a mono write landing on ch0 (what a bare AUX node does) would have played controller effects into the headphone LEFT channel with the speaker silent. - A plain DualSense's USB iProduct is "DualSense Wireless Controller", model word included. The `DualSense_` infix was dropped here to keep `Sony_Interactive_Entertainment_Wireless_Controller` contiguous — a property no real pad has either, so the name now carries the infix and drops the invented MAC (a real pad has no USB iSerialNumber; the trailing ALSA card index disambiguates). `device.vendor.id`/`device.product.id` also gain the `0x` prefix the specimen publishes. `strtol(s,_,16)` and `strtoul(s,_,0)` both yield 0x054c for "0x054c", while the bare "054c" we published is parse-dependent — base 0 reads it as octal 054, stops at the `c`, and yields 44, matching nothing. On glass on .181, index-exact in every leg (amplitudes encode the source channel): positioned coils-only → SpeakerHaptic__sink speaker 0.0000 coils 0.4883 (was 0.0000) AUX coils-only → parent speaker 0.0000 coils 0.4883 (unchanged) positioned front-only → SpeakerHaptic__sink speaker 0.3052 coils 0.0000 mono → Speaker__sink speaker 0.2747 coils 0.0000 both legs concurrently speaker 0.2747 coils 0.4883 Three deliberate deviations from the specimen stay, each documented at the head of the module: `node.description` keeps "Wireless Controller" (FF14/FF7R case-sensitive `wcsstr`), `priority.session` stays low (our nodes come and go with pad arrival and must never win a default-sink election), and `api.alsa.split.position` is not set (it is WirePlumber's own management trigger). Gate: clippy --all-targets -D warnings clean, 9/9 pad_sink tests, fmt clean, all in punktfunk-rust-ci linux/amd64.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -231,11 +231,13 @@ pub fn dualsense_test(args: &[String]) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mint one pad-audio PipeWire sink (the Linux 0xD1 source, `audio::pad_sink`) and capture
|
||||
/// from it — the WP3 on-glass gate with no client involved. Verify the identity with
|
||||
/// `pactl list sinks` (name/description/proplist) and drive it with
|
||||
/// `pw-play --target <node.name> <file>` (or `paplay -d <node.name>`); captured chunks print
|
||||
/// a per-second summary here. `--pad N` (default 0), `--edge`, `--seconds N` (default 30).
|
||||
/// Mint one pad's audio node graph (the Linux 0xD1 source, `audio::pad_sink`) and capture the
|
||||
/// mix — the WP3 on-glass gate with no client involved. Three nodes appear, mirroring the split a
|
||||
/// physically connected DualSense presents: a mono `Speaker__sink`, a positioned-quad
|
||||
/// `SpeakerHaptic__sink`, and the hidden AUX parent. Verify the identity with `pactl list sinks`
|
||||
/// (name/description/proplist) and drive any of them with `pw-play --target <node.name>`;
|
||||
/// captured chunks print a per-second summary here. `--pad N` (default 0), `--edge`,
|
||||
/// `--seconds N` (default 30).
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn pad_sink_test(args: &[String]) -> Result<()> {
|
||||
use crate::audio::AudioCapturer as _;
|
||||
@@ -256,18 +258,26 @@ pub fn pad_sink_test(args: &[String]) -> Result<()> {
|
||||
let mut cap = crate::audio::pad_sink::PadSinkCapturer::open(pad, edge)
|
||||
.context("mint pad-audio sink (is PipeWire running in this session?)")?;
|
||||
println!(
|
||||
"pad sink minted: node.name = {}\n api.alsa.split.name = {} (what GE-Proton opens as \
|
||||
pipewire:NODE=…)\n inspect: pactl list sinks | grep -A25 Speaker__sink\n \
|
||||
drive it: pw-play --target '{}' --channel-map 'AUX0,AUX1,AUX2,AUX3' <48k-file>\n \
|
||||
(a POSITIONED wav folds into the speaker pair and never reaches the coils — the \
|
||||
channel-map is not optional)\nCapturing for {secs}s…",
|
||||
"pad nodes minted (the split a real DualSense presents):\n \
|
||||
speaker sink = {} (mono — GE-Proton's is_dualsense_speaker_sink target)\n \
|
||||
haptic sink = {} (4ch POSITIONED FL,FR,RL,RR — the public quad a real pad shows)\n \
|
||||
parent = {} (4ch AUX0..AUX3, hidden — what GE opens as pipewire:NODE=…)\n \
|
||||
inspect: pactl list sinks | grep -A25 Speaker\n \
|
||||
drive the coils via the POSITIONED sink (what a real pad's writers use):\n \
|
||||
pw-play --target '{}' --channel-map 'front-left,front-right,rear-left,rear-right' <48k-file>\n \
|
||||
drive the coils via the AUX parent (GE's own leg):\n \
|
||||
pw-play --target '{}' --channel-map 'AUX0,AUX1,AUX2,AUX3' <48k-file>\n \
|
||||
(a POSITIONED wav aimed at the AUX PARENT still folds into the speaker pair — that is \
|
||||
why the positioned sink exists)\nCapturing for {secs}s…",
|
||||
cap.node_name,
|
||||
cap.haptic_name,
|
||||
if cap.split_name.is_empty() {
|
||||
"(suppressed)"
|
||||
} else {
|
||||
cap.split_name.as_str()
|
||||
},
|
||||
cap.node_name
|
||||
cap.haptic_name,
|
||||
cap.split_name,
|
||||
);
|
||||
let deadline = Instant::now() + Duration::from_secs(secs);
|
||||
let (mut chunks, mut samples) = (0u64, 0u64);
|
||||
|
||||
Reference in New Issue
Block a user