diff --git a/osu.Game/Audio/DecibelScaling.cs b/osu.Game/Audio/DecibelScaling.cs new file mode 100644 index 000000000000..1ed871ea4a7d --- /dev/null +++ b/osu.Game/Audio/DecibelScaling.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Audio +{ + /// + /// Common functions and constants for implementing decibel scaling into sliders and meters. + /// + public static class DecibelScaling + { + /// + /// Arbitrary silence threshold. Required for sliders, since the decibel scale is bottomless. + /// + public const double DB_MIN = -60; + + /// + /// Decibel equivalent of full volume. + /// + public const double DB_MAX = 0; + + /// + /// Decibel precision level. + /// + public const double DB_PRECISION = 0.5; + + /// + /// Linear equivalent of + /// + private static readonly double cutoff = Math.Pow(10, DB_MIN / 20); + + /// + /// Returns the decibel equivalent of a linear value. + /// + public static double DecibelFromLinear(double linear) => linear <= cutoff ? DB_MIN : 20 * Math.Log10(linear); + + /// + /// Returns the linear equivalent of a decibel value. + /// + public static double LinearFromDecibel(double decibel) => decibel <= DB_MIN ? 0 : Math.Pow(10, decibel / 20); + } +} diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 908f434655dd..041e29d2fd3d 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -99,7 +99,7 @@ protected override void InitialiseDefaults() SetDefault(OsuSetting.NotifyOnFriendPresenceChange, true); // Audio - SetDefault(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01); + SetDefault(OsuSetting.VolumeInactive, 0.25, 0, 1); SetDefault(OsuSetting.MenuVoice, true); SetDefault(OsuSetting.MenuMusic, true); diff --git a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs index 2bb5fa983f54..e4c99cc8f0d7 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs @@ -3,11 +3,13 @@ using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; +using static osu.Game.Audio.DecibelScaling; namespace osu.Game.Overlays.Settings.Sections.Audio { @@ -23,42 +25,83 @@ private void load(AudioManager audio, OsuConfigManager config) new VolumeAdjustSlider { LabelText = AudioSettingsStrings.MasterVolume, - Current = audio.Volume, - KeyboardStep = 0.01f, - DisplayAsPercentage = true + Volume = audio.Volume, }, - new SettingsSlider + new VolumeAdjustSlider { LabelText = AudioSettingsStrings.MasterVolumeInactive, - Current = config.GetBindable(OsuSetting.VolumeInactive), - KeyboardStep = 0.01f, - DisplayAsPercentage = true + Volume = config.GetBindable(OsuSetting.VolumeInactive), + PlaySamplesOnAdjust = true, }, new VolumeAdjustSlider { LabelText = AudioSettingsStrings.EffectVolume, - Current = audio.VolumeSample, - KeyboardStep = 0.01f, - DisplayAsPercentage = true + Volume = audio.VolumeSample, }, - new VolumeAdjustSlider { LabelText = AudioSettingsStrings.MusicVolume, - Current = audio.VolumeTrack, - KeyboardStep = 0.01f, - DisplayAsPercentage = true + Volume = audio.VolumeTrack, }, }; } private partial class VolumeAdjustSlider : SettingsSlider { - protected override Drawable CreateControl() + protected override Drawable CreateControl() => new DecibelSliderBar(); + + public bool PlaySamplesOnAdjust { set => ((DecibelSliderBar)Control).PlaySamplesOnAdjust = value; } + + public Bindable Volume { set => ((DecibelSliderBar)Control).Volume = value; } + + protected partial class DecibelSliderBar : RoundedSliderBar { - var sliderBar = (RoundedSliderBar)base.CreateControl(); - sliderBar.PlaySamplesOnAdjust = false; - return sliderBar; + public override LocalisableString TooltipText => Current.Value <= DB_MIN ? "-∞ dB" : $"{Current.Value:+#0.0;-#0.0;+0.0} dB"; + + public DecibelSliderBar() + { + RelativeSizeAxes = Axes.X; + PlaySamplesOnAdjust = false; + KeyboardStep = (float)DB_PRECISION; + + Current = new BindableNumber(0) + { + Precision = DB_PRECISION, + MinValue = DB_MIN, + MaxValue = DB_MAX, + }; + } + + public Bindable Volume = new Bindable(1); + + private bool currentFirstInvoked; + private bool volumeFirstInvoked; + + protected override void LoadComplete() + { + base.LoadComplete(); + Current.Default = DecibelFromLinear(Volume.Default); + + Current.ValueChanged += v => + { + if (!volumeFirstInvoked) + { + currentFirstInvoked = true; + Volume.Value = LinearFromDecibel(v.NewValue); + currentFirstInvoked = false; + } + }; + + Volume.BindValueChanged(v => + { + if (!currentFirstInvoked) + { + volumeFirstInvoked = true; + Current.Value = DecibelFromLinear(v.NewValue); + volumeFirstInvoked = false; + } + }, true); + } } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs index 51b95b7d3287..e9c178d4dac3 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs @@ -16,6 +16,7 @@ using osu.Game.Input.Bindings; using osuTK.Graphics; using osuTK.Input; +using static osu.Game.Audio.DecibelScaling; namespace osu.Game.Overlays.Toolbar { @@ -78,7 +79,7 @@ protected override void LoadComplete() base.LoadComplete(); globalVolume = audio.Volume.GetBoundCopy(); - globalVolume.BindValueChanged(v => volumeBar.ResizeHeightTo((float)v.NewValue, 200, Easing.OutQuint), true); + globalVolume.BindValueChanged(v => volumeBar.ResizeHeightTo((float)((DecibelFromLinear(v.NewValue) - DB_MIN) / (DB_MAX - DB_MIN)), 200, Easing.OutQuint), true); } protected override bool OnKeyDown(KeyDownEvent e) diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs index 9e0c59938606..45db4ec9eeae 100644 --- a/osu.Game/Overlays/Volume/VolumeMeter.cs +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -27,6 +27,7 @@ using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; +using static osu.Game.Audio.DecibelScaling; namespace osu.Game.Overlays.Volume { @@ -37,7 +38,7 @@ public partial class VolumeMeter : Container, IStateful protected static readonly Vector2 LABEL_SIZE = new Vector2(120, 20); - public BindableDouble Bindable { get; } = new BindableDouble { MinValue = 0, MaxValue = 1, Precision = 0.01 }; + public BindableDouble Bindable { get; } = new BindableDouble { MinValue = 0, MaxValue = 1 }; protected readonly float CircleSize; @@ -237,28 +238,37 @@ private void load(OsuColour colours, AudioManager audio) } }; - Bindable.BindValueChanged(volume => { this.TransformTo(nameof(DisplayVolume), volume.NewValue, 400, Easing.OutQuint); }, true); + Bindable.BindValueChanged(volume => + { + decibel = DecibelFromLinear(volume.NewValue); + this.TransformTo(nameof(DisplayVolume), decibel, 400, Easing.OutQuint); + }, true); bgProgress.Progress = 0.75f; } - private int? displayVolumeInt; + private int currentStep; + private const int step_min = (int)(DB_MIN / DB_PRECISION); + private const int step_max = (int)(DB_MAX / DB_PRECISION); + private double decibel; private double displayVolume; + private double normalizedVolume; protected double DisplayVolume { get => displayVolume; set { - displayVolume = value; + normalizedVolume = (value - DB_MIN) / (DB_MAX - DB_MIN); - int intValue = (int)Math.Round(displayVolume * 100); - bool intVolumeChanged = intValue != displayVolumeInt; + int step = (int)Math.Round(value / DB_PRECISION); + bool stepChanged = step != currentStep; - displayVolumeInt = intValue; + currentStep = step; + displayVolume = currentStep * DB_PRECISION; - if (displayVolume >= 0.995f) + if (currentStep >= step_max) { text.Text = "MAX"; maxGlow.EffectColour = meterColour.Opacity(2f); @@ -266,13 +276,13 @@ protected double DisplayVolume else { maxGlow.EffectColour = Color4.Transparent; - text.Text = intValue.ToString(CultureInfo.CurrentCulture); + text.Text = currentStep <= step_min ? "-INF" : displayVolume.ToString("N1", CultureInfo.CurrentCulture); } - volumeCircle.Progress = displayVolume * 0.75f; - volumeCircleGlow.Progress = displayVolume * 0.75f; + volumeCircle.Progress = normalizedVolume * 0.75f; + volumeCircleGlow.Progress = normalizedVolume * 0.75f; - if (intVolumeChanged && IsLoaded) + if (stepChanged && IsLoaded) Scheduler.AddOnce(playTickSound); } } @@ -286,10 +296,10 @@ private void playTickSound() var channel = notchSample.GetChannel(); - channel.Frequency.Value = 0.99f + RNG.NextDouble(0.02f) + displayVolume * 0.1f; + channel.Frequency.Value = 0.99f + RNG.NextDouble(0.02f) + (normalizedVolume * 0.1f); // intentionally pitched down, even when hitting max. - if (displayVolumeInt == 0 || displayVolumeInt == 100) + if (currentStep == step_min || currentStep == step_max) channel.Frequency.Value -= 0.5f; channel.Play(); @@ -302,8 +312,6 @@ public double Volume private set => Bindable.Value = value; } - private const double adjust_step = 0.01; - public void Increase(double amount = 1, bool isPrecise = false) => adjust(amount, isPrecise); public void Decrease(double amount = 1, bool isPrecise = false) => adjust(-amount, isPrecise); @@ -336,13 +344,13 @@ protected override void OnDrag(DragEvent e) private void adjustFromDrag(Vector2 delta) { - const float mouse_drag_divisor = 200; + const float mouse_drag_divisor = (float)(2 / DB_PRECISION); dragDelta += delta.Y / mouse_drag_divisor; - if (Math.Abs(dragDelta) < 0.01) return; + if (Math.Abs(dragDelta) < DB_PRECISION) return; - Volume -= dragDelta; + Volume = LinearFromDecibel(decibel - dragDelta); dragDelta = 0; } @@ -359,22 +367,25 @@ private void adjust(double delta, bool isPrecise) delta *= accelerationModifier; accelerationModifier = Math.Min(max_acceleration, accelerationModifier * acceleration_multiplier); - double precision = Bindable.Precision; + double dB = decibel; + const double precision = DB_PRECISION; if (isPrecise) { - scrollAccumulation += delta * adjust_step; + scrollAccumulation += delta * precision; while (Precision.AlmostBigger(Math.Abs(scrollAccumulation), precision)) { - Volume += Math.Sign(scrollAccumulation) * precision; + dB += Math.Sign(scrollAccumulation) * precision; scrollAccumulation = scrollAccumulation < 0 ? Math.Min(0, scrollAccumulation + precision) : Math.Max(0, scrollAccumulation - precision); } } else { - Volume += Math.Sign(delta) * Math.Max(precision, Math.Abs(delta * adjust_step)); + dB += Math.Sign(delta) * Math.Max(precision, Math.Abs(delta * precision)); } + + Volume = LinearFromDecibel(dB); } protected override bool OnScroll(ScrollEvent e)