diff --git a/EonaCat.VolumeMixer.Tester.WPF/App.xaml b/EonaCat.VolumeMixer.Tester.WPF/App.xaml
new file mode 100644
index 0000000..4739297
--- /dev/null
+++ b/EonaCat.VolumeMixer.Tester.WPF/App.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/EonaCat.VolumeMixer.Tester.WPF/App.xaml.cs b/EonaCat.VolumeMixer.Tester.WPF/App.xaml.cs
new file mode 100644
index 0000000..0c89563
--- /dev/null
+++ b/EonaCat.VolumeMixer.Tester.WPF/App.xaml.cs
@@ -0,0 +1,14 @@
+using System.Configuration;
+using System.Data;
+using System.Windows;
+
+namespace EonaCat.VolumeMixer.Tester.WPF
+{
+ ///
+ /// Interaction logic for App.xaml
+ ///
+ public partial class App : Application
+ {
+ }
+
+}
diff --git a/EonaCat.VolumeMixer.Tester.WPF/AssemblyInfo.cs b/EonaCat.VolumeMixer.Tester.WPF/AssemblyInfo.cs
new file mode 100644
index 0000000..b0ec827
--- /dev/null
+++ b/EonaCat.VolumeMixer.Tester.WPF/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
diff --git a/EonaCat.VolumeMixer.Tester.WPF/Converter/VolumeToPercentageConverter.cs b/EonaCat.VolumeMixer.Tester.WPF/Converter/VolumeToPercentageConverter.cs
new file mode 100644
index 0000000..3c1a2b8
--- /dev/null
+++ b/EonaCat.VolumeMixer.Tester.WPF/Converter/VolumeToPercentageConverter.cs
@@ -0,0 +1,22 @@
+using System.Globalization;
+using System.Windows.Data;
+
+namespace EonaCat.VolumeMixer.Tester.WPF.Converter
+{
+ public class VolumeToPercentageConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is float volume)
+ {
+ return $"{Math.Round(volume * 100)}%";
+ }
+ return "0%";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/EonaCat.VolumeMixer.Tester.WPF/EonaCat.VolumeMixer.Tester.WPF.csproj b/EonaCat.VolumeMixer.Tester.WPF/EonaCat.VolumeMixer.Tester.WPF.csproj
new file mode 100644
index 0000000..d68f788
--- /dev/null
+++ b/EonaCat.VolumeMixer.Tester.WPF/EonaCat.VolumeMixer.Tester.WPF.csproj
@@ -0,0 +1,25 @@
+
+
+
+ WinExe
+ net8.0-windows
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/EonaCat.VolumeMixer.Tester.WPF/MainWindow.xaml b/EonaCat.VolumeMixer.Tester.WPF/MainWindow.xaml
new file mode 100644
index 0000000..6f6211c
--- /dev/null
+++ b/EonaCat.VolumeMixer.Tester.WPF/MainWindow.xaml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/EonaCat.VolumeMixer.Tester.WPF/MainWindow.xaml.cs b/EonaCat.VolumeMixer.Tester.WPF/MainWindow.xaml.cs
new file mode 100644
index 0000000..16170b1
--- /dev/null
+++ b/EonaCat.VolumeMixer.Tester.WPF/MainWindow.xaml.cs
@@ -0,0 +1,245 @@
+using EonaCat.VolumeMixer.Managers;
+using EonaCat.VolumeMixer.Models;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media.Imaging;
+using System.Windows.Threading;
+
+namespace EonaCat.VolumeMixer.Tester.WPF
+{
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class MainWindow : Window
+ {
+ public ObservableCollection Devices { get; } = new();
+ public ObservableCollection Sessions { get; } = new();
+ private VolumeMixerManager _manager;
+ private AudioDevice _currentDevice;
+ private readonly DispatcherTimer _pollTimer = new() { Interval = TimeSpan.FromMilliseconds(250) };
+
+ private readonly DispatcherTimer _refreshTimer = new() { Interval = TimeSpan.FromSeconds(10) };
+
+ public MainWindow()
+ {
+ InitializeComponent();
+ DeviceSelector.ItemsSource = Devices;
+ SessionList.ItemsSource = Sessions;
+ _refreshTimer.Tick += (s, e) => RefreshSessions().ConfigureAwait(false);
+ _pollTimer.Tick += PollTimer_Tick;
+ LoadDevices();
+ }
+
+ private async void PollTimer_Tick(object sender, EventArgs e)
+ {
+ foreach (var sessionVm in Sessions)
+ {
+ await sessionVm.PollRefreshAsync();
+ }
+ }
+
+ private async void LoadDevices()
+ {
+ _manager = new VolumeMixerManager();
+ var devices = await _manager.GetAudioDevicesAsync(DataFlow.All);
+ Devices.Clear();
+ foreach (var device in devices)
+ {
+ Devices.Add(new AudioDeviceViewModel(device));
+ }
+
+ var defaultDevice = await _manager.GetDefaultAudioDeviceAsync();
+ DeviceSelector.SelectedItem = Devices.FirstOrDefault(d => d.Id == defaultDevice.Id);
+ }
+
+ private async void DeviceSelector_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (DeviceSelector.SelectedItem is not AudioDeviceViewModel selectedVm)
+ {
+ _refreshTimer.Stop();
+ _pollTimer.Stop();
+ return;
+ }
+
+ _currentDevice = selectedVm.Device;
+ await RefreshSessions();
+ _refreshTimer.Start();
+ _pollTimer.Start();
+ }
+
+ private async Task RefreshSessions()
+ {
+ if (_currentDevice == null)
+ {
+ return;
+ }
+
+ var sessions = await _currentDevice.GetAudioSessionsAsync();
+ Sessions.Clear();
+ foreach (var session in sessions)
+ {
+ var vm = new AudioSessionViewModel(session);
+ await vm.RefreshAsync();
+ Sessions.Add(vm);
+ }
+ }
+
+ private void Slider_PreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
+ {
+ if (((FrameworkElement)sender).DataContext is AudioSessionViewModel vm)
+ {
+ vm.IsUserChangingVolume = true;
+ }
+ }
+
+ private void Slider_PreviewMouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
+ {
+ if (((FrameworkElement)sender).DataContext is AudioSessionViewModel vm)
+ {
+ vm.IsUserChangingVolume = false;
+ _ = vm.RefreshAsync();
+ }
+ }
+
+ private void Slider_PreviewTouchDown(object sender, System.Windows.Input.TouchEventArgs e)
+ {
+ if (((FrameworkElement)sender).DataContext is AudioSessionViewModel vm)
+ {
+ vm.IsUserChangingVolume = true;
+ }
+ }
+
+ private void Slider_PreviewTouchUp(object sender, System.Windows.Input.TouchEventArgs e)
+ {
+ if (((FrameworkElement)sender).DataContext is AudioSessionViewModel vm)
+ {
+ vm.IsUserChangingVolume = false;
+ _ = vm.RefreshAsync();
+ }
+ }
+ }
+
+ public class AudioDeviceViewModel
+ {
+ public AudioDevice Device { get; }
+ public string Display => Device.Name;
+ public string Id => Device.Id;
+ public BitmapImage Icon => new BitmapImage(new Uri("pack://application:,,,/Resources/" + (Device.DeviceType == DeviceType.Microphone ? "mic.png" : "speaker.png")));
+
+ public AudioDeviceViewModel(AudioDevice device)
+ {
+ Device = device;
+ }
+ }
+
+ public class AudioSessionViewModel : INotifyPropertyChanged
+ {
+ private readonly AudioSession _session;
+ private float _volume;
+ private bool _isMuted;
+
+ public string DisplayName => _session.DisplayName;
+
+ private bool _isUserChangingVolume;
+
+ public bool IsUserChangingVolume
+ {
+ get => _isUserChangingVolume;
+ set
+ {
+ if (_isUserChangingVolume != value)
+ {
+ _isUserChangingVolume = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+
+ public float Volume
+ {
+ get => _volume;
+ set
+ {
+ if (Math.Abs(_volume - value) > 0.01)
+ {
+ value = Math.Clamp(value, 0, 1);
+
+ if (_volume == value)
+ {
+ return;
+ }
+
+ _volume = value;
+ OnPropertyChanged();
+ _ = SetVolumeSafeAsync(value);
+ }
+ }
+ }
+
+ private async Task SetVolumeSafeAsync(float value)
+ {
+ try
+ {
+ await _session.SetVolumeAsync(value);
+ }
+ catch
+ {
+ // Do nothing
+ }
+ }
+
+
+ public bool IsMuted
+ {
+ get => _isMuted;
+ set
+ {
+ if (_isMuted != value)
+ {
+ _isMuted = value;
+ _ = _session.SetMuteAsync(value);
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public async Task PollRefreshAsync()
+ {
+ if (!IsUserChangingVolume)
+ {
+ float currentVolume = await _session.GetVolumeAsync();
+ if (Math.Abs(currentVolume - _volume) > 0.01)
+ {
+ _volume = currentVolume;
+ OnPropertyChanged(nameof(Volume));
+ }
+
+ bool currentMute = await _session.GetMuteAsync();
+ if (currentMute != _isMuted)
+ {
+ _isMuted = currentMute;
+ OnPropertyChanged(nameof(IsMuted));
+ }
+ }
+ }
+
+
+ public AudioSessionViewModel(AudioSession session)
+ {
+ _session = session;
+ }
+
+ public async Task RefreshAsync()
+ {
+ Volume = await _session.GetVolumeAsync();
+ IsMuted = await _session.GetMuteAsync();
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+ protected void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
\ No newline at end of file
diff --git a/EonaCat.VolumeMixer.Tester.WPF/Resources/mic.png b/EonaCat.VolumeMixer.Tester.WPF/Resources/mic.png
new file mode 100644
index 0000000..7ed7d52
Binary files /dev/null and b/EonaCat.VolumeMixer.Tester.WPF/Resources/mic.png differ
diff --git a/EonaCat.VolumeMixer.Tester.WPF/Resources/speaker.png b/EonaCat.VolumeMixer.Tester.WPF/Resources/speaker.png
new file mode 100644
index 0000000..f4ced9f
Binary files /dev/null and b/EonaCat.VolumeMixer.Tester.WPF/Resources/speaker.png differ
diff --git a/EonaCat.VolumeMixer.Tester/Program.cs b/EonaCat.VolumeMixer.Tester/Program.cs
index dc69901..8afeb67 100644
--- a/EonaCat.VolumeMixer.Tester/Program.cs
+++ b/EonaCat.VolumeMixer.Tester/Program.cs
@@ -41,7 +41,17 @@ class Program
foreach (var session in sessions)
{
Console.WriteLine($"- {session.DisplayName} ({await session.GetProcessNameAsync()})");
- Console.WriteLine($" Volume: {await session.GetVolumeAsync():P0}");
+
+ if ((await session.GetProcessNameAsync()).Equals("msedge", StringComparison.OrdinalIgnoreCase))
+ {
+ Console.WriteLine($" Current Volume: {await session.GetVolumeAsync():P0}");
+ await session.SetVolumeAsync(1f).ConfigureAwait(false);
+ Console.WriteLine($" Set to Volume: {await session.GetVolumeAsync():P0}");
+ }
+ else
+ {
+ Console.WriteLine($" Volume: {await session.GetVolumeAsync():P0}");
+ }
Console.WriteLine($" Muted: {await session.GetMuteAsync()}");
session.Dispose();
}
diff --git a/EonaCat.VolumeMixer.sln b/EonaCat.VolumeMixer.sln
index cc3721b..772823b 100644
--- a/EonaCat.VolumeMixer.sln
+++ b/EonaCat.VolumeMixer.sln
@@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EonaCat.VolumeMixer", "Eona
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.VolumeMixer.Tester", "EonaCat.VolumeMixer.Tester\EonaCat.VolumeMixer.Tester.csproj", "{9156F465-62F7-BA83-40E6-F4FD7F0AA6A2}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.VolumeMixer.Tester.WPF", "EonaCat.VolumeMixer.Tester.WPF\EonaCat.VolumeMixer.Tester.WPF.csproj", "{74CBAEB9-70E4-468B-821C-0D52D58DEE84}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -21,6 +23,10 @@ Global
{9156F465-62F7-BA83-40E6-F4FD7F0AA6A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9156F465-62F7-BA83-40E6-F4FD7F0AA6A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9156F465-62F7-BA83-40E6-F4FD7F0AA6A2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {74CBAEB9-70E4-468B-821C-0D52D58DEE84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {74CBAEB9-70E4-468B-821C-0D52D58DEE84}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {74CBAEB9-70E4-468B-821C-0D52D58DEE84}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {74CBAEB9-70E4-468B-821C-0D52D58DEE84}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/EonaCat.VolumeMixer/DeviceType.cs b/EonaCat.VolumeMixer/DeviceType.cs
new file mode 100644
index 0000000..23c8894
--- /dev/null
+++ b/EonaCat.VolumeMixer/DeviceType.cs
@@ -0,0 +1,28 @@
+namespace EonaCat.VolumeMixer
+{
+ // 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 enum DeviceType
+ {
+ Unknown = 0,
+ Speakers = 1,
+ LineLevel = 2,
+ Headphones = 3,
+ Microphone = 4,
+ Headset = 5,
+ Handset = 6,
+ UnknownDigitalPassthrough = 7,
+ SPDIF = 8,
+ DigitalAudioDisplayDevice = 9,
+ UnknownFormFactor = 10,
+ FMRadio = 11,
+ VideoPhone = 12,
+ RCA = 13,
+ Bluetooth = 14,
+ SPDIFOut = 15,
+ HDMI = 16,
+ DisplayAudio = 17,
+ UnknownFormFactor2 = 18,
+ Other = 19,
+ }
+}
diff --git a/EonaCat.VolumeMixer/Managers/VolumeMixerManager.cs b/EonaCat.VolumeMixer/Managers/VolumeMixerManager.cs
index e1c576c..7e8dadb 100644
--- a/EonaCat.VolumeMixer/Managers/VolumeMixerManager.cs
+++ b/EonaCat.VolumeMixer/Managers/VolumeMixerManager.cs
@@ -12,11 +12,17 @@ namespace EonaCat.VolumeMixer.Managers
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
public class VolumeMixerManager : IDisposable
{
+ private const int VT_UI4 = 0x13;
private readonly IMultiMediaDeviceEnumerator _deviceEnumerator;
private bool _isDisposed = false;
private readonly object _syncLock = new();
private static readonly PropertyKey PKEY_Device_FriendlyName = new PropertyKey(new Guid("a45c254e-df1c-4efd-8020-67d146a850e0"), 14);
+ private static readonly PropertyKey PKEY_AudioEndpoint_FormFactor = new PropertyKey
+ {
+ Fmtid = new Guid("1DA5D803-D492-4EDD-8C23-E0C0FFEE7F0E"),
+ Pid = 0
+ };
public VolumeMixerManager()
{
@@ -30,7 +36,6 @@ namespace EonaCat.VolumeMixer.Managers
}
}
- // --- Get Devices ---
public async Task> GetAudioDevicesAsync(DataFlow dataFlow = DataFlow.Output)
{
return await Task.Run(() =>
@@ -38,7 +43,9 @@ namespace EonaCat.VolumeMixer.Managers
var devices = new List();
if (_deviceEnumerator == null)
+ {
return devices;
+ }
lock (_syncLock)
{
@@ -46,7 +53,9 @@ namespace EonaCat.VolumeMixer.Managers
{
var result = _deviceEnumerator.EnumAudioEndpoints(dataFlow, DeviceState.Active, out var deviceCollection);
if (result != 0 || deviceCollection == null)
+ {
return devices;
+ }
result = deviceCollection.GetCount(out var count);
if (result != 0)
@@ -81,8 +90,9 @@ namespace EonaCat.VolumeMixer.Managers
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));
+ devices.Add(new AudioDevice(device, id, name, isDefault, dataFlow, type));
}
else
{
@@ -92,7 +102,7 @@ namespace EonaCat.VolumeMixer.Managers
}
catch
{
- // Skip individual device on error
+ // Do nothing
}
}
@@ -100,7 +110,7 @@ namespace EonaCat.VolumeMixer.Managers
}
catch
{
- // Ignore all and return partial/empty result
+ // Do nothing
}
}
@@ -108,13 +118,14 @@ namespace EonaCat.VolumeMixer.Managers
});
}
- // --- Get Default Device ---
public async Task GetDefaultAudioDeviceAsync(DataFlow dataFlow = DataFlow.Output)
{
return await Task.Run(() =>
{
if (_deviceEnumerator == null)
+ {
return null;
+ }
lock (_syncLock)
{
@@ -127,7 +138,8 @@ namespace EonaCat.VolumeMixer.Managers
if (result == 0 && !string.IsNullOrEmpty(id))
{
var name = GetDeviceName(device);
- return new AudioDevice(device, id, name, true, dataFlow);
+ var type = GetDeviceType(device);
+ return new AudioDevice(device, id, name, true, dataFlow, type);
}
ComHelper.ReleaseComObject(device);
@@ -135,7 +147,7 @@ namespace EonaCat.VolumeMixer.Managers
}
catch
{
- // Ignore and return null
+ // Do nothing
}
return null;
@@ -152,9 +164,9 @@ namespace EonaCat.VolumeMixer.Managers
{
var propertyKey = PKEY_Device_FriendlyName;
result = propertyStore.GetValue(ref propertyKey, out var propVariant);
- if (result == 0 && propVariant.data != IntPtr.Zero)
+ if (result == 0 && propVariant.data1 != IntPtr.Zero)
{
- string name = Marshal.PtrToStringUni(propVariant.data);
+ string name = Marshal.PtrToStringUni(propVariant.data1);
ComHelper.ReleaseComObject(propertyStore);
return !string.IsNullOrEmpty(name) ? name : "Unknown Device";
}
@@ -164,23 +176,84 @@ namespace EonaCat.VolumeMixer.Managers
}
catch
{
- // Ignore and fall through
+ // Do nothing
}
return "Unknown Device";
}
- // --- System (Output) Volume and Mute ---
+ private DeviceType GetDeviceType(IMultiMediaDevice device)
+ {
+ try
+ {
+ int result = device.OpenPropertyStore(0, out var propertyStore);
+ if (result == 0 && propertyStore != null)
+ {
+ try
+ {
+ 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);
+ }
+ }
+ }
+ catch
+ {
+ // Do nothing
+ }
+
+ return DeviceType.Unknown;
+ }
+
+
public async Task SetSystemVolumeAsync(float volume)
{
if (volume < 0f || volume > 1f)
+ {
return false;
+ }
AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output);
if (defaultDevice == null)
+ {
return false;
+ }
try
{
@@ -197,7 +270,9 @@ namespace EonaCat.VolumeMixer.Managers
AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output);
if (defaultDevice == null)
+ {
return 0f;
+ }
try
{
@@ -214,7 +289,9 @@ namespace EonaCat.VolumeMixer.Managers
AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output);
if (defaultDevice == null)
+ {
return false;
+ }
try
{
@@ -231,7 +308,9 @@ namespace EonaCat.VolumeMixer.Managers
AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output);
if (defaultDevice == null)
+ {
return false;
+ }
try
{
@@ -243,17 +322,19 @@ namespace EonaCat.VolumeMixer.Managers
}
}
- // --- Microphone (Input) Volume and Mute ---
-
public async Task SetMicrophoneVolumeAsync(float volume)
{
if (volume < 0f || volume > 1f)
+ {
return false;
+ }
AudioDevice defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input);
if (defaultMic == null)
+ {
return false;
+ }
try
{
@@ -270,7 +351,9 @@ namespace EonaCat.VolumeMixer.Managers
AudioDevice defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input);
if (defaultMic == null)
+ {
return 0f;
+ }
try
{
@@ -287,7 +370,9 @@ namespace EonaCat.VolumeMixer.Managers
AudioDevice defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input);
if (defaultMic == null)
+ {
return false;
+ }
try
{
@@ -304,7 +389,9 @@ namespace EonaCat.VolumeMixer.Managers
AudioDevice defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input);
if (defaultMic == null)
+ {
return false;
+ }
try
{
@@ -319,7 +406,9 @@ namespace EonaCat.VolumeMixer.Managers
public async Task SetMicrophoneVolumeByNameAsync(string microphoneName, float volume)
{
if (string.IsNullOrWhiteSpace(microphoneName) || volume < 0f || volume > 1f)
+ {
return false;
+ }
List microphones = await GetAudioDevicesAsync(DataFlow.Input);
@@ -341,8 +430,6 @@ namespace EonaCat.VolumeMixer.Managers
return false;
}
- // --- Audio Sessions (All devices) ---
-
public async Task> GetAllActiveSessionsAsync()
{
return await Task.Run(async () =>
@@ -359,7 +446,7 @@ namespace EonaCat.VolumeMixer.Managers
}
catch
{
- // Skip device on error
+ // Do nothing
}
finally
{
@@ -381,8 +468,6 @@ namespace EonaCat.VolumeMixer.Managers
return await GetDefaultAudioDeviceAsync(DataFlow.Input);
}
- // --- Dispose ---
-
public void Dispose()
{
lock (_syncLock)
diff --git a/EonaCat.VolumeMixer/Models/AudioDevice.cs b/EonaCat.VolumeMixer/Models/AudioDevice.cs
index c950a29..519a443 100644
--- a/EonaCat.VolumeMixer/Models/AudioDevice.cs
+++ b/EonaCat.VolumeMixer/Models/AudioDevice.cs
@@ -2,6 +2,7 @@
using EonaCat.VolumeMixer.Interfaces;
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
@@ -20,16 +21,18 @@ namespace EonaCat.VolumeMixer.Models
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; }
- internal AudioDevice(IMultiMediaDevice device, string id, string name, bool isDefault, DataFlow dataFlow)
+ internal AudioDevice(IMultiMediaDevice device, string id, string name, bool isDefault, DataFlow dataFlow, DeviceType deviceType)
{
_device = device;
Id = id;
Name = name;
IsDefault = isDefault;
DataFlow = dataFlow;
+ DeviceType = deviceType;
InitializeEndpointVolume();
InitializeSessionManager();
}
@@ -45,8 +48,9 @@ namespace EonaCat.VolumeMixer.Models
_endpointVolume = ComHelper.GetInterface(ptr);
}
}
- catch
+ catch (Exception ex)
{
+ Debug.WriteLine($"Failed to initialize endpoint volume: {ex}");
_endpointVolume = null;
}
}
@@ -62,8 +66,9 @@ namespace EonaCat.VolumeMixer.Models
_sessionManager = ComHelper.GetInterface(ptr);
}
}
- catch
+ catch (Exception ex)
{
+ Debug.WriteLine($"Failed to initialize session manager: {ex}");
_sessionManager = null;
}
}
@@ -84,52 +89,50 @@ namespace EonaCat.VolumeMixer.Models
var result = _endpointVolume.GetMasterVolumeLevelScalar(out var volume);
return result == 0 ? volume : 0f;
}
- catch
+ catch (Exception ex)
{
+ Debug.WriteLine($"Error getting master volume: {ex}");
return 0f;
}
}
- });
+ }).ConfigureAwait(false);
}
public async Task SetMasterVolumeAsync(float volume, int maxRetries = 5, int delayMs = 20)
{
+ if (_isDisposed || _endpointVolume == null || volume < 0f || volume > 1f)
+ {
+ return false;
+ }
+
+ var guid = Guid.Empty;
+
for (int attempt = 0; attempt <= maxRetries; attempt++)
{
- lock (_syncLock)
+ try
{
- if (_isDisposed || _endpointVolume == null || volume < 0f || volume > 1f)
+ int result;
+ lock (_syncLock)
{
- return false;
+ result = _endpointVolume.SetMasterVolumeLevelScalar(volume, ref guid);
}
- try
+ if (result == 0)
{
- var guid = Guid.Empty;
- var 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)
{
- // Failed to set, will retry
- continue;
+ return true;
}
}
- catch
- {
- // Retry on exception
- continue;
- }
}
-
- await Task.Delay(delayMs);
-
- var currentVolume = await GetMasterVolumeAsync();
-
- if (Math.Abs(currentVolume - volume) < 0.01f)
+ catch (Exception ex)
{
- return true;
+ Debug.WriteLine($"Volume set failed on attempt {attempt + 1}: {ex}");
}
- await Task.Delay(delayMs);
+ await Task.Delay(delayMs).ConfigureAwait(false);
}
return false;
@@ -149,35 +152,32 @@ namespace EonaCat.VolumeMixer.Models
try
{
var result = _endpointVolume.GetMute(out var mute);
- return result == 0 ? mute : false;
+ return result == 0 && mute;
}
- catch
+ catch (Exception ex)
{
+ Debug.WriteLine($"Error getting mute: {ex}");
return false;
}
}
- });
+ }).ConfigureAwait(false);
}
public async Task SetMasterMuteAsync(bool mute)
{
- IAudioEndpointVolume endpointVolumeCopy;
- lock (_syncLock)
+ if (_isDisposed || _endpointVolume == null)
{
- if (_isDisposed || _endpointVolume == null)
- {
- return false;
- }
- endpointVolumeCopy = _endpointVolume;
+ return false;
}
try
{
var guid = Guid.Empty;
- return await Task.Run(() => endpointVolumeCopy.SetMute(mute, ref guid) == 0);
+ return await Task.Run(() => _endpointVolume.SetMute(mute, ref guid) == 0).ConfigureAwait(false);
}
- catch
+ catch (Exception ex)
{
+ Debug.WriteLine($"Error setting mute: {ex}");
return false;
}
}
@@ -229,13 +229,13 @@ namespace EonaCat.VolumeMixer.Models
}
catch
{
- // Skip on error
+ // Do nothing
}
}
}
catch
{
- // Return empty
+ // Do nothing
}
return sessions;
@@ -246,7 +246,9 @@ namespace EonaCat.VolumeMixer.Models
private IAudioSessionControlExtended GetSessionControl2(object sessionControl)
{
if (sessionControl == null)
+ {
return null;
+ }
var unknownPtr = Marshal.GetIUnknownForObject(sessionControl);
try
@@ -255,15 +257,18 @@ namespace EonaCat.VolumeMixer.Models
int result = Marshal.QueryInterface(unknownPtr, ref AudioController2Guid, out sessionControl2Ptr);
if (result == 0 && sessionControl2Ptr != IntPtr.Zero)
{
- var sessionControl2 = (IAudioSessionControlExtended)Marshal.GetObjectForIUnknown(sessionControl2Ptr);
- Marshal.Release(sessionControl2Ptr);
- return sessionControl2;
+ return (IAudioSessionControlExtended)Marshal.GetObjectForIUnknown(sessionControl2Ptr);
}
}
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error querying sessionControl2: {ex}");
+ }
finally
{
Marshal.Release(unknownPtr);
}
+
return null;
}
@@ -280,16 +285,16 @@ namespace EonaCat.VolumeMixer.Models
try
{
var guid = Guid.Empty;
- var result = _endpointVolume.StepUp(ref guid);
- success = result == 0;
+ success = _endpointVolume.StepUp(ref guid) == 0;
}
- catch
+ catch (Exception ex)
{
+ Debug.WriteLine($"Error stepping up: {ex}");
success = false;
}
}
- await Task.Delay(delayMs);
+ await Task.Delay(delayMs).ConfigureAwait(false);
return success;
}
@@ -306,16 +311,16 @@ namespace EonaCat.VolumeMixer.Models
try
{
var guid = Guid.Empty;
- var result = _endpointVolume.StepDown(ref guid);
- success = result == 0;
+ success = _endpointVolume.StepDown(ref guid) == 0;
}
- catch
+ catch (Exception ex)
{
+ Debug.WriteLine($"Error stepping down: {ex}");
success = false;
}
}
- await Task.Delay(delayMs);
+ await Task.Delay(delayMs).ConfigureAwait(false);
return success;
}
@@ -332,6 +337,8 @@ namespace EonaCat.VolumeMixer.Models
ComHelper.ReleaseComObject(_sessionManager);
ComHelper.ReleaseComObject(_device);
_isDisposed = true;
+
+ GC.SuppressFinalize(this);
}
}
}
diff --git a/EonaCat.VolumeMixer/Models/AudioSession.cs b/EonaCat.VolumeMixer/Models/AudioSession.cs
index db0d648..b2d9b82 100644
--- a/EonaCat.VolumeMixer/Models/AudioSession.cs
+++ b/EonaCat.VolumeMixer/Models/AudioSession.cs
@@ -12,7 +12,7 @@ namespace EonaCat.VolumeMixer.Models
private readonly IAudioSessionControlExtended _sessionControl;
private IAudioVolume _audioVolume;
private bool _isDisposed = false;
- private readonly object _syncLock = new object();
+ private readonly object _syncLock = new();
public string DisplayName { get; private set; }
public string IconPath { get; private set; }
@@ -21,9 +21,9 @@ namespace EonaCat.VolumeMixer.Models
internal AudioSession(IAudioSessionControlExtended sessionControl, IAudioSessionManager sessionManager)
{
- _sessionControl = sessionControl;
- LoadSessionInfo();
+ _sessionControl = sessionControl ?? throw new ArgumentNullException(nameof(sessionControl));
InitializeSimpleAudioVolume();
+ LoadSessionInfo();
}
private void InitializeSimpleAudioVolume()
@@ -42,7 +42,6 @@ namespace EonaCat.VolumeMixer.Models
_audioVolume = (IAudioVolume)Marshal.GetObjectForIUnknown(simpleAudioVolumePtr);
Marshal.Release(simpleAudioVolumePtr);
}
-
Marshal.Release(sessionControlUnknown);
}
catch
@@ -59,16 +58,48 @@ namespace EonaCat.VolumeMixer.Models
try
{
var result = _sessionControl.GetDisplayName(out var displayName);
- DisplayName = result == 0 && !string.IsNullOrEmpty(displayName) ? displayName : string.Empty;
+ DisplayName = (result == 0 && !string.IsNullOrEmpty(displayName)) ? displayName : string.Empty;
result = _sessionControl.GetIconPath(out var iconPath);
- IconPath = result == 0 ? iconPath ?? "" : "";
+ IconPath = (result == 0) ? (iconPath ?? string.Empty) : string.Empty;
result = _sessionControl.GetProcessId(out var processId);
- ProcessId = result == 0 ? processId : 0;
+ ProcessId = (result == 0) ? processId : 0;
result = _sessionControl.GetState(out var state);
- State = result == 0 ? state : AudioSessionState.AudioSessionStateInactive;
+ State = (result == 0) ? state : AudioSessionState.AudioSessionStateInactive;
+
+ if (string.IsNullOrEmpty(DisplayName) && ProcessId != 0)
+ {
+ try
+ {
+ var process = Process.GetProcessById((int)ProcessId);
+ DisplayName = process.ProcessName;
+ }
+ catch
+ {
+ DisplayName = "Unknown";
+ }
+ }
+
+ if (string.IsNullOrEmpty(IconPath) && ProcessId != 0)
+ {
+ try
+ {
+ var process = Process.GetProcessById((int)ProcessId);
+ IconPath = process.MainModule?.FileName ?? string.Empty;
+ }
+ catch
+ {
+ IconPath = "Unknown";
+ }
+ }
+
+ if (ProcessId == 0 && _sessionControl.IsSystemSoundsSession() == 0)
+ {
+ DisplayName = "System sounds";
+ IconPath = "Unknown";
+ }
}
catch
{
@@ -82,26 +113,22 @@ namespace EonaCat.VolumeMixer.Models
public async Task GetVolumeAsync()
{
- return await Task.Run(() =>
+ lock (_syncLock)
{
- lock (_syncLock)
+ if (_isDisposed || _audioVolume == null)
{
- if (_isDisposed || _audioVolume == null)
- {
- return 0f;
- }
-
- try
- {
- int result = _audioVolume.GetMasterVolume(out var volume);
- return result == 0 ? volume : 0f;
- }
- catch
- {
- return 0f;
- }
+ return 0f;
}
- });
+ try
+ {
+ int result = _audioVolume.GetMasterVolume(out var volume);
+ return result == 0 ? volume : 0f;
+ }
+ catch
+ {
+ return 0f;
+ }
+ }
}
public async Task SetVolumeAsync(float volume, int maxRetries = 2, int delayMs = 20)
@@ -112,6 +139,7 @@ namespace EonaCat.VolumeMixer.Models
}
IAudioVolume simpleAudioVolCopy;
+
lock (_syncLock)
{
if (_isDisposed || _audioVolume == null)
@@ -131,9 +159,9 @@ namespace EonaCat.VolumeMixer.Models
var result = simpleAudioVolCopy.SetMasterVolume(volume, ref guid);
if (result == 0)
{
- await Task.Delay(delayMs);
+ await Task.Delay(delayMs).ConfigureAwait(false);
- var currentVolume = await GetVolumeAsync();
+ var currentVolume = await GetVolumeAsync().ConfigureAwait(false);
if (Math.Abs(currentVolume - volume) < 0.01f)
{
return true;
@@ -144,8 +172,7 @@ namespace EonaCat.VolumeMixer.Models
{
// Retry
}
-
- await Task.Delay(delayMs);
+ await Task.Delay(delayMs).ConfigureAwait(false);
}
return false;
@@ -153,31 +180,29 @@ namespace EonaCat.VolumeMixer.Models
public async Task GetMuteAsync()
{
- return await Task.Run(() =>
+ lock (_syncLock)
{
- lock (_syncLock)
+ if (_isDisposed || _audioVolume == null)
{
- if (_isDisposed || _audioVolume == null)
- {
- return false;
- }
-
- try
- {
- var result = _audioVolume.GetMute(out var mute);
- return result == 0 ? mute : false;
- }
- catch
- {
- return false;
- }
+ return false;
}
- });
+
+ try
+ {
+ var result = _audioVolume.GetMute(out var mute);
+ return result == 0 && mute;
+ }
+ catch
+ {
+ return false;
+ }
+ }
}
public async Task SetMuteAsync(bool mute)
{
IAudioVolume simpleAudioVolCopy;
+
lock (_syncLock)
{
if (_isDisposed || _audioVolume == null)
@@ -191,7 +216,7 @@ namespace EonaCat.VolumeMixer.Models
try
{
var guid = Guid.Empty;
- return await Task.Run(() => simpleAudioVolCopy.SetMute(mute, ref guid) == 0);
+ return await Task.Run(() => simpleAudioVolCopy.SetMute(mute, ref guid) == 0).ConfigureAwait(false);
}
catch
{
@@ -201,32 +226,26 @@ namespace EonaCat.VolumeMixer.Models
public async Task GetProcessNameAsync()
{
- return await Task.Run(() =>
+ if (ProcessId == 0)
{
- lock (_syncLock)
- {
- if (ProcessId == 0)
- {
- return "Unknown";
- }
+ return "Unknown";
+ }
- try
- {
- var process = Process.GetProcessById((int)ProcessId);
- return process.ProcessName;
- }
- catch
- {
- return "Unknown";
- }
- }
- });
+ try
+ {
+ var process = Process.GetProcessById((int)ProcessId);
+ return process.ProcessName;
+ }
+ catch
+ {
+ return "Unknown";
+ }
}
public async Task GetEffectiveVolumeAsync(AudioDevice device)
{
- var deviceVolume = await device.GetMasterVolumeAsync();
- var sessionVolume = await GetVolumeAsync();
+ var deviceVolume = await device.GetMasterVolumeAsync().ConfigureAwait(false);
+ var sessionVolume = await GetVolumeAsync().ConfigureAwait(false);
return deviceVolume * sessionVolume;
}
@@ -245,6 +264,7 @@ namespace EonaCat.VolumeMixer.Models
_audioVolume = null;
}
ComHelper.ReleaseComObject(_sessionControl);
+
_isDisposed = true;
}
}
diff --git a/EonaCat.VolumeMixer/Models/PropVariant.cs b/EonaCat.VolumeMixer/Models/PropVariant.cs
index 727ace0..5cb979a 100644
--- a/EonaCat.VolumeMixer/Models/PropVariant.cs
+++ b/EonaCat.VolumeMixer/Models/PropVariant.cs
@@ -5,13 +5,14 @@ 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.
- [StructLayout(LayoutKind.Sequential)]
+ [StructLayout(LayoutKind.Explicit)]
internal struct PropVariant
{
- public ushort vt;
- public ushort wReserved1;
- public ushort wReserved2;
- public ushort wReserved3;
- public IntPtr data;
+ [FieldOffset(0)] public ushort vt;
+ [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;
}
}
\ No newline at end of file
diff --git a/EonaCat.VolumeMixer/Models/PropertyKey.cs b/EonaCat.VolumeMixer/Models/PropertyKey.cs
index e08f2d0..3ef0d73 100644
--- a/EonaCat.VolumeMixer/Models/PropertyKey.cs
+++ b/EonaCat.VolumeMixer/Models/PropertyKey.cs
@@ -8,13 +8,13 @@ namespace EonaCat.VolumeMixer.Models
[StructLayout(LayoutKind.Sequential)]
internal struct PropertyKey
{
- public Guid fmtid;
- public uint pid;
+ public Guid Fmtid;
+ public uint Pid;
public PropertyKey(Guid fmtid, uint pid)
{
- this.fmtid = fmtid;
- this.pid = pid;
+ Fmtid = fmtid;
+ Pid = pid;
}
}
}
\ No newline at end of file