From 58b5d08338cd919738577f4d7487dd3fd3d36eeb Mon Sep 17 00:00:00 2001 From: EonaCat Date: Wed, 23 Jul 2025 20:34:40 +0200 Subject: [PATCH] Added WPF tester --- EonaCat.VolumeMixer.Tester.WPF/App.xaml | 9 + EonaCat.VolumeMixer.Tester.WPF/App.xaml.cs | 14 + .../AssemblyInfo.cs | 10 + .../Converter/VolumeToPercentageConverter.cs | 22 ++ .../EonaCat.VolumeMixer.Tester.WPF.csproj | 25 ++ .../MainWindow.xaml | 60 +++++ .../MainWindow.xaml.cs | 245 ++++++++++++++++++ .../Resources/mic.png | Bin 0 -> 11474 bytes .../Resources/speaker.png | Bin 0 -> 10738 bytes EonaCat.VolumeMixer.Tester/Program.cs | 12 +- EonaCat.VolumeMixer.sln | 6 + EonaCat.VolumeMixer/DeviceType.cs | 28 ++ .../Managers/VolumeMixerManager.cs | 121 +++++++-- EonaCat.VolumeMixer/Models/AudioDevice.cs | 111 ++++---- EonaCat.VolumeMixer/Models/AudioSession.cs | 154 ++++++----- EonaCat.VolumeMixer/Models/PropVariant.cs | 13 +- EonaCat.VolumeMixer/Models/PropertyKey.cs | 8 +- 17 files changed, 690 insertions(+), 148 deletions(-) create mode 100644 EonaCat.VolumeMixer.Tester.WPF/App.xaml create mode 100644 EonaCat.VolumeMixer.Tester.WPF/App.xaml.cs create mode 100644 EonaCat.VolumeMixer.Tester.WPF/AssemblyInfo.cs create mode 100644 EonaCat.VolumeMixer.Tester.WPF/Converter/VolumeToPercentageConverter.cs create mode 100644 EonaCat.VolumeMixer.Tester.WPF/EonaCat.VolumeMixer.Tester.WPF.csproj create mode 100644 EonaCat.VolumeMixer.Tester.WPF/MainWindow.xaml create mode 100644 EonaCat.VolumeMixer.Tester.WPF/MainWindow.xaml.cs create mode 100644 EonaCat.VolumeMixer.Tester.WPF/Resources/mic.png create mode 100644 EonaCat.VolumeMixer.Tester.WPF/Resources/speaker.png create mode 100644 EonaCat.VolumeMixer/DeviceType.cs 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 0000000000000000000000000000000000000000..7ed7d52ddbccf6028ec7605b3a4ff020a004b506 GIT binary patch literal 11474 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%0AFSS-97vu1@MX5@J;K~?xO3b6pTPm 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 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