diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs index ee17a345bf..251e6c9c34 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -24,9 +24,6 @@ namespace osu.Game.Screens.Play.HUD private const float bar_height = 10; - [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.GraphType), nameof(SongProgressStrings.GraphTypeDescription))] - public Bindable ShowGraph { get; } = new BindableBool(true); - [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))] public Bindable ShowTime { get; } = new BindableBool(true); @@ -95,23 +92,26 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); + GraphType.ValueChanged += _ => updateGraphVisibility(); + Interactive.BindValueChanged(_ => bar.Interactive = Interactive.Value, true); - ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true); ShowTime.BindValueChanged(_ => info.FadeTo(ShowTime.Value ? 1 : 0, 200, Easing.In), true); AccentColour.BindValueChanged(_ => Colour = AccentColour.Value, true); } - protected override void UpdateFromObjects(IEnumerable objects) + protected override void UpdateTimeBounds() { - graph.Objects = objects; - info.StartTime = bar.StartTime = FirstHitTime; info.EndTime = bar.EndTime = LastHitTime; } + protected override void UpdateFromObjects(IEnumerable objects) => graph.SetFromObjects(objects); + + protected override void UpdateFromStrains(double[] sectionStrains) => graph.SetFromStrains(sectionStrains); + private void updateGraphVisibility() { - graph.FadeTo(ShowGraph.Value ? 1 : 0, 200, Easing.In); + graph.FadeTo(GraphType.Value != DifficultyGraphType.None ? 1 : 0, 200, Easing.In); } protected override void Update() diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs index be570c1578..50ec364c84 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs @@ -10,49 +10,48 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Objects; using osu.Game.Graphics.UserInterface; +using osu.Game.Utils; namespace osu.Game.Screens.Play.HUD { - public partial class ArgonSongProgressGraph : SegmentedGraph + public partial class ArgonSongProgressGraph : SegmentedGraph { private const int tier_count = 5; private const int display_granularity = 200; - private IEnumerable? objects; - - public IEnumerable Objects + public void SetFromObjects(IEnumerable objects) { - set + float[] values = new float[display_granularity]; + + if (!objects.Any()) + return; + + (double firstHit, double lastHit) = BeatmapExtensions.CalculatePlayableBounds(objects); + + if (lastHit == 0) + lastHit = objects.Last().StartTime; + + double interval = (lastHit - firstHit + 1) / display_granularity; + + foreach (var h in objects) { - objects = value; + double endTime = h.GetEndTime(); - int[] values = new int[display_granularity]; + Debug.Assert(endTime >= h.StartTime); - if (!objects.Any()) - return; - - (double firstHit, double lastHit) = BeatmapExtensions.CalculatePlayableBounds(objects); - - if (lastHit == 0) - lastHit = objects.Last().StartTime; - - double interval = (lastHit - firstHit + 1) / display_granularity; - - foreach (var h in objects) - { - double endTime = h.GetEndTime(); - - Debug.Assert(endTime >= h.StartTime); - - int startRange = (int)((h.StartTime - firstHit) / interval); - int endRange = (int)((endTime - firstHit) / interval); - for (int i = startRange; i <= endRange; i++) - values[i]++; - } - - Values = values; + int startRange = (int)((h.StartTime - firstHit) / interval); + int endRange = (int)((endTime - firstHit) / interval); + for (int i = startRange; i <= endRange; i++) + values[i]++; } + + Values = values; + } + + public void SetFromStrains(double[] strains) + { + Values = FormatUtils.ResampleStrains(strains, display_granularity).Select(value => (float)value).ToArray(); } public ArgonSongProgressGraph() diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgressGraph.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgressGraph.cs index 7e3ae504c0..50808e172b 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgressGraph.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgressGraph.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Diagnostics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; +using osu.Game.Utils; namespace osu.Game.Screens.Play.HUD { @@ -44,152 +45,7 @@ namespace osu.Game.Screens.Play.HUD public void SetFromStrains(double[] strains) { - // For some reason it has 1 column delay, account for this by skipping first value - Values = resampling(strains, granularity).Select(value => (float)value).ToArray(); - } - - private static double[] resampling(double[] values, int targetSize) - { - if (targetSize > values.Length) - return resamplingUpscale(values, targetSize); - - else if (targetSize < values.Length) - return resamplingDownscale(values, targetSize); - - return (double[])values.Clone(); - } - - private static double[] resamplingUpscale(double[] values, int targetSize) - { - // Create array filled with -inf - double[] result = Enumerable.Repeat(double.NegativeInfinity, targetSize).ToArray(); - - // First and last peaks are constant - result[0] = values[0]; - result[^1] = values[^1]; - - // On the first pass we place peaks - - int sourceIndex = 1; - int targetIndex = 1; - - // Adjust sizes accounting for the fact that first and last elements already set-up - int sourceSize = values.Length - 1; - targetSize -= 1; - - for (; targetIndex < targetSize - 1; targetIndex++) - { - double sourceProgress = (double)sourceIndex / sourceSize; - - double targetProgressNext = (targetIndex + 1.0) / targetSize; - - // If we reached the point where source is between current and next - then peak is either current or next - if (sourceProgress <= targetProgressNext) - { - double targetProgressCurrent = (double)targetIndex / targetSize; - - double distanceToCurrent = sourceProgress - targetProgressCurrent; - double distanceToNext = targetProgressNext - sourceProgress; - - // If it's next what is closer - abbadon current and move to next immediatly - if (distanceToNext < distanceToCurrent) - { - result[targetIndex] = double.NegativeInfinity; - targetIndex++; - } - - result[targetIndex] = values[sourceIndex]; - sourceIndex++; - } - } - - // On second pass we interpolate between peaks - - sourceIndex = 0; - targetIndex = 1; - - for (; targetIndex < targetSize; targetIndex++) - { - // If we're on peak - skip iteration - if (result[targetIndex] != double.NegativeInfinity) - { - sourceIndex++; - continue; - } - - double targetProgress = (double)targetIndex / targetSize; - - double previousPeakProgress = (double)sourceIndex / sourceSize; - double nextPeakProgress = (sourceIndex + 1.0) / sourceSize; - - double distanceToPreviousPeak = targetProgress - previousPeakProgress; - double distanceToNextPeak = nextPeakProgress - targetProgress; - - double lerpCoef = distanceToPreviousPeak / (distanceToPreviousPeak + distanceToNextPeak); - result[targetIndex] = double.Lerp(values[sourceIndex], values[sourceIndex + 1], lerpCoef); - } - - return result; - } - - private static double[] resamplingDownscale(double[] values, int targetSize) - { - double[] result = new double[targetSize]; - - int sourceIndex = 0; - int targetIndex = 0; - - double currentSampleMax = double.NegativeInfinity; - - for (; sourceIndex < values.Length; sourceIndex++) - { - double currentValue = values[sourceIndex]; - - double sourceProgress = (sourceIndex + 0.5) / values.Length; - double targetProgressBorder = (targetIndex + 1.0) / targetSize; - - double distanceToBorder = targetProgressBorder - sourceProgress; - - // Handle transition to next sample - if (distanceToBorder < 0) - { - double targetProgressCurrent = (targetIndex + 0.5) / targetSize; - double targetProgressNext = (targetIndex + 1.5) / targetSize; - - // Try fit weighted current into still current sample - // It would always be closer to Next than to Current - double weight = (targetProgressNext - sourceProgress) / (sourceProgress - targetProgressCurrent); - double weightedValue = currentValue * weight; - - if (currentSampleMax < weightedValue) currentSampleMax = weightedValue; - - // Flush current max - result[targetIndex] = currentSampleMax; - targetIndex++; - currentSampleMax = double.NegativeInfinity; - - // Try to fit weighted previous into future sample - if (sourceIndex > 0) - { - double prevValue = values[sourceIndex - 1]; - double sourceProgressPrev = (sourceIndex - 0.5) / values.Length; - - // It would always be closer to Current than to Current - weight = (sourceProgressPrev - targetProgressCurrent) / (targetProgressNext - sourceProgressPrev); - weightedValue = prevValue * weight; - - currentSampleMax = weightedValue; - } - } - - // Replace with maximum of the sample - if (currentSampleMax < currentValue) currentSampleMax = currentValue; - } - - // Flush last value - result[targetIndex] = currentSampleMax; - - return result; + Values = FormatUtils.ResampleStrains(strains, granularity).Select(value => (float)value).ToArray(); } } } diff --git a/osu.Game/Screens/Play/HUD/SongProgress.cs b/osu.Game/Screens/Play/HUD/SongProgress.cs index 72261646a3..c442212169 100644 --- a/osu.Game/Screens/Play/HUD/SongProgress.cs +++ b/osu.Game/Screens/Play/HUD/SongProgress.cs @@ -222,7 +222,7 @@ namespace osu.Game.Screens.Play.HUD .GroupBy(x => x.index) .Select(g => g.Max(x => x.value)); - return convertStrains(result); + return result.ToArray(); } private double[] getTotalStrains(List allStrains) @@ -232,13 +232,7 @@ namespace osu.Game.Screens.Play.HUD .GroupBy(x => x.index) .Select(g => Math.Sqrt(g.Sum(x => x.value * x.value))); - return convertStrains(result); - } - - // Strains are ending with StartTime of last object, so we need to add - private double[] convertStrains(IEnumerable strains) - { - return strains.ToArray(); + return result.ToArray(); } protected virtual void UpdateFromStrains(double[] sectionStrains) { } diff --git a/osu.Game/Utils/FormatUtils.cs b/osu.Game/Utils/FormatUtils.cs index cccad3711c..4e1e32f8a3 100644 --- a/osu.Game/Utils/FormatUtils.cs +++ b/osu.Game/Utils/FormatUtils.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using Humanizer; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; @@ -59,5 +60,156 @@ namespace osu.Game.Utils /// The base BPM to round. /// Rate adjustment, if applicable. public static int RoundBPM(double baseBpm, double rate = 1) => (int)Math.Round(Math.Round(baseBpm) * rate); + + /// + /// Resampling strain values to certain bin size. + /// + /// + /// The main feature of this resampling is that peak strains will be always preserved. + /// This means that the highest strain can't be decreased by averaging or interpolation. + /// + public static double[] ResampleStrains(double[] values, int targetSize) + { + if (targetSize > values.Length) + return resamplingUpscale(values, targetSize); + + else if (targetSize < values.Length) + return resamplingDownscale(values, targetSize); + + return (double[])values.Clone(); + } + + private static double[] resamplingUpscale(double[] values, int targetSize) + { + // Create array filled with -inf + double[] result = Enumerable.Repeat(double.NegativeInfinity, targetSize).ToArray(); + + // First and last peaks are constant + result[0] = values[0]; + result[^1] = values[^1]; + + // On the first pass we place peaks + + int sourceIndex = 1; + int targetIndex = 1; + + // Adjust sizes accounting for the fact that first and last elements already set-up + int sourceSize = values.Length - 1; + targetSize -= 1; + + for (; targetIndex < targetSize - 1; targetIndex++) + { + double sourceProgress = (double)sourceIndex / sourceSize; + + double targetProgressNext = (targetIndex + 1.0) / targetSize; + + // If we reached the point where source is between current and next - then peak is either current or next + if (sourceProgress <= targetProgressNext) + { + double targetProgressCurrent = (double)targetIndex / targetSize; + + double distanceToCurrent = sourceProgress - targetProgressCurrent; + double distanceToNext = targetProgressNext - sourceProgress; + + // If it's next what is closer - abbadon current and move to next immediatly + if (distanceToNext < distanceToCurrent) + { + result[targetIndex] = double.NegativeInfinity; + targetIndex++; + } + + result[targetIndex] = values[sourceIndex]; + sourceIndex++; + } + } + + // On second pass we interpolate between peaks + + sourceIndex = 0; + targetIndex = 1; + + for (; targetIndex < targetSize; targetIndex++) + { + // If we're on peak - skip iteration + if (result[targetIndex] != double.NegativeInfinity) + { + sourceIndex++; + continue; + } + + double targetProgress = (double)targetIndex / targetSize; + + double previousPeakProgress = (double)sourceIndex / sourceSize; + double nextPeakProgress = (sourceIndex + 1.0) / sourceSize; + + double distanceToPreviousPeak = targetProgress - previousPeakProgress; + double distanceToNextPeak = nextPeakProgress - targetProgress; + + double lerpCoef = distanceToPreviousPeak / (distanceToPreviousPeak + distanceToNextPeak); + result[targetIndex] = double.Lerp(values[sourceIndex], values[sourceIndex + 1], lerpCoef); + } + + return result; + } + + private static double[] resamplingDownscale(double[] values, int targetSize) + { + double[] result = new double[targetSize]; + + int sourceIndex = 0; + int targetIndex = 0; + + double currentSampleMax = double.NegativeInfinity; + + for (; sourceIndex < values.Length; sourceIndex++) + { + double currentValue = values[sourceIndex]; + + double sourceProgress = (sourceIndex + 0.5) / values.Length; + double targetProgressBorder = (targetIndex + 1.0) / targetSize; + + double distanceToBorder = targetProgressBorder - sourceProgress; + + // Handle transition to next sample + if (distanceToBorder < 0) + { + double targetProgressCurrent = (targetIndex + 0.5) / targetSize; + double targetProgressNext = (targetIndex + 1.5) / targetSize; + + // Try fit weighted current into still current sample + // It would always be closer to Next than to Current + double weight = (targetProgressNext - sourceProgress) / (sourceProgress - targetProgressCurrent); + double weightedValue = currentValue * weight; + + if (currentSampleMax < weightedValue) currentSampleMax = weightedValue; + + // Flush current max + result[targetIndex] = currentSampleMax; + targetIndex++; + currentSampleMax = double.NegativeInfinity; + + // Try to fit weighted previous into future sample + if (sourceIndex > 0) + { + double prevValue = values[sourceIndex - 1]; + double sourceProgressPrev = (sourceIndex - 0.5) / values.Length; + + // It would always be closer to Current than to Current + weight = (sourceProgressPrev - targetProgressCurrent) / (targetProgressNext - sourceProgressPrev); + weightedValue = prevValue * weight; + + currentSampleMax = weightedValue; + } + } + + // Replace with maximum of the sample + if (currentSampleMax < currentValue) currentSampleMax = currentValue; + } + + // Flush last value + result[targetIndex] = currentSampleMax; + + return result; + } } }