Pre-fold per-app audio haptics capture + Volume Sync - #30
Merged
Conversation
Imported/library profiles carrying meta.game landed as game profiles for games never located on this machine, duplicating real game profiles. importProfile now drops meta.game so imports arrive as plain trigger profiles, and resetToLibrary preserves the locally attached game instead of adopting one from the fetched copy.
This reverts commit 1d8b97a.
Merge upstream hurryman2212/vds 1448bb8 (0.3.0-rc2, our import point) ..2d27ab0 (0.3.0-rc7) for Linux and common code, keeping all companion features (per-trigger persistent effects, V2 trigger FFB, global trigger intensity, touchpad grab, RSSI/battery reporting, lightbar handling). Brings in: - Linux microphone and headset support (BT mic decode, headset-aware audio output path, mute button and mic-select handling) - logrotate support and packaging/DKMS cleanup fixes - vds_hcd loaded before vdsd; driver versions reported in logs - upstream haptic-channel pre-gain back at the pre-0.2.1 value Our speaker-pacing fix (1d8b97a) was reverted beforehand; upstream's reworked audio sender replaces it. Windows-only changes (installer, windrv, win32 daemon) were not taken. Conflict resolutions: DsOutputState keeps the companion override layer (recompute_effective_state now also runs after upstream's new audio/mic state mutations); handle_bt_input applies companion input translation to the converted USB report after upstream's mic-audio branch; companion logging moved to the new LogScope enum (new Companion scope).
The companion 0x0B command was acked and discarded, so the app's Audio Buffer Length slider never changed backend behavior; the speaker/haptics queue depth was fixed at 8 chunks (80 ms) with a 4-chunk stale-drop threshold. Store the slider value (16-128, 3 kHz haptics samples) in CompanionSettings and derive the per-port pending 0x36 chunk cap from it in apply_companion_state: one chunk per 10 ms speaker frame (30 samples), clamped to 2-16 chunks, stale-drop threshold at half the cap. Value 0 (never set) keeps the old 8/4 defaults, and ports reset to those on disconnect. Out-of-range values return kAckErrInvalidValue. Adds companion_buffer_length_test covering store, clamps, and rejection.
With SET_HAPTICS_BUFFER_LENGTH now actuated, the old 16-128 range only reached 43 ms while the daemon's previous fixed queue was 80 ms, so the whole slider traded resilience for latency with no way back. Raise the max to 240 samples (8 chunks = the old queue depth) across the renderer slider, bridge-service clamp, settings store, and the vdsd validator. Existing zone labels (High stutter / Risky / Safe) keep their sample thresholds. 692 app tests and the vds companion test pass.
The High stutter / Risky / Safe boundaries (44/63) came from the Windows WASAPI buffer. The Linux daemon rounds the value to whole 10 ms chunks (30 samples each) with a 2-chunk floor, so 16-45 all produce the same 2-chunk queue. Move the boundaries to the chunk edges: <=45 stutter (2-chunk floor), 46-75 risky (3 chunks), >=76 safe (4+ chunks).
…y-out fix Mute sync (both directions): - vdsd: a physical mute-button toggle now mirrors into companion.settings.mic_muted (and bumps the settings revision), so the STATUS report reflects it and the app's existing micMuted reconcile picks it up. - vdsd: apply_companion_state actuates an app-initiated SET_MIC_MUTE on the controller with the same BT mic-state report the button path uses. Mic grayed out: - Disabling the audio section turned off mic pass-through, but enabling it back only restored the speaker, leaving duplexMicEnabled stuck off and every mic control disabled. Re-enabling audio now restores the mic too. Audio buffer default: - Default hapticsBufferLength 64 -> 120 (4 chunks / 40 ms): 64 sat on the daemon's 2-chunk floor. Zone boundaries corrected to the actual integer chunk rounding: <=74 floor, 75-104 = 3 chunks, >=105 safe. 692 app tests and the vds companion test pass.
Manually pinning a trigger profile (matchedBy 'pin') set the engine's activeProfileId, which both the game-profile card and the game-settings coordinator read as 'this game is running': the card showed the game as active and the coordinator swapped in the full game settings set. Require matchedBy 'process' in both places so a manual trigger override stays a trigger override, and auto switching state follows actual game detection only. Adds a coordinator regression test (693 total).
Review finding: port.mic_muted was flipped before try_send_output_report, so a blocked HID queue dropped the mic-state report with no retry and the controller LED desynced from the app. Keep the mismatch until the report actually goes out; the next companion write retries.
…audio, actuated audio buffer, mic mute sync
The BT mic-state report hardcoded the mute LED byte to 0, so app- or button-initiated mute toggles produced no visible feedback on the controller: an app-side mute looked like it did nothing. Set the LED from the mute state (solid while muted), matching native PS5 behavior.
The status poll reads the STATUS report, then does several other feature reads before reconciling micMuted. A mute toggled in the app inside that window was compared against the pre-command status and snapped back to the stale state, fighting the daemon (alternating mute/unmute in the vdsd log). Skip the reconcile for 2 s after this app sends SET_MIC_MUTE; controller-button changes still reconcile on the next poll.
Occasional stale mute LED: a 0x31 state report queued while the HID queue was blocked was built from the pre-toggle output state, and when it flushed after the mic-state report it re-asserted the old mute LED (controller showed muted after an unmute). Rebuild any queued state report from the current state after every mic-state send, and when the button path's mic report itself fails to send, queue a full state report so the flush loop delivers the change instead of dropping it.
Root cause of the recurring stale mute LED (trace-confirmed): the host kernel's hid-playstation driver bound to the virtual DualSense toggles its own mic-mute state on every mute-button press and sends output reports with allow_mute_light/allow_audio_mute. It cannot see companion-initiated mute changes, so after an app mute its phase is inverted and its next output report re-asserts the old LED and mic-mute bit over ours. Drop allow_mute_light entirely and preserve our mic-mute power-save bit when merging host reports; other power-save bits still pass through.
Emit an 'OpenDS5: Microphone muted/unmuted' toast from a single helper on all four mute-change paths: app toggle (setMicMute), controller button adoption via the status reconcile, the controller mic-mute shortcut event, and the implicit change when mic pass-through is enabled/disabled. Change-detected so redundant commands stay silent.
Feedback loop: with the headset plugged into the controller the default sink can be the bridge's own 4-channel device. The haptics helper captured the default sink monitor without a channel map, so PipeWire's 4ch->2ch downmix folded the rear haptics channels we play back into the capture - the engine amplified its own output into a constant buzz. Pin the capture to FL,FR (identical on stereo sinks; front-only on the bridge sink). Capture device picker: the Audio Haptics source dropdown now lists the system's output sinks (bridge sink excluded) alongside app sessions. Default stays 'System', which follows the default output device; picking a device pins pw-record --target to that sink's monitor. New 'output-device' source kind plumbed through protocol types, settings normalization, engine args (--haptics-output-device), Linux helper (--list-output-sinks mode), IPC, and the renderer picker.
…-down The FL,FR channel-map alone did not break the feedback loop: PipeWire's channel mixer still folds the 4ch bridge monitor's rear (haptics) channels into a narrower capture's fronts (standard rear fold-down), so the helper kept hearing its own output. Capture 4ch FL,FR,RL,RR - a matching format links passthrough with no mixing - and read only the front channels in the processor. Stereo sinks upmix with silent rears, fronts intact.
…conf The Direct 4-channel PCM is the device the whole audio stack targets (daemon, haptics helper, app). Without pinning, WirePlumber can bring the card up in a convenience profile whose stereo path goes through the channel mixer and software volume, which audibly degrades the BT audio and hides the haptics channels. Users previously needed a separate local pro-audio override (50-vds-proaudio.conf from beta setups); the shipped 99-vds conf now covers it in the existing card rule.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Enhances Audio Haptics on Linux with pre-fold per-app capture and adds a Volume Sync feature, plus three volume-attenuation bug fixes found during hardware verification.
Pre-fold per-app capture
--haptics-app-*flags the engine already sends: hot-attaches (2s pw-dump poll) to the selected app's own PipeWire output stream and re-attaches if the game restarts its stream.left/right = FL/FR + 0.5×FC + 1.0×LFE(rears excluded). Stereo streams and the no-app sink-monitor path are numerically identical to before.Volume Sync
Haptics are delivered as audio on the DualSense sink, so listening volume scaled them. Two per-card toggles (both default ON = follow the volume knob, historical behavior):
Hardware-verification bug fixes
object.serial— a plain--target <id>silently fell back to the default microphone.pw-playstream is pinned at--volume 1withstate.restore-props = false(a saved 50% had halved haptics across sessions).Testing
npm run test:companion); typecheck + build clean.Remaining hardware checks (post-merge, user-assisted)
Specs/plans:
docs/superpowers/specs/2026-07-17-*.md,docs/superpowers/plans/2026-07-17-*.md.