From 6f639622a51bcfca7ca8a6a34fc2800a6ddf1a31 Mon Sep 17 00:00:00 2001 From: Jeroen Saey Date: Mon, 1 Sep 2025 16:00:13 +0200 Subject: [PATCH] Fixed com disposal --- EonaCat.VolumeMixer.Tester/Program.cs | 23 + EonaCat.VolumeMixer/Helpers/ComHelper.cs | 25 +- .../Managers/VolumeMixerManager.cs | 851 ++++++++---------- EonaCat.VolumeMixer/Models/AudioSession.cs | 10 +- 4 files changed, 404 insertions(+), 505 deletions(-) diff --git a/EonaCat.VolumeMixer.Tester/Program.cs b/EonaCat.VolumeMixer.Tester/Program.cs index 8afeb67..527ed7c 100644 --- a/EonaCat.VolumeMixer.Tester/Program.cs +++ b/EonaCat.VolumeMixer.Tester/Program.cs @@ -15,6 +15,29 @@ class Program { try { + while (true) + { + var input = await volumeMixer.GetAudioDevicesAsync(DataFlow.Input); + var output = await volumeMixer.GetAudioDevicesAsync(DataFlow.Output); + var input2 = await volumeMixer.GetMicrophonesAsync(); + + foreach (var item in input) + { + item.Dispose(); + } + + foreach (var item in input2) + { + item.Dispose(); + } + + foreach (var item in output) + { + item.Dispose(); + } + } + + // Get all audio PLAYBACK devices var devices = await volumeMixer.GetAudioDevicesAsync(DataFlow.Output); Console.WriteLine($"Found {devices.Count} playback devices:"); diff --git a/EonaCat.VolumeMixer/Helpers/ComHelper.cs b/EonaCat.VolumeMixer/Helpers/ComHelper.cs index 46cde05..37632d2 100644 --- a/EonaCat.VolumeMixer/Helpers/ComHelper.cs +++ b/EonaCat.VolumeMixer/Helpers/ComHelper.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using System.Threading.Tasks; namespace EonaCat.VolumeMixer.Helpers { @@ -29,14 +30,24 @@ namespace EonaCat.VolumeMixer.Helpers Marshal.Release(ptr); } } + } + + public static async Task ReleaseComObject(object obj) + { + if (obj != null && Marshal.IsComObject(obj)) + { + if (obj == null || !Marshal.IsComObject(obj)) + { + return; + } + + Marshal.ReleaseComObject(obj); + obj = null; + } + + GC.Collect(); + GC.WaitForPendingFinalizers(); } - public static void ReleaseComObject(object obj) - { - if (obj != null && Marshal.IsComObject(obj)) - { - Marshal.ReleaseComObject(obj); - } - } } } \ No newline at end of file diff --git a/EonaCat.VolumeMixer/Managers/VolumeMixerManager.cs b/EonaCat.VolumeMixer/Managers/VolumeMixerManager.cs index f51d5b2..486ae38 100644 --- a/EonaCat.VolumeMixer/Managers/VolumeMixerManager.cs +++ b/EonaCat.VolumeMixer/Managers/VolumeMixerManager.cs @@ -1,181 +1,158 @@ -using EonaCat.VolumeMixer.Helpers; -using EonaCat.VolumeMixer.Interfaces; -using EonaCat.VolumeMixer.Models; -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Threading.Tasks; - -namespace EonaCat.VolumeMixer.Managers -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - public class VolumeMixerManager : IDisposable - { - private const int VT_UI4 = 0x13; - private readonly IMultiMediaDeviceEnumerator _deviceEnumerator; - private bool _isDisposed = false; - private readonly object _syncLock = new(); - - private static readonly PropertyKey PKEY_Device_FriendlyName = new PropertyKey(new Guid("a45c254e-df1c-4efd-8020-67d146a850e0"), 14); - private static readonly PropertyKey PKEY_AudioEndpoint_FormFactor = new PropertyKey - { - Fmtid = new Guid("1DA5D803-D492-4EDD-8C23-E0C0FFEE7F0E"), - Pid = 0 - }; - - public VolumeMixerManager() - { - try - { - _deviceEnumerator = (IMultiMediaDeviceEnumerator)new MMDeviceEnumerator(); - } - catch (Exception ex) - { - throw new InvalidOperationException("Failed to initialize audio device enumerator. Make sure you're running on Windows Vista or later.", ex); - } - } - - public async Task> GetAudioDevicesAsync(DataFlow dataFlow = DataFlow.Output) - { - return await Task.Run(() => - { - var devices = new List(); - - if (_deviceEnumerator == null) - { - return devices; - } - - lock (_syncLock) - { - IMultiMediaDeviceCollection deviceCollection = null; - IMultiMediaDevice defaultDevice = null; +using EonaCat.VolumeMixer.Helpers; +using EonaCat.VolumeMixer.Interfaces; +using EonaCat.VolumeMixer.Models; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading.Tasks; - try - { - var result = _deviceEnumerator.EnumAudioEndpoints(dataFlow, DeviceState.Active, out deviceCollection); - if (result != 0 || deviceCollection == null) - { - return devices; - } - - result = deviceCollection.GetCount(out var count); - if (result != 0) - { - return devices; - } - - string defaultId = ""; - try - { - result = _deviceEnumerator.GetDefaultAudioEndpoint(dataFlow, Role.Multimedia, out defaultDevice); - if (result == 0 && defaultDevice != null) - { - defaultDevice.GetId(out defaultId); - } - } - catch - { - defaultId = ""; - } - - for (uint i = 0; i < count; i++) - { - IMultiMediaDevice device = null; - - try - { - result = deviceCollection.Item(i, out device); - if (result == 0 && device != null) - { - result = device.GetId(out var id); - if (result == 0 && !string.IsNullOrEmpty(id)) - { - var name = GetDeviceName(device); - var type = GetDeviceType(device); - bool isDefault = id == defaultId; - devices.Add(new AudioDevice(device, id, name, isDefault, dataFlow, type)); - } - } - } - catch - { - // Do nothing - } - finally - { - if (device != null) - { - ComHelper.ReleaseComObject(device); - } - } - } - } - catch - { - // Do nothing - } - finally - { - if (deviceCollection != null) - { - ComHelper.ReleaseComObject(deviceCollection); - } - - if (defaultDevice != null) - { - ComHelper.ReleaseComObject(defaultDevice); - } - } - } - - return devices; - }); - } - - public async Task GetDefaultAudioDeviceAsync(DataFlow dataFlow = DataFlow.Output) - { - return await Task.Run(() => - { - if (_deviceEnumerator == null) - { - return null; - } - - lock (_syncLock) - { - IMultiMediaDevice device = null; - - try - { - var result = _deviceEnumerator.GetDefaultAudioEndpoint(dataFlow, Role.Multimedia, out device); - if (result == 0 && device != null) - { - result = device.GetId(out var id); - if (result == 0 && !string.IsNullOrEmpty(id)) - { - var name = GetDeviceName(device); - var type = GetDeviceType(device); - return new AudioDevice(device, id, name, true, dataFlow, type); - } - } - } - catch - { - // Do nothing - } - finally +namespace EonaCat.VolumeMixer.Managers +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + public class VolumeMixerManager : IDisposable + { + private const int VT_UI4 = 0x13; + private readonly IMultiMediaDeviceEnumerator _deviceEnumerator; + private bool _isDisposed = false; + private readonly object _syncLock = new(); + + private static readonly PropertyKey PKEY_Device_FriendlyName = new PropertyKey(new Guid("a45c254e-df1c-4efd-8020-67d146a850e0"), 14); + private static readonly PropertyKey PKEY_AudioEndpoint_FormFactor = new PropertyKey + { + Fmtid = new Guid("1DA5D803-D492-4EDD-8C23-E0C0FFEE7F0E"), + Pid = 0 + }; + + public VolumeMixerManager() + { + try + { + _deviceEnumerator = (IMultiMediaDeviceEnumerator)new MMDeviceEnumerator(); + } + catch (Exception ex) + { + throw new InvalidOperationException("Failed to initialize audio device enumerator. Make sure you're running on Windows Vista or later.", ex); + } + } + + public Task> GetAudioDevicesAsync(DataFlow dataFlow = DataFlow.Output) + { + var devices = new List(); + + if (_deviceEnumerator == null) + { + return Task.FromResult(devices); + } + + lock (_syncLock) + { + IMultiMediaDeviceCollection deviceCollection = null; + IMultiMediaDevice defaultDevice = null; + + try + { + var result = _deviceEnumerator.EnumAudioEndpoints(dataFlow, DeviceState.Active, out deviceCollection); + if (result != 0 || deviceCollection == null) { - if (device != null) + return Task.FromResult(devices); + } + + result = deviceCollection.GetCount(out var count); + if (result != 0) + { + return Task.FromResult(devices); + } + + string defaultId = ""; + try + { + result = _deviceEnumerator.GetDefaultAudioEndpoint(dataFlow, Role.Multimedia, out defaultDevice); + if (result == 0 && defaultDevice != null) + { + defaultDevice.GetId(out defaultId); + } + } + catch + { + defaultId = ""; + } + + for (uint i = 0; i < count; i++) + { + IMultiMediaDevice device = null; + try + { + result = deviceCollection.Item(i, out device); + if (result == 0 && device != null) + { + result = device.GetId(out var id); + if (result == 0 && !string.IsNullOrEmpty(id)) + { + var name = GetDeviceName(device); + var type = GetDeviceType(device); + bool isDefault = id == defaultId; + + devices.Add(new AudioDevice(device, id, name, isDefault, dataFlow, type)); + device = null; + } + } + } + catch + { + // Do nothing + } + finally { ComHelper.ReleaseComObject(device); } - } - - return null; - } - }); + } + } + finally + { + ComHelper.ReleaseComObject(deviceCollection); + ComHelper.ReleaseComObject(defaultDevice); + } + } + + return Task.FromResult(devices); + } + + public Task GetDefaultAudioDeviceAsync(DataFlow dataFlow = DataFlow.Output) + { + if (_deviceEnumerator == null) + { + return Task.FromResult(null); + } + + lock (_syncLock) + { + IMultiMediaDevice device = null; + + try + { + var result = _deviceEnumerator.GetDefaultAudioEndpoint(dataFlow, Role.Multimedia, out device); + if (result == 0 && device != null) + { + result = device.GetId(out var id); + if (result == 0 && !string.IsNullOrEmpty(id)) + { + var name = GetDeviceName(device); + var type = GetDeviceType(device); + + var audioDevice = new AudioDevice(device, id, name, true, dataFlow, type); + device = null; // ownership transferred + return Task.FromResult(audioDevice); + } + } + } + finally + { + ComHelper.ReleaseComObject(device); + } + + return Task.FromResult(null); + } } [DllImport("ole32.dll")] @@ -183,7 +160,7 @@ namespace EonaCat.VolumeMixer.Managers private string GetDeviceName(IMultiMediaDevice device) { - IPropertyStore? propertyStore = null; + IPropertyStore propertyStore = null; PropVariant propVariant = new PropVariant(); try @@ -193,339 +170,229 @@ namespace EonaCat.VolumeMixer.Managers { var propertyKey = PKEY_Device_FriendlyName; result = propertyStore.GetValue(ref propertyKey, out propVariant); - if (result == 0) + if (result == 0 && + (propVariant.vt == (ushort)VarEnum.VT_LPWSTR || propVariant.vt == (ushort)VarEnum.VT_BSTR)) { - // Get string string name = Marshal.PtrToStringUni(propVariant.data1); return !string.IsNullOrEmpty(name) ? name : "Unknown Device"; } } } - catch - { - // Do nothing - } finally { - // Clear memory PropVariantClear(ref propVariant); - - if (propertyStore != null) - { - ComHelper.ReleaseComObject(propertyStore); - } + ComHelper.ReleaseComObject(propertyStore); } return "Unknown Device"; } - - - private DeviceType GetDeviceType(IMultiMediaDevice device) - { + + private DeviceType GetDeviceType(IMultiMediaDevice device) + { IPropertyStore propertyStore = null; - PropVariant propVariant = new PropVariant(); - - try - { - int result = device.OpenPropertyStore(0, out propertyStore); - if (result == 0 && propertyStore != null) - { - try - { - var propertyKey = PKEY_AudioEndpoint_FormFactor; - result = propertyStore.GetValue(ref propertyKey, out propVariant); - - // 0x13 == VT_UI4 - if (result == 0 && propVariant.vt == 0x13) - { - int formFactor = propVariant.data1.ToInt32(); - - return formFactor switch - { - 0 => DeviceType.Unknown, - 1 => DeviceType.Speakers, - 2 => DeviceType.LineLevel, - 3 => DeviceType.Headphones, - 4 => DeviceType.Microphone, - 5 => DeviceType.Headset, - 6 => DeviceType.Handset, - 7 => DeviceType.UnknownDigitalPassthrough, - 8 => DeviceType.SPDIF, - 9 => DeviceType.DigitalAudioDisplayDevice, - 10 => DeviceType.UnknownFormFactor, - 11 => DeviceType.FMRadio, - 12 => DeviceType.VideoPhone, - 13 => DeviceType.RCA, - 14 => DeviceType.Bluetooth, - 15 => DeviceType.SPDIFOut, - 16 => DeviceType.HDMI, - 17 => DeviceType.DisplayAudio, - 18 => DeviceType.UnknownFormFactor2, - 19 => DeviceType.Other, - _ => DeviceType.Unknown, - }; - } - } - finally - { - - } - } - } - catch - { - // Do nothing - } - finally + PropVariant propVariant = new PropVariant(); + + try + { + int result = device.OpenPropertyStore(0, out propertyStore); + if (result == 0 && propertyStore != null) + { + var propertyKey = PKEY_AudioEndpoint_FormFactor; + result = propertyStore.GetValue(ref propertyKey, out propVariant); + + if (result == 0 && propVariant.vt == VT_UI4) + { + int formFactor = propVariant.data1.ToInt32(); + + return formFactor switch + { + 0 => DeviceType.Unknown, + 1 => DeviceType.Speakers, + 2 => DeviceType.LineLevel, + 3 => DeviceType.Headphones, + 4 => DeviceType.Microphone, + 5 => DeviceType.Headset, + 6 => DeviceType.Handset, + 7 => DeviceType.UnknownDigitalPassthrough, + 8 => DeviceType.SPDIF, + 9 => DeviceType.DigitalAudioDisplayDevice, + 10 => DeviceType.UnknownFormFactor, + 11 => DeviceType.FMRadio, + 12 => DeviceType.VideoPhone, + 13 => DeviceType.RCA, + 14 => DeviceType.Bluetooth, + 15 => DeviceType.SPDIFOut, + 16 => DeviceType.HDMI, + 17 => DeviceType.DisplayAudio, + 18 => DeviceType.UnknownFormFactor2, + 19 => DeviceType.Other, + _ => DeviceType.Unknown, + }; + } + } + } + finally { PropVariantClear(ref propVariant); - if (propertyStore != null) + ComHelper.ReleaseComObject(propertyStore); + } + + return DeviceType.Unknown; + } + + public async Task SetSystemVolumeAsync(float volume) + { + if (volume < 0f || volume > 1f) + { + return false; + } + + using var defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output); + if (defaultDevice == null) + { + return false; + } + + return await defaultDevice.SetMasterVolumeAsync(volume); + } + + public async Task GetSystemVolumeAsync() + { + using var defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output); + return defaultDevice == null ? 0f : await defaultDevice.GetMasterVolumeAsync(); + } + + public async Task SetSystemMuteAsync(bool mute) + { + using var defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output); + return defaultDevice != null && await defaultDevice.SetMasterMuteAsync(mute); + } + + public async Task GetSystemMuteAsync() + { + using var defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output); + return defaultDevice != null && await defaultDevice.GetMasterMuteAsync(); + } + + public async Task SetMicrophoneVolumeAsync(float volume) + { + if (volume < 0f || volume > 1f) + { + return false; + } + + using var defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input); + return defaultMic != null && await defaultMic.SetMasterVolumeAsync(volume); + } + + public async Task GetMicrophoneVolumeAsync() + { + using var defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input); + return defaultMic == null ? 0f : await defaultMic.GetMasterVolumeAsync(); + } + + public async Task SetMicrophoneMuteAsync(bool mute) + { + using var defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input); + return defaultMic != null && await defaultMic.SetMasterMuteAsync(mute); + } + + public async Task GetMicrophoneMuteAsync() + { + using var defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input); + return defaultMic != null && await defaultMic.GetMasterMuteAsync(); + } + + public async Task SetMicrophoneVolumeByNameAsync(string microphoneName, float volume) + { + if (string.IsNullOrWhiteSpace(microphoneName) || volume < 0f || volume > 1f) + { + return false; + } + + List microphones = null; + + try + { + microphones = await GetAudioDevicesAsync(DataFlow.Input); + + foreach (var mic in microphones) { - ComHelper.ReleaseComObject(propertyStore); + using (mic) + { + if (mic.Name.IndexOf(microphoneName, StringComparison.OrdinalIgnoreCase) >= 0) + { + return await mic.SetMasterVolumeAsync(volume); + } + } } - } - return DeviceType.Unknown; - } - - - - public async Task SetSystemVolumeAsync(float volume) - { - if (volume < 0f || volume > 1f) - { - return false; - } - - AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output); - - if (defaultDevice == null) - { - return false; - } - - try - { - return await defaultDevice.SetMasterVolumeAsync(volume); - } - finally - { - defaultDevice.Dispose(); - } - } - - public async Task GetSystemVolumeAsync() - { - AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output); - - if (defaultDevice == null) - { - return 0f; - } - - try - { - return await defaultDevice.GetMasterVolumeAsync(); - } - finally - { - defaultDevice.Dispose(); - } - } - - public async Task SetSystemMuteAsync(bool mute) - { - AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output); - - if (defaultDevice == null) - { - return false; - } - - try - { - return await defaultDevice.SetMasterMuteAsync(mute); - } - finally - { - defaultDevice.Dispose(); - } - } - - public async Task GetSystemMuteAsync() - { - AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output); - - if (defaultDevice == null) - { - return false; - } - - try - { - return await defaultDevice.GetMasterMuteAsync(); - } - finally - { - defaultDevice.Dispose(); - } - } - - public async Task SetMicrophoneVolumeAsync(float volume) - { - if (volume < 0f || volume > 1f) - { - return false; - } - - AudioDevice defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input); - - if (defaultMic == null) - { - return false; - } - - try - { - return await defaultMic.SetMasterVolumeAsync(volume); - } - finally - { - defaultMic.Dispose(); - } - } - - public async Task GetMicrophoneVolumeAsync() - { - AudioDevice defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input); - - if (defaultMic == null) - { - return 0f; - } - - try - { - return await defaultMic.GetMasterVolumeAsync(); - } - finally - { - defaultMic.Dispose(); - } - } - - public async Task SetMicrophoneMuteAsync(bool mute) - { - AudioDevice defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input); - - if (defaultMic == null) - { - return false; - } - - try - { - return await defaultMic.SetMasterMuteAsync(mute); - } - finally - { - defaultMic.Dispose(); - } - } - - public async Task GetMicrophoneMuteAsync() - { - AudioDevice defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input); - - if (defaultMic == null) - { - return false; - } - - try - { - return await defaultMic.GetMasterMuteAsync(); - } - finally - { - defaultMic.Dispose(); - } - } - - public async Task SetMicrophoneVolumeByNameAsync(string microphoneName, float volume) - { - if (string.IsNullOrWhiteSpace(microphoneName) || volume < 0f || volume > 1f) - { - return false; - } - - List microphones = await GetAudioDevicesAsync(DataFlow.Input); - - foreach (var mic in microphones) - { - try - { - if (mic.Name.IndexOf(microphoneName, StringComparison.OrdinalIgnoreCase) >= 0) - { - return await mic.SetMasterVolumeAsync(volume); - } - } - finally - { - mic.Dispose(); - } - } - - return false; - } - - public async Task> GetAllActiveSessionsAsync() - { - return await Task.Run(async () => - { - var allSessions = new List(); - - List devices = await GetAudioDevicesAsync(DataFlow.Output); - - foreach (var device in devices) - { - try - { - allSessions.AddRange(await device.GetAudioSessionsAsync()); - } - catch - { - // Do nothing - } - finally - { - device.Dispose(); - } - } - - return allSessions; - }); - } - - public async Task> GetMicrophonesAsync() - { - return await GetAudioDevicesAsync(DataFlow.Input); - } - - public async Task GetDefaultMicrophoneAsync() - { - return await GetDefaultAudioDeviceAsync(DataFlow.Input); - } - - public void Dispose() - { - lock (_syncLock) - { - if (!_isDisposed) - { - ComHelper.ReleaseComObject(_deviceEnumerator); - _isDisposed = true; - } - } - } - } -} + } + finally + { + // Release all resources + if (microphones != null) + { + foreach (var mic in microphones) + { + mic.Dispose(); + } + } + } + + return false; + } + + public async Task> GetAllActiveSessionsAsync() + { + var allSessions = new List(); + List devices = await GetAudioDevicesAsync(DataFlow.Output); + + foreach (var device in devices) + { + using (device) + { + try + { + allSessions.AddRange(await device.GetAudioSessionsAsync()); + } + catch + { + // Do nothing + } + } + } + + // Release resources + foreach (var device in devices) + { + device.Dispose(); + } + devices.Clear(); + devices = null; + + return allSessions; + } + + public Task> GetMicrophonesAsync() + { + return GetAudioDevicesAsync(DataFlow.Input); + } + + public Task GetDefaultMicrophoneAsync() + { + return GetDefaultAudioDeviceAsync(DataFlow.Input); + } + + public void Dispose() + { + lock (_syncLock) + { + if (!_isDisposed) + { + ComHelper.ReleaseComObject(_deviceEnumerator); + _isDisposed = true; + } + } + } + } +} diff --git a/EonaCat.VolumeMixer/Models/AudioSession.cs b/EonaCat.VolumeMixer/Models/AudioSession.cs index b2d9b82..c617531 100644 --- a/EonaCat.VolumeMixer/Models/AudioSession.cs +++ b/EonaCat.VolumeMixer/Models/AudioSession.cs @@ -258,13 +258,11 @@ namespace EonaCat.VolumeMixer.Models return; } - if (_audioVolume != null) - { - Marshal.ReleaseComObject(_audioVolume); - _audioVolume = null; - } - ComHelper.ReleaseComObject(_sessionControl); + Marshal.ReleaseComObject(_audioVolume); + _audioVolume = null; + ComHelper.ReleaseComObject(_sessionControl); + _isDisposed = true; } }