Added WPF tester

This commit is contained in:
EonaCat 2025-07-23 20:34:40 +02:00
parent 4b226f9601
commit 58b5d08338
17 changed files with 690 additions and 148 deletions

View File

@ -0,0 +1,9 @@
<Application x:Class="EonaCat.VolumeMixer.Tester.WPF.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:EonaCat.VolumeMixer.Tester.WPF"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>

View File

@ -0,0 +1,14 @@
using System.Configuration;
using System.Data;
using System.Windows;
namespace EonaCat.VolumeMixer.Tester.WPF
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
}

View File

@ -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)
)]

View File

@ -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();
}
}
}

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<None Remove="Resources\mic.png" />
<None Remove="Resources\speaker.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\mic.png" />
<Resource Include="Resources\speaker.png" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EonaCat.VolumeMixer\EonaCat.VolumeMixer.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,60 @@
<Window x:Class="EonaCat.VolumeMixer.Tester.WPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:EonaCat.VolumeMixer.Tester.WPF"
xmlns:converters="clr-namespace:EonaCat.VolumeMixer.Tester.WPF.Converter"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<converters:VolumeToPercentageConverter x:Key="VolumeToPercentageConverter" />
</Window.Resources>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ListBox x:Name="DeviceSelector" SelectionChanged="DeviceSelector_SelectionChanged" Margin="0,0,0,10">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Icon}" Width="16" Height="16" Margin="2" />
<TextBlock Text="{Binding Display}" VerticalAlignment="Center" Margin="5,0,0,0"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ListBox x:Name="SessionList" Grid.Row="1"
BorderThickness="0"
Background="Transparent"
SelectionMode="Single">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<ContentPresenter />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="5" VerticalAlignment="Center">
<TextBlock Text="{Binding DisplayName}" Width="200" VerticalAlignment="Center"/>
<Slider Width="200" Minimum="0" Maximum="1" Value="{Binding Volume, Mode=TwoWay}" PreviewMouseDown="Slider_PreviewMouseDown" PreviewMouseUp="Slider_PreviewMouseUp" PreviewTouchDown="Slider_PreviewTouchDown" PreviewTouchUp="Slider_PreviewTouchUp" />
<TextBlock Text="{Binding Volume, Converter={StaticResource VolumeToPercentageConverter}}" Width="50" Margin="5,0,0,0" VerticalAlignment="Center"/>
<CheckBox Content="Mute" IsChecked="{Binding IsMuted, Mode=TwoWay}" Margin="10,0,0,0"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>

View File

@ -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
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public ObservableCollection<AudioDeviceViewModel> Devices { get; } = new();
public ObservableCollection<AudioSessionViewModel> 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));
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -41,7 +41,17 @@ class Program
foreach (var session in sessions)
{
Console.WriteLine($"- {session.DisplayName} ({await session.GetProcessNameAsync()})");
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();
}

View File

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

View File

@ -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,
}
}

View File

@ -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<List<AudioDevice>> GetAudioDevicesAsync(DataFlow dataFlow = DataFlow.Output)
{
return await Task.Run(() =>
@ -38,7 +43,9 @@ namespace EonaCat.VolumeMixer.Managers
var devices = new List<AudioDevice>();
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<AudioDevice> 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<bool> 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<bool> 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<bool> SetMicrophoneVolumeByNameAsync(string microphoneName, float volume)
{
if (string.IsNullOrWhiteSpace(microphoneName) || volume < 0f || volume > 1f)
{
return false;
}
List<AudioDevice> microphones = await GetAudioDevicesAsync(DataFlow.Input);
@ -341,8 +430,6 @@ namespace EonaCat.VolumeMixer.Managers
return false;
}
// --- Audio Sessions (All devices) ---
public async Task<List<AudioSession>> 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)

View File

@ -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<IAudioEndpointVolume>(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<IAudioSessionManager>(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<bool> SetMasterVolumeAsync(float volume, int maxRetries = 5, int delayMs = 20)
{
for (int attempt = 0; attempt <= maxRetries; attempt++)
{
lock (_syncLock)
{
if (_isDisposed || _endpointVolume == null || volume < 0f || volume > 1f)
{
return false;
}
var guid = Guid.Empty;
for (int attempt = 0; attempt <= maxRetries; attempt++)
{
try
{
var guid = Guid.Empty;
var result = _endpointVolume.SetMasterVolumeLevelScalar(volume, ref guid);
if (result != 0)
int result;
lock (_syncLock)
{
// Failed to set, will retry
continue;
result = _endpointVolume.SetMasterVolumeLevelScalar(volume, ref guid);
}
}
catch
if (result == 0)
{
// Retry on exception
continue;
}
}
await Task.Delay(delayMs);
var currentVolume = await GetMasterVolumeAsync();
await Task.Delay(delayMs).ConfigureAwait(false);
var currentVolume = await GetMasterVolumeAsync().ConfigureAwait(false);
if (Math.Abs(currentVolume - volume) < 0.01f)
{
return true;
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"Volume set failed on attempt {attempt + 1}: {ex}");
}
await Task.Delay(delayMs);
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<bool> SetMasterMuteAsync(bool mute)
{
IAudioEndpointVolume endpointVolumeCopy;
lock (_syncLock)
{
if (_isDisposed || _endpointVolume == null)
{
return false;
}
endpointVolumeCopy = _endpointVolume;
}
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);
}
}
}

View File

@ -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
{
@ -81,8 +112,6 @@ namespace EonaCat.VolumeMixer.Models
}
public async Task<float> GetVolumeAsync()
{
return await Task.Run(() =>
{
lock (_syncLock)
{
@ -90,7 +119,6 @@ namespace EonaCat.VolumeMixer.Models
{
return 0f;
}
try
{
int result = _audioVolume.GetMasterVolume(out var volume);
@ -101,7 +129,6 @@ namespace EonaCat.VolumeMixer.Models
return 0f;
}
}
});
}
public async Task<bool> 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,16 +172,13 @@ namespace EonaCat.VolumeMixer.Models
{
// Retry
}
await Task.Delay(delayMs);
await Task.Delay(delayMs).ConfigureAwait(false);
}
return false;
}
public async Task<bool> GetMuteAsync()
{
return await Task.Run(() =>
{
lock (_syncLock)
{
@ -165,19 +190,19 @@ namespace EonaCat.VolumeMixer.Models
try
{
var result = _audioVolume.GetMute(out var mute);
return result == 0 ? mute : false;
return result == 0 && mute;
}
catch
{
return false;
}
}
});
}
public async Task<bool> 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
{
@ -200,10 +225,6 @@ namespace EonaCat.VolumeMixer.Models
}
public async Task<string> GetProcessNameAsync()
{
return await Task.Run(() =>
{
lock (_syncLock)
{
if (ProcessId == 0)
{
@ -220,13 +241,11 @@ namespace EonaCat.VolumeMixer.Models
return "Unknown";
}
}
});
}
public async Task<float> 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}