Fixed com disposal

This commit is contained in:
Jeroen Saey 2025-09-01 16:00:13 +02:00
parent be67abaf64
commit 6f639622a5
4 changed files with 404 additions and 505 deletions

View File

@ -15,6 +15,29 @@ class Program
{ {
try 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 // Get all audio PLAYBACK devices
var devices = await volumeMixer.GetAudioDevicesAsync(DataFlow.Output); var devices = await volumeMixer.GetAudioDevicesAsync(DataFlow.Output);
Console.WriteLine($"Found {devices.Count} playback devices:"); Console.WriteLine($"Found {devices.Count} playback devices:");

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace EonaCat.VolumeMixer.Helpers namespace EonaCat.VolumeMixer.Helpers
{ {
@ -29,14 +30,24 @@ namespace EonaCat.VolumeMixer.Helpers
Marshal.Release(ptr); 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);
}
}
} }
} }

View File

@ -1,181 +1,158 @@
using EonaCat.VolumeMixer.Helpers; using EonaCat.VolumeMixer.Helpers;
using EonaCat.VolumeMixer.Interfaces; using EonaCat.VolumeMixer.Interfaces;
using EonaCat.VolumeMixer.Models; using EonaCat.VolumeMixer.Models;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; 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<List<AudioDevice>> GetAudioDevicesAsync(DataFlow dataFlow = DataFlow.Output)
{
return await Task.Run(() =>
{
var devices = new List<AudioDevice>();
if (_deviceEnumerator == null)
{
return devices;
}
lock (_syncLock)
{
IMultiMediaDeviceCollection deviceCollection = null;
IMultiMediaDevice defaultDevice = null;
try namespace EonaCat.VolumeMixer.Managers
{ {
var result = _deviceEnumerator.EnumAudioEndpoints(dataFlow, DeviceState.Active, out deviceCollection); // This file is part of the EonaCat project(s) which is released under the Apache License.
if (result != 0 || deviceCollection == null) // See the LICENSE file or go to https://EonaCat.com/License for full license details.
{ public class VolumeMixerManager : IDisposable
return devices; {
} private const int VT_UI4 = 0x13;
private readonly IMultiMediaDeviceEnumerator _deviceEnumerator;
result = deviceCollection.GetCount(out var count); private bool _isDisposed = false;
if (result != 0) private readonly object _syncLock = new();
{
return devices; 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
{
string defaultId = ""; Fmtid = new Guid("1DA5D803-D492-4EDD-8C23-E0C0FFEE7F0E"),
try Pid = 0
{ };
result = _deviceEnumerator.GetDefaultAudioEndpoint(dataFlow, Role.Multimedia, out defaultDevice);
if (result == 0 && defaultDevice != null) public VolumeMixerManager()
{ {
defaultDevice.GetId(out defaultId); try
} {
} _deviceEnumerator = (IMultiMediaDeviceEnumerator)new MMDeviceEnumerator();
catch }
{ catch (Exception ex)
defaultId = ""; {
} throw new InvalidOperationException("Failed to initialize audio device enumerator. Make sure you're running on Windows Vista or later.", ex);
}
for (uint i = 0; i < count; i++) }
{
IMultiMediaDevice device = null; public Task<List<AudioDevice>> GetAudioDevicesAsync(DataFlow dataFlow = DataFlow.Output)
{
try var devices = new List<AudioDevice>();
{
result = deviceCollection.Item(i, out device); if (_deviceEnumerator == null)
if (result == 0 && device != null) {
{ return Task.FromResult(devices);
result = device.GetId(out var id); }
if (result == 0 && !string.IsNullOrEmpty(id))
{ lock (_syncLock)
var name = GetDeviceName(device); {
var type = GetDeviceType(device); IMultiMediaDeviceCollection deviceCollection = null;
bool isDefault = id == defaultId; IMultiMediaDevice defaultDevice = null;
devices.Add(new AudioDevice(device, id, name, isDefault, dataFlow, type));
} try
} {
} var result = _deviceEnumerator.EnumAudioEndpoints(dataFlow, DeviceState.Active, out deviceCollection);
catch if (result != 0 || deviceCollection == null)
{
// 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<AudioDevice> 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
{ {
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); ComHelper.ReleaseComObject(device);
} }
} }
}
return null; finally
} {
}); ComHelper.ReleaseComObject(deviceCollection);
ComHelper.ReleaseComObject(defaultDevice);
}
}
return Task.FromResult(devices);
}
public Task<AudioDevice> GetDefaultAudioDeviceAsync(DataFlow dataFlow = DataFlow.Output)
{
if (_deviceEnumerator == null)
{
return Task.FromResult<AudioDevice>(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<AudioDevice>(null);
}
} }
[DllImport("ole32.dll")] [DllImport("ole32.dll")]
@ -183,7 +160,7 @@ namespace EonaCat.VolumeMixer.Managers
private string GetDeviceName(IMultiMediaDevice device) private string GetDeviceName(IMultiMediaDevice device)
{ {
IPropertyStore? propertyStore = null; IPropertyStore propertyStore = null;
PropVariant propVariant = new PropVariant(); PropVariant propVariant = new PropVariant();
try try
@ -193,339 +170,229 @@ namespace EonaCat.VolumeMixer.Managers
{ {
var propertyKey = PKEY_Device_FriendlyName; var propertyKey = PKEY_Device_FriendlyName;
result = propertyStore.GetValue(ref propertyKey, out propVariant); 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); string name = Marshal.PtrToStringUni(propVariant.data1);
return !string.IsNullOrEmpty(name) ? name : "Unknown Device"; return !string.IsNullOrEmpty(name) ? name : "Unknown Device";
} }
} }
} }
catch
{
// Do nothing
}
finally finally
{ {
// Clear memory
PropVariantClear(ref propVariant); PropVariantClear(ref propVariant);
ComHelper.ReleaseComObject(propertyStore);
if (propertyStore != null)
{
ComHelper.ReleaseComObject(propertyStore);
}
} }
return "Unknown Device"; return "Unknown Device";
} }
private DeviceType GetDeviceType(IMultiMediaDevice device)
private DeviceType GetDeviceType(IMultiMediaDevice device) {
{
IPropertyStore propertyStore = null; IPropertyStore propertyStore = null;
PropVariant propVariant = new PropVariant(); PropVariant propVariant = new PropVariant();
try try
{ {
int result = device.OpenPropertyStore(0, out propertyStore); int result = device.OpenPropertyStore(0, out propertyStore);
if (result == 0 && propertyStore != null) if (result == 0 && propertyStore != null)
{ {
try var propertyKey = PKEY_AudioEndpoint_FormFactor;
{ result = propertyStore.GetValue(ref propertyKey, out propVariant);
var propertyKey = PKEY_AudioEndpoint_FormFactor;
result = propertyStore.GetValue(ref propertyKey, out propVariant); if (result == 0 && propVariant.vt == VT_UI4)
{
// 0x13 == VT_UI4 int formFactor = propVariant.data1.ToInt32();
if (result == 0 && propVariant.vt == 0x13)
{ return formFactor switch
int formFactor = propVariant.data1.ToInt32(); {
0 => DeviceType.Unknown,
return formFactor switch 1 => DeviceType.Speakers,
{ 2 => DeviceType.LineLevel,
0 => DeviceType.Unknown, 3 => DeviceType.Headphones,
1 => DeviceType.Speakers, 4 => DeviceType.Microphone,
2 => DeviceType.LineLevel, 5 => DeviceType.Headset,
3 => DeviceType.Headphones, 6 => DeviceType.Handset,
4 => DeviceType.Microphone, 7 => DeviceType.UnknownDigitalPassthrough,
5 => DeviceType.Headset, 8 => DeviceType.SPDIF,
6 => DeviceType.Handset, 9 => DeviceType.DigitalAudioDisplayDevice,
7 => DeviceType.UnknownDigitalPassthrough, 10 => DeviceType.UnknownFormFactor,
8 => DeviceType.SPDIF, 11 => DeviceType.FMRadio,
9 => DeviceType.DigitalAudioDisplayDevice, 12 => DeviceType.VideoPhone,
10 => DeviceType.UnknownFormFactor, 13 => DeviceType.RCA,
11 => DeviceType.FMRadio, 14 => DeviceType.Bluetooth,
12 => DeviceType.VideoPhone, 15 => DeviceType.SPDIFOut,
13 => DeviceType.RCA, 16 => DeviceType.HDMI,
14 => DeviceType.Bluetooth, 17 => DeviceType.DisplayAudio,
15 => DeviceType.SPDIFOut, 18 => DeviceType.UnknownFormFactor2,
16 => DeviceType.HDMI, 19 => DeviceType.Other,
17 => DeviceType.DisplayAudio, _ => DeviceType.Unknown,
18 => DeviceType.UnknownFormFactor2, };
19 => DeviceType.Other, }
_ => DeviceType.Unknown, }
}; }
} finally
}
finally
{
}
}
}
catch
{
// Do nothing
}
finally
{ {
PropVariantClear(ref propVariant); PropVariantClear(ref propVariant);
if (propertyStore != null) ComHelper.ReleaseComObject(propertyStore);
}
return DeviceType.Unknown;
}
public async Task<bool> 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<float> GetSystemVolumeAsync()
{
using var defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output);
return defaultDevice == null ? 0f : await defaultDevice.GetMasterVolumeAsync();
}
public async Task<bool> SetSystemMuteAsync(bool mute)
{
using var defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output);
return defaultDevice != null && await defaultDevice.SetMasterMuteAsync(mute);
}
public async Task<bool> GetSystemMuteAsync()
{
using var defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output);
return defaultDevice != null && await defaultDevice.GetMasterMuteAsync();
}
public async Task<bool> 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<float> GetMicrophoneVolumeAsync()
{
using var defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input);
return defaultMic == null ? 0f : await defaultMic.GetMasterVolumeAsync();
}
public async Task<bool> SetMicrophoneMuteAsync(bool mute)
{
using var defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input);
return defaultMic != null && await defaultMic.SetMasterMuteAsync(mute);
}
public async Task<bool> GetMicrophoneMuteAsync()
{
using var defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input);
return defaultMic != null && await defaultMic.GetMasterMuteAsync();
}
public async Task<bool> SetMicrophoneVolumeByNameAsync(string microphoneName, float volume)
{
if (string.IsNullOrWhiteSpace(microphoneName) || volume < 0f || volume > 1f)
{
return false;
}
List<AudioDevice> 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; finally
} {
// Release all resources
if (microphones != null)
{
public async Task<bool> SetSystemVolumeAsync(float volume) foreach (var mic in microphones)
{ {
if (volume < 0f || volume > 1f) mic.Dispose();
{ }
return false; }
} }
AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output); return false;
}
if (defaultDevice == null)
{ public async Task<List<AudioSession>> GetAllActiveSessionsAsync()
return false; {
} var allSessions = new List<AudioSession>();
List<AudioDevice> devices = await GetAudioDevicesAsync(DataFlow.Output);
try
{ foreach (var device in devices)
return await defaultDevice.SetMasterVolumeAsync(volume); {
} using (device)
finally {
{ try
defaultDevice.Dispose(); {
} allSessions.AddRange(await device.GetAudioSessionsAsync());
} }
catch
public async Task<float> GetSystemVolumeAsync() {
{ // Do nothing
AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output); }
}
if (defaultDevice == null) }
{
return 0f; // Release resources
} foreach (var device in devices)
{
try device.Dispose();
{ }
return await defaultDevice.GetMasterVolumeAsync(); devices.Clear();
} devices = null;
finally
{ return allSessions;
defaultDevice.Dispose(); }
}
} public Task<List<AudioDevice>> GetMicrophonesAsync()
{
public async Task<bool> SetSystemMuteAsync(bool mute) return GetAudioDevicesAsync(DataFlow.Input);
{ }
AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output);
public Task<AudioDevice> GetDefaultMicrophoneAsync()
if (defaultDevice == null) {
{ return GetDefaultAudioDeviceAsync(DataFlow.Input);
return false; }
}
public void Dispose()
try {
{ lock (_syncLock)
return await defaultDevice.SetMasterMuteAsync(mute); {
} if (!_isDisposed)
finally {
{ ComHelper.ReleaseComObject(_deviceEnumerator);
defaultDevice.Dispose(); _isDisposed = true;
} }
} }
}
public async Task<bool> GetSystemMuteAsync() }
{ }
AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output);
if (defaultDevice == null)
{
return false;
}
try
{
return await defaultDevice.GetMasterMuteAsync();
}
finally
{
defaultDevice.Dispose();
}
}
public async Task<bool> 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<float> GetMicrophoneVolumeAsync()
{
AudioDevice defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input);
if (defaultMic == null)
{
return 0f;
}
try
{
return await defaultMic.GetMasterVolumeAsync();
}
finally
{
defaultMic.Dispose();
}
}
public async Task<bool> 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<bool> GetMicrophoneMuteAsync()
{
AudioDevice defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input);
if (defaultMic == null)
{
return false;
}
try
{
return await defaultMic.GetMasterMuteAsync();
}
finally
{
defaultMic.Dispose();
}
}
public async Task<bool> SetMicrophoneVolumeByNameAsync(string microphoneName, float volume)
{
if (string.IsNullOrWhiteSpace(microphoneName) || volume < 0f || volume > 1f)
{
return false;
}
List<AudioDevice> 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<List<AudioSession>> GetAllActiveSessionsAsync()
{
return await Task.Run(async () =>
{
var allSessions = new List<AudioSession>();
List<AudioDevice> 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<List<AudioDevice>> GetMicrophonesAsync()
{
return await GetAudioDevicesAsync(DataFlow.Input);
}
public async Task<AudioDevice> GetDefaultMicrophoneAsync()
{
return await GetDefaultAudioDeviceAsync(DataFlow.Input);
}
public void Dispose()
{
lock (_syncLock)
{
if (!_isDisposed)
{
ComHelper.ReleaseComObject(_deviceEnumerator);
_isDisposed = true;
}
}
}
}
}

View File

@ -258,13 +258,11 @@ namespace EonaCat.VolumeMixer.Models
return; return;
} }
if (_audioVolume != null) Marshal.ReleaseComObject(_audioVolume);
{ _audioVolume = null;
Marshal.ReleaseComObject(_audioVolume);
_audioVolume = null;
}
ComHelper.ReleaseComObject(_sessionControl);
ComHelper.ReleaseComObject(_sessionControl);
_isDisposed = true; _isDisposed = true;
} }
} }