diff --git a/EonaCat.VolumeMixer.Tester/Program.cs b/EonaCat.VolumeMixer.Tester/Program.cs index 8afeb67..c378f03 100644 --- a/EonaCat.VolumeMixer.Tester/Program.cs +++ b/EonaCat.VolumeMixer.Tester/Program.cs @@ -15,6 +15,16 @@ class Program { try { + while (true) + { + var devicesTest = await volumeMixer.GetAudioDevicesAsync(DataFlow.Output); + var micrphonesTest = await volumeMixer.GetMicrophonesAsync(); + Console.WriteLine($"Found {devicesTest.Count} playback devices"); + Console.WriteLine($"Found {micrphonesTest.Count} microphones"); + await Task.Delay(100); + } + + // Get all audio PLAYBACK devices var devices = await volumeMixer.GetAudioDevicesAsync(DataFlow.Output); Console.WriteLine($"Found {devices.Count} playback devices:"); diff --git a/EonaCat.VolumeMixer/EonaCat.VolumeMixer.csproj b/EonaCat.VolumeMixer/EonaCat.VolumeMixer.csproj index 9298826..6180a9a 100644 --- a/EonaCat.VolumeMixer/EonaCat.VolumeMixer.csproj +++ b/EonaCat.VolumeMixer/EonaCat.VolumeMixer.csproj @@ -18,9 +18,9 @@ EonaCat, Audio, Volume, Mixer .NET Standard, Jeroen, Saey EonaCat VolumeMixer - 1.0.5 - 1.0.0.5 - 1.0.0.5 + 1.0.6 + 1.0.0.6 + 1.0.0.6 icon.png https://git.saey.me/EonaCat/EonaCat.VolumeMixer git diff --git a/EonaCat.VolumeMixer/Helpers/ComHelper.cs b/EonaCat.VolumeMixer/Helpers/ComHelper.cs index 46cde05..16ee5a2 100644 --- a/EonaCat.VolumeMixer/Helpers/ComHelper.cs +++ b/EonaCat.VolumeMixer/Helpers/ComHelper.cs @@ -31,12 +31,32 @@ namespace EonaCat.VolumeMixer.Helpers } } - public static void ReleaseComObject(object obj) + public static void ReleaseComObject(object comObj) { - if (obj != null && Marshal.IsComObject(obj)) + if (comObj == null) { - Marshal.ReleaseComObject(obj); + return; + } + + try + { + while (Marshal.ReleaseComObject(comObj) > 0) + { + // Do nothing + } + } + catch + { + try + { + Marshal.FinalReleaseComObject(comObj); + } + catch + { + // Do nothing + } } } + } } \ No newline at end of file diff --git a/EonaCat.VolumeMixer/Interfaces/IPropertyStore.cs b/EonaCat.VolumeMixer/Interfaces/IPropertyStore.cs index 184b4ce..8a45dd4 100644 --- a/EonaCat.VolumeMixer/Interfaces/IPropertyStore.cs +++ b/EonaCat.VolumeMixer/Interfaces/IPropertyStore.cs @@ -1,4 +1,5 @@ -using EonaCat.VolumeMixer.Models; +using EonaCat.VolumeMixer.Helpers; +using EonaCat.VolumeMixer.Models; using System; using System.Runtime.InteropServices; @@ -13,8 +14,8 @@ namespace EonaCat.VolumeMixer.Interfaces { int GetCount(out uint cProps); int GetAt(uint iProp, out PropertyKey pkey); - int GetValue(ref PropertyKey key, out PropVariant pv); - int SetValue(ref PropertyKey key, ref PropVariant propvar); + int GetValue(ref PropertyKey key, out PROPVARIANT pv); + int SetValue(ref PropertyKey key, ref PROPVARIANT propvar); int Commit(); } } \ No newline at end of file diff --git a/EonaCat.VolumeMixer/Managers/VolumeMixerManager.cs b/EonaCat.VolumeMixer/Managers/VolumeMixerManager.cs index 7e8dadb..23b6b7c 100644 --- a/EonaCat.VolumeMixer/Managers/VolumeMixerManager.cs +++ b/EonaCat.VolumeMixer/Managers/VolumeMixerManager.cs @@ -8,17 +8,16 @@ 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 IMultiMediaDeviceEnumerator _deviceEnumerator; + private bool _isDisposed; private readonly object _syncLock = new(); + public event EventHandler OnException; - 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 + private static PropertyKey PKEY_Device_FriendlyName = new(new Guid("a45c254e-df1c-4efd-8020-67d146a850e0"), 14); + + private static PropertyKey PKEY_AudioEndpoint_FormFactor = new PropertyKey { Fmtid = new Guid("1DA5D803-D492-4EDD-8C23-E0C0FFEE7F0E"), Pid = 0 @@ -32,7 +31,8 @@ namespace EonaCat.VolumeMixer.Managers } catch (Exception ex) { - throw new InvalidOperationException("Failed to initialize audio device enumerator. Make sure you're running on Windows Vista or later.", ex); + throw new InvalidOperationException( + "Failed to initialize audio device enumerator. Make sure you're running on Windows Vista or later.", ex); } } @@ -41,76 +41,87 @@ namespace EonaCat.VolumeMixer.Managers return await Task.Run(() => { var devices = new List(); - - if (_deviceEnumerator == null) - { - return devices; - } + if (_isDisposed || _deviceEnumerator == null) return devices; lock (_syncLock) { + IMultiMediaDeviceCollection collection = null; + IMultiMediaDevice defaultDevice = null; + try { - var result = _deviceEnumerator.EnumAudioEndpoints(dataFlow, DeviceState.Active, out var deviceCollection); - if (result != 0 || deviceCollection == null) - { - return devices; - } + int hr = _deviceEnumerator.EnumAudioEndpoints(dataFlow, DeviceState.Active, out collection); + if (hr != 0 || collection == null) return devices; - result = deviceCollection.GetCount(out var count); - if (result != 0) - { - ComHelper.ReleaseComObject(deviceCollection); - return devices; - } + hr = collection.GetCount(out var count); + if (hr != 0) return devices; - string defaultId = ""; + // Get default device + string defaultId = string.Empty; try { - result = _deviceEnumerator.GetDefaultAudioEndpoint(dataFlow, Role.Multimedia, out var defaultDevice); - if (result == 0 && defaultDevice != null) + hr = _deviceEnumerator.GetDefaultAudioEndpoint(dataFlow, Role.Multimedia, out defaultDevice); + if (hr == 0 && defaultDevice != null) { defaultDevice.GetId(out defaultId); - ComHelper.ReleaseComObject(defaultDevice); } } - catch + catch (Exception ex) { - defaultId = ""; + OnException?.Invoke(this, ex); } + // Enumerate devices for (uint i = 0; i < count; i++) { + IMultiMediaDevice device = null; try { - result = deviceCollection.Item(i, out var device); - if (result == 0 && device != null) + if (collection.Item(i, out device) != 0 || device == null) continue; + + if (device.GetId(out var id) != 0 || string.IsNullOrEmpty(id)) continue; + + string name = GetDeviceName(device); + DeviceType type = GetDeviceType(device); + bool isDefault = id == defaultId; + + // Increase ref count before passing to AudioDevice + Marshal.AddRef(Marshal.GetIUnknownForObject(device)); + + devices.Add(new AudioDevice(device, id, name, isDefault, dataFlow, type)); + device = null; + } + catch (Exception ex) + { + OnException?.Invoke(this, ex); + } + finally + { + if (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)); - } - else - { - ComHelper.ReleaseComObject(device); - } + Marshal.ReleaseComObject(device); + device = null; } } - catch - { - // Do nothing - } + } + } + catch (Exception ex) + { + OnException?.Invoke(this, ex); + } + finally + { + if (collection != null) + { + Marshal.ReleaseComObject(collection); + collection = null; } - ComHelper.ReleaseComObject(deviceCollection); - } - catch - { - // Do nothing + if (defaultDevice != null) + { + Marshal.ReleaseComObject(defaultDevice); + defaultDevice = null; + } } } @@ -120,36 +131,28 @@ namespace EonaCat.VolumeMixer.Managers public async Task GetDefaultAudioDeviceAsync(DataFlow dataFlow = DataFlow.Output) { + if (_isDisposed || _deviceEnumerator == null) return null; + return await Task.Run(() => { - if (_deviceEnumerator == null) - { - return null; - } - lock (_syncLock) { + IMultiMediaDevice device = null; try { - var result = _deviceEnumerator.GetDefaultAudioEndpoint(dataFlow, Role.Multimedia, out var device); - if (result == 0 && device != null) + int hr = _deviceEnumerator.GetDefaultAudioEndpoint(dataFlow, Role.Multimedia, out device); + if (hr == 0 && device != null) { - result = device.GetId(out var id); - if (result == 0 && !string.IsNullOrEmpty(id)) + if (device.GetId(out var id) == 0 && !string.IsNullOrEmpty(id)) { - var name = GetDeviceName(device); - var type = GetDeviceType(device); + string name = GetDeviceName(device); + DeviceType type = GetDeviceType(device); return new AudioDevice(device, id, name, true, dataFlow, type); } - - ComHelper.ReleaseComObject(device); } } - catch - { - // Do nothing - } - + catch (Exception ex) { OnException?.Invoke(this, ex); } + finally { SafeRelease(ref device); } return null; } }); @@ -157,26 +160,24 @@ namespace EonaCat.VolumeMixer.Managers private string GetDeviceName(IMultiMediaDevice device) { + IPropertyStore store = null; + PROPVARIANT prop = default; + try { - var result = device.OpenPropertyStore(0, out var propertyStore); - if (result == 0 && propertyStore != null) + if (device.OpenPropertyStore(0, out store) != 0 || store == null) return "Unknown Device"; + if (store.GetValue(ref PKEY_Device_FriendlyName, out prop) == 0 && + prop.vt == VarEnumConstants.VT_LPWSTR && prop.pointerValue != IntPtr.Zero) { - var propertyKey = PKEY_Device_FriendlyName; - result = propertyStore.GetValue(ref propertyKey, out var propVariant); - if (result == 0 && propVariant.data1 != IntPtr.Zero) - { - string name = Marshal.PtrToStringUni(propVariant.data1); - ComHelper.ReleaseComObject(propertyStore); - return !string.IsNullOrEmpty(name) ? name : "Unknown Device"; - } - - ComHelper.ReleaseComObject(propertyStore); + string name = Marshal.PtrToStringUni(prop.pointerValue); + return string.IsNullOrEmpty(name) ? "Unknown Device" : name; } } - catch + catch (Exception ex) { OnException?.Invoke(this, ex); } + finally { - // Do nothing + try { PropVariantHelper.PropVariantClear(ref prop); } catch { } + SafeRelease(ref store); } return "Unknown Device"; @@ -184,63 +185,43 @@ namespace EonaCat.VolumeMixer.Managers private DeviceType GetDeviceType(IMultiMediaDevice device) { + IPropertyStore store = null; + PROPVARIANT prop = default; + try { - int result = device.OpenPropertyStore(0, out var propertyStore); - if (result == 0 && propertyStore != null) + if (device.OpenPropertyStore(0, out store) != 0 || store == null) return DeviceType.Unknown; + if (store.GetValue(ref PKEY_AudioEndpoint_FormFactor, out prop) == 0 && prop.vt == VarEnumConstants.VT_UI4) { - try + return prop.uintValue switch { - var propertyKey = PKEY_AudioEndpoint_FormFactor; - result = propertyStore.GetValue(ref propertyKey, out var 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 - { - ComHelper.ReleaseComObject(propertyStore); - } + 1 => DeviceType.Speakers, + 2 => DeviceType.LineLevel, + 3 => DeviceType.Headphones, + 4 => DeviceType.Microphone, + 5 => DeviceType.Headset, + 6 => DeviceType.Handset, + 8 => DeviceType.SPDIF, + 9 => DeviceType.DigitalAudioDisplayDevice, + 14 => DeviceType.Bluetooth, + 15 => DeviceType.SPDIFOut, + 16 => DeviceType.HDMI, + 17 => DeviceType.DisplayAudio, + 19 => DeviceType.Other, + _ => DeviceType.Unknown + }; } } - catch + catch (Exception ex) { OnException?.Invoke(this, ex); } + finally { - // Do nothing + try { PropVariantHelper.PropVariantClear(ref prop); } catch { } + SafeRelease(ref store); } return DeviceType.Unknown; } - - public async Task SetSystemVolumeAsync(float volume) { if (volume < 0f || volume > 1f) @@ -430,34 +411,6 @@ namespace EonaCat.VolumeMixer.Managers 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); @@ -472,11 +425,18 @@ namespace EonaCat.VolumeMixer.Managers { lock (_syncLock) { - if (!_isDisposed) - { - ComHelper.ReleaseComObject(_deviceEnumerator); - _isDisposed = true; - } + if (_isDisposed) return; + _isDisposed = true; + SafeRelease(ref _deviceEnumerator); + } + } + + private static void SafeRelease(ref T comObj) where T : class + { + if (comObj != null) + { + try { Marshal.FinalReleaseComObject(comObj); } catch { } + comObj = null; } } } diff --git a/EonaCat.VolumeMixer/Models/AudioDevice.cs b/EonaCat.VolumeMixer/Models/AudioDevice.cs index 519a443..85a0d3b 100644 --- a/EonaCat.VolumeMixer/Models/AudioDevice.cs +++ b/EonaCat.VolumeMixer/Models/AudioDevice.cs @@ -8,267 +8,212 @@ using System.Threading.Tasks; namespace EonaCat.VolumeMixer.Models { - // 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 AudioDevice : IDisposable { - internal Guid AudioController2Guid = new Guid("bfb7ff88-7239-4fc9-8fa2-07c950be9c6d"); - private readonly IMultiMediaDevice _device; + internal Guid AudioController2Guid = new("bfb7ff88-7239-4fc9-8fa2-07c950be9c6d"); + private IMultiMediaDevice _device; private IAudioEndpointVolume _endpointVolume; private IAudioSessionManager _sessionManager; - private bool _isDisposed = false; - private readonly object _syncLock = new object(); + private bool _isDisposed; + private readonly object _syncLock = new(); - public string Id { get; private set; } - public string Name { get; private set; } - public DeviceType DeviceType { get; private set; } - public bool IsDefault { get; private set; } - public DataFlow DataFlow { get; private set; } + public string Id { get; } + public string Name { get; } + public DeviceType DeviceType { get; } + public bool IsDefault { get; } + public DataFlow DataFlow { get; } + + public event EventHandler OnException; internal AudioDevice(IMultiMediaDevice device, string id, string name, bool isDefault, DataFlow dataFlow, DeviceType deviceType) { - _device = device; + _device = device ?? throw new ArgumentNullException(nameof(device)); Id = id; Name = name; IsDefault = isDefault; DataFlow = dataFlow; DeviceType = deviceType; + InitializeEndpointVolume(); InitializeSessionManager(); } private void InitializeEndpointVolume() { + IntPtr ptr = IntPtr.Zero; try { - var audioEndpointGuid = typeof(IAudioEndpointVolume).GUID; - var result = _device.Activate(ref audioEndpointGuid, 0, IntPtr.Zero, out var ptr); - if (result == 0 && ptr != IntPtr.Zero) + var guid = typeof(IAudioEndpointVolume).GUID; + int hr = _device.Activate(ref guid, 0, IntPtr.Zero, out ptr); + if (hr == 0 && ptr != IntPtr.Zero) { - _endpointVolume = ComHelper.GetInterface(ptr); + _endpointVolume = (IAudioEndpointVolume)Marshal.GetObjectForIUnknown(ptr); } } catch (Exception ex) { - Debug.WriteLine($"Failed to initialize endpoint volume: {ex}"); + Debug.WriteLine($"InitializeEndpointVolume failed: {ex}"); _endpointVolume = null; } + finally + { + if (ptr != IntPtr.Zero) + { + Marshal.Release(ptr); + } + } } private void InitializeSessionManager() { + IntPtr ptr = IntPtr.Zero; try { - var audioSessionGuid = typeof(IAudioSessionManager).GUID; - var result = _device.Activate(ref audioSessionGuid, 0, IntPtr.Zero, out var ptr); - if (result == 0 && ptr != IntPtr.Zero) + var guid = typeof(IAudioSessionManager).GUID; + int hr = _device.Activate(ref guid, 0, IntPtr.Zero, out ptr); + if (hr == 0 && ptr != IntPtr.Zero) { - _sessionManager = ComHelper.GetInterface(ptr); + _sessionManager = (IAudioSessionManager)Marshal.GetObjectForIUnknown(ptr); } } catch (Exception ex) { - Debug.WriteLine($"Failed to initialize session manager: {ex}"); + Debug.WriteLine($"InitializeSessionManager failed: {ex}"); _sessionManager = null; } - } - - public async Task GetMasterVolumeAsync() - { - return await Task.Run(() => + finally { - lock (_syncLock) + if (ptr != IntPtr.Zero) { - if (_isDisposed || _endpointVolume == null) - { - return 0f; - } - - try - { - var result = _endpointVolume.GetMasterVolumeLevelScalar(out var volume); - return result == 0 ? volume : 0f; - } - catch (Exception ex) - { - Debug.WriteLine($"Error getting master volume: {ex}"); - return 0f; - } + Marshal.Release(ptr); } - }).ConfigureAwait(false); + } } - public async Task SetMasterVolumeAsync(float volume, int maxRetries = 5, int delayMs = 20) + public Task GetMasterVolumeAsync() { - if (_isDisposed || _endpointVolume == null || volume < 0f || volume > 1f) + lock (_syncLock) { - return false; - } + if (_isDisposed || _endpointVolume == null) + { + return Task.FromResult(0f); + } - var guid = Guid.Empty; - - for (int attempt = 0; attempt <= maxRetries; attempt++) - { try { - int result; - lock (_syncLock) - { - result = _endpointVolume.SetMasterVolumeLevelScalar(volume, ref guid); - } - - if (result == 0) - { - await Task.Delay(delayMs).ConfigureAwait(false); - var currentVolume = await GetMasterVolumeAsync().ConfigureAwait(false); - if (Math.Abs(currentVolume - volume) < 0.01f) - { - return true; - } - } + int hr = _endpointVolume.GetMasterVolumeLevelScalar(out float volume); + return Task.FromResult(hr == 0 ? volume : 0f); } - catch (Exception ex) - { - Debug.WriteLine($"Volume set failed on attempt {attempt + 1}: {ex}"); - } - - await Task.Delay(delayMs).ConfigureAwait(false); + catch { return Task.FromResult(0f); } } - - return false; } - public async Task GetMasterMuteAsync() + public async Task SetMasterVolumeAsync(float volume) { - return await Task.Run(() => - { - lock (_syncLock) - { - if (_isDisposed || _endpointVolume == null) - { - return false; - } + if (_isDisposed || _endpointVolume == null || volume < 0f || volume > 1f) return false; - try - { - var result = _endpointVolume.GetMute(out var mute); - return result == 0 && mute; - } - catch (Exception ex) - { - Debug.WriteLine($"Error getting mute: {ex}"); - return false; - } - } - }).ConfigureAwait(false); + var guid = Guid.Empty; + lock (_syncLock) + { + return _endpointVolume.SetMasterVolumeLevelScalar(volume, ref guid) == 0; + } } - public async Task SetMasterMuteAsync(bool mute) + public Task GetMasterMuteAsync() { - if (_isDisposed || _endpointVolume == null) + lock (_syncLock) { - return false; + if (_isDisposed || _endpointVolume == null) return Task.FromResult(false); + try + { + int hr = _endpointVolume.GetMute(out bool mute); + return Task.FromResult(hr == 0 && mute); + } + catch { return Task.FromResult(false); } } + } - try + public Task SetMasterMuteAsync(bool mute) + { + if (_isDisposed || _endpointVolume == null) return Task.FromResult(false); + + var guid = Guid.Empty; + lock (_syncLock) { - var guid = Guid.Empty; - return await Task.Run(() => _endpointVolume.SetMute(mute, ref guid) == 0).ConfigureAwait(false); - } - catch (Exception ex) - { - Debug.WriteLine($"Error setting mute: {ex}"); - return false; + return Task.FromResult(_endpointVolume.SetMute(mute, ref guid) == 0); } } public async Task> GetAudioSessionsAsync() { - return await Task.Run(() => - { - lock (_syncLock) - { - var sessions = new List(); + var sessions = new List(); + if (_isDisposed || _sessionManager == null) return sessions; - if (_isDisposed || _sessionManager == null) - { - return sessions; - } + lock (_syncLock) + { + try + { + int hr = _sessionManager.GetSessionEnumerator(out var sessionEnum); + if (hr != 0 || sessionEnum == null) return sessions; try { - var result = _sessionManager.GetSessionEnumerator(out var sessionEnum); - if (result != 0 || sessionEnum == null) - { - return sessions; - } - - result = sessionEnum.GetCount(out var count); - if (result != 0) - { - return sessions; - } + hr = sessionEnum.GetCount(out int count); + if (hr != 0) return sessions; for (int i = 0; i < count; i++) { try { - result = sessionEnum.GetSession(i, out var sessionControl); - if (result == 0 && sessionControl != null) + hr = sessionEnum.GetSession(i, out var sessionControl); + if (hr == 0 && sessionControl != null) { var sessionControl2 = GetSessionControl2(sessionControl); if (sessionControl2 != null) { sessions.Add(new AudioSession(sessionControl2, _sessionManager)); - Marshal.ReleaseComObject(sessionControl2); } - - // Release the original sessionControl COM object Marshal.ReleaseComObject(sessionControl); } } - catch - { - // Do nothing - } + catch (Exception ex) { OnException?.Invoke(this, ex); } } } - catch + finally { - // Do nothing + Marshal.ReleaseComObject(sessionEnum); } - - return sessions; } - }); + catch (Exception ex) + { + OnException?.Invoke(this, ex); + } + } + + return sessions; } private IAudioSessionControlExtended GetSessionControl2(object sessionControl) { - if (sessionControl == null) - { - return null; - } + if (sessionControl == null) return null; - var unknownPtr = Marshal.GetIUnknownForObject(sessionControl); + IntPtr unknownPtr = IntPtr.Zero; + IntPtr sessionControl2Ptr = IntPtr.Zero; try { - IntPtr sessionControl2Ptr; - int result = Marshal.QueryInterface(unknownPtr, ref AudioController2Guid, out sessionControl2Ptr); - if (result == 0 && sessionControl2Ptr != IntPtr.Zero) + unknownPtr = Marshal.GetIUnknownForObject(sessionControl); + int hr = Marshal.QueryInterface(unknownPtr, ref AudioController2Guid, out sessionControl2Ptr); + if (hr == 0 && sessionControl2Ptr != IntPtr.Zero) { return (IAudioSessionControlExtended)Marshal.GetObjectForIUnknown(sessionControl2Ptr); } } - catch (Exception ex) - { - Debug.WriteLine($"Error querying sessionControl2: {ex}"); - } + catch (Exception ex) { Debug.WriteLine($"GetSessionControl2 failed: {ex}"); } finally { - Marshal.Release(unknownPtr); + if (sessionControl2Ptr != IntPtr.Zero) Marshal.Release(sessionControl2Ptr); + if (unknownPtr != IntPtr.Zero) Marshal.Release(unknownPtr); } - return null; } @@ -328,17 +273,21 @@ namespace EonaCat.VolumeMixer.Models { lock (_syncLock) { - if (_isDisposed) - { - return; - } - - ComHelper.ReleaseComObject(_endpointVolume); - ComHelper.ReleaseComObject(_sessionManager); - ComHelper.ReleaseComObject(_device); + if (_isDisposed) return; _isDisposed = true; - GC.SuppressFinalize(this); + SafeRelease(ref _endpointVolume); + SafeRelease(ref _sessionManager); + SafeRelease(ref _device); + } + } + + private static void SafeRelease(ref T comObj) where T : class + { + if (comObj != null) + { + try { Marshal.FinalReleaseComObject(comObj); } catch { } + comObj = null; } } } diff --git a/EonaCat.VolumeMixer/Models/AudioSession.cs b/EonaCat.VolumeMixer/Models/AudioSession.cs index b2d9b82..7449868 100644 --- a/EonaCat.VolumeMixer/Models/AudioSession.cs +++ b/EonaCat.VolumeMixer/Models/AudioSession.cs @@ -9,9 +9,9 @@ namespace EonaCat.VolumeMixer.Models { public class AudioSession : IDisposable { - private readonly IAudioSessionControlExtended _sessionControl; + private IAudioSessionControlExtended _sessionControl; private IAudioVolume _audioVolume; - private bool _isDisposed = false; + private bool _isDisposed; private readonly object _syncLock = new(); public string DisplayName { get; private set; } @@ -19,7 +19,7 @@ namespace EonaCat.VolumeMixer.Models public uint ProcessId { get; private set; } public AudioSessionState State { get; private set; } - internal AudioSession(IAudioSessionControlExtended sessionControl, IAudioSessionManager sessionManager) + internal AudioSession(IAudioSessionControlExtended sessionControl, IAudioSessionManager manager) { _sessionControl = sessionControl ?? throw new ArgumentNullException(nameof(sessionControl)); InitializeSimpleAudioVolume(); @@ -30,23 +30,25 @@ namespace EonaCat.VolumeMixer.Models { lock (_syncLock) { + if (_sessionControl == null) return; + + IntPtr unknownPtr = IntPtr.Zero; try { - var sessionControlUnknown = Marshal.GetIUnknownForObject(_sessionControl); - var iidSimpleAudioVolume = typeof(IAudioVolume).GUID; - IntPtr simpleAudioVolumePtr = IntPtr.Zero; + unknownPtr = Marshal.GetIUnknownForObject(_sessionControl); + IntPtr volumePtr = IntPtr.Zero; - int result = Marshal.QueryInterface(sessionControlUnknown, ref iidSimpleAudioVolume, out simpleAudioVolumePtr); - if (result == 0 && simpleAudioVolumePtr != IntPtr.Zero) + var iid = typeof(IAudioVolume).GUID; + int hr = Marshal.QueryInterface(unknownPtr, ref iid, out volumePtr); + if (hr == 0 && volumePtr != IntPtr.Zero) { - _audioVolume = (IAudioVolume)Marshal.GetObjectForIUnknown(simpleAudioVolumePtr); - Marshal.Release(simpleAudioVolumePtr); + _audioVolume = (IAudioVolume)Marshal.GetObjectForIUnknown(volumePtr); } - Marshal.Release(sessionControlUnknown); } - catch + catch { _audioVolume = null; } + finally { - _audioVolume = null; + if (unknownPtr != IntPtr.Zero) Marshal.Release(unknownPtr); } } } @@ -253,19 +255,20 @@ namespace EonaCat.VolumeMixer.Models { lock (_syncLock) { - if (_isDisposed) - { - return; - } - - if (_audioVolume != null) - { - Marshal.ReleaseComObject(_audioVolume); - _audioVolume = null; - } - ComHelper.ReleaseComObject(_sessionControl); - + if (_isDisposed) return; _isDisposed = true; + + SafeRelease(ref _audioVolume); + SafeRelease(ref _sessionControl); + } + } + + private static void SafeRelease(ref T comObj) where T : class + { + if (comObj != null) + { + try { Marshal.FinalReleaseComObject(comObj); } catch { } + comObj = null; } } } diff --git a/EonaCat.VolumeMixer/Models/PropVariant.cs b/EonaCat.VolumeMixer/Models/PropVariant.cs index 5cb979a..c32e098 100644 --- a/EonaCat.VolumeMixer/Models/PropVariant.cs +++ b/EonaCat.VolumeMixer/Models/PropVariant.cs @@ -1,18 +1,46 @@ using System; using System.Runtime.InteropServices; -namespace EonaCat.VolumeMixer.Models +namespace EonaCat.VolumeMixer.Helpers { // 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. - [StructLayout(LayoutKind.Explicit)] - internal struct PropVariant + internal static class PropVariantHelper { - [FieldOffset(0)] public ushort vt; + /// + /// Calls the native PropVariantClear function in ole32.dll to free memory allocated inside a PROPVARIANT. + /// + /// Reference to the PROPVARIANT structure to clear. + /// HRESULT — 0 (S_OK) if successful. + [DllImport("ole32.dll", PreserveSig = true)] + internal static extern int PropVariantClear(ref PROPVARIANT pvar); + } + + /// + /// Managed definition of the native PROPVARIANT structure used by Windows COM APIs. + /// Only includes fields required for strings (VT_LPWSTR) and integers (VT_UI4). + /// + [StructLayout(LayoutKind.Explicit)] + internal struct PROPVARIANT + { + [FieldOffset(0)] public ushort vt; // Variant type [FieldOffset(2)] public ushort wReserved1; [FieldOffset(4)] public ushort wReserved2; [FieldOffset(6)] public ushort wReserved3; - [FieldOffset(8)] public IntPtr data1; - [FieldOffset(16)] public IntPtr data2; + + // Union data starts at offset 8 + [FieldOffset(8)] public IntPtr pointerValue; // For VT_LPWSTR (LPWSTR) + [FieldOffset(8)] public uint uintValue; // For VT_UI4 + [FieldOffset(8)] public int intValue; // For VT_I4 } -} \ No newline at end of file + + /// + /// VARIANT type constants (from WinNT.h). + /// + internal static class VarEnumConstants + { + public const ushort VT_EMPTY = 0; + public const ushort VT_UI4 = 0x13; // Unsigned 4-byte integer + public const ushort VT_LPWSTR = 0x1F; // Wide string (LPWSTR) + } +}