Updated
This commit is contained in:
@@ -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:");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
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<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,
|
||||
};
|
||||
}
|
||||
}
|
||||
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<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,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<T>(ref T comObj) where T : class
|
||||
{
|
||||
if (comObj != null)
|
||||
{
|
||||
try { Marshal.FinalReleaseComObject(comObj); } catch { }
|
||||
comObj = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<float> 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<bool> SetMasterVolumeAsync(float volume, int maxRetries = 5, int delayMs = 20)
|
||||
public Task<float> 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<bool> GetMasterMuteAsync()
|
||||
public async Task<bool> 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<bool> SetMasterMuteAsync(bool mute)
|
||||
public Task<bool> 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<bool> 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<List<AudioSession>> GetAudioSessionsAsync()
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
var sessions = new List<AudioSession>();
|
||||
var sessions = new List<AudioSession>();
|
||||
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<T>(ref T comObj) where T : class
|
||||
{
|
||||
if (comObj != null)
|
||||
{
|
||||
try { Marshal.FinalReleaseComObject(comObj); } catch { }
|
||||
comObj = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user