Skip to content

feat: encrypted client microphone passthrough for Windows hosts#169

Open
xenstalker02 wants to merge 4 commits into
Nonary:masterfrom
xenstalker02:pr/mic-passthrough-v2
Open

feat: encrypted client microphone passthrough for Windows hosts#169
xenstalker02 wants to merge 4 commits into
Nonary:masterfrom
xenstalker02:pr/mic-passthrough-v2

Conversation

@xenstalker02
Copy link
Copy Markdown

@xenstalker02 xenstalker02 commented Mar 24, 2026

Summary

Adds microphone passthrough from a Vibelight client (Steam Deck) to the Windows host,
rendered via Steam Streaming Microphone. VB-Audio Virtual Cable is the automatic fallback.

Satisfies ClassicOldSong's stated requirements:

✅ Encrypted mic stream — mic data rides the AES-GCM control stream (SS_ENC_CONTROL_V2); plaintext refused
✅ Steam Streaming Microphone — no third-party driver required; uses Steam's audio driver
✅ Per-session decoder — concurrent session safe, no shared global state
✅ Session routing by control stream context — NAT-safe by design

Implementation

Protocol: 0x3003 packet on the existing encrypted control stream — no new ports, no firewall changes required.

Transport: mic data is AES-GCM encrypted as part of SS_ENC_CONTROL_V2. Unencrypted sessions are refused at the server.

Codec: Opus 64kbps mono, VBR, complexity 10, FEC enabled, DTX enabled, 20ms frames.

Client capture (Vibelight): SDL2 at 48kHz, stereo-to-mono (L+R)/2 downmix, deadline-based 20ms pacer with re-sync guard, 12-frame buffer cap. Sends via LiSendRawControlStreamPacket(0x3003) using a minimal fork of moonlight-common-c.

Server render (Vibepollo): WASAPI to Steam Streaming Microphone (primary). Falls back to VB-Cable automatically if Steam is not running. IPolicyConfig device format normalized to PCM 32-bit before WASAPI initialization — this is the key fix that makes Steam Streaming Microphone work where all previous attempts failed.

Session lifecycle: At stream start, Windows default capture device is switched to the passthrough device (both eConsole and eCommunications roles). Restored to previous default on stream end via RAII guard.

Config options: mic_sink, mic_capture_device, mic_buffer_packets in sunshine.conf.

Files changed

  • src/stream.cpp — 0x3003 IDX_MIC_AUDIO_DATA handler, mic render thread lifecycle
  • src/platform/windows/audio.cpp — WASAPI render, Steam mic backend, VB-Cable fallback
  • src/platform/windows/mic_write.cpp + mic_write.h — new: WASAPI mic render backend
  • src/platform/windows/apollo_vmic.cpp + apollo_vmic.h — new: Steam mic IPolicyConfig init
  • src/config.h / src/config.cpp — mic configuration options
  • src/system_tray.cpp — toast notification fixes

Testing

  • Steam Deck built-in mic → Windows host via LAN — recv=28955 decoded=28966 plc=0 silence=0 errors=0 ✅
  • VB-Cable fallback path — recv=30190 decoded=30190 plc=0 silence=0 errors=0 ✅
  • Confirmed working in Discord (Default device), Windows Voice Recorder, game voice chat
  • Two friends confirmed audio quality good in live testing

Related

  • Parallel client implementation: xenstalker02/Vibelight
  • Parallel server implementation reference: logabell/Apollo PR #1428
  • Both implementations were developed in parallel and cross-referenced. Key technical difference: we use 0x3003 on the existing control stream rather than a dedicated UDP port via LiSendMicrophoneOpusDataEx.

xenstalker02 and others added 4 commits March 24, 2026 13:17
…mentation

Adds speaker_t abstract class and capture_snapshot_t struct to the platform
audio interface. Implements speaker_wasapi_t for writing decoded mono PCM to
a WASAPI render endpoint (e.g. VB-Audio Virtual Cable Input) with a
4-packet prebuffer render thread.

Fix 2: snapshot_capture_defaults()/switch_capture_to()/restore_capture_from()
snapshot all three ERole values before switching and restore each individually,
eliminating the per-role race and removing the detached retry thread.

Fix 3: IsFormatSupported() negotiation — on S_FALSE uses closest_match; on
FAILED falls back to GetMixFormat(). Prevents AUDCLNT_E_UNSUPPORTED_FORMAT on
devices where float32/2ch/48kHz is not natively supported.

Removes install_steam_audio_drivers() and the startup call site; VB-Audio
Virtual Cable is the supported loopback path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds IDX_MIC_AUDIO_DATA=19 / packetTypes[19]=0x3003 for the Apollo mic
passthrough control stream packet type.

Fix 1: parses the real sender sequence number from the 4-byte framing header
[seq_hi, seq_lo, ch, flags] prepended by the Vibelight client, instead of
using a fake local counter. This makes the jitter buffer correctly order
packets from the client's perspective.

Session lifecycle: on start(), allocates OpusDecoder + speaker_t, calls
snapshot_capture_defaults() and switch_capture_to(mic_capture_device) as a
one-shot switch with no retry thread. On join(), calls restore_capture_from()
using the saved snapshot to restore all three ERole defaults.

Requires SS_ENC_CONTROL_V2 (AES-GCM) on the control stream; plaintext mic is
refused per upstream requirements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…_drivers

Adds four new audio_t fields:
  mic_sink           — render endpoint name (e.g. 'CABLE Input ...')
  mic_capture_device — capture endpoint to set as default on session start
  mic_buffer_ms      — underrun gap threshold (default 500 ms)
  mic_buffer_packets — jitter buffer prebuffer size (default 3 packets)

Removes install_steam_drivers from audio_t and the corresponding
bool_f() binding in apply_config(). Steam Streaming Speakers are no longer
the supported loopback path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds four ConfigFieldRenderer blocks for mic_sink, mic_capture_device,
mic_buffer_ms, and mic_buffer_packets to the Audio/Video configuration tab.

Adds corresponding English locale strings with descriptions explaining
VB-Audio Virtual Cable usage for each field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@cmibarnwell
Copy link
Copy Markdown

Great to see the development progress on microphone passthrough. It would be extremely useful for my use case to have this feature. Where did this leave off? Will this be added to Vibepollo and is there any way to use this currently? @xenstalker02

@Nonary
Copy link
Copy Markdown
Owner

Nonary commented May 19, 2026

Great to see the development progress on microphone passthrough. It would be extremely useful for my use case to have this feature. Where did this leave off? Will this be added to Vibepollo and is there any way to use this currently? @xenstalker02

Because it requires a client sided change (on Moonlights side) adding this functionality isn't going to work anyway.

@xenstalker02
Copy link
Copy Markdown
Author

Great to see the development progress on microphone passthrough. It would be extremely useful for my use case to have this feature. Where did this leave off? Will this be added to Vibepollo and is there any way to use this currently? @xenstalker02

Hey there! If you check my repositories, you’ll find that I’ve forked a build of Moonlight, which I’ve named “Vibelight.” This fork serves as a companion app for my own fork of Nonary’s Vibepollo. Currently, it’s fully functional, but my fork of Moonlight is currently only compatible with the Steam Deck.

@radugrecu97
Copy link
Copy Markdown

@xenstalker02 Any plans on a windows client and updating the upstream?

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