Added WPF tester

This commit is contained in:
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