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 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 // 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

@@ -18,9 +18,9 @@
<PackageTags>EonaCat, Audio, Volume, Mixer .NET Standard, Jeroen, Saey</PackageTags> <PackageTags>EonaCat, Audio, Volume, Mixer .NET Standard, Jeroen, Saey</PackageTags>
<PackageReleaseNotes></PackageReleaseNotes> <PackageReleaseNotes></PackageReleaseNotes>
<Description>EonaCat VolumeMixer</Description> <Description>EonaCat VolumeMixer</Description>
<Version>1.0.5</Version> <Version>1.0.6</Version>
<AssemblyVersion>1.0.0.5</AssemblyVersion> <AssemblyVersion>1.0.0.6</AssemblyVersion>
<FileVersion>1.0.0.5</FileVersion> <FileVersion>1.0.0.6</FileVersion>
<PackageIcon>icon.png</PackageIcon> <PackageIcon>icon.png</PackageIcon>
<RepositoryUrl>https://git.saey.me/EonaCat/EonaCat.VolumeMixer</RepositoryUrl> <RepositoryUrl>https://git.saey.me/EonaCat/EonaCat.VolumeMixer</RepositoryUrl>
<RepositoryType>git</RepositoryType> <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;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -13,8 +14,8 @@ namespace EonaCat.VolumeMixer.Interfaces
{ {
int GetCount(out uint cProps); int GetCount(out uint cProps);
int GetAt(uint iProp, out PropertyKey pkey); int GetAt(uint iProp, out PropertyKey pkey);
int GetValue(ref PropertyKey key, out PropVariant pv); int GetValue(ref PropertyKey key, out PROPVARIANT pv);
int SetValue(ref PropertyKey key, ref PropVariant propvar); int SetValue(ref PropertyKey key, ref PROPVARIANT propvar);
int Commit(); int Commit();
} }
} }

View File

@@ -8,17 +8,16 @@ using System.Threading.Tasks;
namespace EonaCat.VolumeMixer.Managers 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 public class VolumeMixerManager : IDisposable
{ {
private const int VT_UI4 = 0x13; private IMultiMediaDeviceEnumerator _deviceEnumerator;
private readonly IMultiMediaDeviceEnumerator _deviceEnumerator; private bool _isDisposed;
private bool _isDisposed = false;
private readonly object _syncLock = new(); 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 PropertyKey PKEY_Device_FriendlyName = new(new Guid("a45c254e-df1c-4efd-8020-67d146a850e0"), 14);
private static readonly PropertyKey PKEY_AudioEndpoint_FormFactor = new PropertyKey
private static PropertyKey PKEY_AudioEndpoint_FormFactor = new PropertyKey
{ {
Fmtid = new Guid("1DA5D803-D492-4EDD-8C23-E0C0FFEE7F0E"), Fmtid = new Guid("1DA5D803-D492-4EDD-8C23-E0C0FFEE7F0E"),
Pid = 0 Pid = 0
@@ -32,7 +31,8 @@ namespace EonaCat.VolumeMixer.Managers
} }
catch (Exception ex) 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(() => return await Task.Run(() =>
{ {
var devices = new List<AudioDevice>(); var devices = new List<AudioDevice>();
if (_isDisposed || _deviceEnumerator == null) return devices;
if (_deviceEnumerator == null)
{
return devices;
}
lock (_syncLock) lock (_syncLock)
{ {
IMultiMediaDeviceCollection collection = null;
IMultiMediaDevice defaultDevice = null;
try try
{ {
var result = _deviceEnumerator.EnumAudioEndpoints(dataFlow, DeviceState.Active, out var deviceCollection); int hr = _deviceEnumerator.EnumAudioEndpoints(dataFlow, DeviceState.Active, out collection);
if (result != 0 || deviceCollection == null) if (hr != 0 || collection == null) return devices;
{
return devices;
}
result = deviceCollection.GetCount(out var count); hr = collection.GetCount(out var count);
if (result != 0) if (hr != 0) return devices;
{
ComHelper.ReleaseComObject(deviceCollection);
return devices;
}
string defaultId = ""; // Get default device
string defaultId = string.Empty;
try try
{ {
result = _deviceEnumerator.GetDefaultAudioEndpoint(dataFlow, Role.Multimedia, out var defaultDevice); hr = _deviceEnumerator.GetDefaultAudioEndpoint(dataFlow, Role.Multimedia, out defaultDevice);
if (result == 0 && defaultDevice != null) if (hr == 0 && defaultDevice != null)
{ {
defaultDevice.GetId(out defaultId); 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++) for (uint i = 0; i < count; i++)
{ {
IMultiMediaDevice device = null;
try try
{ {
result = deviceCollection.Item(i, out var device); if (collection.Item(i, out device) != 0 || device == null) continue;
if (result == 0 && device != null)
{ if (device.GetId(out var id) != 0 || string.IsNullOrEmpty(id)) continue;
result = device.GetId(out var id);
if (result == 0 && !string.IsNullOrEmpty(id)) string name = GetDeviceName(device);
{ DeviceType type = GetDeviceType(device);
var name = GetDeviceName(device);
var type = GetDeviceType(device);
bool isDefault = id == defaultId; 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)); devices.Add(new AudioDevice(device, id, name, isDefault, dataFlow, type));
device = null;
} }
else catch (Exception ex)
{ {
ComHelper.ReleaseComObject(device); OnException?.Invoke(this, ex);
} }
} finally
}
catch
{ {
// 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); if (defaultDevice != null)
}
catch
{ {
// Do nothing Marshal.ReleaseComObject(defaultDevice);
defaultDevice = null;
}
} }
} }
@@ -120,36 +131,28 @@ namespace EonaCat.VolumeMixer.Managers
public async Task<AudioDevice> GetDefaultAudioDeviceAsync(DataFlow dataFlow = DataFlow.Output) public async Task<AudioDevice> GetDefaultAudioDeviceAsync(DataFlow dataFlow = DataFlow.Output)
{ {
if (_isDisposed || _deviceEnumerator == null) return null;
return await Task.Run(() => return await Task.Run(() =>
{ {
if (_deviceEnumerator == null)
{
return null;
}
lock (_syncLock) lock (_syncLock)
{ {
IMultiMediaDevice device = null;
try try
{ {
var result = _deviceEnumerator.GetDefaultAudioEndpoint(dataFlow, Role.Multimedia, out var device); int hr = _deviceEnumerator.GetDefaultAudioEndpoint(dataFlow, Role.Multimedia, out device);
if (result == 0 && device != null) if (hr == 0 && device != null)
{ {
result = device.GetId(out var id); if (device.GetId(out var id) == 0 && !string.IsNullOrEmpty(id))
if (result == 0 && !string.IsNullOrEmpty(id))
{ {
var name = GetDeviceName(device); string name = GetDeviceName(device);
var type = GetDeviceType(device); DeviceType type = GetDeviceType(device);
return new AudioDevice(device, id, name, true, dataFlow, type); return new AudioDevice(device, id, name, true, dataFlow, type);
} }
ComHelper.ReleaseComObject(device);
} }
} }
catch catch (Exception ex) { OnException?.Invoke(this, ex); }
{ finally { SafeRelease(ref device); }
// Do nothing
}
return null; return null;
} }
}); });
@@ -157,26 +160,24 @@ namespace EonaCat.VolumeMixer.Managers
private string GetDeviceName(IMultiMediaDevice device) private string GetDeviceName(IMultiMediaDevice device)
{ {
IPropertyStore store = null;
PROPVARIANT prop = default;
try try
{ {
var result = device.OpenPropertyStore(0, out var propertyStore); if (device.OpenPropertyStore(0, out store) != 0 || store == null) return "Unknown Device";
if (result == 0 && propertyStore != null) if (store.GetValue(ref PKEY_Device_FriendlyName, out prop) == 0 &&
prop.vt == VarEnumConstants.VT_LPWSTR && prop.pointerValue != IntPtr.Zero)
{ {
var propertyKey = PKEY_Device_FriendlyName; string name = Marshal.PtrToStringUni(prop.pointerValue);
result = propertyStore.GetValue(ref propertyKey, out var propVariant); return string.IsNullOrEmpty(name) ? "Unknown Device" : name;
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);
} }
} }
catch catch (Exception ex) { OnException?.Invoke(this, ex); }
finally
{ {
// Do nothing try { PropVariantHelper.PropVariantClear(ref prop); } catch { }
SafeRelease(ref store);
} }
return "Unknown Device"; return "Unknown Device";
@@ -184,63 +185,43 @@ namespace EonaCat.VolumeMixer.Managers
private DeviceType GetDeviceType(IMultiMediaDevice device) private DeviceType GetDeviceType(IMultiMediaDevice device)
{ {
IPropertyStore store = null;
PROPVARIANT prop = default;
try try
{ {
int result = device.OpenPropertyStore(0, out var propertyStore); if (device.OpenPropertyStore(0, out store) != 0 || store == null) return DeviceType.Unknown;
if (result == 0 && propertyStore != null) 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, 1 => DeviceType.Speakers,
2 => DeviceType.LineLevel, 2 => DeviceType.LineLevel,
3 => DeviceType.Headphones, 3 => DeviceType.Headphones,
4 => DeviceType.Microphone, 4 => DeviceType.Microphone,
5 => DeviceType.Headset, 5 => DeviceType.Headset,
6 => DeviceType.Handset, 6 => DeviceType.Handset,
7 => DeviceType.UnknownDigitalPassthrough,
8 => DeviceType.SPDIF, 8 => DeviceType.SPDIF,
9 => DeviceType.DigitalAudioDisplayDevice, 9 => DeviceType.DigitalAudioDisplayDevice,
10 => DeviceType.UnknownFormFactor,
11 => DeviceType.FMRadio,
12 => DeviceType.VideoPhone,
13 => DeviceType.RCA,
14 => DeviceType.Bluetooth, 14 => DeviceType.Bluetooth,
15 => DeviceType.SPDIFOut, 15 => DeviceType.SPDIFOut,
16 => DeviceType.HDMI, 16 => DeviceType.HDMI,
17 => DeviceType.DisplayAudio, 17 => DeviceType.DisplayAudio,
18 => DeviceType.UnknownFormFactor2,
19 => DeviceType.Other, 19 => DeviceType.Other,
_ => DeviceType.Unknown, _ => DeviceType.Unknown
}; };
} }
} }
catch (Exception ex) { OnException?.Invoke(this, ex); }
finally finally
{ {
ComHelper.ReleaseComObject(propertyStore); try { PropVariantHelper.PropVariantClear(ref prop); } catch { }
} SafeRelease(ref store);
}
}
catch
{
// Do nothing
} }
return DeviceType.Unknown; return DeviceType.Unknown;
} }
public async Task<bool> SetSystemVolumeAsync(float volume) public async Task<bool> SetSystemVolumeAsync(float volume)
{ {
if (volume < 0f || volume > 1f) if (volume < 0f || volume > 1f)
@@ -430,34 +411,6 @@ namespace EonaCat.VolumeMixer.Managers
return false; 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() public async Task<List<AudioDevice>> GetMicrophonesAsync()
{ {
return await GetAudioDevicesAsync(DataFlow.Input); return await GetAudioDevicesAsync(DataFlow.Input);
@@ -472,11 +425,18 @@ namespace EonaCat.VolumeMixer.Managers
{ {
lock (_syncLock) lock (_syncLock)
{ {
if (!_isDisposed) if (_isDisposed) return;
{
ComHelper.ReleaseComObject(_deviceEnumerator);
_isDisposed = true; _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 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 public class AudioDevice : IDisposable
{ {
internal Guid AudioController2Guid = new Guid("bfb7ff88-7239-4fc9-8fa2-07c950be9c6d"); internal Guid AudioController2Guid = new("bfb7ff88-7239-4fc9-8fa2-07c950be9c6d");
private readonly IMultiMediaDevice _device; private IMultiMediaDevice _device;
private IAudioEndpointVolume _endpointVolume; private IAudioEndpointVolume _endpointVolume;
private IAudioSessionManager _sessionManager; private IAudioSessionManager _sessionManager;
private bool _isDisposed = false; private bool _isDisposed;
private readonly object _syncLock = new object(); private readonly object _syncLock = new();
public string Id { get; private set; } public string Id { get; }
public string Name { get; private set; } public string Name { get; }
public DeviceType DeviceType { get; private set; } public DeviceType DeviceType { get; }
public bool IsDefault { get; private set; } public bool IsDefault { get; }
public DataFlow DataFlow { get; private set; } public DataFlow DataFlow { get; }
public event EventHandler<Exception> OnException;
internal AudioDevice(IMultiMediaDevice device, string id, string name, bool isDefault, DataFlow dataFlow, DeviceType deviceType) 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; Id = id;
Name = name; Name = name;
IsDefault = isDefault; IsDefault = isDefault;
DataFlow = dataFlow; DataFlow = dataFlow;
DeviceType = deviceType; DeviceType = deviceType;
InitializeEndpointVolume(); InitializeEndpointVolume();
InitializeSessionManager(); InitializeSessionManager();
} }
private void InitializeEndpointVolume() private void InitializeEndpointVolume()
{ {
IntPtr ptr = IntPtr.Zero;
try try
{ {
var audioEndpointGuid = typeof(IAudioEndpointVolume).GUID; var guid = typeof(IAudioEndpointVolume).GUID;
var result = _device.Activate(ref audioEndpointGuid, 0, IntPtr.Zero, out var ptr); int hr = _device.Activate(ref guid, 0, IntPtr.Zero, out ptr);
if (result == 0 && ptr != IntPtr.Zero) if (hr == 0 && ptr != IntPtr.Zero)
{ {
_endpointVolume = ComHelper.GetInterface<IAudioEndpointVolume>(ptr); _endpointVolume = (IAudioEndpointVolume)Marshal.GetObjectForIUnknown(ptr);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.WriteLine($"Failed to initialize endpoint volume: {ex}"); Debug.WriteLine($"InitializeEndpointVolume failed: {ex}");
_endpointVolume = null; _endpointVolume = null;
} }
finally
{
if (ptr != IntPtr.Zero)
{
Marshal.Release(ptr);
}
}
} }
private void InitializeSessionManager() private void InitializeSessionManager()
{ {
IntPtr ptr = IntPtr.Zero;
try try
{ {
var audioSessionGuid = typeof(IAudioSessionManager).GUID; var guid = typeof(IAudioSessionManager).GUID;
var result = _device.Activate(ref audioSessionGuid, 0, IntPtr.Zero, out var ptr); int hr = _device.Activate(ref guid, 0, IntPtr.Zero, out ptr);
if (result == 0 && ptr != IntPtr.Zero) if (hr == 0 && ptr != IntPtr.Zero)
{ {
_sessionManager = ComHelper.GetInterface<IAudioSessionManager>(ptr); _sessionManager = (IAudioSessionManager)Marshal.GetObjectForIUnknown(ptr);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.WriteLine($"Failed to initialize session manager: {ex}"); Debug.WriteLine($"InitializeSessionManager failed: {ex}");
_sessionManager = null; _sessionManager = null;
} }
finally
{
if (ptr != IntPtr.Zero)
{
Marshal.Release(ptr);
}
}
} }
public async Task<float> GetMasterVolumeAsync() public Task<float> GetMasterVolumeAsync()
{
return await Task.Run(() =>
{ {
lock (_syncLock) lock (_syncLock)
{ {
if (_isDisposed || _endpointVolume == null) if (_isDisposed || _endpointVolume == null)
{ {
return 0f; return Task.FromResult(0f);
} }
try try
{ {
var result = _endpointVolume.GetMasterVolumeLevelScalar(out var volume); int hr = _endpointVolume.GetMasterVolumeLevelScalar(out float volume);
return result == 0 ? volume : 0f; return Task.FromResult(hr == 0 ? volume : 0f);
} }
catch (Exception ex) catch { return Task.FromResult(0f); }
{
Debug.WriteLine($"Error getting master volume: {ex}");
return 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) if (_isDisposed || _endpointVolume == null || volume < 0f || volume > 1f) return false;
{
return false;
}
var guid = Guid.Empty; var guid = Guid.Empty;
for (int attempt = 0; attempt <= maxRetries; attempt++)
{
try
{
int result;
lock (_syncLock) lock (_syncLock)
{ {
result = _endpointVolume.SetMasterVolumeLevelScalar(volume, ref guid); return _endpointVolume.SetMasterVolumeLevelScalar(volume, ref guid) == 0;
}
} }
if (result == 0) public Task<bool> GetMasterMuteAsync()
{
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(() =>
{ {
lock (_syncLock) lock (_syncLock)
{ {
if (_isDisposed || _endpointVolume == null) if (_isDisposed || _endpointVolume == null) return Task.FromResult(false);
{
return false;
}
try try
{ {
var result = _endpointVolume.GetMute(out var mute); int hr = _endpointVolume.GetMute(out bool mute);
return result == 0 && mute; return Task.FromResult(hr == 0 && mute);
} }
catch (Exception ex) catch { return Task.FromResult(false); }
{
Debug.WriteLine($"Error getting mute: {ex}");
return false;
} }
} }
}).ConfigureAwait(false);
}
public async Task<bool> SetMasterMuteAsync(bool mute) public Task<bool> SetMasterMuteAsync(bool mute)
{ {
if (_isDisposed || _endpointVolume == null) if (_isDisposed || _endpointVolume == null) return Task.FromResult(false);
{
return false;
}
try
{
var guid = Guid.Empty; var guid = Guid.Empty;
return await Task.Run(() => _endpointVolume.SetMute(mute, ref guid) == 0).ConfigureAwait(false); lock (_syncLock)
}
catch (Exception ex)
{ {
Debug.WriteLine($"Error setting mute: {ex}"); return Task.FromResult(_endpointVolume.SetMute(mute, ref guid) == 0);
return false;
} }
} }
public async Task<List<AudioSession>> GetAudioSessionsAsync() public async Task<List<AudioSession>> GetAudioSessionsAsync()
{ {
return await Task.Run(() => var sessions = new List<AudioSession>();
{ if (_isDisposed || _sessionManager == null) return sessions;
lock (_syncLock) lock (_syncLock)
{ {
var sessions = new List<AudioSession>(); try
if (_isDisposed || _sessionManager == null)
{ {
return sessions; int hr = _sessionManager.GetSessionEnumerator(out var sessionEnum);
} if (hr != 0 || sessionEnum == null) return sessions;
try try
{ {
var result = _sessionManager.GetSessionEnumerator(out var sessionEnum); hr = sessionEnum.GetCount(out int count);
if (result != 0 || sessionEnum == null) if (hr != 0) return sessions;
{
return sessions;
}
result = sessionEnum.GetCount(out var count);
if (result != 0)
{
return sessions;
}
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
try try
{ {
result = sessionEnum.GetSession(i, out var sessionControl); hr = sessionEnum.GetSession(i, out var sessionControl);
if (result == 0 && sessionControl != null) if (hr == 0 && sessionControl != null)
{ {
var sessionControl2 = GetSessionControl2(sessionControl); var sessionControl2 = GetSessionControl2(sessionControl);
if (sessionControl2 != null) if (sessionControl2 != null)
{ {
sessions.Add(new AudioSession(sessionControl2, _sessionManager)); sessions.Add(new AudioSession(sessionControl2, _sessionManager));
Marshal.ReleaseComObject(sessionControl2);
} }
// Release the original sessionControl COM object
Marshal.ReleaseComObject(sessionControl); Marshal.ReleaseComObject(sessionControl);
} }
} }
catch catch (Exception ex) { OnException?.Invoke(this, ex); }
}
}
finally
{ {
// Do nothing Marshal.ReleaseComObject(sessionEnum);
}
}
}
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);
} }
} }
catch (Exception ex) 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; return null;
} }
@@ -328,17 +273,21 @@ namespace EonaCat.VolumeMixer.Models
{ {
lock (_syncLock) lock (_syncLock)
{ {
if (_isDisposed) if (_isDisposed) return;
{
return;
}
ComHelper.ReleaseComObject(_endpointVolume);
ComHelper.ReleaseComObject(_sessionManager);
ComHelper.ReleaseComObject(_device);
_isDisposed = true; _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 public class AudioSession : IDisposable
{ {
private readonly IAudioSessionControlExtended _sessionControl; private IAudioSessionControlExtended _sessionControl;
private IAudioVolume _audioVolume; private IAudioVolume _audioVolume;
private bool _isDisposed = false; private bool _isDisposed;
private readonly object _syncLock = new(); private readonly object _syncLock = new();
public string DisplayName { get; private set; } public string DisplayName { get; private set; }
@@ -19,7 +19,7 @@ namespace EonaCat.VolumeMixer.Models
public uint ProcessId { get; private set; } public uint ProcessId { get; private set; }
public AudioSessionState State { 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)); _sessionControl = sessionControl ?? throw new ArgumentNullException(nameof(sessionControl));
InitializeSimpleAudioVolume(); InitializeSimpleAudioVolume();
@@ -30,23 +30,25 @@ namespace EonaCat.VolumeMixer.Models
{ {
lock (_syncLock) lock (_syncLock)
{ {
if (_sessionControl == null) return;
IntPtr unknownPtr = IntPtr.Zero;
try try
{ {
var sessionControlUnknown = Marshal.GetIUnknownForObject(_sessionControl); unknownPtr = Marshal.GetIUnknownForObject(_sessionControl);
var iidSimpleAudioVolume = typeof(IAudioVolume).GUID; IntPtr volumePtr = IntPtr.Zero;
IntPtr simpleAudioVolumePtr = IntPtr.Zero;
int result = Marshal.QueryInterface(sessionControlUnknown, ref iidSimpleAudioVolume, out simpleAudioVolumePtr); var iid = typeof(IAudioVolume).GUID;
if (result == 0 && simpleAudioVolumePtr != IntPtr.Zero) int hr = Marshal.QueryInterface(unknownPtr, ref iid, out volumePtr);
if (hr == 0 && volumePtr != IntPtr.Zero)
{ {
_audioVolume = (IAudioVolume)Marshal.GetObjectForIUnknown(simpleAudioVolumePtr); _audioVolume = (IAudioVolume)Marshal.GetObjectForIUnknown(volumePtr);
Marshal.Release(simpleAudioVolumePtr);
} }
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) lock (_syncLock)
{ {
if (_isDisposed) if (_isDisposed) return;
{
return;
}
if (_audioVolume != null)
{
Marshal.ReleaseComObject(_audioVolume);
_audioVolume = null;
}
ComHelper.ReleaseComObject(_sessionControl);
_isDisposed = true; _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;
using System.Runtime.InteropServices; 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. // 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. // See the LICENSE file or go to https://EonaCat.com/License for full license details.
[StructLayout(LayoutKind.Explicit)] internal static class PropVariantHelper
internal struct PropVariant
{ {
[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(2)] public ushort wReserved1;
[FieldOffset(4)] public ushort wReserved2; [FieldOffset(4)] public ushort wReserved2;
[FieldOffset(6)] public ushort wReserved3; [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)
} }
} }