From c6e9a6cd5a3d990b2c72fab3f82148cd65dfb6fe Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 15 Jan 2021 14:28:49 +0900 Subject: [PATCH 1/4] Make most common BPM more accurate --- osu.Game/Beatmaps/Beatmap.cs | 38 +++++++++++++++++++ osu.Game/Beatmaps/BeatmapManager.cs | 2 +- .../ControlPoints/ControlPointInfo.cs | 7 ---- osu.Game/Beatmaps/IBeatmap.cs | 5 +++ osu.Game/Screens/Edit/EditorBeatmap.cs | 2 + osu.Game/Screens/Play/GameplayBeatmap.cs | 2 + osu.Game/Screens/Select/BeatmapInfoWedge.cs | 2 +- 7 files changed, 49 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index be2006e67a..410fd5e92e 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -48,6 +48,44 @@ namespace osu.Game.Beatmaps public virtual IEnumerable GetStatistics() => Enumerable.Empty(); + public double GetMostCommonBeatLength() + { + // The last playable time in the beatmap - the last timing point extends to this time. + // Note: This is more accurate and may present different results because osu-stable didn't have the ability to calculate slider durations in this context. + double lastTime = HitObjects.LastOrDefault()?.GetEndTime() ?? ControlPointInfo.TimingPoints.LastOrDefault()?.Time ?? 0; + + var beatLengthsAndDurations = + // Construct a set of (beatLength, duration) tuples for each individual timing point. + ControlPointInfo.TimingPoints.Select((t, i) => + { + if (t.Time > lastTime) + return (beatLength: t.BeatLength, 0); + + var nextTime = i == ControlPointInfo.TimingPoints.Count - 1 ? lastTime : ControlPointInfo.TimingPoints[i + 1].Time; + return (beatLength: t.BeatLength, duration: nextTime - t.Time); + }) + // Aggregate durations into a set of (beatLength, duration) tuples for each beat length + .GroupBy(t => t.beatLength) + .Select(g => (beatLength: g.Key, duration: g.Sum(t => t.duration))) + // And if there are no timing points, use a default. + .DefaultIfEmpty((TimingControlPoint.DEFAULT_BEAT_LENGTH, 0)); + + // Find the single beat length with the maximum aggregate duration. + double maxDurationBeatLength = double.NegativeInfinity; + double maxDuration = double.NegativeInfinity; + + foreach (var (beatLength, duration) in beatLengthsAndDurations) + { + if (duration > maxDuration) + { + maxDuration = duration; + maxDurationBeatLength = beatLength; + } + } + + return 60000 / maxDurationBeatLength; + } + IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 42418e532b..0d4cc38ac3 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -451,7 +451,7 @@ namespace osu.Game.Beatmaps // TODO: this should be done in a better place once we actually need to dynamically update it. beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0; beatmap.BeatmapInfo.Length = calculateLength(beatmap); - beatmap.BeatmapInfo.BPM = beatmap.ControlPointInfo.BPMMode; + beatmap.BeatmapInfo.BPM = beatmap.GetMostCommonBeatLength(); beatmapInfos.Add(beatmap.BeatmapInfo); } diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index e8a91e4001..5cc60a5758 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -101,13 +101,6 @@ namespace osu.Game.Beatmaps.ControlPoints public double BPMMinimum => 60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength; - /// - /// Finds the mode BPM (most common BPM) represented by the control points. - /// - [JsonIgnore] - public double BPMMode => - 60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength; - /// /// Remove all s and return to a pristine state. /// diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 8f27e0b0e9..aaca8f7658 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -47,6 +47,11 @@ namespace osu.Game.Beatmaps /// IEnumerable GetStatistics(); + /// + /// Finds the most common beat length represented by the control points in this beatmap. + /// + double GetMostCommonBeatLength(); + /// /// Creates a shallow-clone of this beatmap and returns it. /// diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 165d2ba278..0e9008ba68 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -84,6 +84,8 @@ namespace osu.Game.Screens.Edit public IEnumerable GetStatistics() => PlayableBeatmap.GetStatistics(); + public double GetMostCommonBeatLength() => PlayableBeatmap.GetMostCommonBeatLength(); + public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; diff --git a/osu.Game/Screens/Play/GameplayBeatmap.cs b/osu.Game/Screens/Play/GameplayBeatmap.cs index 64894544f4..53c1360bfa 100644 --- a/osu.Game/Screens/Play/GameplayBeatmap.cs +++ b/osu.Game/Screens/Play/GameplayBeatmap.cs @@ -39,6 +39,8 @@ namespace osu.Game.Screens.Play public IEnumerable GetStatistics() => PlayableBeatmap.GetStatistics(); + public double GetMostCommonBeatLength() => PlayableBeatmap.GetMostCommonBeatLength(); + public IBeatmap Clone() => PlayableBeatmap.Clone(); private readonly Bindable lastJudgementResult = new Bindable(); diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 04c1f6efe4..abcd697d85 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -391,7 +391,7 @@ namespace osu.Game.Screens.Select if (Precision.AlmostEquals(bpmMin, bpmMax)) return $"{bpmMin:0}"; - return $"{bpmMin:0}-{bpmMax:0} (mostly {beatmap.ControlPointInfo.BPMMode:0})"; + return $"{bpmMin:0}-{bpmMax:0} (mostly {beatmap.GetMostCommonBeatLength():0})"; } private OsuSpriteText[] getMapper(BeatmapMetadata metadata) From 24e991a5ef9c1797e4bebddf1d0a2f623845a40f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 15 Jan 2021 14:32:06 +0900 Subject: [PATCH 2/4] Actually return beat length and not BPM --- osu.Game/Beatmaps/Beatmap.cs | 2 +- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 410fd5e92e..4e2e9eb96d 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -83,7 +83,7 @@ namespace osu.Game.Beatmaps } } - return 60000 / maxDurationBeatLength; + return maxDurationBeatLength; } IBeatmap IBeatmap.Clone() => Clone(); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 0d4cc38ac3..b934ac556d 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -451,7 +451,7 @@ namespace osu.Game.Beatmaps // TODO: this should be done in a better place once we actually need to dynamically update it. beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0; beatmap.BeatmapInfo.Length = calculateLength(beatmap); - beatmap.BeatmapInfo.BPM = beatmap.GetMostCommonBeatLength(); + beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); beatmapInfos.Add(beatmap.BeatmapInfo); } diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index abcd697d85..86cb561bc7 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -391,7 +391,7 @@ namespace osu.Game.Screens.Select if (Precision.AlmostEquals(bpmMin, bpmMax)) return $"{bpmMin:0}"; - return $"{bpmMin:0}-{bpmMax:0} (mostly {beatmap.GetMostCommonBeatLength():0})"; + return $"{bpmMin:0}-{bpmMax:0} (mostly {60000 / beatmap.GetMostCommonBeatLength():0})"; } private OsuSpriteText[] getMapper(BeatmapMetadata metadata) From b40b159acb276d35bc553a597bc58980f3d3c1dd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Feb 2021 18:52:50 +0900 Subject: [PATCH 3/4] Round beatlength --- osu.Game/Beatmaps/Beatmap.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 51fdbce96d..434bff14b5 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Objects; using System.Collections.Generic; @@ -65,7 +66,7 @@ namespace osu.Game.Beatmaps return (beatLength: t.BeatLength, duration: nextTime - t.Time); }) // Aggregate durations into a set of (beatLength, duration) tuples for each beat length - .GroupBy(t => t.beatLength) + .GroupBy(t => Math.Round(t.beatLength * 1000) / 1000) .Select(g => (beatLength: g.Key, duration: g.Sum(t => t.duration))) // And if there are no timing points, use a default. .DefaultIfEmpty((TimingControlPoint.DEFAULT_BEAT_LENGTH, 0)); From 18e3f8c233da2ed3eb8b859944994d39f9f54512 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Feb 2021 19:03:19 +0900 Subject: [PATCH 4/4] Sort beat lengths rather than linear search --- osu.Game/Beatmaps/Beatmap.cs | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 434bff14b5..e5b6a4bc44 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -55,7 +55,7 @@ namespace osu.Game.Beatmaps // Note: This is more accurate and may present different results because osu-stable didn't have the ability to calculate slider durations in this context. double lastTime = HitObjects.LastOrDefault()?.GetEndTime() ?? ControlPointInfo.TimingPoints.LastOrDefault()?.Time ?? 0; - var beatLengthsAndDurations = + var mostCommon = // Construct a set of (beatLength, duration) tuples for each individual timing point. ControlPointInfo.TimingPoints.Select((t, i) => { @@ -68,23 +68,10 @@ namespace osu.Game.Beatmaps // Aggregate durations into a set of (beatLength, duration) tuples for each beat length .GroupBy(t => Math.Round(t.beatLength * 1000) / 1000) .Select(g => (beatLength: g.Key, duration: g.Sum(t => t.duration))) - // And if there are no timing points, use a default. - .DefaultIfEmpty((TimingControlPoint.DEFAULT_BEAT_LENGTH, 0)); + // Get the most common one, or 0 as a suitable default + .OrderByDescending(i => i.duration).FirstOrDefault(); - // Find the single beat length with the maximum aggregate duration. - double maxDurationBeatLength = double.NegativeInfinity; - double maxDuration = double.NegativeInfinity; - - foreach (var (beatLength, duration) in beatLengthsAndDurations) - { - if (duration > maxDuration) - { - maxDuration = duration; - maxDurationBeatLength = beatLength; - } - } - - return maxDurationBeatLength; + return mostCommon.beatLength; } IBeatmap IBeatmap.Clone() => Clone();