Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ppy
GitHub Repository: ppy/osu
Path: blob/master/osu.Game.Rulesets.Taiko/Difficulty/Utils/DeltaTimeNormaliser.cs
4571 views
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;

namespace osu.Game.Rulesets.Taiko.Difficulty.Utils
{
    /// <summary>
    /// Normalises deltaTime values for TaikoDifficultyHitObjects.
    /// </summary>
    public static class DeltaTimeNormaliser
    {
        /// <summary>
        /// Combines deltaTime values that differ by at most <paramref name="marginOfError"/>
        /// and replaces each value with the median of its range. This is used to reduce timing noise
        /// and improve rhythm grouping consistency, especially for maps with inconsistent or 'off-snapped' timing.
        /// </summary>
        public static Dictionary<TaikoDifficultyHitObject, double> Normalise(
            IReadOnlyList<TaikoDifficultyHitObject> hitObjects,
            double marginOfError)
        {
            var deltaTimes = hitObjects.Select(h => h.DeltaTime).Distinct().OrderBy(d => d).ToList();

            var sets = new List<List<double>>();
            List<double>? current = null;

            foreach (double value in deltaTimes)
            {
                // Add to the current group if within margin of error
                if (current != null && Math.Abs(value - current[0]) <= marginOfError)
                {
                    current.Add(value);
                    continue;
                }

                // Otherwise begin a new group
                current = new List<double> { value };
                sets.Add(current);
            }

            // Compute median for each group
            var medianLookup = new Dictionary<double, double>();

            foreach (var set in sets)
            {
                set.Sort();
                int mid = set.Count / 2;
                double median = set.Count % 2 == 1
                    ? set[mid]
                    : (set[mid - 1] + set[mid]) / 2;

                foreach (double v in set)
                    medianLookup[v] = median;
            }

            // Assign each hitobjects deltaTime the corresponding median value
            return hitObjects.ToDictionary(
                h => h,
                h => medianLookup.TryGetValue(h.DeltaTime, out double median) ? median : h.DeltaTime
            );
        }
    }
}