Skip to content

Add Windows microphone passthrough via Steam Streaming Microphone#1428

Open
logabell wants to merge 6 commits intoClassicOldSong:masterfrom
logabell:master
Open

Add Windows microphone passthrough via Steam Streaming Microphone#1428
logabell wants to merge 6 commits intoClassicOldSong:masterfrom
logabell:master

Conversation

@logabell
Copy link
Copy Markdown

@logabell logabell commented Mar 18, 2026

Summary

  • add Windows remote microphone passthrough for Apollo sessions
  • accept redirected microphone audio from a compatible Moonlight build and inject it into Steam Streaming Microphone on the host
  • add host-side troubleshooting UI for microphone validation and event visibility

Current requirement

How it works on Windows

  • Apollo negotiates redirected microphone support during session setup
  • incoming microphone Opus frames are received on the host, decrypted when negotiated, decoded, and rendered into Speakers (Steam Streaming Microphone)
  • host applications should read from Microphone (Steam Streaming Microphone)
  • Apollo normalizes only the Steam microphone pair to 2 channels / 32-bit / 48000 Hz when microphone streaming starts
  • if the client negotiates plaintext mic transport, Apollo disables microphone passthrough for that session
  • Apollo can install the local Steam audio drivers when Steam is present

Web UI and troubleshooting

  • adds microphone passthrough configuration in the Audio/Video tab
  • shortens the Windows backend/setup copy to match the rest of the settings page
  • moves microphone troubleshooting into the Troubleshooting page
  • includes a 5-stage validation ladder and integrates recent microphone events into the main troubleshooting log view

Validation

  • validated against this Apollo fork together with https://github.com/logabell/moonlight-qt-mic
  • confirmed end-to-end that microphone packets are received, decoded, rendered into Steam Streaming Microphone, and usable by host applications

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e556bb6d95

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2251 to +2253
if (running_mic_sessions.fetch_add(1, std::memory_order_acq_rel) == 0) {
if (audio::init_mic_redirect_device() != 0) {
running_mic_sessions.fetch_sub(1, std::memory_order_acq_rel);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prevent concurrent sessions from sharing one mic decoder

When two remote sessions enable microphone redirection at the same time, this branch only initializes one global redirect backend and lets every later session reuse it. That backend is held globally in src/audio.cpp and platf::audio::mic_write_wasapi_t owns a single stateful OpusDecoder, so packets from independent clients will be decoded through the same decoder/render path and corrupt each other’s audio. Either reject overlapping mic sessions or allocate backend/decoder state per session before leaving session.audio.enable_mic set for the second session.

Useful? React with 👍 / 👎.

Comment on lines +1350 to +1351
if (stream_session->audio.peer.address() == peer.address()) {
return stream_session;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match microphone packets on more than the source IP

find_mic_session() currently routes mic packets solely by peer.address(). If two RUNNING mic-enabled sessions come from the same public IP (for example, two clients behind one NAT, or a user starting a second stream before the first is torn down), the first matching session in _sessions will claim every packet and the other session never receives its own microphone audio. Unlike the video/audio paths, this code ignores the source port and any per-session token, so same-address sessions are not distinguishable here.

Useful? React with 👍 / 👎.

Comment on lines +282 to +285
if (command == L"uninstall") {
const int removed_count = remove_existing_devices();
std::wcout << L"Removed " << removed_count << L" Apollo virtual microphone device(s)\n";
return 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate uninstall errors from apollovmicctl

If remove_existing_devices() fails here (for example because the command is not elevated or SetupAPI enumeration fails), it returns 1, but this branch prints Removed 1 Apollo virtual microphone device(s) and exits with status 0 anyway. That makes installer scripts and users believe the driver was successfully removed when nothing was actually uninstalled.

Useful? React with 👍 / 👎.

@logabell logabell changed the title first working version working microphone passthrough on windows Mar 18, 2026
@logabell
Copy link
Copy Markdown
Author

This is a work in progress, but I currently have a working moonlight-qt fork with client side microphone support, and vb-cable integrated with the Apollo installer to automate the dependency install. The vb-cable licensing supports integration into third party apps like Apollo. https://vb-audio.com/Services/licensing.htm

After extensive research I found that this was the best path forward for implementing microphone support, as Windows audio drivers are kernel level, which require code signing. The steam Virtual Microphone driver was tested and failed, but there are known reports of this not actually working as an audio input driver.

My current Apollo fork where I am working on this can be found here. https://github.com/logabell/Apollo

This requires my Moonlight fork, which can be found here. https://github.com/logabell/moonlight-qt-mic

If there was an Artimis client for desktop, the required changes to support client-side Microphone passthrough could be easily implemented.

All feedback welcome, happy to work on this if anyone wants to test. Not sure if this is something the devs would entertain supporting as it does require an outside dependency (vb-cable), but in it's current state, that package installation is automated within the Apollo installer for Windows.

I would need to add support for MacOS and Linux but can be done.

Thanks guys, cheers

@ClassicOldSong
Copy link
Copy Markdown
Owner

FYI, Steam itself provides "Stream Streaming Microphone" which can be used for microphone pass through, so we don't need to install vb cable separately here.

Also, the microphone PR to Sunshine was blocked by not implementing data encryption. It's also a requirement here to merge the feature.

@logabell
Copy link
Copy Markdown
Author

I can take another look at trying to get Steam Streaming Microphone to work, that would be the preferred method, I agree. I was running into issues.

Encryption components are staged for Apollo, as it requests mic encryption (rtsp.cpp line 797), and is staged to refuse unencrypted microphone. However, on the Moonlight client side, the mic stream could be falling back to plaintext because Apollo only decrypts when the bit is set and otherwise treats the payload as raw Opus. Thanks for pointing this out.

I'll tighten this up and try to get the Steam microphone working over vb-cable. This PR needs some fine tuning in its current state, but I at least wanted to open it as proof of concept to get your feedback.

What is the current state of the desktop release of Artimis? Rather than relying on a Moonlight PR which likely wont get any feedback, I'd rather get this working in Artimis.

Thank you!

@xenstalker02
Copy link
Copy Markdown

xenstalker02 commented Mar 18, 2026

I can take another look at trying to get Steam Streaming Microphone to work, that would be the preferred method, I agree. I was running into issues.

Encryption components are staged for Apollo, as it requests mic encryption (rtsp.cpp line 797), and is staged to refuse unencrypted microphone. However, on the Moonlight client side, the mic stream could be falling back to plaintext because Apollo only decrypts when the bit is set and otherwise treats the payload as raw Opus. Thanks for pointing this out.

I'll tighten this up and try to get the Steam microphone working over vb-cable. This PR needs some fine tuning in its current state, but I at least wanted to open it as proof of concept to get your feedback.

What is the current state of the desktop release of Artimis? Rather than relying on a Moonlight PR which likely wont get any feedback, I'd rather get this working in Artimis.

Thank you!

Funny timing. I’ve been working on the same idea in parallel and I’m close to finishing a VB-Cable based solution similar to yours.

I ended up vibe-coding a pair of forks, one for Vibepollo and one for Moonlight, with microphone passthrough wired through VB-Cable:

Curious how they compare in terms of latency and stability once both are fully tested.

@logabell
Copy link
Copy Markdown
Author

I'm wrapping up steam streaming microphone implementation now. It's working really well, encrypted, and I have been doing tests in discord calls and receiving good feedback from people on the other end.

I got rid of vb-cable all together in my latest release on my fork. Going to clean up a few different things and resubmit the PRs soon.

@logabell logabell changed the title working microphone passthrough on windows Add Windows microphone passthrough via Steam Streaming Microphone Mar 19, 2026
@logabell
Copy link
Copy Markdown
Author

@ClassicOldSong I updated this PR description to reflect the current Steam Streaming Microphone implementation, the troubleshooting UI changes, and the temporary requirement to use the matching Moonlight fork on the client side: https://github.com/logabell/moonlight-qt-mic

@xenstalker02
Copy link
Copy Markdown

@ClassicOldSong I updated this PR description to reflect the current Steam Streaming Microphone implementation, the troubleshooting UI changes, and the temporary requirement to use the matching Moonlight fork on the client side: https://github.com/logabell/moonlight-qt-mic

404 when accessing it btw

@logabell
Copy link
Copy Markdown
Author

@ClassicOldSong I updated this PR description to reflect the current Steam Streaming Microphone implementation, the troubleshooting UI changes, and the temporary requirement to use the matching Moonlight fork on the client side: https://github.com/logabell/moonlight-qt-mic

404 when accessing it btw

fixed.

@logabell
Copy link
Copy Markdown
Author

Companion PR has been submitted to Artemis ClassicOldSong/moonlight-android#537

@xenstalker02
Copy link
Copy Markdown

Following up on my earlier comment — I wanted to add more detail since I spent significant time on the Steam Streaming Microphone path and hit a wall that seems worth documenting.

Worth saying upfront: I'm not a programmer and this entire project was vibe-coded with AI assistance, so I may well be missing something here. But I think the observations are worth sharing.

What I tried

I started with a working VB-Cable implementation, then on March 21 spent an entire day attempting to switch to Steam Streaming Mic as the primary backend with VB-Cable as a fallback. Across ~17 commits I tried every variation I could think of, including things I pulled directly from @logabell's code:

  • Simple device substitution — find Speakers (Steam Streaming Microphone) in the WASAPI device list, pass to existing render client
  • GetMixFormat() fallback after IsFormatSupported() rejection
  • Fixing a device matching bug where the substring "Steam Streaming Microphone" matched both the render and capture endpoints, causing WASAPI writes to go to the wrong device
  • Fixing a routing bug in stream.cpp where mic_sink (the VB-Cable name) was still being passed to virtual_microphone() instead of the Steam device name
  • A full port of @logabell's mic_write.cpp including SetDeviceFormat via IPolicyConfig before Initialize(), the event-driven render loop, the playout prebuffer, and the exact init order

Every attempt produced the same result: loud full-volume white noise in the Windows audio test and garbled or silent audio in Discord. I reverted to VB-Cable and it worked immediately. At the time I didn't understand why it kept failing even when I was essentially copying @logabell's implementation.

What I found today (though I could be wrong about this)

I tested something directly today: with Steam running but no active Remote Play session, I played the Windows audio test on Speakers (Steam Streaming Microphone) with my output set to headphones. Result: 0% volume, no audio at all. The endpoint exists in Windows and WASAPI accepts writes to it, but nothing comes out.

My best guess at why — and this is where I'd welcome correction from people who know Windows audio better — is that Steam Streaming Mic/Speakers may be a bridge device pair rather than a loopback device. VB-Cable has a permanent hardware loopback baked into the driver: whatever you write to CABLE Input always comes out CABLE Output regardless of any session state. The Steam devices seem to behave differently — the render→capture routing may only be active when Steam's Remote Play service is bridging a session. Without that active session the render side accepts writes but the capture side receives silence.

If that's right, it would explain why @logabell's implementation works — in an Apollo/Steam-adjacent context Steam's bridging service would be active during sessions — but fails in a Vibepollo/Moonlight context where Steam has no involvement in the session at all.

It also matches @logabell's own original finding in this thread: "The steam Virtual Microphone driver was tested and failed, but there are known reports of this not actually working as an audio input driver."

VB-Cable has been working reliably and stably in my own testing so far. Still need to do a proper end-to-end test, but it's been solid in preliminary runs.

Tagging @Nonary since this may be relevant to his Vibepollo fork and his open PR for mic passthrough — the distinction between bridge and loopback devices would affect any Moonlight-based implementation targeting Steam Streaming Mic.

@ClassicOldSong
Copy link
Copy Markdown
Owner

ClassicOldSong commented Mar 26, 2026

Steam streaming microphone is still exactly a loop back, no hardware is involved. VB Cable is also pure software loopback.

What you encountered is sample rate/bit depth/channel configuration mismatch. Steam Streaming Microphone doesn't do resampling so different bit depth can directly cause garbled output. VB cable may have done resampling so it can work across different input rates and output rates at the cost of added cpu usage and increased latency.

That's why you still need to have knowledge in programming to use AI. Pure vibe coding won't cover parts you overlooked and not have understanding about.

@ClassicOldSong
Copy link
Copy Markdown
Owner

I suggest we keep one PR through Apollo and Vibepollo for microphone pass through so it won't be fragmented.

@xenstalker02
Copy link
Copy Markdown

I suggest we keep one PR through Apollo and Vibepollo for microphone pass through so it won't be fragmented.

Thanks for the correction.

Just to give some context on where I'm coming from — I built this purely for personal use because I wanted mic passthrough for my own Steam Deck setup. I figured sharing it here might give someone with more actual knowledge a useful starting point to build something cleaner. That's really all it was meant to be.
The consolidation idea sounds good to me. Happy to contribute anything useful from what I built. I'll leave it to you and @logabell to figure out what that looks like 👍🏻

@logabell
Copy link
Copy Markdown
Author

Yes, @ClassicOldSong is correct. The issue you faced was the audio formatting. The Apollo PR automatically sets the steam streaming microphone to 32 bit when a session from moonlight is started. The PR is specifically intended to not interfere or touch the steam streaming speaker device.

Also, just a heads up, my child was just born the other day so I probably won't have the time to work on this PR for a little while.

@ClassicOldSong I saw your comments the other day, thank you for the static, I appreciate the feedback. I'll get to it when I can, unless anyone wants to clean it up.

Also, just want to make sure you saw the pairing Artemis PR that goes along with this one.

Cheers

@Nonary
Copy link
Copy Markdown
Contributor

Nonary commented Mar 27, 2026

Yeah,

I suggest we keep one PR through Apollo and Vibepollo for microphone pass through so it won't be fragmented.

Thanks for the correction.

Just to give some context on where I'm coming from — I built this purely for personal use because I wanted mic passthrough for my own Steam Deck setup. I figured sharing it here might give someone with more actual knowledge a useful starting point to build something cleaner. That's really all it was meant to be. The consolidation idea sounds good to me. Happy to contribute anything useful from what I built. I'll leave it to you and @logabell to figure out what that looks like 👍🏻

Yeah, programmers generally already know that.

When someone vibe-codes something, it is usually because they wanted that feature for themselves first. Then, once they finally get it working, they often open a PR to help out. But it is a bit more nuanced than that.

It was especially bad during the first wave of AI, with models like Sonnet 4. People were so hooked on vibe coding that a lot of low-quality stuff got pushed everywhere. The AI is much smarter now. It still has problems, but at least it is not completely incoherent anymore.

Anyway, just adding my thoughts here. I also agree with @ClassicOldSong. If I were going to add virtual microphone support, I would rather depend on Steam audio drivers, since we are already using that, instead of VB-Cable. It seems like it should be a pretty simple thing to do.

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