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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user