Skip to content

feat(mic): client microphone passthrough (Steam Streaming Microphone) rebased onto v1.17.0 - #293

Open
vladbogun1 wants to merge 8 commits into
Nonary:masterfrom
vladbogun1:feat/mic
Open

feat(mic): client microphone passthrough (Steam Streaming Microphone) rebased onto v1.17.0#293
vladbogun1 wants to merge 8 commits into
Nonary:masterfrom
vladbogun1:feat/mic

Conversation

@vladbogun1

Copy link
Copy Markdown

Client microphone passthrough (Steam Streaming Microphone) on current base

This ports the Steam Streaming Microphone client-mic passthrough from
xenstalker02/Vibepollo (which sits ~1.5 minor versions behind)
onto the current Vibepollo base (branched from v1.17.0). All credit for the original mic implementation and
protocol goes to @xenstalker02; this PR is a clean-room rebase of just the mic feature onto the newest code,
dropping the unrelated rebranding/installer/playnite churn.

What it does

Receives an Opus-encoded microphone stream from a compatible Moonlight client over the encrypted control
stream (0x3003), decodes + jitter-buffers it, and renders it into the Steam Streaming Microphone virtual
render endpoint so host apps (Discord, etc.) can read it as a normal mic input.

Changes (mic-only)

  • config.{h,cpp}: mic_sink, mic_capture_device, mic_buffer_ms, mic_buffer_packets (with sane defaults — no config edit required).
  • stream.{h,cpp}: 0x3003 packet type, session_t::mic jitter-buffer state, init_mic_passthrough() (lazy init on first packet), IDX_MIC_AUDIO_DATA handler via server->map(), get_mic_status(). Kept the v1.17.0 decode_control_packet/control_packet_view_t model.
  • platform/common.h: audio_control_t mic interface (switch_capture_to, snapshot_capture_defaults, restore_capture_from, restore_default_render_if_virtual, init/release_mic_redirect_device) with safe non-Windows defaults.
  • platform/windows/audio.cpp: WASAPI default-capture switch + render-restore guard, merged with v1.17.0's reset_default_device_impl(bool, preferred) and jthread pending-restore.
  • New: platform/windows/mic_write.{cpp,h} (WASAPI render to the Steam mic endpoint) + vibepollo_vmic.{cpp,h}; wired into windows.cmake.

Testing

  • ✅ Compiles green on the Windows CI (unsigned installer artifact produced).
  • Runtime-verified end-to-end: built installer deployed on a real host (Windows 11, RTX 5070 Ti); a Steam-Deck Moonlight fork with client mic capture streams audio that arrives at Microphone (Steam Streaming Microphone) on the host and is usable in Discord.
  • Requires Steam running on the host (the virtual render endpoint is provided by Steam) and a client that sends mic.

Notes for review

  • platform/windows/PolicyConfig.h already exists in the base.
  • A small .github/workflows/mic-ci.yml (windows-only workflow_dispatch) was added as a dev build helper and can be dropped on merge.
  • The Steam-Deck client side (matching moonlight-qt mic fork on the newest base) is being prepared as a separate effort.

vladbogun1 and others added 7 commits June 28, 2026 23:56
…ring

Port of xenstalker02/Vibepollo Steam Streaming Microphone passthrough onto
newest Nonary/Vibepollo base (v1.17.0). This checkpoint adds the standalone
mic engine files and wires them into the Windows build. Shared-file hunks
(stream.cpp routing, platform/windows/audio.cpp WASAPI) still to port.

New files (copied from xen/master):
- src/platform/windows/mic_write.{cpp,h}
- src/platform/windows/vibepollo_vmic.{cpp,h}

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hand-merged xenstalker02/Vibepollo mic passthrough onto the newest Nonary
base (v1.17.0). Mic-only changes; rebranding/installer/playnite churn dropped.

Resolved via git apply --3way + manual conflict resolution:
- config.{h,cpp}: mic_sink / mic_capture_device / mic_buffer_ms / mic_buffer_packets
  (kept install_steam_drivers, unlike xen)
- stream.{h,cpp}: 0x3003 mic packet type, session_t::mic jitter-buffer state,
  init_mic_passthrough(), IDX_MIC_AUDIO_DATA handler via server->map(),
  get_mic_status(); kept v1.17.0 decode_control_packet/control_packet_view_t model
- platform/common.h: audio_control_t mic interface (switch_capture_to,
  snapshot_capture_defaults, restore_capture_from, restore_default_render_if_virtual,
  init/release_mic_redirect_device) with safe non-Windows defaults
- platform/windows/audio.cpp: WASAPI capture-switch + render-restore; region-4
  reconstructed to keep v1.17.0 jthread pending-restore + add xen render guard,
  adapted to 2-arg reset_default_device_impl
- new files: platform/windows/mic_write.{cpp,h} (WASAPI render to Steam Streaming
  Microphone) + vibepollo_vmic.{cpp,h}; wired into windows.cmake
- nvhttp/audio.cpp: kept v1.17.0 (HDR + safe parsing)

Web UI mic status card (confighttp) intentionally deferred — not needed for the
audio path. Builds unsigned via mic-ci.yml (workflow_dispatch, Windows only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
stream.cpp teardown used xen-only proc::is_placebo_app() and
system_tray::update_tray_idle() (absent in v1.17.0). Revert to v1.17.0's
update_tray_pausing/stopped. Non-mic churn pulled in by full-file 3way.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dio.cpp

xen mic code used unqualified to_utf8/from_utf8 (resolved via a using-decl in
xen's tree); v1.17.0 needs explicit utf_utils:: qualification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-Werror=cpp made the 'include winsock2.h before windows.h' warning fatal;
mmdeviceapi.h/Audioclient.h pull windows.h. Mirrors v1.17.0 audio.cpp order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@xenstalker02

Copy link
Copy Markdown

Client microphone passthrough (Steam Streaming Microphone) on current base

This ports the Steam Streaming Microphone client-mic passthrough from xenstalker02/Vibepollo (which sits ~1.5 minor versions behind) onto the current Vibepollo base (branched from v1.17.0). All credit for the original mic implementation and protocol goes to @xenstalker02; this PR is a clean-room rebase of just the mic feature onto the newest code, dropping the unrelated rebranding/installer/playnite churn.

What it does

Receives an Opus-encoded microphone stream from a compatible Moonlight client over the encrypted control stream (0x3003), decodes + jitter-buffers it, and renders it into the Steam Streaming Microphone virtual render endpoint so host apps (Discord, etc.) can read it as a normal mic input.

Changes (mic-only)

  • config.{h,cpp}: mic_sink, mic_capture_device, mic_buffer_ms, mic_buffer_packets (with sane defaults — no config edit required).
  • stream.{h,cpp}: 0x3003 packet type, session_t::mic jitter-buffer state, init_mic_passthrough() (lazy init on first packet), IDX_MIC_AUDIO_DATA handler via server->map(), get_mic_status(). Kept the v1.17.0 decode_control_packet/control_packet_view_t model.
  • platform/common.h: audio_control_t mic interface (switch_capture_to, snapshot_capture_defaults, restore_capture_from, restore_default_render_if_virtual, init/release_mic_redirect_device) with safe non-Windows defaults.
  • platform/windows/audio.cpp: WASAPI default-capture switch + render-restore guard, merged with v1.17.0's reset_default_device_impl(bool, preferred) and jthread pending-restore.
  • New: platform/windows/mic_write.{cpp,h} (WASAPI render to the Steam mic endpoint) + vibepollo_vmic.{cpp,h}; wired into windows.cmake.

Testing

  • ✅ Compiles green on the Windows CI (unsigned installer artifact produced).
  • Runtime-verified end-to-end: built installer deployed on a real host (Windows 11, RTX 5070 Ti); a Steam-Deck Moonlight fork with client mic capture streams audio that arrives at Microphone (Steam Streaming Microphone) on the host and is usable in Discord.
  • Requires Steam running on the host (the virtual render endpoint is provided by Steam) and a client that sends mic.

Notes for review

  • platform/windows/PolicyConfig.h already exists in the base.
  • A small .github/workflows/mic-ci.yml (windows-only workflow_dispatch) was added as a dev build helper and can be dropped on merge.
  • The Steam-Deck client side (matching moonlight-qt mic fork on the newest base) is being prepared as a separate effort.

Hey @vladbogun1 — this is awesome, thank you. And thanks for the really generous credit. Seeing the mic feature rebased cleanly onto v1.17, minus all my fork's rebranding/installer noise, is honestly exactly how I'd have wanted it upstreamed.

Two things that might save you time on the client side you mentioned — it already exists: xenstalker02/Vibelight, a moonlight-qt fork. It's the reference implementation for the 0x3003 protocol (Opus 48k mono, 20ms frames, 4-byte header, encrypted control session required), so it should map fairly directly onto a newer base.

And I'm glad to answer anything about the protocol or the design calls — the wire format and the downmix/jitter-buffer constants are documented in the repos, and I can point you at specific hardening fixes if any are relevant to the port.

Really appreciate you carrying this upstream. Glad it's useful to more people. 🙏

@vladbogun1

Copy link
Copy Markdown
Author

Thanks a lot for commenting, I just did it a day before I went on vacation with my steam deck and did a quick initial merging latest updates on vibepollo because I need a micro during vacation.

Now yeah, I also have plans to do same with client, and most needed for me is Android app, so I'll take a look on that first, thanks for useful links.

For 2 weeks on my PC this build works pretty stable btw.

@Akikazehub

Copy link
Copy Markdown

As a user, I’d love to see microphone support land here! Currently, mic support is scattered across various forks with fragmented implementations.

If this PR provides a clean, generic server protocol (or aligns with existing popular client implementations), I'm sure third-party clients will naturally adapt to it without putting any client-maintenance burden on this repo. Really hope this gets considered!

@000MDK

000MDK commented Jul 27, 2026

Copy link
Copy Markdown

@vladbogun1 how is compatibility with foundation-sunshine and punktfunk?
both projects already have the mic feature and thus have clients/servers with mic support for all kind of systems.
examples:

  • on linux hosts you might wanna use punktfunk server while using vibepollo on windows, but you don't want to maintain two different apps on your clientside, thus being compatible with punkfunk client would be something worth having
  • moonlight clients with foundation-sunshine feature support are available for android, macos/ios, windows, steamdeck/linux, steamlink and all kinds of platforms that punktfunk client doesn't support

being compatible with foundation-sunshine is likely more important since afair punktfunk mic support isn't in the best state rn

https://github.com/AlkaidLab/foundation-sunshine
https://git.unom.io/unom/punktfunk
https://github.com/qiin2333/moonlight-qt
https://github.com/qiin2333/moonlight-vplus
https://github.com/The-Fried-Fish/VoidLink-previously-moonlight-zwm

# Conflicts:
#	src/platform/common.h
#	src/platform/windows/audio.cpp
@vladbogun1

vladbogun1 commented Jul 29, 2026

Copy link
Copy Markdown
Author

Thanks @000MDK, @Akikazehub — fair points, worth being explicit about.

First, on what this PR is: it's not a new protocol. It's a faithful rebase of @xenstalker02's existing 0x3003 control-stream mic implementation (the one Vibelight already speaks) onto the current v1.17 base, minus the fork's unrelated churn. So it doesn't add a new flavor of fragmentation — it brings an already-deployed fork feature onto the current base. It's wire-compatible with Vibelight today, and I've been running it daily for ~2 weeks.

On compatibility with foundation-sunshine / punktfunk — honestly, no, they're different wire formats. From what I can see, foundation-sunshine + qiin2333's client use a moonlight-common-c-based mic extension (its "HQ Mic" degrades against non-Foundation servers), while this PR uses control-stream 0x3003 framing (Opus 48k mono, 20 ms, 4-byte header, encrypted control session required). They won't interoperate as-is.

On which protocol should win — I think that's genuinely a call for @Nonary and the project, not something I can settle by merging this. I'm a contributor who needed mic on my Steam Deck and rebased the cleanest existing implementation. A few honest tradeoffs for whoever makes that call:

  • foundation-sunshine: broadest existing client support (Android/iOS/Win/Deck/…) — a real advantage — but the protocol isn't openly documented, which makes an independent, verifiable server implementation harder.
  • 0x3003 (this PR): simple, documented in @xenstalker02's repos, verified end-to-end, self-contained on the host side — but its only client today is Vibelight.
  • ClassicOldSong / logabell line (LiSendMicrophoneOpusDataEx in common-c): puts the protocol in the shared library, which is the most client-portable of the three.

If the project wants to standardize on foundation-sunshine's protocol for the client breadth, that's a defensible direction — but it's a separate, larger effort (a host-side reimplementation against a less-documented spec), not a change to this PR. The value here is a working, minimal, verifiable mic path today that can be aligned to whatever direction @Nonary settles on.

@xenstalker02 — you designed 0x3003, so you'd be the best person to weigh in on convergence.

@xenstalker02

Copy link
Copy Markdown

Happy to weigh in, though my answer argues against my own protocol.

Don't standardize on 0x3003. If one mic protocol is going to win, it should live in moonlight-common-c — the logabell line. I have no stake in that: my fork and Vibelight already speak to each other and will keep working whichever way this goes.

The tradeoff list above has one thing slightly off, and that's my fault for never documenting it clearly: 0x3003 is not self-contained on the client side. It rides LiSendRawControlStreamPacket(), which does not exist in upstream moonlight-common-c. I added it in my fork (625a7d7, "add LiSendRawControlStreamPacket() public API") as a generic escape hatch for sending arbitrary control packets. So any client that wants to speak 0x3003 still has to carry a common-c patch. It pays the same portability cost as the common-c approach and gets none of the benefit of the transport living in the shared library.

Two more things that are fine for a fork feature and bad for a spec:

  • It squats in Apollo's namespace. 0x3000/0x3001/0x3002 are Apollo's — exec server command, clipboard, file-transfer nonce. I took the next free number. Nothing collides today (I just checked; ClassicOldSong/Apollo's packetTypes still stops at 0x3002), but that's luck, not coordination. A standard shouldn't rest on nobody else picking 3003.
  • 248-byte packet ceiling, for a reason that has nothing to do with audio. Riding the control stream bounds the payload by ENet's 252-byte tempBuffer; I cap at 248 and drop anything larger, because __memcpy_chk aborts past it. That's the only reason there's an oversized-packet guard in Vibelight — VBR peaks were killing the client outright.

Compare what logabell's fork actually exposes: enableMic negotiated in the stream config, STAGE_MICROPHONE_STREAM_INIT as a real connection stage, LiSendMicrophoneOpusDataEx(..., frameDurationSamples) so frame duration isn't hardcoded to my 20 ms, and LiIsMicrophoneStreamActive() / LiIsMicrophoneEncryptionEnabled() for capability checks. That's the shape a mic protocol should have. Mine has none of it — I wasn't designing a standard, I needed a mic on my Steam Deck.

What shouldn't get thrown away is @vladbogun1's PR. The wire format is the cheapest part of it. The host-side receive path is the expensive part, and it's transport-agnostic: the jitter buffer, lazy init on first packet, the WASAPI default-capture switch with its render-restore guard, the self-heal for when Windows has the Steam Streaming Microphone endpoint disabled (that one cost me a day and presented exactly like a protocol bug), and the mono downmix that has to average rather than clamped-sum or it clips. Re-point that at LiSendMicrophoneOpusData and most of the work survives the swap.

One piece of 0x3003 I do think is worth keeping, separately from mic: LiSendRawControlStreamPacket() is a generic escape hatch, not a mic API. Sunshine's 0x550x extensions and Apollo's 0x300x ones all need the same thing, and each currently gets it by forking common-c. Upstreaming the generic primitive would be useful whether or not anyone ever sends another mic packet with it.

The wire format and the failure modes are already written down in the Vibelight README and CHANGELOG — the packet layout, the 248-byte cap, the downmix, the endpoint self-heal. Happy to answer questions against those; whichever transport wins, that crash list is worth more than the protocol.

@000MDK

000MDK commented Jul 30, 2026

Copy link
Copy Markdown

@vladbogun1 @xenstalker02 thanks for the great responses, really appreciate it!

Optimum would be compatibility with all of them, detecting what moonlight provides with foundation compatibility being the most important one - from a more practical pov ofc.

I bring this up because of the denial of the other mic PR by logabell, reason given was "feature has no clients supporting it".
That's a bit of a hen/egg problem, right xD
Though, logabell provided moonlight builds for windows, that's why I think vibelight is not enough - and I really wanna see mic-support in vibepollo happen this year.

@Nonary: quick question about your actual requirements to accept this PR:
Is it enough when there are clients avilable for all platforms (in code and binary) or do you actually only consider this at all after it has been added to mainline moonlight?

anyways, big thanks to everyone involved, keep up the good work!

@Nonary

Nonary commented Jul 30, 2026

Copy link
Copy Markdown
Owner

I am not sure if I will do this yet, but I am considering forking all major moonlight clients. Android/iOS and PC

I think it might be time to do so soon because there is very few things I can improve for streaming with only the host being involved.

For example I just fixed VRR for streaming on the PC side

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.

5 participants