Added WPF tester
This commit is contained in:
parent
4b226f9601
commit
58b5d08338
|
@ -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>
|
|
@ -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
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 |
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue