Added WPF tester
This commit is contained in:
9
EonaCat.VolumeMixer.Tester.WPF/App.xaml
Normal file
9
EonaCat.VolumeMixer.Tester.WPF/App.xaml
Normal 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>
|
||||
14
EonaCat.VolumeMixer.Tester.WPF/App.xaml.cs
Normal file
14
EonaCat.VolumeMixer.Tester.WPF/App.xaml.cs
Normal 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
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
10
EonaCat.VolumeMixer.Tester.WPF/AssemblyInfo.cs
Normal file
10
EonaCat.VolumeMixer.Tester.WPF/AssemblyInfo.cs
Normal 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)
|
||||
)]
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
60
EonaCat.VolumeMixer.Tester.WPF/MainWindow.xaml
Normal file
60
EonaCat.VolumeMixer.Tester.WPF/MainWindow.xaml
Normal 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>
|
||||
245
EonaCat.VolumeMixer.Tester.WPF/MainWindow.xaml.cs
Normal file
245
EonaCat.VolumeMixer.Tester.WPF/MainWindow.xaml.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
BIN
EonaCat.VolumeMixer.Tester.WPF/Resources/mic.png
Normal file
BIN
EonaCat.VolumeMixer.Tester.WPF/Resources/mic.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
EonaCat.VolumeMixer.Tester.WPF/Resources/speaker.png
Normal file
BIN
EonaCat.VolumeMixer.Tester.WPF/Resources/speaker.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Reference in New Issue
Block a user