From 49bba2587b50b2ef3d5131e5a59c8442fc1e9204 Mon Sep 17 00:00:00 2001 From: Dwscdv3 Date: Tue, 12 Aug 2025 20:20:19 +0800 Subject: [PATCH 01/18] Implement issue #1703 --- EarTrumpet/App.xaml.cs | 33 +++++---- EarTrumpet/AppSettings.cs | 17 ++++- .../DataModel/Audio/Mocks/AudioDevice.cs | 12 +-- .../Audio/Mocks/AudioDeviceSession.cs | 8 +- .../WindowsAudio/Internal/AudioDevice.cs | 73 ++++++++++++++----- .../Internal/AudioDeviceChannel.cs | 21 +++++- .../Internal/AudioDeviceSession.cs | 48 +++++++----- EarTrumpet/Extensions/FloatExtensions.cs | 18 ++--- EarTrumpet/UI/Controls/VolumeSlider.cs | 55 +++++++++++++- .../UI/Converters/VolumeToStringConverter.cs | 30 ++++++++ .../UI/ViewModels/AudioSessionViewModel.cs | 12 ++- .../ViewModels/DeviceCollectionViewModel.cs | 5 +- EarTrumpet/UI/ViewModels/DeviceViewModel.cs | 19 +++++ ...arTrumpetCommunitySettingsPageViewModel.cs | 13 ++++ EarTrumpet/UI/ViewModels/IAppItemViewModel.cs | 2 +- .../UI/ViewModels/SettingsAppItemViewModel.cs | 4 +- .../ViewModels/TemporaryAppItemViewModel.cs | 4 +- EarTrumpet/UI/Views/AppItemView.xaml | 6 +- EarTrumpet/UI/Views/DeviceView.xaml | 6 +- EarTrumpet/UI/Views/SettingsWindow.xaml | 24 +++++- 20 files changed, 313 insertions(+), 97 deletions(-) create mode 100644 EarTrumpet/UI/Converters/VolumeToStringConverter.cs diff --git a/EarTrumpet/App.xaml.cs b/EarTrumpet/App.xaml.cs index 0d6b110d1..7130edb5b 100644 --- a/EarTrumpet/App.xaml.cs +++ b/EarTrumpet/App.xaml.cs @@ -81,19 +81,19 @@ private void OnAppStartup(object sender, StartupEventArgs e) _errorReporter = new ErrorReporter(Settings); if (SingleInstanceAppMutex.TakeExclusivity()) - { + { Exit += (_, __) => SingleInstanceAppMutex.ReleaseExclusivity(); try - { + { NotifyOnMissingStartupPolicies(); ContinueStartup(); - } + } catch (Exception ex) when (IsCriticalFontLoadFailure(ex)) { ErrorReporter.LogWarning(ex); OnCriticalFontLoadFailure(); - } + } } else { @@ -126,13 +126,13 @@ private void ContinueStartup() } private void SystemEvents_SessionSwitch(object sender, SessionSwitchEventArgs e) - { + { Trace.WriteLine($"Detected User Session Switch: {e.Reason}"); if (e.Reason == SessionSwitchReason.ConsoleConnect) - { + { var devManager = WindowsAudioFactory.Create(AudioDeviceKind.Playback); devManager.RefreshAllDevices(); - } + } } private void CompleteStartup() @@ -151,6 +151,7 @@ private void CompleteStartup() Settings.AbsoluteVolumeUpHotkeyTyped += AbsoluteVolumeIncrement; Settings.AbsoluteVolumeDownHotkeyTyped += AbsoluteVolumeDecrement; Settings.RegisterHotkeys(); + Settings.UseLogarithmicVolumeChanged += (_, __) => UpdateTrayTooltip(); _trayIcon.PrimaryInvoke += (_, type) => _flyoutViewModel.OpenFlyout(type); _trayIcon.SecondaryInvoke += (_, args) => _trayIcon.ShowContextMenu(GetTrayContextMenuItems(), args.Point); @@ -255,7 +256,7 @@ private static bool IsAnyStartupPolicyMissing() "EnableUwpStartupTasks", "SupportFullTrustStartupTasks", "SupportUwpStartupTasks" - }; + }; foreach (var dword in dwords) { @@ -266,7 +267,7 @@ private static bool IsAnyStartupPolicyMissing() { Trace.WriteLine($"Missing or invalid: {dword}"); return true; - } + } } } catch (Exception ex) @@ -282,20 +283,20 @@ private static void NotifyOnMissingStartupPolicies() if (!IsAnyStartupPolicyMissing()) { return; - } + } new Thread(() => - { + { if (MessageBox.Show( EarTrumpet.Properties.Resources.MissingPoliciesHelpText, EarTrumpet.Properties.Resources.MissingPoliciesDialogHeaderText, MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.OK) == MessageBoxResult.OK) - { + { Trace.WriteLine($"App NotifyOnMissingStartupPolicies OK"); ProcessHelper.StartNoThrow("https://eartrumpet.app/jmp/fixstartup"); - } + } }).Start(); } @@ -309,13 +310,13 @@ private List GetTrayContextMenuItems() })); if (ret.Count == 0) - { + { ret.Add(new ContextMenuItem { DisplayName = EarTrumpet.Properties.Resources.ContextMenuNoDevices, IsEnabled = false, }); - } + } ret.AddRange( [ @@ -392,7 +393,7 @@ private static SettingsCategoryViewModel CreateAddonSettingsPage(IEarTrumpetAddo if (!addon.IsInternal()) { category.Pages.Add(new AddonAboutPageViewModel(addon)); - } + } return category; } diff --git a/EarTrumpet/AppSettings.cs b/EarTrumpet/AppSettings.cs index 8c3ec8715..1b8cb24fa 100644 --- a/EarTrumpet/AppSettings.cs +++ b/EarTrumpet/AppSettings.cs @@ -10,6 +10,7 @@ namespace EarTrumpet; public class AppSettings { public event EventHandler UseLegacyIconChanged; + public event EventHandler UseLogarithmicVolumeChanged; public event Action FlyoutHotkeyTyped; public event Action MixerHotkeyTyped; public event Action SettingsHotkeyTyped; @@ -160,7 +161,21 @@ public bool IsTelemetryEnabled public bool UseLogarithmicVolume { get => _settings.Get("UseLogarithmicVolume", false); - set => _settings.Set("UseLogarithmicVolume", value); + set + { + _settings.Set("UseLogarithmicVolume", value); + UseLogarithmicVolumeChanged?.Invoke(this, value); + } + } + + public float LogarithmicVolumeMindB + { + get => _settings.Get("LogarithmicVolumeMindB", -40f); + set + { + _settings.Set("LogarithmicVolumeMindB", value); + UseLogarithmicVolumeChanged?.Invoke(this, UseLogarithmicVolume); + } } public WINDOWPLACEMENT? FullMixerWindowPlacement diff --git a/EarTrumpet/DataModel/Audio/Mocks/AudioDevice.cs b/EarTrumpet/DataModel/Audio/Mocks/AudioDevice.cs index 52a8a6255..18b218e99 100644 --- a/EarTrumpet/DataModel/Audio/Mocks/AudioDevice.cs +++ b/EarTrumpet/DataModel/Audio/Mocks/AudioDevice.cs @@ -1,6 +1,5 @@ using EarTrumpet.DataModel.WindowsAudio; using EarTrumpet.DataModel.WindowsAudio.Internal; -using EarTrumpet.Extensions; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -42,18 +41,9 @@ public bool IsMuted private float _volume = 1; public float Volume { - get - { - return App.Settings.UseLogarithmicVolume ? _volume.ToDisplayVolume() : _volume; - } - + get => _volume; set { - if (App.Settings.UseLogarithmicVolume) - { - value = value.ToLogVolume(); - } - if (_volume != value) { _volume = value; diff --git a/EarTrumpet/DataModel/Audio/Mocks/AudioDeviceSession.cs b/EarTrumpet/DataModel/Audio/Mocks/AudioDeviceSession.cs index 9f53a77aa..8b32c54a2 100644 --- a/EarTrumpet/DataModel/Audio/Mocks/AudioDeviceSession.cs +++ b/EarTrumpet/DataModel/Audio/Mocks/AudioDeviceSession.cs @@ -51,16 +51,12 @@ public bool IsMuted private float _volume = 1; public float Volume { - get - { - return App.Settings.UseLogarithmicVolume ? _volume.ToDisplayVolume() : _volume; - } - + get => _volume; set { if (App.Settings.UseLogarithmicVolume) { - value = value.ToLogVolume(); + value = value.LinearToLogNormalized(); } if (_volume != value) diff --git a/EarTrumpet/DataModel/WindowsAudio/Internal/AudioDevice.cs b/EarTrumpet/DataModel/WindowsAudio/Internal/AudioDevice.cs index 8f2f30e86..eec7adbb5 100644 --- a/EarTrumpet/DataModel/WindowsAudio/Internal/AudioDevice.cs +++ b/EarTrumpet/DataModel/WindowsAudio/Internal/AudioDevice.cs @@ -25,6 +25,8 @@ public class AudioDevice : BindableBase, IAudioEndpointVolumeCallback, IAudioDev private readonly WeakReference _deviceManager; private readonly string _id; private readonly AudioDeviceChannelCollection _channels; + private readonly float _deviceVolumeMindB; + private readonly float _deviceVolumeMaxdB; private IMMDevice _device; private string _displayName; private string _iconPath; @@ -51,8 +53,16 @@ public AudioDevice(IAudioDeviceManager deviceManager, IMMDevice device, Dispatch { _deviceVolume = device.Activate(); _deviceVolume.RegisterControlChangeNotify(this); - _deviceVolume.GetMasterVolumeLevelScalar(out _volume); + if (App.Settings.UseLogarithmicVolume) + { + _deviceVolume.GetMasterVolumeLevel(out _volume); + } + else + { + _deviceVolume.GetMasterVolumeLevelScalar(out _volume); + } _deviceVolume.GetMute(out var isMuted); + _deviceVolume.GetVolumeRange(out _deviceVolumeMindB, out _deviceVolumeMaxdB, out _); _isMuted = isMuted; _isRegistered = true; _meter = device.Activate(); @@ -67,6 +77,19 @@ public AudioDevice(IAudioDeviceManager deviceManager, IMMDevice device, Dispatch } ReadProperties(); + + App.Settings.UseLogarithmicVolumeChanged += (sender, args) => + { + if (App.Settings.UseLogarithmicVolume) + { + _deviceVolume.GetMasterVolumeLevel(out _volume); + } + else + { + _deviceVolume.GetMasterVolumeLevelScalar(out _volume); + } + RaisePropertyChanged(nameof(Volume)); + }; } ~AudioDevice() @@ -87,6 +110,10 @@ public AudioDevice(IAudioDeviceManager deviceManager, IMMDevice device, Dispatch public unsafe void OnNotify(AUDIO_VOLUME_NOTIFICATION_DATA* pNotify) { _volume = (*pNotify).fMasterVolume; + if (App.Settings.UseLogarithmicVolume) + { + _deviceVolume.GetMasterVolumeLevel(out _volume); + } _isMuted = (*pNotify).bMuted != 0; _channels.OnNotify((nint)pNotify, *pNotify); @@ -100,29 +127,31 @@ public unsafe void OnNotify(AUDIO_VOLUME_NOTIFICATION_DATA* pNotify) public float Volume { - get => App.Settings.UseLogarithmicVolume ? _volume.ToDisplayVolume() : _volume; + get => _volume; set { - value = value.Bound(0, 1f); - - if (App.Settings.UseLogarithmicVolume) + try { - value = value.ToLogVolume(); - } - - if (_volume != value) - { - try + if (App.Settings.UseLogarithmicVolume) { + value = value.Bound(App.Settings.LogarithmicVolumeMindB, 0); + value = value.Bound(_deviceVolumeMindB, _deviceVolumeMaxdB); _volume = value; - _deviceVolume.SetMasterVolumeLevelScalar(value, Guid.Empty); + _deviceVolume.SetMasterVolumeLevel(value, Guid.Empty); + IsMuted = false; // Mute is equals to -∞ in dB, so we always unmute when setting volume. } - catch (Exception ex) when (ex.Is(HRESULT.AUDCLNT_E_DEVICE_INVALIDATED)) + else { - // Expected in some cases. + value = value.Bound(0, 1f); + _volume = value; + _deviceVolume.SetMasterVolumeLevelScalar(value, Guid.Empty); + IsMuted = _volume.ToVolumeInt() == 0; } - - IsMuted = App.Settings.UseLogarithmicVolume ? _volume <= (1 / 100f).ToLogVolume() : _volume.ToVolumeInt() == 0; + } + catch (Exception ex) when (ex.Is(HRESULT.AUDCLNT_E_DEVICE_INVALIDATED)) + { + // Expected in some cases. + // Known: when the master volume in dB is beyond device capabilities } } } @@ -176,8 +205,16 @@ public IAudioDeviceManager Parent public void UpdatePeakValue() { var newValues = Helpers.ReadPeakValues(_meter); - PeakValue1 = newValues[0]; - PeakValue2 = newValues[1]; + if (App.Settings.UseLogarithmicVolume) + { + PeakValue1 = newValues[0].LinearToLogNormalized(); + PeakValue2 = newValues[1].LinearToLogNormalized(); + } + else + { + PeakValue1 = newValues[0]; + PeakValue2 = newValues[1]; + } foreach(var session in _sessions.Sessions.ToArray()) { diff --git a/EarTrumpet/DataModel/WindowsAudio/Internal/AudioDeviceChannel.cs b/EarTrumpet/DataModel/WindowsAudio/Internal/AudioDeviceChannel.cs index 2a5a00ca8..96ec670b2 100644 --- a/EarTrumpet/DataModel/WindowsAudio/Internal/AudioDeviceChannel.cs +++ b/EarTrumpet/DataModel/WindowsAudio/Internal/AudioDeviceChannel.cs @@ -14,7 +14,14 @@ public AudioDeviceChannel(IAudioEndpointVolume deviceVolume, uint index) { _index = index; _deviceVolume = deviceVolume; - _deviceVolume.GetChannelVolumeLevelScalar(index, out _level); + if (App.Settings.UseLogarithmicVolume) + { + _deviceVolume.GetChannelVolumeLevel(index, out _level); + } + else + { + _deviceVolume.GetChannelVolumeLevelScalar(index, out _level); + } } public float Level @@ -25,7 +32,17 @@ public float Level if (_level != value) { var context = Guid.Empty; - unsafe { _deviceVolume.SetChannelVolumeLevelScalar(_index, value, &context); } + unsafe + { + if (App.Settings.UseLogarithmicVolume) + { + _deviceVolume.SetChannelVolumeLevel(_index, value, &context); + } + else + { + _deviceVolume.SetChannelVolumeLevelScalar(_index, value, &context); + } + } _level = value; RaisePropertyChanged(nameof(Level)); diff --git a/EarTrumpet/DataModel/WindowsAudio/Internal/AudioDeviceSession.cs b/EarTrumpet/DataModel/WindowsAudio/Internal/AudioDeviceSession.cs index 88987874e..e2828b1a0 100644 --- a/EarTrumpet/DataModel/WindowsAudio/Internal/AudioDeviceSession.cs +++ b/EarTrumpet/DataModel/WindowsAudio/Internal/AudioDeviceSession.cs @@ -33,29 +33,32 @@ public IAudioDevice Parent public float Volume { - get => App.Settings.UseLogarithmicVolume ? _volume.ToDisplayVolume() : _volume; + get => App.Settings.UseLogarithmicVolume + ? _volume.LinearToLog() + : _volume; set { - value = value.Bound(0, 1f); - - if (App.Settings.UseLogarithmicVolume) + try { - value = value.ToLogVolume(); - } - - if (_volume != value) - { - try + if (App.Settings.UseLogarithmicVolume) { + value = value.Bound(App.Settings.LogarithmicVolumeMindB, 0); + // We must convert manually here because sessions use linear volume. + _simpleVolume.SetMasterVolume(value.LogToLinear(), Guid.Empty); _volume = value; - _simpleVolume.SetMasterVolume(value, Guid.Empty); + IsMuted = false; // Mute is equals to -∞ in dB, so we always unmute when setting volume. } - catch (Exception ex) when (ex.Is(HRESULT.AUDCLNT_E_DEVICE_INVALIDATED)) + else { - // Expected in some cases. + value = value.Bound(0, 1f); + _simpleVolume.SetMasterVolume(value, Guid.Empty); + _volume = value; + IsMuted = _volume.ToVolumeInt() == 0; } - - IsMuted = App.Settings.UseLogarithmicVolume ? _volume <= (1 / 100f).ToLogVolume() : _volume.ToVolumeInt() == 0; + } + catch (Exception ex) when (ex.Is(HRESULT.AUDCLNT_E_DEVICE_INVALIDATED)) + { + // Expected in some cases. } } } @@ -189,6 +192,9 @@ public AudioDeviceSession(IAudioDevice parent, IAudioSessionControl session, Dis SyncPersistedEndpoint(parent); } } + + // Potential memory leak: this class is not IDisposable, so we cannot unregister the event. + App.Settings.UseLogarithmicVolumeChanged += (sender, args) => RaisePropertyChanged(nameof(Volume)); } ~AudioDeviceSession() @@ -242,8 +248,16 @@ public void MoveToDevice(string id, bool hide) public void UpdatePeakValueBackground() { var newValues = Helpers.ReadPeakValues(_meter); - PeakValue1 = newValues[0]; - PeakValue2 = newValues[1]; + if (App.Settings.UseLogarithmicVolume) + { + PeakValue1 = newValues[0].LinearToLogNormalized(); + PeakValue2 = newValues[1].LinearToLogNormalized(); + } + else + { + PeakValue1 = newValues[0]; + PeakValue2 = newValues[1]; + } } private void ChooseDisplayName(string displayNameFromSession) diff --git a/EarTrumpet/Extensions/FloatExtensions.cs b/EarTrumpet/Extensions/FloatExtensions.cs index cc940e902..5960edce0 100644 --- a/EarTrumpet/Extensions/FloatExtensions.cs +++ b/EarTrumpet/Extensions/FloatExtensions.cs @@ -4,8 +4,6 @@ namespace EarTrumpet.Extensions; public static class FloatExtensions { - private const float CURVE_FACTOR = 5.757f; - public static int ToVolumeInt(this float val) { return Convert.ToInt32(Math.Round(val * 100, MidpointRounding.AwayFromZero)); @@ -16,13 +14,13 @@ public static float Bound(this float val, float min, float max) return Math.Max(min, Math.Min(max, val)); } - public static float ToLogVolume(this float val) - { - return ((float)(Math.Exp(CURVE_FACTOR * val) / Math.Exp(CURVE_FACTOR))).Bound(0, 1f); - } + public static float LinearToLog(this float val) => (float)(20 * Math.Log10(val)); - public static float ToDisplayVolume(this float val) - { - return ((float)(Math.Log(val * Math.Exp(CURVE_FACTOR)) / CURVE_FACTOR)).Bound(0, 1f); - } + public static float LinearToLogNormalized(this float val) => + val == 0 + ? 0 + : ((float)(20 * Math.Log10(val) / -App.Settings.LogarithmicVolumeMindB + 1)) + .Bound(0, 1f); + + public static float LogToLinear(this float val) => (float)Math.Pow(10, val / 20); } diff --git a/EarTrumpet/UI/Controls/VolumeSlider.cs b/EarTrumpet/UI/Controls/VolumeSlider.cs index 6a89ee8cd..77e0f9043 100644 --- a/EarTrumpet/UI/Controls/VolumeSlider.cs +++ b/EarTrumpet/UI/Controls/VolumeSlider.cs @@ -39,6 +39,16 @@ public VolumeSlider() : base() MouseMove += OnMouseMove; MouseWheel += OnMouseWheel; Loaded += OnLoaded; + Unloaded += OnUnloaded; + + UseLogarithmicVolumeChangedHandler(null, App.Settings.UseLogarithmicVolume); + App.Settings.UseLogarithmicVolumeChanged += UseLogarithmicVolumeChangedHandler; + } + + private void UseLogarithmicVolumeChangedHandler(object sender, bool value) + { + UpdateVolumeRange(); + SizeOrVolumeOrPeakValueChanged(); } private void OnLoaded(object sender, RoutedEventArgs e) @@ -48,6 +58,11 @@ private void OnLoaded(object sender, RoutedEventArgs e) _peakMeter2 = (Border)GetTemplateChild("PeakMeter2"); } + private void OnUnloaded(object sender, RoutedEventArgs e) + { + App.Settings.UseLogarithmicVolumeChanged -= UseLogarithmicVolumeChangedHandler; + } + protected override Size ArrangeOverride(Size arrangeBounds) { var ret = base.ArrangeOverride(arrangeBounds); @@ -60,16 +75,46 @@ private static void PeakValueChanged(DependencyObject d, DependencyPropertyChang ((VolumeSlider)d).SizeOrVolumeOrPeakValueChanged(); } + private void UpdateVolumeRange() + { + if (App.Settings.UseLogarithmicVolume) + { + Minimum = App.Settings.LogarithmicVolumeMindB; + Maximum = 0f; + TickFrequency = 0.1; + } + else + { + Minimum = 0f; + Maximum = 100f; + TickFrequency = 1; + } + } + private void SizeOrVolumeOrPeakValueChanged() { if (_peakMeter1 != null) { - _peakMeter1.Width = Math.Max(0, (ActualWidth - _thumb.ActualWidth) * PeakValue1 * (Value / 100f)); + if (App.Settings.UseLogarithmicVolume) + { + _peakMeter1.Width = Math.Max(0, (ActualWidth - _thumb.ActualWidth) * (PeakValue1 - Value / Minimum)); + } + else + { + _peakMeter1.Width = Math.Max(0, (ActualWidth - _thumb.ActualWidth) * PeakValue1 * (Value / 100f)); + } } if (_peakMeter2 != null) { - _peakMeter2.Width = Math.Max(0, (ActualWidth - _thumb.ActualWidth) * PeakValue2 * (Value / 100f)); + if (App.Settings.UseLogarithmicVolume) + { + _peakMeter2.Width = Math.Max(0, (ActualWidth - _thumb.ActualWidth) * (PeakValue2 - Value / Minimum)); + } + else + { + _peakMeter2.Width = Math.Max(0, (ActualWidth - _thumb.ActualWidth) * PeakValue2 * (Value / 100f)); + } } } @@ -145,7 +190,7 @@ private void OnMouseMove(object sender, MouseEventArgs e) private void OnMouseWheel(object sender, MouseWheelEventArgs e) { - var amount = Math.Sign(e.Delta) * 2.0; + var amount = Math.Sign(e.Delta) * (App.Settings.UseLogarithmicVolume ? 0.2 : 2.0); ChangePositionByAmount(amount); e.Handled = true; } @@ -153,7 +198,9 @@ private void OnMouseWheel(object sender, MouseWheelEventArgs e) public void SetPositionByControlPoint(Point point) { var percent = point.X / ActualWidth; - Value = Bound((Maximum - Minimum) * percent); + Value = App.Settings.UseLogarithmicVolume + ? (percent - 1) * -App.Settings.LogarithmicVolumeMindB + : Bound((Maximum - Minimum) * percent); } public void ChangePositionByAmount(double amount) diff --git a/EarTrumpet/UI/Converters/VolumeToStringConverter.cs b/EarTrumpet/UI/Converters/VolumeToStringConverter.cs new file mode 100644 index 000000000..b6c356643 --- /dev/null +++ b/EarTrumpet/UI/Converters/VolumeToStringConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Windows.Data; + +namespace EarTrumpet.UI.Converters; + +public class VolumeToStringConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + if (value is float vol) + { + if (App.Settings.UseLogarithmicVolume) + { + // Special case for -0.0 display + if (vol >= -0.05) + { + return "-0.0"; + } + return $"{vol:0.0}"; + } + return vol.ToString(); + } + return ""; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/EarTrumpet/UI/ViewModels/AudioSessionViewModel.cs b/EarTrumpet/UI/ViewModels/AudioSessionViewModel.cs index d01394004..0ad6737bc 100644 --- a/EarTrumpet/UI/ViewModels/AudioSessionViewModel.cs +++ b/EarTrumpet/UI/ViewModels/AudioSessionViewModel.cs @@ -44,10 +44,16 @@ public bool IsAbsMuted set => _isAbsMuted = value; } - public int Volume + // For compatibility reasons, we use 0-100 for linear volume, + // and negative number for logarithmic volume. + public float Volume { - get => _stream.Volume.ToVolumeInt(); - set => _stream.Volume = value/100f; + get => App.Settings.UseLogarithmicVolume + ? _stream.Volume + : _stream.Volume.ToVolumeInt(); + set => _stream.Volume = App.Settings.UseLogarithmicVolume + ? value + : value / 100f; } public virtual float PeakValue1 => _stream.PeakValue1; public virtual float PeakValue2 => _stream.PeakValue2; diff --git a/EarTrumpet/UI/ViewModels/DeviceCollectionViewModel.cs b/EarTrumpet/UI/ViewModels/DeviceCollectionViewModel.cs index 94dedb92d..51f0adf94 100644 --- a/EarTrumpet/UI/ViewModels/DeviceCollectionViewModel.cs +++ b/EarTrumpet/UI/ViewModels/DeviceCollectionViewModel.cs @@ -245,7 +245,10 @@ public string GetTrayToolTip() { if (Default != null) { - var stateText = Default.IsMuted ? Properties.Resources.MutedText : $"{Default.Volume}%"; + var volumeText = App.Settings.UseLogarithmicVolume + ? $"{Default.Volume:0.0} dB" + : $"{Default.Volume}%"; + var stateText = Default.IsMuted ? Properties.Resources.MutedText : volumeText; var prefixText = $"EarTrumpet: {stateText} - "; var deviceName = $"{Default.DeviceDescription} ({Default.EnumeratorName})"; diff --git a/EarTrumpet/UI/ViewModels/DeviceViewModel.cs b/EarTrumpet/UI/ViewModels/DeviceViewModel.cs index f4acce8aa..bd924804f 100644 --- a/EarTrumpet/UI/ViewModels/DeviceViewModel.cs +++ b/EarTrumpet/UI/ViewModels/DeviceViewModel.cs @@ -134,6 +134,25 @@ private void UpdateMasterVolumeIcon() { IconKind = DeviceIconKind.Mute; } + else if (App.Settings.UseLogarithmicVolume) + { + if (_device.Volume > -6.2f) + { + IconKind = DeviceIconKind.Bar3; + } + else if (_device.Volume > -16.6f) + { + IconKind = DeviceIconKind.Bar2; + } + else if (_device.Volume > -96f) + { + IconKind = DeviceIconKind.Bar1; + } + else + { + IconKind = DeviceIconKind.Bar0; + } + } else if (isOnWindows11 && _device.Volume > 0.66f) { IconKind = DeviceIconKind.Bar3; diff --git a/EarTrumpet/UI/ViewModels/EarTrumpetCommunitySettingsPageViewModel.cs b/EarTrumpet/UI/ViewModels/EarTrumpetCommunitySettingsPageViewModel.cs index 587c7d35b..9ce4001b1 100644 --- a/EarTrumpet/UI/ViewModels/EarTrumpetCommunitySettingsPageViewModel.cs +++ b/EarTrumpet/UI/ViewModels/EarTrumpetCommunitySettingsPageViewModel.cs @@ -9,6 +9,19 @@ public bool UseLogarithmicVolume set => _settings.UseLogarithmicVolume = value; } + public double LogarithmicVolumeMindB + { + get => _settings.LogarithmicVolumeMindB; + set + { + if (_settings.LogarithmicVolumeMindB != (float)value) + { + _settings.LogarithmicVolumeMindB = (float)value; + RaisePropertyChanged(nameof(LogarithmicVolumeMindB)); + } + } + } + public bool ShowFullMixerWindowOnStartup { get => _settings.ShowFullMixerWindowOnStartup; diff --git a/EarTrumpet/UI/ViewModels/IAppItemViewModel.cs b/EarTrumpet/UI/ViewModels/IAppItemViewModel.cs index 7d49add44..119d5551b 100644 --- a/EarTrumpet/UI/ViewModels/IAppItemViewModel.cs +++ b/EarTrumpet/UI/ViewModels/IAppItemViewModel.cs @@ -9,7 +9,7 @@ public interface IAppItemViewModel : IAppIconSource, INotifyPropertyChanged { string Id { get; } bool IsMuted { get; set; } - int Volume { get; set; } + float Volume { get; set; } Color Background { get; } ObservableCollection ChildApps { get; } string DisplayName { get; } diff --git a/EarTrumpet/UI/ViewModels/SettingsAppItemViewModel.cs b/EarTrumpet/UI/ViewModels/SettingsAppItemViewModel.cs index 0c5e5612c..5a1379fbf 100644 --- a/EarTrumpet/UI/ViewModels/SettingsAppItemViewModel.cs +++ b/EarTrumpet/UI/ViewModels/SettingsAppItemViewModel.cs @@ -27,8 +27,8 @@ public bool IsMuted } } - private int _volume; - public int Volume + private float _volume; + public float Volume { get => _volume; set diff --git a/EarTrumpet/UI/ViewModels/TemporaryAppItemViewModel.cs b/EarTrumpet/UI/ViewModels/TemporaryAppItemViewModel.cs index 85a75b446..c4769dbf9 100644 --- a/EarTrumpet/UI/ViewModels/TemporaryAppItemViewModel.cs +++ b/EarTrumpet/UI/ViewModels/TemporaryAppItemViewModel.cs @@ -34,7 +34,7 @@ public bool IsMuted } } } - public int Volume + public float Volume { get => ChildApps != null ? ChildApps[0].Volume : _volume; set @@ -71,7 +71,7 @@ public int Volume private readonly WeakReference _parent; private readonly Dispatcher _currentDispatcher = Dispatcher.CurrentDispatcher; private uint[] _processIds; - private int _volume; + private float _volume; private bool _isMuted; internal TemporaryAppItemViewModel(DeviceCollectionViewModel parent, IAudioDeviceManager deviceManager, IAppItemViewModel app, bool isChild = false) diff --git a/EarTrumpet/UI/Views/AppItemView.xaml b/EarTrumpet/UI/Views/AppItemView.xaml index 1aa5b0e99..16e997817 100644 --- a/EarTrumpet/UI/Views/AppItemView.xaml +++ b/EarTrumpet/UI/Views/AppItemView.xaml @@ -3,8 +3,12 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Theme="clr-namespace:EarTrumpet.UI.Themes" xmlns:ctl="clr-namespace:EarTrumpet.UI.Controls" + xmlns:conv="clr-namespace:EarTrumpet.UI.Converters" Height="{DynamicResource Mutable_AppItemCellHeight}" IsTabStop="False"> + + + @@ -94,7 +98,7 @@ - +