[GH-ISSUE #460] Multi-user Wine/Proton audio streams jump sinks on 2nd client connection (winepulse.drv default sink routing) #282

Open
opened 2026-10-15 12:59:18 +00:00 by bsakaguchi · 3 comments
Owner

Originally created by @dustinblack on GitHub (Jul 21, 2026).
Original GitHub issue: https://github.com/games-on-whales/wolf/issues/460

Problem Description

In multi-user Wolf setups running simultaneous Moonlight client sessions across distinct user profiles, launching a second client session causes active Wine/Proton game audio from the first client to immediately stop playing on Client 1 and jump over to Client 2's audio output.

Steps to Reproduce

  1. Connect Moonlight Client 1 under Profile A (mom) and launch a Proton/Wine game (e.g., Meccha Chameleon). Confirm game audio is playing.
  2. Connect Moonlight Client 2 under Profile B (dad).
  3. Observe that Client 1's game audio immediately stops on Client 1's speakers and briefly plays out of Client 2's speakers.

Root Cause Analysis

  1. PulseAudio Default Sink Switch: When Client 2 connects, Wolf creates virtual_sink_client2 and executes pactl set-default-sink virtual_sink_client2.
  2. Wine Pulse Driver Default Device: By default, Proton/Wine's winepulse.drv connects audio playback streams without specifying a target device (pa_stream_connect_playback(stream, dev=NULL)).
  3. Because dev=NULL, PulseAudio attaches the stream to the global Default Sink. When Wolf updates the default sink for Client 2, PulseAudio automatically shifts Client 1's active Wine audio stream over to Client 2's new sink.

Verified Fix & Resolution

To isolate audio streams in multi-tenant environments, Wine's winepulse.drv must be bound directly to the profile's virtual sink at stream initialization, rather than allowing dev=NULL fallback to the global default sink.

1. Wine Registry Device Lock

Adding the following key to the Wine prefix registry (user.reg):

[Software\Wine\PulseAudio]
"Device"="virtual_sink_<profile_id>"

Forces winepulse.drv to pass virtual_sink_<profile_id> directly into pa_stream_connect_playback().

2. Result

Once set, PulseAudio pactl set-default-sink calls for newly connected clients are completely ignored by active Wine streams, guaranteeing 100% audio stream isolation across concurrent multi-user sessions.


Suggested Upstream Enhancements

  • Profile-level winepulse.drv registry injection: Automate writing HKCU\Software\Wine\PulseAudio\Device during container prefix initialization for Wine/Proton app runners.
  • Environment propagation: Ensure PULSE_SINK environment variables are inherited cleanly down through sub-shells launched by Steam/Proton wrappers.
Originally created by @dustinblack on GitHub (Jul 21, 2026). Original GitHub issue: https://github.com/games-on-whales/wolf/issues/460 ### Problem Description In multi-user Wolf setups running simultaneous Moonlight client sessions across distinct user profiles, launching a second client session causes active Wine/Proton game audio from the first client to immediately stop playing on Client 1 and jump over to Client 2's audio output. ### Steps to Reproduce 1. Connect Moonlight Client 1 under Profile A (`mom`) and launch a Proton/Wine game (*e.g., Meccha Chameleon*). Confirm game audio is playing. 2. Connect Moonlight Client 2 under Profile B (`dad`). 3. Observe that Client 1's game audio immediately stops on Client 1's speakers and briefly plays out of Client 2's speakers. ### Root Cause Analysis 1. **PulseAudio Default Sink Switch**: When Client 2 connects, Wolf creates `virtual_sink_client2` and executes `pactl set-default-sink virtual_sink_client2`. 2. **Wine Pulse Driver Default Device**: By default, Proton/Wine's `winepulse.drv` connects audio playback streams without specifying a target device (`pa_stream_connect_playback(stream, dev=NULL)`). 3. Because `dev=NULL`, PulseAudio attaches the stream to the global **Default Sink**. When Wolf updates the default sink for Client 2, PulseAudio automatically shifts Client 1's active Wine audio stream over to Client 2's new sink. ### Verified Fix & Resolution To isolate audio streams in multi-tenant environments, Wine's `winepulse.drv` must be bound directly to the profile's virtual sink at stream initialization, rather than allowing `dev=NULL` fallback to the global default sink. #### 1. Wine Registry Device Lock Adding the following key to the Wine prefix registry (`user.reg`): ```ini [Software\Wine\PulseAudio] "Device"="virtual_sink_<profile_id>" ``` Forces `winepulse.drv` to pass `virtual_sink_<profile_id>` directly into `pa_stream_connect_playback()`. #### 2. Result Once set, PulseAudio `pactl set-default-sink` calls for newly connected clients are completely ignored by active Wine streams, guaranteeing 100% audio stream isolation across concurrent multi-user sessions. --- ### Suggested Upstream Enhancements * **Profile-level `winepulse.drv` registry injection**: Automate writing `HKCU\Software\Wine\PulseAudio\Device` during container prefix initialization for Wine/Proton app runners. * **Environment propagation**: Ensure `PULSE_SINK` environment variables are inherited cleanly down through sub-shells launched by Steam/Proton wrappers.
Author
Owner

@ABeltramo commented on GitHub (Jul 21, 2026):

Audio overlaps between sessions have been addressed already in https://github.com/games-on-whales/wolf/pull/347
Can you share more details on exactly how this is failing for you? Which GOW app image are you running? Can you replicate it in Firefox for example? How can we reproduce this?

<!-- gh-comment-id:5032312948 --> @ABeltramo commented on GitHub (Jul 21, 2026): Audio overlaps between sessions have been addressed already in https://github.com/games-on-whales/wolf/pull/347 Can you share more details on exactly how this is failing for you? Which GOW app image are you running? Can you replicate it in Firefox for example? How can we reproduce this?
Author
Owner

@dustinblack commented on GitHub (Jul 21, 2026):

Hi @ABeltramo,

Thanks for pointing to PR #347 ([PULSE_ROUTER]). After further testing across different container images, we can confirm why PR #347 works for native Linux apps (like Firefox) but fails specifically for Wine/Proton games.

Here are the details and reproduction steps:

1. Image & App Difference

  • Firefox (ghcr.io/games-on-whales/firefox:edge): Does NOT reproduce. Native Linux applications respect `` or anchor their playback streams to specific sinks. PR #347 routes them cleanly and they stay isolated.
  • Steam / Proton (ghcr.io/games-on-whales/steam:edge): Reproduces consistently with Windows games running under Proton/Wine (e.g., Meccha Chameleon).

2. Root Cause & Why PR #347 Is Bypassed by Wine

  1. winepulse.drv Stream Connection (dev=NULL): Proton's audio driver (winepulse.drv) ignores `` and connects playback streams without a target device name (pa_stream_connect_playback(stream, dev=NULL)).
  2. PulseAudio Default Sink Binding: Because dev=NULL is passed, PulseAudio dynamically binds the active game audio stream to PulseAudio's Global Default Sink.
  3. Default Sink Reassignment on Client 2 Connection: When Client 2 connects, Wolf runs pactl set-default-sink virtual_sink_client2.
  4. The Stream Jump: PulseAudio's core daemon automatically reassigns all active unanchored streams (dev=NULL) across the daemon to the newly assigned default sink. Even though PR #347 moves the stream initially, PulseAudio's global default sink handler forcibly shifts Client 1's active Proton stream over to Client 2's sink the instant Client 2 connects.

3. How to Reproduce

  1. Start a Moonlight session on Client 1 (ghcr.io/games-on-whales/steam:edge) and launch any Proton game (e.g., Meccha Chameleon). Confirm game audio plays on Client 1.
  2. Connect a second Moonlight client (Client 2).
  3. As soon as Client 2 connects and triggers set-default-sink, Client 1's game audio immediately stops on Client 1 and plays out of Client 2.

4. Tested Solution / Workaround

Setting the target device explicitly in Wine's registry prefix (user.reg):

[Software\Wine\PulseAudio]
"Device"="virtual_sink_<profile_id>"

This forces winepulse.drv to pass dev="virtual_sink_<profile_id>" instead of dev=NULL during pa_stream_connect_playback(). Once set, winepulse.drv streams become anchored to that sink and completely ignore global pactl set-default-sink changes.

Possible upstream fixes could include injecting HKCU\Software\Wine\PulseAudio\Device during Wine container prefix initialization, or having [PULSE_ROUTER] re-verify/re-anchor dev=NULL streams when default sink change events fire.

<!-- gh-comment-id:5039155814 --> @dustinblack commented on GitHub (Jul 21, 2026): Hi @ABeltramo, Thanks for pointing to PR #347 (`[PULSE_ROUTER]`). After further testing across different container images, we can confirm why PR #347 works for native Linux apps (like Firefox) but **fails specifically for Wine/Proton games**. Here are the details and reproduction steps: #### 1. Image & App Difference * **Firefox (`ghcr.io/games-on-whales/firefox:edge`)**: **Does NOT reproduce**. Native Linux applications respect `` or anchor their playback streams to specific sinks. PR #347 routes them cleanly and they stay isolated. * **Steam / Proton (`ghcr.io/games-on-whales/steam:edge`)**: **Reproduces consistently** with Windows games running under Proton/Wine (e.g., *Meccha Chameleon*). #### 2. Root Cause & Why PR #347 Is Bypassed by Wine 1. **`winepulse.drv` Stream Connection (`dev=NULL`)**: Proton's audio driver (`winepulse.drv`) ignores `` and connects playback streams without a target device name (`pa_stream_connect_playback(stream, dev=NULL)`). 2. **PulseAudio Default Sink Binding**: Because `dev=NULL` is passed, PulseAudio dynamically binds the active game audio stream to PulseAudio's **Global Default Sink**. 3. **Default Sink Reassignment on Client 2 Connection**: When Client 2 connects, Wolf runs `pactl set-default-sink virtual_sink_client2`. 4. **The Stream Jump**: PulseAudio's core daemon automatically reassigns *all active unanchored streams (`dev=NULL`)* across the daemon to the newly assigned default sink. Even though PR #347 moves the stream initially, PulseAudio's global default sink handler forcibly shifts Client 1's active Proton stream over to Client 2's sink the instant Client 2 connects. #### 3. How to Reproduce 1. Start a Moonlight session on Client 1 (`ghcr.io/games-on-whales/steam:edge`) and launch any Proton game (e.g., *Meccha Chameleon*). Confirm game audio plays on Client 1. 2. Connect a second Moonlight client (Client 2). 3. As soon as Client 2 connects and triggers `set-default-sink`, Client 1's game audio immediately stops on Client 1 and plays out of Client 2. #### 4. Tested Solution / Workaround Setting the target device explicitly in Wine's registry prefix (`user.reg`): ```ini [Software\Wine\PulseAudio] "Device"="virtual_sink_<profile_id>" ``` This forces `winepulse.drv` to pass `dev="virtual_sink_<profile_id>"` instead of `dev=NULL` during `pa_stream_connect_playback()`. Once set, `winepulse.drv` streams become anchored to that sink and completely ignore global `pactl set-default-sink` changes. Possible upstream fixes could include injecting `HKCU\Software\Wine\PulseAudio\Device` during Wine container prefix initialization, or having `[PULSE_ROUTER]` re-verify/re-anchor `dev=NULL` streams when default sink change events fire.
Author
Owner

@To0wnn commented on GitHub (Oct 5, 2026):

Confirming the registry workaround works for us (Wolf stable 2026-09-29, Proton-GE, 3 players). We set it automatically on every game start in our launcher: HKCU\Software\Wine\PulseAudio "Device" = the container's $PULSE_SINK (Wolf already passes the session sink in that variable), written into the player's own Wine prefix. The Wine stream then stays on that session's sink when other clients connect. Doing this in the GoW Steam/Lutris images would fix it for everyone.

<!-- gh-comment-id:6000487303 --> @To0wnn commented on GitHub (Oct 5, 2026): Confirming the registry workaround works for us (Wolf stable 2026-09-29, Proton-GE, 3 players). We set it automatically on every game start in our launcher: `HKCU\Software\Wine\PulseAudio "Device"` = the container's `$PULSE_SINK` (Wolf already passes the session sink in that variable), written into the player's own Wine prefix. The Wine stream then stays on that session's sink when other clients connect. Doing this in the GoW Steam/Lutris images would fix it for everyone.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
games-on-whales/wolf#282
No description provided.