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) foreach (var session in sessions)
{ {
Console.WriteLine($"- {session.DisplayName} ({await session.GetProcessNameAsync()})"); 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()}"); Console.WriteLine($" Muted: {await session.GetMuteAsync()}");
session.Dispose(); session.Dispose();
} }

View File

@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EonaCat.VolumeMixer", "Eona
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.VolumeMixer.Tester", "EonaCat.VolumeMixer.Tester\EonaCat.VolumeMixer.Tester.csproj", "{9156F465-62F7-BA83-40E6-F4FD7F0AA6A2}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.VolumeMixer.Tester", "EonaCat.VolumeMixer.Tester\EonaCat.VolumeMixer.Tester.csproj", "{9156F465-62F7-BA83-40E6-F4FD7F0AA6A2}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{9156F465-62F7-BA83-40E6-F4FD7F0AA6A2}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE 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. // See the LICENSE file or go to https://EonaCat.com/License for full license details.
public class VolumeMixerManager : IDisposable public class VolumeMixerManager : IDisposable
{ {
private const int VT_UI4 = 0x13;
private readonly IMultiMediaDeviceEnumerator _deviceEnumerator; private readonly IMultiMediaDeviceEnumerator _deviceEnumerator;
private bool _isDisposed = false; private bool _isDisposed = false;
private readonly object _syncLock = new(); 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_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() public VolumeMixerManager()
{ {
@ -30,7 +36,6 @@ namespace EonaCat.VolumeMixer.Managers
} }
} }
// --- Get Devices ---
public async Task<List<AudioDevice>> GetAudioDevicesAsync(DataFlow dataFlow = DataFlow.Output) public async Task<List<AudioDevice>> GetAudioDevicesAsync(DataFlow dataFlow = DataFlow.Output)
{ {
return await Task.Run(() => return await Task.Run(() =>
@ -38,7 +43,9 @@ namespace EonaCat.VolumeMixer.Managers
var devices = new List<AudioDevice>(); var devices = new List<AudioDevice>();
if (_deviceEnumerator == null) if (_deviceEnumerator == null)
{
return devices; return devices;
}
lock (_syncLock) lock (_syncLock)
{ {
@ -46,7 +53,9 @@ namespace EonaCat.VolumeMixer.Managers
{ {
var result = _deviceEnumerator.EnumAudioEndpoints(dataFlow, DeviceState.Active, out var deviceCollection); var result = _deviceEnumerator.EnumAudioEndpoints(dataFlow, DeviceState.Active, out var deviceCollection);
if (result != 0 || deviceCollection == null) if (result != 0 || deviceCollection == null)
{
return devices; return devices;
}
result = deviceCollection.GetCount(out var count); result = deviceCollection.GetCount(out var count);
if (result != 0) if (result != 0)
@ -81,8 +90,9 @@ namespace EonaCat.VolumeMixer.Managers
if (result == 0 && !string.IsNullOrEmpty(id)) if (result == 0 && !string.IsNullOrEmpty(id))
{ {
var name = GetDeviceName(device); var name = GetDeviceName(device);
var type = GetDeviceType(device);
bool isDefault = id == defaultId; bool isDefault = id == defaultId;
devices.Add(new AudioDevice(device, id, name, isDefault, dataFlow)); devices.Add(new AudioDevice(device, id, name, isDefault, dataFlow, type));
} }
else else
{ {
@ -92,7 +102,7 @@ namespace EonaCat.VolumeMixer.Managers
} }
catch catch
{ {
// Skip individual device on error // Do nothing
} }
} }
@ -100,7 +110,7 @@ namespace EonaCat.VolumeMixer.Managers
} }
catch 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) public async Task<AudioDevice> GetDefaultAudioDeviceAsync(DataFlow dataFlow = DataFlow.Output)
{ {
return await Task.Run(() => return await Task.Run(() =>
{ {
if (_deviceEnumerator == null) if (_deviceEnumerator == null)
{
return null; return null;
}
lock (_syncLock) lock (_syncLock)
{ {
@ -127,7 +138,8 @@ namespace EonaCat.VolumeMixer.Managers
if (result == 0 && !string.IsNullOrEmpty(id)) if (result == 0 && !string.IsNullOrEmpty(id))
{ {
var name = GetDeviceName(device); 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); ComHelper.ReleaseComObject(device);
@ -135,7 +147,7 @@ namespace EonaCat.VolumeMixer.Managers
} }
catch catch
{ {
// Ignore and return null // Do nothing
} }
return null; return null;
@ -152,9 +164,9 @@ namespace EonaCat.VolumeMixer.Managers
{ {
var propertyKey = PKEY_Device_FriendlyName; var propertyKey = PKEY_Device_FriendlyName;
result = propertyStore.GetValue(ref propertyKey, out var propVariant); 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); ComHelper.ReleaseComObject(propertyStore);
return !string.IsNullOrEmpty(name) ? name : "Unknown Device"; return !string.IsNullOrEmpty(name) ? name : "Unknown Device";
} }
@ -164,23 +176,84 @@ namespace EonaCat.VolumeMixer.Managers
} }
catch catch
{ {
// Ignore and fall through // Do nothing
} }
return "Unknown Device"; 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) public async Task<bool> SetSystemVolumeAsync(float volume)
{ {
if (volume < 0f || volume > 1f) if (volume < 0f || volume > 1f)
{
return false; return false;
}
AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output); AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output);
if (defaultDevice == null) if (defaultDevice == null)
{
return false; return false;
}
try try
{ {
@ -197,7 +270,9 @@ namespace EonaCat.VolumeMixer.Managers
AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output); AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output);
if (defaultDevice == null) if (defaultDevice == null)
{
return 0f; return 0f;
}
try try
{ {
@ -214,7 +289,9 @@ namespace EonaCat.VolumeMixer.Managers
AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output); AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output);
if (defaultDevice == null) if (defaultDevice == null)
{
return false; return false;
}
try try
{ {
@ -231,7 +308,9 @@ namespace EonaCat.VolumeMixer.Managers
AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output); AudioDevice defaultDevice = await GetDefaultAudioDeviceAsync(DataFlow.Output);
if (defaultDevice == null) if (defaultDevice == null)
{
return false; return false;
}
try try
{ {
@ -243,17 +322,19 @@ namespace EonaCat.VolumeMixer.Managers
} }
} }
// --- Microphone (Input) Volume and Mute ---
public async Task<bool> SetMicrophoneVolumeAsync(float volume) public async Task<bool> SetMicrophoneVolumeAsync(float volume)
{ {
if (volume < 0f || volume > 1f) if (volume < 0f || volume > 1f)
{
return false; return false;
}
AudioDevice defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input); AudioDevice defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input);
if (defaultMic == null) if (defaultMic == null)
{
return false; return false;
}
try try
{ {
@ -270,7 +351,9 @@ namespace EonaCat.VolumeMixer.Managers
AudioDevice defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input); AudioDevice defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input);
if (defaultMic == null) if (defaultMic == null)
{
return 0f; return 0f;
}
try try
{ {
@ -287,7 +370,9 @@ namespace EonaCat.VolumeMixer.Managers
AudioDevice defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input); AudioDevice defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input);
if (defaultMic == null) if (defaultMic == null)
{
return false; return false;
}
try try
{ {
@ -304,7 +389,9 @@ namespace EonaCat.VolumeMixer.Managers
AudioDevice defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input); AudioDevice defaultMic = await GetDefaultAudioDeviceAsync(DataFlow.Input);
if (defaultMic == null) if (defaultMic == null)
{
return false; return false;
}
try try
{ {
@ -319,7 +406,9 @@ namespace EonaCat.VolumeMixer.Managers
public async Task<bool> SetMicrophoneVolumeByNameAsync(string microphoneName, float volume) public async Task<bool> SetMicrophoneVolumeByNameAsync(string microphoneName, float volume)
{ {
if (string.IsNullOrWhiteSpace(microphoneName) || volume < 0f || volume > 1f) if (string.IsNullOrWhiteSpace(microphoneName) || volume < 0f || volume > 1f)
{
return false; return false;
}
List<AudioDevice> microphones = await GetAudioDevicesAsync(DataFlow.Input); List<AudioDevice> microphones = await GetAudioDevicesAsync(DataFlow.Input);
@ -341,8 +430,6 @@ namespace EonaCat.VolumeMixer.Managers
return false; return false;
} }
// --- Audio Sessions (All devices) ---
public async Task<List<AudioSession>> GetAllActiveSessionsAsync() public async Task<List<AudioSession>> GetAllActiveSessionsAsync()
{ {
return await Task.Run(async () => return await Task.Run(async () =>
@ -359,7 +446,7 @@ namespace EonaCat.VolumeMixer.Managers
} }
catch catch
{ {
// Skip device on error // Do nothing
} }
finally finally
{ {
@ -381,8 +468,6 @@ namespace EonaCat.VolumeMixer.Managers
return await GetDefaultAudioDeviceAsync(DataFlow.Input); return await GetDefaultAudioDeviceAsync(DataFlow.Input);
} }
// --- Dispose ---
public void Dispose() public void Dispose()
{ {
lock (_syncLock) lock (_syncLock)

View File

@ -2,6 +2,7 @@
using EonaCat.VolumeMixer.Interfaces; using EonaCat.VolumeMixer.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -20,16 +21,18 @@ namespace EonaCat.VolumeMixer.Models
public string Id { get; private set; } public string Id { get; private set; }
public string Name { get; private set; } public string Name { get; private set; }
public DeviceType DeviceType { get; private set; }
public bool IsDefault { get; private set; } public bool IsDefault { get; private set; }
public DataFlow DataFlow { 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; _device = device;
Id = id; Id = id;
Name = name; Name = name;
IsDefault = isDefault; IsDefault = isDefault;
DataFlow = dataFlow; DataFlow = dataFlow;
DeviceType = deviceType;
InitializeEndpointVolume(); InitializeEndpointVolume();
InitializeSessionManager(); InitializeSessionManager();
} }
@ -45,8 +48,9 @@ namespace EonaCat.VolumeMixer.Models
_endpointVolume = ComHelper.GetInterface<IAudioEndpointVolume>(ptr); _endpointVolume = ComHelper.GetInterface<IAudioEndpointVolume>(ptr);
} }
} }
catch catch (Exception ex)
{ {
Debug.WriteLine($"Failed to initialize endpoint volume: {ex}");
_endpointVolume = null; _endpointVolume = null;
} }
} }
@ -62,8 +66,9 @@ namespace EonaCat.VolumeMixer.Models
_sessionManager = ComHelper.GetInterface<IAudioSessionManager>(ptr); _sessionManager = ComHelper.GetInterface<IAudioSessionManager>(ptr);
} }
} }
catch catch (Exception ex)
{ {
Debug.WriteLine($"Failed to initialize session manager: {ex}");
_sessionManager = null; _sessionManager = null;
} }
} }
@ -84,52 +89,50 @@ namespace EonaCat.VolumeMixer.Models
var result = _endpointVolume.GetMasterVolumeLevelScalar(out var volume); var result = _endpointVolume.GetMasterVolumeLevelScalar(out var volume);
return result == 0 ? volume : 0f; return result == 0 ? volume : 0f;
} }
catch catch (Exception ex)
{ {
Debug.WriteLine($"Error getting master volume: {ex}");
return 0f; return 0f;
} }
} }
}); }).ConfigureAwait(false);
} }
public async Task<bool> SetMasterVolumeAsync(float volume, int maxRetries = 5, int delayMs = 20) public async Task<bool> 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++) 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; await Task.Delay(delayMs).ConfigureAwait(false);
var result = _endpointVolume.SetMasterVolumeLevelScalar(volume, ref guid); var currentVolume = await GetMasterVolumeAsync().ConfigureAwait(false);
if (result != 0) if (Math.Abs(currentVolume - volume) < 0.01f)
{ {
// Failed to set, will retry return true;
continue;
} }
} }
catch
{
// Retry on exception
continue;
}
} }
catch (Exception ex)
await Task.Delay(delayMs);
var currentVolume = await GetMasterVolumeAsync();
if (Math.Abs(currentVolume - volume) < 0.01f)
{ {
return true; Debug.WriteLine($"Volume set failed on attempt {attempt + 1}: {ex}");
} }
await Task.Delay(delayMs); await Task.Delay(delayMs).ConfigureAwait(false);
} }
return false; return false;
@ -149,35 +152,32 @@ namespace EonaCat.VolumeMixer.Models
try try
{ {
var result = _endpointVolume.GetMute(out var mute); 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; return false;
} }
} }
}); }).ConfigureAwait(false);
} }
public async Task<bool> SetMasterMuteAsync(bool mute) public async Task<bool> SetMasterMuteAsync(bool mute)
{ {
IAudioEndpointVolume endpointVolumeCopy; if (_isDisposed || _endpointVolume == null)
lock (_syncLock)
{ {
if (_isDisposed || _endpointVolume == null) return false;
{
return false;
}
endpointVolumeCopy = _endpointVolume;
} }
try try
{ {
var guid = Guid.Empty; 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; return false;
} }
} }
@ -229,13 +229,13 @@ namespace EonaCat.VolumeMixer.Models
} }
catch catch
{ {
// Skip on error // Do nothing
} }
} }
} }
catch catch
{ {
// Return empty // Do nothing
} }
return sessions; return sessions;
@ -246,7 +246,9 @@ namespace EonaCat.VolumeMixer.Models
private IAudioSessionControlExtended GetSessionControl2(object sessionControl) private IAudioSessionControlExtended GetSessionControl2(object sessionControl)
{ {
if (sessionControl == null) if (sessionControl == null)
{
return null; return null;
}
var unknownPtr = Marshal.GetIUnknownForObject(sessionControl); var unknownPtr = Marshal.GetIUnknownForObject(sessionControl);
try try
@ -255,15 +257,18 @@ namespace EonaCat.VolumeMixer.Models
int result = Marshal.QueryInterface(unknownPtr, ref AudioController2Guid, out sessionControl2Ptr); int result = Marshal.QueryInterface(unknownPtr, ref AudioController2Guid, out sessionControl2Ptr);
if (result == 0 && sessionControl2Ptr != IntPtr.Zero) if (result == 0 && sessionControl2Ptr != IntPtr.Zero)
{ {
var sessionControl2 = (IAudioSessionControlExtended)Marshal.GetObjectForIUnknown(sessionControl2Ptr); return (IAudioSessionControlExtended)Marshal.GetObjectForIUnknown(sessionControl2Ptr);
Marshal.Release(sessionControl2Ptr);
return sessionControl2;
} }
} }
catch (Exception ex)
{
Debug.WriteLine($"Error querying sessionControl2: {ex}");
}
finally finally
{ {
Marshal.Release(unknownPtr); Marshal.Release(unknownPtr);
} }
return null; return null;
} }
@ -280,16 +285,16 @@ namespace EonaCat.VolumeMixer.Models
try try
{ {
var guid = Guid.Empty; var guid = Guid.Empty;
var result = _endpointVolume.StepUp(ref guid); success = _endpointVolume.StepUp(ref guid) == 0;
success = result == 0;
} }
catch catch (Exception ex)
{ {
Debug.WriteLine($"Error stepping up: {ex}");
success = false; success = false;
} }
} }
await Task.Delay(delayMs); await Task.Delay(delayMs).ConfigureAwait(false);
return success; return success;
} }
@ -306,16 +311,16 @@ namespace EonaCat.VolumeMixer.Models
try try
{ {
var guid = Guid.Empty; var guid = Guid.Empty;
var result = _endpointVolume.StepDown(ref guid); success = _endpointVolume.StepDown(ref guid) == 0;
success = result == 0;
} }
catch catch (Exception ex)
{ {
Debug.WriteLine($"Error stepping down: {ex}");
success = false; success = false;
} }
} }
await Task.Delay(delayMs); await Task.Delay(delayMs).ConfigureAwait(false);
return success; return success;
} }
@ -332,6 +337,8 @@ namespace EonaCat.VolumeMixer.Models
ComHelper.ReleaseComObject(_sessionManager); ComHelper.ReleaseComObject(_sessionManager);
ComHelper.ReleaseComObject(_device); ComHelper.ReleaseComObject(_device);
_isDisposed = true; _isDisposed = true;
GC.SuppressFinalize(this);
} }
} }
} }

View File

@ -12,7 +12,7 @@ namespace EonaCat.VolumeMixer.Models
private readonly IAudioSessionControlExtended _sessionControl; private readonly IAudioSessionControlExtended _sessionControl;
private IAudioVolume _audioVolume; private IAudioVolume _audioVolume;
private bool _isDisposed = false; private bool _isDisposed = false;
private readonly object _syncLock = new object(); private readonly object _syncLock = new();
public string DisplayName { get; private set; } public string DisplayName { get; private set; }
public string IconPath { get; private set; } public string IconPath { get; private set; }
@ -21,9 +21,9 @@ namespace EonaCat.VolumeMixer.Models
internal AudioSession(IAudioSessionControlExtended sessionControl, IAudioSessionManager sessionManager) internal AudioSession(IAudioSessionControlExtended sessionControl, IAudioSessionManager sessionManager)
{ {
_sessionControl = sessionControl; _sessionControl = sessionControl ?? throw new ArgumentNullException(nameof(sessionControl));
LoadSessionInfo();
InitializeSimpleAudioVolume(); InitializeSimpleAudioVolume();
LoadSessionInfo();
} }
private void InitializeSimpleAudioVolume() private void InitializeSimpleAudioVolume()
@ -42,7 +42,6 @@ namespace EonaCat.VolumeMixer.Models
_audioVolume = (IAudioVolume)Marshal.GetObjectForIUnknown(simpleAudioVolumePtr); _audioVolume = (IAudioVolume)Marshal.GetObjectForIUnknown(simpleAudioVolumePtr);
Marshal.Release(simpleAudioVolumePtr); Marshal.Release(simpleAudioVolumePtr);
} }
Marshal.Release(sessionControlUnknown); Marshal.Release(sessionControlUnknown);
} }
catch catch
@ -59,16 +58,48 @@ namespace EonaCat.VolumeMixer.Models
try try
{ {
var result = _sessionControl.GetDisplayName(out var displayName); 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); result = _sessionControl.GetIconPath(out var iconPath);
IconPath = result == 0 ? iconPath ?? "" : ""; IconPath = (result == 0) ? (iconPath ?? string.Empty) : string.Empty;
result = _sessionControl.GetProcessId(out var processId); result = _sessionControl.GetProcessId(out var processId);
ProcessId = result == 0 ? processId : 0; ProcessId = (result == 0) ? processId : 0;
result = _sessionControl.GetState(out var state); 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 catch
{ {
@ -82,26 +113,22 @@ namespace EonaCat.VolumeMixer.Models
public async Task<float> GetVolumeAsync() public async Task<float> GetVolumeAsync()
{ {
return await Task.Run(() => lock (_syncLock)
{ {
lock (_syncLock) if (_isDisposed || _audioVolume == null)
{ {
if (_isDisposed || _audioVolume == null) return 0f;
{
return 0f;
}
try
{
int result = _audioVolume.GetMasterVolume(out var volume);
return result == 0 ? volume : 0f;
}
catch
{
return 0f;
}
} }
}); try
{
int result = _audioVolume.GetMasterVolume(out var volume);
return result == 0 ? volume : 0f;
}
catch
{
return 0f;
}
}
} }
public async Task<bool> SetVolumeAsync(float volume, int maxRetries = 2, int delayMs = 20) public async Task<bool> SetVolumeAsync(float volume, int maxRetries = 2, int delayMs = 20)
@ -112,6 +139,7 @@ namespace EonaCat.VolumeMixer.Models
} }
IAudioVolume simpleAudioVolCopy; IAudioVolume simpleAudioVolCopy;
lock (_syncLock) lock (_syncLock)
{ {
if (_isDisposed || _audioVolume == null) if (_isDisposed || _audioVolume == null)
@ -131,9 +159,9 @@ namespace EonaCat.VolumeMixer.Models
var result = simpleAudioVolCopy.SetMasterVolume(volume, ref guid); var result = simpleAudioVolCopy.SetMasterVolume(volume, ref guid);
if (result == 0) 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) if (Math.Abs(currentVolume - volume) < 0.01f)
{ {
return true; return true;
@ -144,8 +172,7 @@ namespace EonaCat.VolumeMixer.Models
{ {
// Retry // Retry
} }
await Task.Delay(delayMs).ConfigureAwait(false);
await Task.Delay(delayMs);
} }
return false; return false;
@ -153,31 +180,29 @@ namespace EonaCat.VolumeMixer.Models
public async Task<bool> GetMuteAsync() public async Task<bool> GetMuteAsync()
{ {
return await Task.Run(() => lock (_syncLock)
{ {
lock (_syncLock) if (_isDisposed || _audioVolume == null)
{ {
if (_isDisposed || _audioVolume == null) return false;
{
return false;
}
try
{
var result = _audioVolume.GetMute(out var mute);
return result == 0 ? mute : false;
}
catch
{
return false;
}
} }
});
try
{
var result = _audioVolume.GetMute(out var mute);
return result == 0 && mute;
}
catch
{
return false;
}
}
} }
public async Task<bool> SetMuteAsync(bool mute) public async Task<bool> SetMuteAsync(bool mute)
{ {
IAudioVolume simpleAudioVolCopy; IAudioVolume simpleAudioVolCopy;
lock (_syncLock) lock (_syncLock)
{ {
if (_isDisposed || _audioVolume == null) if (_isDisposed || _audioVolume == null)
@ -191,7 +216,7 @@ namespace EonaCat.VolumeMixer.Models
try try
{ {
var guid = Guid.Empty; 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 catch
{ {
@ -201,32 +226,26 @@ namespace EonaCat.VolumeMixer.Models
public async Task<string> GetProcessNameAsync() public async Task<string> GetProcessNameAsync()
{ {
return await Task.Run(() => if (ProcessId == 0)
{ {
lock (_syncLock) return "Unknown";
{ }
if (ProcessId == 0)
{
return "Unknown";
}
try try
{ {
var process = Process.GetProcessById((int)ProcessId); var process = Process.GetProcessById((int)ProcessId);
return process.ProcessName; return process.ProcessName;
} }
catch catch
{ {
return "Unknown"; return "Unknown";
} }
}
});
} }
public async Task<float> GetEffectiveVolumeAsync(AudioDevice device) public async Task<float> GetEffectiveVolumeAsync(AudioDevice device)
{ {
var deviceVolume = await device.GetMasterVolumeAsync(); var deviceVolume = await device.GetMasterVolumeAsync().ConfigureAwait(false);
var sessionVolume = await GetVolumeAsync(); var sessionVolume = await GetVolumeAsync().ConfigureAwait(false);
return deviceVolume * sessionVolume; return deviceVolume * sessionVolume;
} }
@ -245,6 +264,7 @@ namespace EonaCat.VolumeMixer.Models
_audioVolume = null; _audioVolume = null;
} }
ComHelper.ReleaseComObject(_sessionControl); ComHelper.ReleaseComObject(_sessionControl);
_isDisposed = true; _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. // 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. // See the LICENSE file or go to https://EonaCat.com/License for full license details.
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Explicit)]
internal struct PropVariant internal struct PropVariant
{ {
public ushort vt; [FieldOffset(0)] public ushort vt;
public ushort wReserved1; [FieldOffset(2)] public ushort wReserved1;
public ushort wReserved2; [FieldOffset(4)] public ushort wReserved2;
public ushort wReserved3; [FieldOffset(6)] public ushort wReserved3;
public IntPtr data; [FieldOffset(8)] public IntPtr data1;
[FieldOffset(16)] public IntPtr data2;
} }
} }

View File

@ -8,13 +8,13 @@ namespace EonaCat.VolumeMixer.Models
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
internal struct PropertyKey internal struct PropertyKey
{ {
public Guid fmtid; public Guid Fmtid;
public uint pid; public uint Pid;
public PropertyKey(Guid fmtid, uint pid) public PropertyKey(Guid fmtid, uint pid)
{ {
this.fmtid = fmtid; Fmtid = fmtid;
this.pid = pid; Pid = pid;
} }
} }
} }