Skip to content

vds 0.3.0-rc7 sync: working mic/headset audio, buffer control, mute sync, haptics fixes - #28

Merged
LordVicky merged 18 commits into
devfrom
feature/vds-rc7-audio-mic
Jul 18, 2026
Merged

vds 0.3.0-rc7 sync: working mic/headset audio, buffer control, mute sync, haptics fixes#28
LordVicky merged 18 commits into
devfrom
feature/vds-rc7-audio-mic

Conversation

@LordVicky

Copy link
Copy Markdown
Owner

Summary

Syncs the vendored vds tree with upstream 0.3.0-rc7 (Linux-relevant changes only) and builds out the audio/mic feature set on top. All changes hardware-verified on a DualSense over Bluetooth.

Upstream sync (1448bb82d27ab0)

  • Linux microphone + headset-jack support (BT mic Opus decode, headset-aware output path, mute-button handling), logrotate, DKMS/packaging fixes
  • Our speaker-pacing fix reverted in favor of upstream's reworked audio sender
  • All custom features preserved: V2 trigger FFB, per-trigger persistent effects, global trigger intensity, touchpad grab, RSSI/battery, companion runtime
  • Windows-only changes not taken

Audio buffer control

  • SET_HAPTICS_BUFFER_LENGTH (0x0B) now actuated: slider maps to the daemon's pending 0x36 chunk cap (was hardcoded 80 ms)
  • Range extended to 16–240 samples (5–80 ms), default 120, zone labels aligned to the daemon's 10 ms chunk rounding

Mic mute sync

  • Physical mute button ↔ app state, both directions, with the DualSense mute LED driven from actual state (was hardcoded off)
  • Kernel hid-playstation's out-of-phase mute opinion ignored; daemon owns mic mute
  • Stale queued 0x31 state reports rebuilt after mic-state changes; app holds off status reconcile right after its own mute command
  • Toast on every mute/unmute

Audio-reactive haptics

  • Fixed self-oscillation (constant buzz) with headset plugged: capture is now 4 discrete channels, fronts only in the processor — PipeWire's rear fold-down can no longer feed our own haptics output back
  • Capture-device picker in the Audio Haptics card (follows default output by default; sinks enumerated via the helper, bridge sink excluded)

Misc

  • vds card pinned to the pro-audio profile in the shipped wireplumber conf (Direct 4-ch PCM guaranteed; fixes out-of-box audio quality)
  • Manually pinned trigger profiles no longer masquerade as the active game (card + game-settings coordinator)
  • Imported profiles no longer create unwanted game profiles

Test plan

  • 694 app tests passing; vds builds clean, companion buffer + trigger V2 tests pass
  • Hardware-verified: speaker/headset/mic playback + capture, mute sync from both sides, buffer slider behavior, haptics with headset plugged, device picker enumeration

LordVicky added 18 commits July 16, 2026 21:25
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.
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.
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.
@LordVicky
LordVicky merged commit a582dea into dev Jul 18, 2026
3 of 4 checks passed
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.

1 participant