Skip to content

FIX(windows client): Fix audio dropout on RDP session start/end#7134

Open
MatthewFagan wants to merge 4 commits intomumble-voip:masterfrom
MatthewFagan:fix-windows-audio-dropouts
Open

FIX(windows client): Fix audio dropout on RDP session start/end#7134
MatthewFagan wants to merge 4 commits intomumble-voip:masterfrom
MatthewFagan:fix-windows-audio-dropouts

Conversation

@MatthewFagan
Copy link
Copy Markdown

@MatthewFagan MatthewFagan commented Mar 20, 2026

FIX(windows client): Fix audio dropout on RDP session start/end

WASAPI notifications were not being handled correctly, resulting in complete audio dropout when the system audio config was changed, such as during RDP session start/stop or when starting/changing audio software such as a DAW.

WASAPINotification: The original code only triggered an audio restart if a notification was for a device currently in-use. However, during RDP session start, all audio devices are removed from the system, and then re-added. Once they are all removed, they are no longer in-use, and so the notification was not triggering when the devices got re-added. I changed it so that WASAPIInput and WASAPIOutput registered "wanted" devices if they cannot open the desired device (and also wantDefault if opening the default device fails) with WASAPINotification. When device added event occurs, it can see that we want to use that device and restart the audio system which picks that up.

WASAPIInput/WASAPIOutput: A tightly related issue was the AUDCLNT_E_DEVICE_INVALIDATED error. This occurs not when a device is removed then added, but when the configuration of the device changes (such as bit rate). It previously just caused the audio thread to exit, which stopped the audio until it was manually restarted. So I changed it to add a retry loop for only when this error occurs. (See https://learn.microsoft.com/en-us/windows/win32/coreaudio/recovering-from-an-invalid-device-error for an explanation)

Talking indicator: The talking indicator also continued to show talking/shouting if a mic was disconnected while the user was talking/shouting. This has now been fixed to clear the indicator if no mic can be opened. The indicator gets updated again on the first frame captured once a mic is re-connected.

Fixes #4992
Potentially fixes #5542

Checks

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 20, 2026

Walkthrough

This change introduces a WASAPIDevice RAII wrapper to replace raw pointer ownership patterns in WASAPI device handling. Device lifetime is managed through constructors, destructors, and move operations with proper cleanup semantics. The implementation adds an outer retry loop to both input and output processing that detects AUDCLNT_E_DEVICE_INVALIDATED errors and performs full teardown and re-initialization. Additionally, the notification client API is refactored to track "wanted" devices separately from "used" devices, using QString-based identifiers instead of LPCWSTR and introducing a counter for default device preferences to improve device invalidation recovery.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement device-wanted tracking in WASAPINotification and add retry logic for AUDCLNT_E_DEVICE_INVALIDATED to maintain audio functionality during device reconfigurations, directly addressing the sporadic audio dropout issue #4992.
Out of Scope Changes check ✅ Passed All changes are focused on fixing audio dropout: introducing WASAPIDevice RAII wrapper, device-wanted tracking, retry loops for invalid device errors, and refactored notification handlers—all directly related to the linked issue.
Title check ✅ Passed The title accurately describes the main change: fixing audio dropout on RDP session start/end by reworking WASAPI device and notification handling to detect and recover from device invalidation and removal events.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering problem statement, root cause analysis, solution approach, and related issues.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

Migrating from UI to YAML configuration.

Use the @coderabbitai configuration command in a PR comment to get a dump of all your UI settings in YAML format. You can then edit this YAML file and upload it to the root of your repository to configure CodeRabbit programmatically.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/mumble/WASAPINotificationClient.cpp (1)

46-55: ⚠️ Potential issue | 🔴 Critical

Data race: Missing mutex lock when accessing wantedDevices.

OnDeviceAdded accesses wantedDevices.contains(device) without acquiring listsMutex. This is a data race since wantedDevices can be modified by the audio thread via enlistDeviceAsWanted/unlistDeviceAsWanted while the COM notification thread calls OnDeviceAdded. Compare with OnPropertyValueChanged (line 36-37) which correctly acquires the lock.

Proposed fix
 HRESULT STDMETHODCALLTYPE WASAPINotificationClient::OnDeviceAdded(LPCWSTR pwstrDeviceId) {
 	const QString device = QString::fromWCharArray(pwstrDeviceId);
 	qDebug() << "WASAPINotificationClient: Device added=" << device;

+	QMutexLocker lock(&listsMutex);
 	if (wantedDevices.contains(device)) {
 		restartAudio();
 	}

 	return S_OK;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mumble/WASAPINotificationClient.cpp` around lines 46 - 55, OnDeviceAdded
is reading wantedDevices without holding listsMutex, causing a data race; wrap
the access to wantedDevices (the contains check) inside the same lock used
elsewhere (listsMutex) like in OnPropertyValueChanged so that the COM
notification thread acquires listsMutex before checking wantedDevices and then
releases it before calling restartAudio if needed, ensuring thread-safe access
in WASAPINotificationClient::OnDeviceAdded.
src/mumble/WASAPI.cpp (1)

1010-1012: ⚠️ Potential issue | 🟡 Minor

Reset ns, lastspoke, and mixed on retry.

The variables ns, lastspoke, and mixed are declared before the retry loop but used within it. On retry after AUDCLNT_E_DEVICE_INVALIDATED, these variables retain their previous values, which could cause incorrect behavior:

  • ns is used to count channel masks and would accumulate across retries
  • lastspoke affects volume attenuation state
  • mixed affects audio mixing state
Proposed fix
 	bool doOuterRetry = true;
 	while (doOuterRetry) {
 		hr = 0;
+		ns = 0;
+		lastspoke = false;
+		mixed = false;

 		hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);

Also applies to: 1025-1027

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mumble/WASAPI.cpp` around lines 1010 - 1012, The variables ns, lastspoke,
and mixed are declared outside the retry loop but used inside it, so on an
AUDCLNT_E_DEVICE_INVALIDATED retry they retain stale state; move the
declarations or reset them at the top of the retry loop (before the logic that
counts channel masks and handles attenuation/mixing) so ns is zeroed, lastspoke
is set to false, and mixed is set to its default value each attempt; update
references to ns, lastspoke, and mixed in the functions handling channel mask
counting and volume/mixing to rely on these freshly initialized values.
🧹 Nitpick comments (2)
src/mumble/WASAPI.h (1)

41-66: Consider explicitly deleting copy constructor and copy assignment.

The WASAPIDevice struct implements move semantics but doesn't explicitly delete the copy constructor and copy assignment operator. While the compiler may implicitly delete them because a move constructor is declared, it's better practice to be explicit for clarity and to prevent accidental copies.

Proposed fix
 struct WASAPIDevice {
 public:
 	WASAPIDevice();
 	WASAPIDevice(nullptr_t);
 	WASAPIDevice(WASAPIDevice&& other);
+	WASAPIDevice(const WASAPIDevice&) = delete;
 	~WASAPIDevice();
 	void ClearDevice();
 	void ClearUsage();
 
 	operator bool() const;
 	operator IMMDevice*() const;
 	IMMDevice* operator->() const;
 	WASAPIDevice& operator=(WASAPIDevice&& other);
+	WASAPIDevice& operator=(const WASAPIDevice&) = delete;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mumble/WASAPI.h` around lines 41 - 66, The struct WASAPIDevice currently
declares move ctor/assign but not the copy operations; explicitly delete the
copy constructor and copy copy-assignment by adding declarations
WASAPIDevice(const WASAPIDevice&) = delete; and WASAPIDevice& operator=(const
WASAPIDevice&) = delete; inside the WASAPIDevice struct (near the other
special-member declarations like WASAPIDevice(WASAPIDevice&&) and WASAPIDevice&
operator=(WASAPIDevice&&)) so accidental copying is prevented and intent is
clear.
src/mumble/WASAPI.cpp (1)

500-528: Consider re-evaluating doecho on retry.

The doecho flag is set once at line 500 before the retry loop. If the echo device fails to open (line 526-527 sets doecho = false), subsequent retry iterations won't attempt to open the echo device again, even if it becomes available. If the goal is full recovery on device invalidation, consider re-evaluating doecho at the start of each retry iteration.

Proposed fix
 	// Outer retry loop - For handling AUDCLNT_E_DEVICE_INVALIDATED which requires complete cleanup and then re-creation of audio endpoint.
 	bool doOuterRetry = true;
 	while (doOuterRetry) {
 		hr = 0;
+		doecho = Global::get().s.doEcho();

 		hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mumble/WASAPI.cpp` around lines 500 - 528, doecho is initialized once
from Global::get().s.doEcho() before the outer retry loop, so if
pEchoDevice.OpenNamedOrDefaultDevice fails and sets doecho = false, subsequent
retries will never attempt to reopen the echo device even if it becomes
available; move or re-evaluate the initial assignment so that doecho is (re)read
from Global::get().s.doEcho() at the start of each outer retry iteration (or
reset doecho before calling pEchoDevice.OpenNamedOrDefaultDevice inside the
loop), ensuring pEchoDevice.OpenNamedOrDefaultDevice is retried on each
doOuterRetry cycle.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/mumble/WASAPI.cpp`:
- Around line 383-387: The move assignment operator
WASAPIDevice::operator=(WASAPIDevice&& other) is missing a return statement;
update the function to return *this at the end after calling ClearDevice(),
ClearUsage() and Move(std::move(other)) so it correctly returns a reference to
the assigned object and avoids undefined behavior in chained assignments.

In `@src/mumble/WASAPINotificationClient.cpp`:
- Around line 57-64: OnDeviceRemoved reads usedDefaultDevices and usedDevices
without holding listsMutex; lock listsMutex (e.g., QMutexLocker or
std::lock_guard on listsMutex) before checking contains(), set a local flag
(e.g., bool doRestart) if either contains() is true, then release the lock and
call restartAudio() only if doRestart is true to avoid holding the mutex while
restarting; update WASAPINotificationClient::OnDeviceRemoved accordingly using
the listsMutex, usedDefaultDevices, usedDevices, and restartAudio symbols.

---

Outside diff comments:
In `@src/mumble/WASAPI.cpp`:
- Around line 1010-1012: The variables ns, lastspoke, and mixed are declared
outside the retry loop but used inside it, so on an AUDCLNT_E_DEVICE_INVALIDATED
retry they retain stale state; move the declarations or reset them at the top of
the retry loop (before the logic that counts channel masks and handles
attenuation/mixing) so ns is zeroed, lastspoke is set to false, and mixed is set
to its default value each attempt; update references to ns, lastspoke, and mixed
in the functions handling channel mask counting and volume/mixing to rely on
these freshly initialized values.

In `@src/mumble/WASAPINotificationClient.cpp`:
- Around line 46-55: OnDeviceAdded is reading wantedDevices without holding
listsMutex, causing a data race; wrap the access to wantedDevices (the contains
check) inside the same lock used elsewhere (listsMutex) like in
OnPropertyValueChanged so that the COM notification thread acquires listsMutex
before checking wantedDevices and then releases it before calling restartAudio
if needed, ensuring thread-safe access in
WASAPINotificationClient::OnDeviceAdded.

---

Nitpick comments:
In `@src/mumble/WASAPI.cpp`:
- Around line 500-528: doecho is initialized once from Global::get().s.doEcho()
before the outer retry loop, so if pEchoDevice.OpenNamedOrDefaultDevice fails
and sets doecho = false, subsequent retries will never attempt to reopen the
echo device even if it becomes available; move or re-evaluate the initial
assignment so that doecho is (re)read from Global::get().s.doEcho() at the start
of each outer retry iteration (or reset doecho before calling
pEchoDevice.OpenNamedOrDefaultDevice inside the loop), ensuring
pEchoDevice.OpenNamedOrDefaultDevice is retried on each doOuterRetry cycle.

In `@src/mumble/WASAPI.h`:
- Around line 41-66: The struct WASAPIDevice currently declares move ctor/assign
but not the copy operations; explicitly delete the copy constructor and copy
copy-assignment by adding declarations WASAPIDevice(const WASAPIDevice&) =
delete; and WASAPIDevice& operator=(const WASAPIDevice&) = delete; inside the
WASAPIDevice struct (near the other special-member declarations like
WASAPIDevice(WASAPIDevice&&) and WASAPIDevice& operator=(WASAPIDevice&&)) so
accidental copying is prevented and intent is clear.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ed96d898-57b2-406e-9730-3dc9ae5f8b0c

📥 Commits

Reviewing files that changed from the base of the PR and between 4dc9f7b and 2cd6e99.

📒 Files selected for processing (4)
  • src/mumble/WASAPI.cpp
  • src/mumble/WASAPI.h
  • src/mumble/WASAPINotificationClient.cpp
  • src/mumble/WASAPINotificationClient.h

Comment thread src/mumble/WASAPI.cpp
Comment thread src/mumble/WASAPINotificationClient.cpp
@MatthewFagan MatthewFagan changed the title Rework WASAPNINotifications to properly check for added devices Rework WASAPINotifications to properly check for added devices Mar 20, 2026
@MatthewFagan MatthewFagan changed the title Rework WASAPINotifications to properly check for added devices FIX(windows client): Fix audio dropout on RDP session start/end Mar 20, 2026
@Hartmnt Hartmnt added client windows audio bug A bug (error) in the software labels Mar 21, 2026
@Hartmnt Hartmnt added this to the 1.6.0 milestone Mar 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[1.4.230] [Windows 10] Sound sporadically stops entirely Audio doesn't work sporadically

2 participants