osu/osu.Game/Overlays/Volume/VolumeMeter.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

420 lines
15 KiB
C#
Raw Normal View History

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2018-04-13 09:19:50 +00:00
2022-06-17 07:37:17 +00:00
#nullable disable
using System;
2018-02-28 15:14:52 +00:00
using System.Globalization;
2024-02-02 10:48:13 +00:00
using JetBrains.Annotations;
using osu.Framework;
2018-02-28 15:14:52 +00:00
using osu.Framework.Allocation;
2021-07-01 11:41:30 +00:00
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
2019-02-21 10:04:31 +00:00
using osu.Framework.Bindables;
using osu.Framework.Extensions;
2018-03-03 18:25:34 +00:00
using osu.Framework.Extensions.Color4Extensions;
2018-02-28 15:14:52 +00:00
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
2018-10-02 03:02:47 +00:00
using osu.Framework.Input.Events;
using osu.Framework.Threading;
2020-01-09 04:43:44 +00:00
using osu.Framework.Utils;
2018-02-28 15:14:52 +00:00
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
2018-11-20 07:51:59 +00:00
using osuTK;
using osuTK.Graphics;
2018-04-13 09:19:50 +00:00
2018-02-28 15:14:52 +00:00
namespace osu.Game.Overlays.Volume
{
public partial class VolumeMeter : Container, IStateful<SelectionState>
2018-02-28 15:14:52 +00:00
{
private CircularProgress volumeCircle;
private CircularProgress volumeCircleGlow;
public BindableDouble Bindable { get; } = new BindableDouble { MinValue = 0, MaxValue = 1, Precision = 0.01 };
2018-02-28 15:14:52 +00:00
private readonly float circleSize;
private readonly Color4 meterColour;
private readonly string name;
2018-04-13 09:19:50 +00:00
2018-03-07 05:50:50 +00:00
private OsuSpriteText text;
private BufferedContainer maxGlow;
2018-04-13 09:19:50 +00:00
private Container selectedGlowContainer;
2021-07-05 13:47:35 +00:00
private Sample hoverSample;
private Sample notchSample;
2021-07-01 11:41:30 +00:00
private double sampleLastPlaybackTime;
2024-02-02 10:48:13 +00:00
[CanBeNull]
public event Action<SelectionState> StateChanged;
private SelectionState state;
public SelectionState State
{
get => state;
set
{
if (state == value)
return;
state = value;
StateChanged?.Invoke(value);
updateSelectedState();
}
}
private const float transition_length = 500;
2018-02-28 15:14:52 +00:00
public VolumeMeter(string name, float circleSize, Color4 meterColour)
{
this.circleSize = circleSize;
this.meterColour = meterColour;
this.name = name;
2018-04-13 09:19:50 +00:00
2018-02-28 15:14:52 +00:00
AutoSizeAxes = Axes.Both;
}
2018-04-13 09:19:50 +00:00
2018-02-28 15:14:52 +00:00
[BackgroundDependencyLoader]
2021-07-01 11:41:30 +00:00
private void load(OsuColour colours, AudioManager audio)
2018-02-28 15:14:52 +00:00
{
hoverSample = audio.Samples.Get($@"UI/{HoverSampleSet.Button.GetDescription()}-hover");
notchSample = audio.Samples.Get(@"UI/notch-tick");
2021-07-01 11:41:30 +00:00
sampleLastPlaybackTime = Time.Current;
Color4 backgroundColour = colours.Gray1;
2018-04-13 09:19:50 +00:00
2018-02-28 15:14:52 +00:00
CircularProgress bgProgress;
2018-04-13 09:19:50 +00:00
const float progress_start_radius = 0.75f;
const float progress_size = 0.03f;
const float progress_end_radius = progress_start_radius + progress_size;
const float blur_amount = 5;
Children = new Drawable[]
2018-02-28 15:14:52 +00:00
{
new Container
2018-02-28 15:14:52 +00:00
{
Size = new Vector2(circleSize),
Children = new Drawable[]
2018-02-28 15:14:52 +00:00
{
new BufferedContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour,
},
new CircularContainer
{
Masking = true,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(progress_end_radius),
Children = new Drawable[]
{
bgProgress = new CircularProgress
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Rotation = 180,
Colour = backgroundColour,
},
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Name = @"Progress under covers for smoothing",
RelativeSizeAxes = Axes.Both,
Rotation = 180,
Child = volumeCircle = new CircularProgress
{
RelativeSizeAxes = Axes.Both,
}
},
}
},
new Circle
{
Name = @"Inner Cover",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour,
Size = new Vector2(progress_start_radius),
},
new Container
{
Name = @"Progress overlay for glow",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(progress_start_radius + progress_size / 1.5f),
Rotation = 180,
Padding = new MarginPadding(-Blur.KernelSize(blur_amount)),
Child = (volumeCircleGlow = new CircularProgress
{
RelativeSizeAxes = Axes.Both,
InnerRadius = progress_size * 0.8f,
}).WithEffect(new GlowEffect
{
Colour = meterColour,
BlurSigma = new Vector2(blur_amount),
Strength = 5,
PadExtent = true
}),
},
},
},
selectedGlowContainer = new CircularContainer
{
Masking = true,
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true,
},
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = meterColour.Opacity(0.1f),
Radius = 10,
}
},
maxGlow = (text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Numeric.With(size: 0.16f * circleSize)
}).WithEffect(new GlowEffect
{
Colour = Color4.Transparent,
PadExtent = true,
})
}
},
new Container
{
Size = new Vector2(120, 20),
CornerRadius = 10,
Masking = true,
Margin = new MarginPadding { Left = circleSize + 10 },
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Children = new Drawable[]
2018-02-28 15:14:52 +00:00
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour,
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(weight: FontWeight.Bold),
Text = name
}
}
2018-02-28 15:14:52 +00:00
}
};
Bindable.BindValueChanged(volume => { this.TransformTo(nameof(DisplayVolume), volume.NewValue, 400, Easing.OutQuint); }, true);
bgProgress.Progress = 0.75f;
2018-03-07 05:50:50 +00:00
}
2018-04-13 09:19:50 +00:00
private int? displayVolumeInt;
2018-04-13 09:19:50 +00:00
2018-03-07 05:50:50 +00:00
private double displayVolume;
2018-04-13 09:19:50 +00:00
2018-03-07 05:50:50 +00:00
protected double DisplayVolume
{
get => displayVolume;
set
2018-02-28 15:14:52 +00:00
{
2018-03-07 05:50:50 +00:00
displayVolume = value;
2018-04-13 09:19:50 +00:00
int intValue = (int)Math.Round(displayVolume * 100);
bool intVolumeChanged = intValue != displayVolumeInt;
displayVolumeInt = intValue;
if (displayVolume >= 0.995f)
2018-02-28 15:14:52 +00:00
{
2018-03-03 18:25:34 +00:00
text.Text = "MAX";
maxGlow.EffectColour = meterColour.Opacity(2f);
2018-02-28 15:14:52 +00:00
}
else
{
2018-03-07 05:50:50 +00:00
maxGlow.EffectColour = Color4.Transparent;
text.Text = intValue.ToString(CultureInfo.CurrentCulture);
2018-02-28 15:14:52 +00:00
}
2018-04-13 09:19:50 +00:00
volumeCircle.Progress = displayVolume * 0.75f;
volumeCircleGlow.Progress = displayVolume * 0.75f;
2021-07-01 11:41:30 +00:00
if (intVolumeChanged && IsLoaded)
Scheduler.AddOnce(playTickSound);
}
}
2021-07-01 11:41:30 +00:00
private void playTickSound()
{
const int tick_debounce_time = 30;
2021-07-01 11:41:30 +00:00
if (Time.Current - sampleLastPlaybackTime <= tick_debounce_time)
return;
2021-07-01 11:41:30 +00:00
var channel = notchSample.GetChannel();
2021-07-01 11:41:30 +00:00
channel.Frequency.Value = 0.99f + RNG.NextDouble(0.02f) + displayVolume * 0.1f;
// intentionally pitched down, even when hitting max.
if (displayVolumeInt == 0 || displayVolumeInt == 100)
channel.Frequency.Value -= 0.5f;
channel.Play();
sampleLastPlaybackTime = Time.Current;
2018-02-28 15:14:52 +00:00
}
2018-04-13 09:19:50 +00:00
2018-02-28 15:14:52 +00:00
public double Volume
{
2019-02-21 09:56:34 +00:00
get => Bindable.Value;
2018-02-28 15:14:52 +00:00
private set => Bindable.Value = value;
}
2018-04-13 09:19:50 +00:00
private const double adjust_step = 0.01;
2018-04-13 09:19:50 +00:00
public void Increase(double amount = 1, bool isPrecise = false) => adjust(amount, isPrecise);
public void Decrease(double amount = 1, bool isPrecise = false) => adjust(-amount, isPrecise);
// because volume precision is set to 0.01, this local is required to keep track of more precise adjustments and only apply when possible.
private double scrollAccumulation;
2018-04-13 09:19:50 +00:00
private double accelerationModifier = 1;
private const double max_acceleration = 5;
2021-04-15 14:24:13 +00:00
private const double acceleration_multiplier = 1.8;
private ScheduledDelegate accelerationDebounce;
private void resetAcceleration() => accelerationModifier = 1;
2024-01-17 05:29:55 +00:00
private float dragDelta;
protected override bool OnDragStart(DragStartEvent e)
{
dragDelta = 0;
adjustFromDrag(e.Delta);
return true;
}
protected override void OnDrag(DragEvent e)
{
adjustFromDrag(e.Delta);
base.OnDrag(e);
}
private void adjustFromDrag(Vector2 delta)
{
const float mouse_drag_divisor = 200;
dragDelta += delta.Y / mouse_drag_divisor;
if (Math.Abs(dragDelta) < 0.01) return;
Volume -= dragDelta;
dragDelta = 0;
}
private void adjust(double delta, bool isPrecise)
2018-02-28 15:14:52 +00:00
{
if (delta == 0)
return;
// every adjust increment increases the rate at which adjustments happen up to a cutoff.
// this debounce will reset on inactivity.
accelerationDebounce?.Cancel();
accelerationDebounce = Scheduler.AddDelayed(resetAcceleration, 150);
delta *= accelerationModifier;
2021-04-15 14:24:13 +00:00
accelerationModifier = Math.Min(max_acceleration, accelerationModifier * acceleration_multiplier);
double precision = Bindable.Precision;
if (isPrecise)
{
scrollAccumulation += delta * adjust_step;
while (Precision.AlmostBigger(Math.Abs(scrollAccumulation), precision))
{
Volume += 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));
}
2018-02-28 15:14:52 +00:00
}
2018-04-13 09:19:50 +00:00
2018-10-02 03:02:47 +00:00
protected override bool OnScroll(ScrollEvent e)
{
adjust(e.ScrollDelta.Y, e.IsPrecise);
return true;
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
State = SelectionState.Selected;
return base.OnMouseMove(e);
}
protected override bool OnHover(HoverEvent e)
{
State = SelectionState.Selected;
return false;
}
2018-10-02 03:02:47 +00:00
protected override void OnHoverLost(HoverLostEvent e)
{
}
2021-09-16 09:26:12 +00:00
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
private void updateSelectedState()
{
switch (state)
{
case SelectionState.Selected:
this.ScaleTo(1.04f, transition_length, Easing.OutExpo);
selectedGlowContainer.FadeIn(transition_length, Easing.OutExpo);
2021-07-08 11:36:25 +00:00
hoverSample?.Play();
break;
case SelectionState.NotSelected:
this.ScaleTo(1f, transition_length, Easing.OutExpo);
selectedGlowContainer.FadeOut(transition_length, Easing.OutExpo);
break;
}
}
2018-02-28 15:14:52 +00:00
}
}