The issue is that FMixerPlatformXAudio2::GetOutputDeviceInfo does not call SAFE_RELEASE(DefaultDevice).
When GetOutputDeviceInfo is called for a non-default device, GetDefaultAudioEndpoint acquires the default device anyway. If the index is not the “default device index” (which it won’t be if you’re iterating through devices using GetNumOutputDevices), then DefaultDevice is not assigned to Device. Later, even if information about a non-default Device is loaded, and GetMMDeviceInfo is called on the DefaultDevice, nothing actually releases the DefaultDevice handle.
The fix is that the Cleanup label should be changed to:
Cleanup:
if (Device != DefaultDevice)
{
SAFE_RELEASE(DefaultDevice);
}
SAFE_RELEASE(Device);
SAFE_RELEASE(DeviceCollection);
SAFE_RELEASE(DeviceEnumerator);
[Attachment Removed]
Thanks for this June (you probably didn’t expect to hear from me! - quick fyi: I changed roles a few months ago and now partially cover UE Audio).
I will make sure this gets logged.
[Attachment Removed]
Hi June,
A quick follow up. In UE5.8 we upgraded to using the WASAPI audio interface for Microsoft platforms rather than XAudio2. You can set this in your project settings in the editor or by updating your config .ini files to set: AudioMixerModuleName=AudioMixerWasapi
Have you tried this?
[Attachment Removed]
Hi June. Apologies for the slow response; I was away on vacation last week.
I spoke to one of our engineers and he responded with the following:
I looked at the partner’s suggested fix. Seems reasonable. A few thoughts…
They should look at MMDeviceCache… it does management of all those handles and typically is a better place to query devices.
We did maintain 2 paths for a while:
- One is inlined inside the XAudio2 files.
- The other is in MMDeviceCache.
We kept 1 while 2 matured, but almost all of our production paths favor 2 now. Although you may have found 1 spot where it did it the old way.
XAudio2 has also been largely replaced in our 1party titles with WASAPI, but sounds like they’re locked into XAudio2, but that’s fine. It just means XAudio2 hasn’t got a lot of eyes (and ears) on it these days.
If this change fixes it for them, seems like it will stop the leak. They can always do PR into our github and we’ll take the fix back
XAudio2 is something Microsoft is actively discouraging people to use now as its quite old. It also uses WASAPI internally, so it should be compatible with most of what they’re doing, unless they’re directly needing some Xaudio2 isms. But if they’re interfacing with our stuff via our AudioMixer abstractions (which appears to be the case), maybe it’s ok to switch. Maybe worth a try.
The MMDeviceCache does a better job of using RAII to correctly lifetime some of those handles. As they often get lost (leaked) when doing that kind of C (goto fail) style of handling that the original XAudio2 code used to do.
[Attachment Removed]
I haven’t tried WASAPI audio interface to see if it happens there (I suspect not).
Since it’s our downstream customers that are impacted (they are the ones that configure the project), are there any foreseeable issues / trade-offs if we just force WASAPI as the audio interface for all customer projects on Windows via dynamic config layers?
[Attachment Removed]
Our plugin implements voice chat from scratch (we basically only use the EOS SDK for the transmission of audio data). That means our plugin is polling for input and output devices, and instantiating audio mixers to route voice chat audio to specific selected devices. This is similar to the way the EOS SDK provides a list of audio devices, and allows voice chat to be sent to a specific one (independent of the main game audio).
On Windows, on a background thread, we poll the audio output devices every second so that we can keep the list of audio devices up-to-date:
[Image Removed]
The code above is the reason why we found this leak.
To route audio to specific devices, our audio device pool will open the requested devices as needed (this isn’t all of the code since the actual function to open an audio mixer is quite long, but it has the important calls):
[Image Removed]
[Attachment Removed]