1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-21 21:40:56 +08:00

Merge pull request #32820 from peppy/global-audio-offset-control-fixes

Fix global offset adjust control showing adjustment available when it shouldn't
This commit is contained in:
Dean Herbert
2025-04-18 18:04:18 +09:00
committed by GitHub
Unverified
7 changed files with 187 additions and 39 deletions
@@ -0,0 +1,47 @@
// 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.
using NUnit.Framework;
using osu.Game.Extensions;
namespace osu.Game.Tests.Extensions
{
[TestFixture]
public class NumberFormattingExtensionsTest
{
[TestCase(-1, false, 0, ExpectedResult = "-1")]
[TestCase(0, false, 0, ExpectedResult = "0")]
[TestCase(1, false, 0, ExpectedResult = "1")]
[TestCase(500, false, 10, ExpectedResult = "500")]
[TestCase(-1, true, 0, ExpectedResult = "-1%")]
[TestCase(0, true, 0, ExpectedResult = "0%")]
[TestCase(1, true, 0, ExpectedResult = "1%")]
[TestCase(50, true, 0, ExpectedResult = "50%")]
public string TestInteger(int input, bool percent, int decimalDigits)
{
return input.ToStandardFormattedString(decimalDigits, percent);
}
[TestCase(-1, false, 0, ExpectedResult = "-1")]
[TestCase(-1e-6, false, 0, ExpectedResult = "0")]
[TestCase(-1e-6, false, 6, ExpectedResult = "-0.000001")]
[TestCase(0, false, 10, ExpectedResult = "0")]
[TestCase(0, false, 0, ExpectedResult = "0")]
[TestCase(double.NegativeZero, false, 0, ExpectedResult = "0")]
[TestCase(1e-6, false, 0, ExpectedResult = "0")]
[TestCase(1e-6, false, 6, ExpectedResult = "0.000001")]
[TestCase(1, false, 0, ExpectedResult = "1")]
[TestCase(1.528, false, 2, ExpectedResult = "1.53")]
[TestCase(500, false, 10, ExpectedResult = "500")]
[TestCase(-0.1, true, 0, ExpectedResult = "-10%")]
[TestCase(0, true, 0, ExpectedResult = "0%")]
[TestCase(0.4, true, 0, ExpectedResult = "40%")]
[TestCase(0.48333, true, 2, ExpectedResult = "48%")]
[TestCase(0.48333, true, 4, ExpectedResult = "48.33%")]
[TestCase(1, true, 0, ExpectedResult = "100%")]
public string TestDouble(double input, bool percent, int decimalDigits)
{
return input.ToStandardFormattedString(decimalDigits, percent);
}
}
}
@@ -168,6 +168,19 @@ namespace osu.Game.Tests.Visual.Ranking
};
});
public static List<HitEvent> CreateHitEvents(double offset = 0, int count = 50)
{
var hitEvents = new List<HitEvent>();
for (int i = 0; i < count; i++)
{
for (int j = 0; j < count; j++)
hitEvents.Add(new HitEvent(offset, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null));
}
return hitEvents;
}
public static List<HitEvent> CreateDistributedHitEvents(double centre = 0, double range = 25)
{
var hitEvents = new List<HitEvent>();
@@ -1,11 +1,14 @@
// 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.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings.Sections.Audio;
@@ -70,16 +73,54 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("clear history", () => tracker.ClearHistory());
}
[Test]
public void TestRounding()
{
AddStep("set new score", () => statics.SetValue(Static.LastLocalUserScore, new ScoreInfo
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateHitEvents(0.6),
BeatmapInfo = Beatmap.Value.BeatmapInfo,
}));
checkButtonEnabled();
AddStep("click button", () => adjustControl.ChildrenOfType<Button>().Single().TriggerClick());
checkButtonDisabled();
AddAssert("global offset set correctly", () => localConfig.Get<double>(OsuSetting.AudioOffset), () => Is.EqualTo(-1));
}
[Test]
public void TestNegligibleChangeNotApplicable()
{
AddStep("set new score", () => statics.SetValue(Static.LastLocalUserScore, new ScoreInfo
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateHitEvents(0.5),
BeatmapInfo = Beatmap.Value.BeatmapInfo,
}));
checkButtonDisabled();
AddStep("adjust global offset", () => localConfig.SetValue(OsuSetting.AudioOffset, 50.0));
checkButtonEnabled();
AddStep("click button", () => adjustControl.ChildrenOfType<Button>().Single().TriggerClick());
checkButtonDisabled();
AddAssert("global offset set correctly", () => localConfig.Get<double>(OsuSetting.AudioOffset), () => Is.EqualTo(0));
AddStep("clear history", () => tracker.ClearHistory());
}
[Test]
public void TestBehaviour()
{
AddStep("set score with -20ms", () => setScore(-20));
AddAssert("suggested global offset is 20ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(20));
checkButtonEnabled();
AddStep("clear history", () => tracker.ClearHistory());
checkButtonDisabled();
AddStep("set score with 40ms", () => setScore(40));
checkButtonEnabled();
AddAssert("suggested global offset is -40ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(-40));
AddStep("clear history", () => tracker.ClearHistory());
checkButtonDisabled();
}
[Test]
@@ -111,6 +152,16 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("clear history", () => tracker.ClearHistory());
}
private void checkButtonDisabled()
{
AddAssert("button is disabled", () => adjustControl.ChildrenOfType<Button>().Single().Enabled.Value, () => Is.False);
}
private void checkButtonEnabled()
{
AddAssert("button is enabled", () => adjustControl.ChildrenOfType<Button>().Single().Enabled.Value, () => Is.True);
}
private void setScore(double averageHitError)
{
statics.SetValue(Static.LastLocalUserScore, new ScoreInfo
@@ -0,0 +1,51 @@
// 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.
using System;
using System.Globalization;
using System.Numerics;
using osu.Game.Utils;
namespace osu.Game.Extensions
{
public static class NumberFormattingExtensions
{
/// <summary>
/// For a given numeric type, return a formatted string in the standard format we use for display everywhere.
/// </summary>
/// <param name="value">The numeric value.</param>
/// <param name="maxDecimalDigits">The maximum number of decimals to be considered in the original value.</param>
/// <param name="asPercentage">Whether the output should be a percentage. For integer types, 0-100 is mapped to 0-100%; for other types 0-1 is mapped to 0-100%.</param>
/// <returns>The formatted output.</returns>
public static string ToStandardFormattedString<T>(this T value, int maxDecimalDigits, bool asPercentage) where T : struct, INumber<T>, IMinMaxValue<T>
{
double floatValue = double.CreateTruncating(value);
decimal decimalPrecision = normalise(decimal.CreateTruncating(value), maxDecimalDigits);
// Find the number of significant digits (we could have less than maxDecimalDigits after normalize())
int significantDigits = FormatUtils.FindPrecision(decimalPrecision);
if (asPercentage)
{
if (value is int)
floatValue /= 100;
return floatValue.ToString($@"P{Math.Max(0, significantDigits - 2)}");
}
string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty;
return $"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}")}";
}
/// <summary>
/// Removes all non-significant digits, keeping at most a requested number of decimal digits.
/// </summary>
/// <param name="d">The decimal to normalize.</param>
/// <param name="sd">The maximum number of decimal digits to keep. The final result may have fewer decimal digits than this value.</param>
/// <returns>The normalised decimal.</returns>
private static decimal normalise(decimal d, int sd)
=> decimal.Parse(Math.Round(d, sd).ToString(string.Concat("0.", new string('#', sd)), CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);
}
}
@@ -1,9 +1,7 @@
// 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.
using System;
using System.Numerics;
using System.Globalization;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -11,7 +9,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Utils;
using osu.Game.Extensions;
namespace osu.Game.Graphics.UserInterface
{
@@ -85,35 +83,6 @@ namespace osu.Game.Graphics.UserInterface
channel.Play();
}
public LocalisableString GetDisplayableValue(T value)
{
if (CurrentNumber.IsInteger)
return int.CreateTruncating(value).ToString("N0");
double floatValue = double.CreateTruncating(value);
decimal decimalPrecision = normalise(decimal.CreateTruncating(CurrentNumber.Precision), max_decimal_digits);
// Find the number of significant digits (we could have less than 5 after normalize())
int significantDigits = FormatUtils.FindPrecision(decimalPrecision);
if (DisplayAsPercentage)
{
return floatValue.ToString($@"P{Math.Max(0, significantDigits - 2)}");
}
string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty;
return $"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}")}";
}
/// <summary>
/// Removes all non-significant digits, keeping at most a requested number of decimal digits.
/// </summary>
/// <param name="d">The decimal to normalize.</param>
/// <param name="sd">The maximum number of decimal digits to keep. The final result may have fewer decimal digits than this value.</param>
/// <returns>The normalised decimal.</returns>
private decimal normalise(decimal d, int sd)
=> decimal.Parse(Math.Round(d, sd).ToString(string.Concat("0.", new string('#', sd)), CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);
public LocalisableString GetDisplayableValue(T value) => CurrentNumber.Value.ToStandardFormattedString(max_decimal_digits, DisplayAsPercentage);
}
}
@@ -69,6 +69,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString SuggestedOffsetNote => new TranslatableString(getKey(@"suggested_offset_note"), @"Play a few beatmaps to receive a suggested offset!");
/// <summary>
/// "Based on the last {0} play(s), your offset is set correctly!"
/// </summary>
public static LocalisableString SuggestedOffsetCorrect(int plays) => new TranslatableString(getKey(@"suggested_offset_correct"), @"Based on the last {0} play(s), your offset is set correctly!", plays);
/// <summary>
/// "Based on the last {0} play(s), the suggested offset is {1} ms."
/// </summary>
@@ -8,13 +8,13 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
@@ -109,6 +109,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
base.LoadComplete();
averageHitErrorHistory.BindCollectionChanged(updateDisplay, true);
current.BindValueChanged(_ => updateHintText());
SuggestedOffset.BindValueChanged(_ => updateHintText(), true);
}
@@ -148,17 +149,28 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
break;
}
SuggestedOffset.Value = averageHitErrorHistory.Any() ? averageHitErrorHistory.Average(dataPoint => dataPoint.SuggestedGlobalAudioOffset) : null;
SuggestedOffset.Value = averageHitErrorHistory.Any() ? Math.Round(averageHitErrorHistory.Average(dataPoint => dataPoint.SuggestedGlobalAudioOffset)) : null;
}
private float getXPositionForOffset(double offset) => (float)(Math.Clamp(offset, current.MinValue, current.MaxValue) / (2 * current.MaxValue));
private void updateHintText()
{
hintText.Text = SuggestedOffset.Value == null
? AudioSettingsStrings.SuggestedOffsetNote
: AudioSettingsStrings.SuggestedOffsetValueReceived(averageHitErrorHistory.Count, SuggestedOffset.Value.ToLocalisableString(@"N0"));
applySuggestion.Enabled.Value = SuggestedOffset.Value != null;
if (SuggestedOffset.Value == null)
{
applySuggestion.Enabled.Value = false;
hintText.Text = AudioSettingsStrings.SuggestedOffsetNote;
}
else if (Math.Abs(SuggestedOffset.Value.Value - current.Value) < 1)
{
applySuggestion.Enabled.Value = false;
hintText.Text = AudioSettingsStrings.SuggestedOffsetCorrect(averageHitErrorHistory.Count);
}
else
{
applySuggestion.Enabled.Value = true;
hintText.Text = AudioSettingsStrings.SuggestedOffsetValueReceived(averageHitErrorHistory.Count, SuggestedOffset.Value.Value.ToStandardFormattedString(0, false));
}
}
private partial class OffsetSliderBar : RoundedSliderBar<double>