This commit is contained in:
2025-10-08 18:15:19 +02:00
parent faf091ac2c
commit 8b1dd5fae9
8 changed files with 339 additions and 368 deletions

View File

@@ -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:");

View File

@@ -18,9 +18,9 @@
<PackageTags>EonaCat, Audio, Volume, Mixer .NET Standard, Jeroen, Saey</PackageTags>
<PackageReleaseNotes></PackageReleaseNotes>
<Description>EonaCat VolumeMixer</Description>
<Version>1.0.5</Version>
<AssemblyVersion>1.0.0.5</AssemblyVersion>
<FileVersion>1.0.0.5</FileVersion>
<Version>1.0.6</Version>
<AssemblyVersion>1.0.0.6</AssemblyVersion>
<FileVersion>1.0.0.6</FileVersion>
<PackageIcon>icon.png</PackageIcon>
<RepositoryUrl>https://git.saey.me/EonaCat/EonaCat.VolumeMixer</RepositoryUrl>
<RepositoryType>git</RepositoryType>

View File

@@ -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
}
}
}
}
}

View File

@@ -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();
}
}

View File

@@ -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<Exception> 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<AudioDevice>();
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)
{
result = device.GetId(out var id);
if (result == 0 && !string.IsNullOrEmpty(id))
{
var name = GetDeviceName(device);
var type = GetDeviceType(device);
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;
}
else
catch (Exception ex)
{
ComHelper.ReleaseComObject(device);
OnException?.Invoke(this, ex);
}
}
}
catch
finally
{
// Do nothing
if (device != null)
{
Marshal.ReleaseComObject(device);
device = null;
}
}
}
}
catch (Exception ex)
{
OnException?.Invoke(this, ex);
}
finally
{
if (collection != null)
{
Marshal.ReleaseComObject(collection);
collection = null;
}
ComHelper.ReleaseComObject(deviceCollection);
}
catch
if (defaultDevice != null)
{
// Do nothing
Marshal.ReleaseComObject(defaultDevice);
defaultDevice = null;
}
}
}
@@ -120,36 +131,28 @@ namespace EonaCat.VolumeMixer.Managers
public async Task<AudioDevice> 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,
_ => DeviceType.Unknown
};
}
}
catch (Exception ex) { OnException?.Invoke(this, ex); }
finally
{
ComHelper.ReleaseComObject(propertyStore);
}
}
}
catch
{
// Do nothing
try { PropVariantHelper.PropVariantClear(ref prop); } catch { }
SafeRelease(ref store);
}
return DeviceType.Unknown;
}
public async Task<bool> SetSystemVolumeAsync(float volume)
{
if (volume < 0f || volume > 1f)
@@ -430,34 +411,6 @@ namespace EonaCat.VolumeMixer.Managers
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);
@@ -472,12 +425,19 @@ namespace EonaCat.VolumeMixer.Managers
{
lock (_syncLock)
{
if (!_isDisposed)
{
ComHelper.ReleaseComObject(_deviceEnumerator);
if (_isDisposed) return;
_isDisposed = true;
SafeRelease(ref _deviceEnumerator);
}
}
private static void SafeRelease<T>(ref T comObj) where T : class
{
if (comObj != null)
{
try { Marshal.FinalReleaseComObject(comObj); } catch { }
comObj = null;
}
}
}
}

View File

@@ -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<Exception> 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<IAudioEndpointVolume>(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<IAudioSessionManager>(ptr);
_sessionManager = (IAudioSessionManager)Marshal.GetObjectForIUnknown(ptr);
}
}
catch (Exception ex)
{
Debug.WriteLine($"Failed to initialize session manager: {ex}");
Debug.WriteLine($"InitializeSessionManager failed: {ex}");
_sessionManager = null;
}
finally
{
if (ptr != IntPtr.Zero)
{
Marshal.Release(ptr);
}
}
}
public async Task<float> GetMasterVolumeAsync()
{
return await Task.Run(() =>
public Task<float> GetMasterVolumeAsync()
{
lock (_syncLock)
{
if (_isDisposed || _endpointVolume == null)
{
return 0f;
return Task.FromResult(0f);
}
try
{
var result = _endpointVolume.GetMasterVolumeLevelScalar(out var volume);
return result == 0 ? volume : 0f;
int hr = _endpointVolume.GetMasterVolumeLevelScalar(out float volume);
return Task.FromResult(hr == 0 ? volume : 0f);
}
catch (Exception ex)
{
Debug.WriteLine($"Error getting master volume: {ex}");
return 0f;
catch { return Task.FromResult(0f); }
}
}
}).ConfigureAwait(false);
}
public async Task<bool> SetMasterVolumeAsync(float volume, int maxRetries = 5, int delayMs = 20)
public async Task<bool> SetMasterVolumeAsync(float volume)
{
if (_isDisposed || _endpointVolume == null || volume < 0f || volume > 1f)
{
return false;
}
if (_isDisposed || _endpointVolume == null || volume < 0f || volume > 1f) return false;
var guid = Guid.Empty;
for (int attempt = 0; attempt <= maxRetries; attempt++)
{
try
{
int result;
lock (_syncLock)
{
result = _endpointVolume.SetMasterVolumeLevelScalar(volume, ref guid);
return _endpointVolume.SetMasterVolumeLevelScalar(volume, ref guid) == 0;
}
}
if (result == 0)
{
await Task.Delay(delayMs).ConfigureAwait(false);
var currentVolume = await GetMasterVolumeAsync().ConfigureAwait(false);
if (Math.Abs(currentVolume - volume) < 0.01f)
{
return true;
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"Volume set failed on attempt {attempt + 1}: {ex}");
}
await Task.Delay(delayMs).ConfigureAwait(false);
}
return false;
}
public async Task<bool> GetMasterMuteAsync()
{
return await Task.Run(() =>
public Task<bool> GetMasterMuteAsync()
{
lock (_syncLock)
{
if (_isDisposed || _endpointVolume == null)
{
return false;
}
if (_isDisposed || _endpointVolume == null) return Task.FromResult(false);
try
{
var result = _endpointVolume.GetMute(out var mute);
return result == 0 && mute;
int hr = _endpointVolume.GetMute(out bool mute);
return Task.FromResult(hr == 0 && mute);
}
catch (Exception ex)
{
Debug.WriteLine($"Error getting mute: {ex}");
return false;
catch { return Task.FromResult(false); }
}
}
}).ConfigureAwait(false);
}
public async Task<bool> SetMasterMuteAsync(bool mute)
public Task<bool> SetMasterMuteAsync(bool mute)
{
if (_isDisposed || _endpointVolume == null)
{
return false;
}
if (_isDisposed || _endpointVolume == null) return Task.FromResult(false);
try
{
var guid = Guid.Empty;
return await Task.Run(() => _endpointVolume.SetMute(mute, ref guid) == 0).ConfigureAwait(false);
}
catch (Exception ex)
lock (_syncLock)
{
Debug.WriteLine($"Error setting mute: {ex}");
return false;
return Task.FromResult(_endpointVolume.SetMute(mute, ref guid) == 0);
}
}
public async Task<List<AudioSession>> GetAudioSessionsAsync()
{
return await Task.Run(() =>
{
var sessions = new List<AudioSession>();
if (_isDisposed || _sessionManager == null) return sessions;
lock (_syncLock)
{
var sessions = new List<AudioSession>();
if (_isDisposed || _sessionManager == null)
try
{
return sessions;
}
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
catch (Exception ex) { OnException?.Invoke(this, ex); }
}
}
finally
{
// Do nothing
}
}
}
catch
{
// Do nothing
}
return sessions;
}
});
}
private IAudioSessionControlExtended GetSessionControl2(object sessionControl)
{
if (sessionControl == null)
{
return null;
}
var unknownPtr = Marshal.GetIUnknownForObject(sessionControl);
try
{
IntPtr sessionControl2Ptr;
int result = Marshal.QueryInterface(unknownPtr, ref AudioController2Guid, out sessionControl2Ptr);
if (result == 0 && sessionControl2Ptr != IntPtr.Zero)
{
return (IAudioSessionControlExtended)Marshal.GetObjectForIUnknown(sessionControl2Ptr);
Marshal.ReleaseComObject(sessionEnum);
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error querying sessionControl2: {ex}");
OnException?.Invoke(this, ex);
}
finally
{
Marshal.Release(unknownPtr);
}
return sessions;
}
private IAudioSessionControlExtended GetSessionControl2(object sessionControl)
{
if (sessionControl == null) return null;
IntPtr unknownPtr = IntPtr.Zero;
IntPtr sessionControl2Ptr = IntPtr.Zero;
try
{
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($"GetSessionControl2 failed: {ex}"); }
finally
{
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<T>(ref T comObj) where T : class
{
if (comObj != null)
{
try { Marshal.FinalReleaseComObject(comObj); } catch { }
comObj = null;
}
}
}

View File

@@ -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<T>(ref T comObj) where T : class
{
if (comObj != null)
{
try { Marshal.FinalReleaseComObject(comObj); } catch { }
comObj = null;
}
}
}

View File

@@ -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;
/// <summary>
/// Calls the native PropVariantClear function in ole32.dll to free memory allocated inside a PROPVARIANT.
/// </summary>
/// <param name="pvar">Reference to the PROPVARIANT structure to clear.</param>
/// <returns>HRESULT — 0 (S_OK) if successful.</returns>
[DllImport("ole32.dll", PreserveSig = true)]
internal static extern int PropVariantClear(ref PROPVARIANT pvar);
}
/// <summary>
/// Managed definition of the native PROPVARIANT structure used by Windows COM APIs.
/// Only includes fields required for strings (VT_LPWSTR) and integers (VT_UI4).
/// </summary>
[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
}
/// <summary>
/// VARIANT type constants (from WinNT.h).
/// </summary>
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)
}
}