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