From 8cc2d2e79e6679c3c541b6999bc1b8dbdd332f1d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 13:51:28 +0900 Subject: [PATCH 01/14] Update beat synced container tests to be usable --- .../TestSceneBeatSyncedContainer.cs | 82 ++++++++++++------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 82b7e65c4f..c952163ba5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -5,18 +5,18 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Overlays; +using osu.Game.Rulesets.Osu; using osuTK.Graphics; namespace osu.Game.Tests.Visual.UserInterface @@ -24,37 +24,55 @@ namespace osu.Game.Tests.Visual.UserInterface [TestFixture] public class TestSceneBeatSyncedContainer : OsuTestScene { - private readonly NowPlayingOverlay np; + private BeatContainer beatContainer; + private DecoupleableInterpolatingFramedClock decoupledClock; - public TestSceneBeatSyncedContainer() + [SetUpSteps] + public void SetUpSteps() { - Clock = new FramedClock(); - Clock.ProcessFrame(); - - AddRange(new Drawable[] + AddStep("Set beatmap", () => { - new BeatContainer - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - }, - np = new NowPlayingOverlay - { - Origin = Anchor.TopRight, - Anchor = Anchor.TopRight, - } + Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); }); + + AddStep("Create beat sync container", () => + { + Children = new Drawable[] + { + beatContainer = new BeatContainer + { + Clock = decoupledClock = new DecoupleableInterpolatingFramedClock + { + IsCoupled = false, + }, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + }, + }; + + decoupledClock.ChangeSource(Beatmap.Value.Track); + }); + + AddStep("Start playback", () => decoupledClock.Start()); } - protected override void LoadComplete() + [Test] + public void TestFirstBeatAtFirstTimingPoint() { - base.LoadComplete(); - np.ToggleVisibility(); + AddStep("Set time before zero", () => + { + decoupledClock.Seek(-1000); + }); + + AddStep("bind event", () => + { + beatContainer.NewBeat = (i, point, effectControlPoint, channelAmplitudes) => { }; + }); } private class BeatContainer : BeatSyncedContainer { - private const int flash_layer_heigth = 150; + private const int flash_layer_height = 150; private readonly InfoString timingPointCount; private readonly InfoString currentTimingPoint; @@ -64,12 +82,10 @@ namespace osu.Game.Tests.Visual.UserInterface private readonly InfoString adjustedBeatLength; private readonly InfoString timeUntilNextBeat; private readonly InfoString timeSinceLastBeat; + private readonly InfoString currentTime; private readonly Box flashLayer; - [Resolved] - private MusicController musicController { get; set; } - public BeatContainer() { RelativeSizeAxes = Axes.X; @@ -82,7 +98,7 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Bottom = flash_layer_heigth }, + Margin = new MarginPadding { Bottom = flash_layer_height }, Children = new Drawable[] { new Box @@ -98,6 +114,7 @@ namespace osu.Game.Tests.Visual.UserInterface Direction = FillDirection.Vertical, Children = new Drawable[] { + currentTime = new InfoString(@"Current time"), timingPointCount = new InfoString(@"Timing points amount"), currentTimingPoint = new InfoString(@"Current timing point"), beatCount = new InfoString(@"Beats amount (in the current timing point)"), @@ -116,7 +133,7 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, RelativeSizeAxes = Axes.X, - Height = flash_layer_heigth, + Height = flash_layer_height, Children = new Drawable[] { new Box @@ -164,7 +181,7 @@ namespace osu.Game.Tests.Visual.UserInterface if (timingPoints.Count == 0) return 0; if (timingPoints[^1] == current) - return (int)Math.Ceiling((musicController.CurrentTrack.Length - current.Time) / current.BeatLength); + return (int)Math.Ceiling((Clock.CurrentTime - current.Time) / current.BeatLength); return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength); } @@ -174,8 +191,11 @@ namespace osu.Game.Tests.Visual.UserInterface base.Update(); timeUntilNextBeat.Value = TimeUntilNextBeat; timeSinceLastBeat.Value = TimeSinceLastBeat; + currentTime.Value = Clock.CurrentTime; } + public Action NewBeat; + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); @@ -187,7 +207,9 @@ namespace osu.Game.Tests.Visual.UserInterface beatsPerMinute.Value = 60000 / timingPoint.BeatLength; adjustedBeatLength.Value = timingPoint.BeatLength; - flashLayer.FadeOutFromOne(timingPoint.BeatLength); + flashLayer.FadeOutFromOne(timingPoint.BeatLength / 4); + + NewBeat?.Invoke(beatIndex, timingPoint, effectPoint, amplitudes); } } @@ -200,7 +222,7 @@ namespace osu.Game.Tests.Visual.UserInterface public double Value { - set => valueText.Text = $"{value:G}"; + set => valueText.Text = $"{value:0.##}"; } public InfoString(string header) From a3129ad00e22dc44c9f4965f8ef0702b22ce6a91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 14:30:38 +0900 Subject: [PATCH 02/14] Refactor `BeatSyncedContainer` to support `GameplayClock` --- .../TestSceneBeatSyncedContainer.cs | 35 ++++++----- .../Containers/BeatSyncedContainer.cs | 62 ++++++++++++++----- 2 files changed, 66 insertions(+), 31 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index c952163ba5..f58f9fcaaf 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -11,12 +11,12 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; -using osu.Framework.Timing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Play; using osuTK.Graphics; namespace osu.Game.Tests.Visual.UserInterface @@ -25,7 +25,8 @@ namespace osu.Game.Tests.Visual.UserInterface public class TestSceneBeatSyncedContainer : OsuTestScene { private BeatContainer beatContainer; - private DecoupleableInterpolatingFramedClock decoupledClock; + + private MasterGameplayClockContainer gameplayClockContainer; [SetUpSteps] public void SetUpSteps() @@ -39,21 +40,18 @@ namespace osu.Game.Tests.Visual.UserInterface { Children = new Drawable[] { - beatContainer = new BeatContainer + gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0) { - Clock = decoupledClock = new DecoupleableInterpolatingFramedClock + Child = beatContainer = new BeatContainer { - IsCoupled = false, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, }, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - }, + } }; - - decoupledClock.ChangeSource(Beatmap.Value.Track); }); - AddStep("Start playback", () => decoupledClock.Start()); + AddStep("Start playback", () => gameplayClockContainer.Start()); } [Test] @@ -61,7 +59,7 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep("Set time before zero", () => { - decoupledClock.Seek(-1000); + gameplayClockContainer.Seek(-1000); }); AddStep("bind event", () => @@ -150,8 +148,13 @@ namespace osu.Game.Tests.Visual.UserInterface } } }; + } - Beatmap.ValueChanged += delegate + protected override void LoadComplete() + { + base.LoadComplete(); + + Beatmap.BindValueChanged(_ => { timingPointCount.Value = 0; currentTimingPoint.Value = 0; @@ -161,7 +164,7 @@ namespace osu.Game.Tests.Visual.UserInterface adjustedBeatLength.Value = 0; timeUntilNextBeat.Value = 0; timeSinceLastBeat.Value = 0; - }; + }, true); } private List timingPoints => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ToList(); @@ -181,7 +184,7 @@ namespace osu.Game.Tests.Visual.UserInterface if (timingPoints.Count == 0) return 0; if (timingPoints[^1] == current) - return (int)Math.Ceiling((Clock.CurrentTime - current.Time) / current.BeatLength); + return (int)Math.Ceiling((BeatSyncClock.CurrentTime - current.Time) / current.BeatLength); return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength); } @@ -191,7 +194,7 @@ namespace osu.Game.Tests.Visual.UserInterface base.Update(); timeUntilNextBeat.Value = TimeUntilNextBeat; timeSinceLastBeat.Value = TimeSinceLastBeat; - currentTime.Value = Clock.CurrentTime; + currentTime.Value = BeatSyncClock.CurrentTime; } public Action NewBeat; diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index e2a0c09a6b..cb26406d64 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -1,19 +1,29 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Screens.Play; namespace osu.Game.Graphics.Containers { + /// + /// A container which fires a callback when a new beat is reached. + /// Consumes a parent or (whichever is first available). + /// + /// + /// This container does not set its own clock to the source used for beat matching. + /// This means that if the beat source clock is playing faster or slower, animations may unexpectedly overlap. + /// Make sure this container's Clock is also set to the expected source (or within a parent element which provides this). + /// public class BeatSyncedContainer : Container { - protected readonly IBindable Beatmap = new Bindable(); - private int lastBeat; private TimingControlPoint lastTimingPoint; @@ -45,15 +55,45 @@ namespace osu.Game.Graphics.Containers protected bool IsBeatSyncedWithTrack { get; private set; } + protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + } + + [Resolved] + protected IBindable Beatmap { get; private set; } + + [Resolved(canBeNull: true)] + protected GameplayClock GameplayClock { get; private set; } + + protected IClock BeatSyncClock + { + get + { + if (GameplayClock != null) + return GameplayClock; + + if (Beatmap.Value.TrackLoaded) + return Beatmap.Value.Track; + + return null; + } + } + protected override void Update() { ITrack track = null; IBeatmap beatmap = null; double currentTrackTime = 0; + TimingControlPoint timingPoint = null; EffectControlPoint effectPoint = null; + var clock = BeatSyncClock; + + if (clock == null) + return; + if (Beatmap.Value.TrackLoaded && Beatmap.Value.BeatmapLoaded) { track = Beatmap.Value.Track; @@ -62,7 +102,7 @@ namespace osu.Game.Graphics.Containers if (track != null && beatmap != null && track.IsRunning && track.Length > 0) { - currentTrackTime = track.CurrentTime + EarlyActivationMilliseconds; + currentTrackTime = clock.CurrentTime + EarlyActivationMilliseconds; timingPoint = beatmap.ControlPointInfo.TimingPointAt(currentTrackTime); effectPoint = beatmap.ControlPointInfo.EffectPointAt(currentTrackTime); @@ -70,13 +110,15 @@ namespace osu.Game.Graphics.Containers IsBeatSyncedWithTrack = timingPoint?.BeatLength > 0; - if (timingPoint == null || !IsBeatSyncedWithTrack) + if (!IsBeatSyncedWithTrack) { - currentTrackTime = Clock.CurrentTime; + currentTrackTime = clock.CurrentTime; timingPoint = TimingControlPoint.DEFAULT; effectPoint = EffectControlPoint.DEFAULT; } + Debug.Assert(timingPoint != null); + double beatLength = timingPoint.BeatLength / Divisor; while (beatLength < MinimumBeatLength) @@ -103,15 +145,5 @@ namespace osu.Game.Graphics.Containers lastBeat = beatIndex; lastTimingPoint = timingPoint; } - - [BackgroundDependencyLoader] - private void load(IBindable beatmap) - { - Beatmap.BindTo(beatmap); - } - - protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) - { - } } } From cab8b941322b2731605778a914badf9388e5b67c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 14:36:37 +0900 Subject: [PATCH 03/14] Add failing test --- .../TestSceneBeatSyncedContainer.cs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index f58f9fcaaf..ac1f88ad33 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -57,15 +58,28 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestFirstBeatAtFirstTimingPoint() { - AddStep("Set time before zero", () => - { - gameplayClockContainer.Seek(-1000); - }); + int? lastBeatIndex = null; + double? lastBpm = null; AddStep("bind event", () => { - beatContainer.NewBeat = (i, point, effectControlPoint, channelAmplitudes) => { }; + beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => + { + lastBeatIndex = i; + lastBpm = timingControlPoint.BPM; + }; }); + + AddStep("Set time before zero", () => + { + lastBeatIndex = null; + lastBpm = null; + gameplayClockContainer.Seek(-1000); + }); + + AddUntilStep("wait for trigger", () => lastBpm != null); + AddAssert("bpm is from beatmap", () => lastBpm != null&&Precision.AlmostEquals(lastBpm.Value, 128)); + AddAssert("beat index is less than zero", () => lastBeatIndex < 0); } private class BeatContainer : BeatSyncedContainer From 5ecf6511e6543300fdba03197b5ec53583db54be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 14:37:08 +0900 Subject: [PATCH 04/14] Fix default timing points being used if "track" is not running --- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index cb26406d64..6d5f4d63ac 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -100,7 +100,7 @@ namespace osu.Game.Graphics.Containers beatmap = Beatmap.Value.Beatmap; } - if (track != null && beatmap != null && track.IsRunning && track.Length > 0) + if (track != null && beatmap != null && clock.IsRunning && track.Length > 0) { currentTrackTime = clock.CurrentTime + EarlyActivationMilliseconds; From c47ff1919c0075cb64b7e1539885f26b6adc012a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 14:56:49 +0900 Subject: [PATCH 05/14] Fix regression in idle behaviour and refactor further --- .../Containers/BeatSyncedContainer.cs | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 6d5f4d63ac..929a300831 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -21,6 +21,8 @@ namespace osu.Game.Graphics.Containers /// This container does not set its own clock to the source used for beat matching. /// This means that if the beat source clock is playing faster or slower, animations may unexpectedly overlap. /// Make sure this container's Clock is also set to the expected source (or within a parent element which provides this). + /// + /// This container will also trigger beat events when the beat matching clock is paused at 's BPM. /// public class BeatSyncedContainer : Container { @@ -53,6 +55,9 @@ namespace osu.Game.Graphics.Containers /// public double MinimumBeatLength { get; set; } + /// + /// Whether this container is currently tracking a beatmap's timing data. + /// protected bool IsBeatSyncedWithTrack { get; private set; } protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) @@ -84,35 +89,36 @@ namespace osu.Game.Graphics.Containers ITrack track = null; IBeatmap beatmap = null; - double currentTrackTime = 0; + TimingControlPoint timingPoint; + EffectControlPoint effectPoint; - TimingControlPoint timingPoint = null; - EffectControlPoint effectPoint = null; - - var clock = BeatSyncClock; + IClock clock = BeatSyncClock; if (clock == null) return; + double currentTrackTime = clock.CurrentTime + EarlyActivationMilliseconds; + if (Beatmap.Value.TrackLoaded && Beatmap.Value.BeatmapLoaded) { track = Beatmap.Value.Track; beatmap = Beatmap.Value.Beatmap; } - if (track != null && beatmap != null && clock.IsRunning && track.Length > 0) + IsBeatSyncedWithTrack = beatmap != null && clock.IsRunning && track?.Length > 0; + + if (IsBeatSyncedWithTrack) { - currentTrackTime = clock.CurrentTime + EarlyActivationMilliseconds; + Debug.Assert(beatmap != null); timingPoint = beatmap.ControlPointInfo.TimingPointAt(currentTrackTime); effectPoint = beatmap.ControlPointInfo.EffectPointAt(currentTrackTime); } - - IsBeatSyncedWithTrack = timingPoint?.BeatLength > 0; - - if (!IsBeatSyncedWithTrack) + else { - currentTrackTime = clock.CurrentTime; + // this may be the case where the beat syncing clock has been paused. + // we still want to show an idle animation, so use this container's time instead. + currentTrackTime = Clock.CurrentTime; timingPoint = TimingControlPoint.DEFAULT; effectPoint = EffectControlPoint.DEFAULT; } From 77bfe700e091fd16af9b27edddfd987ec0094579 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 14:59:57 +0900 Subject: [PATCH 06/14] Add test coverage of idle beat --- .../TestSceneBeatSyncedContainer.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index ac1f88ad33..3ca6bf782a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -78,10 +78,33 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddUntilStep("wait for trigger", () => lastBpm != null); - AddAssert("bpm is from beatmap", () => lastBpm != null&&Precision.AlmostEquals(lastBpm.Value, 128)); + AddAssert("bpm is from beatmap", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 128)); AddAssert("beat index is less than zero", () => lastBeatIndex < 0); } + [Test] + public void TestIdleBeatOnPausedClock() + { + double? lastBpm = null; + + AddStep("bind event", () => + { + beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => lastBpm = timingControlPoint.BPM; + }); + + AddUntilStep("wait for trigger", () => lastBpm != null); + AddAssert("bpm is from beatmap", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 128)); + + AddStep("pause gameplay clock", () => + { + lastBpm = null; + gameplayClockContainer.Stop(); + }); + + AddUntilStep("wait for trigger", () => lastBpm != null); + AddAssert("bpm is from beatmap", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 60)); + } + private class BeatContainer : BeatSyncedContainer { private const int flash_layer_height = 150; From 98a1f40a982fe52d6482877c2850ab8cc13ab309 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 15:10:14 +0900 Subject: [PATCH 07/14] Ensure `EarlyActivationMilliseconds` is applied even in idle state --- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 929a300831..55f694f17e 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -97,7 +97,7 @@ namespace osu.Game.Graphics.Containers if (clock == null) return; - double currentTrackTime = clock.CurrentTime + EarlyActivationMilliseconds; + double currentTrackTime = clock.CurrentTime; if (Beatmap.Value.TrackLoaded && Beatmap.Value.BeatmapLoaded) { @@ -123,7 +123,7 @@ namespace osu.Game.Graphics.Containers effectPoint = EffectControlPoint.DEFAULT; } - Debug.Assert(timingPoint != null); + currentTrackTime += EarlyActivationMilliseconds; double beatLength = timingPoint.BeatLength / Divisor; From d0fc25888683d7fc75e4da27c111acc77ad08364 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 15:11:42 +0900 Subject: [PATCH 08/14] Remove unused `OsuLogo.BeatMatching` --- osu.Game/Screens/Menu/OsuLogo.cs | 4 ---- osu.Game/Screens/OsuScreen.cs | 1 - 2 files changed, 5 deletions(-) diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 283be913b0..a9376325cd 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -72,8 +72,6 @@ namespace osu.Game.Screens.Menu set => colourAndTriangles.FadeTo(value ? 1 : 0, transition_length, Easing.OutQuint); } - public bool BeatMatching = true; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => logoContainer.ReceivePositionalInputAt(screenSpacePos); public bool Ripple @@ -272,8 +270,6 @@ namespace osu.Game.Screens.Menu { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); - if (!BeatMatching) return; - lastBeatIndex = beatIndex; var beatLength = timingPoint.BeatLength; diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index aeb51813e4..c3b2612e79 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -242,7 +242,6 @@ namespace osu.Game.Screens logo.Anchor = Anchor.TopLeft; logo.Origin = Anchor.Centre; logo.RelativePositionAxes = Axes.Both; - logo.BeatMatching = true; logo.Triangles = true; logo.Ripple = true; } From 3197f599bb2a9d5a1f516416293d5ac0ab0e636b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 16:01:46 +0900 Subject: [PATCH 09/14] Add failing test showing `OnNewBeat` can execute far away from an actual beat --- .../TestSceneBeatSyncedContainer.cs | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 3ca6bf782a..5bfaf35bf1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -56,7 +56,34 @@ namespace osu.Game.Tests.Visual.UserInterface } [Test] - public void TestFirstBeatAtFirstTimingPoint() + public void TestSeekBackDoesntPlayMidBeat() + { + int? lastBeatIndex = null; + double? lastActuationTime = null; + TimingControlPoint lastTimingPoint = null; + + AddStep("bind event", () => + { + beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => + { + lastActuationTime = gameplayClockContainer.CurrentTime; + lastTimingPoint = timingControlPoint; + lastBeatIndex = i; + }; + }); + + AddStep("Set time before zero", () => + { + lastBeatIndex = null; + gameplayClockContainer.Seek(-1000); + }); + + AddUntilStep("wait for trigger", () => lastBeatIndex != null); + AddAssert("trigger is near beat length", () => lastActuationTime != null && lastBeatIndex != null && Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength, lastActuationTime.Value, 32)); + } + + [Test] + public void TestNegativeBeatsStillUsingBeatmapTiming() { int? lastBeatIndex = null; double? lastBpm = null; From b6996d647e9cf62232412e350eb4cc3d98f51680 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 16:13:13 +0900 Subject: [PATCH 10/14] Add ability to disable mistimed event firings --- .../TestSceneBeatSyncedContainer.cs | 31 ++++++++++++++----- .../Containers/BeatSyncedContainer.cs | 23 ++++++++++++-- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 5bfaf35bf1..2c5433e4aa 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.UserInterface [TestFixture] public class TestSceneBeatSyncedContainer : OsuTestScene { - private BeatContainer beatContainer; + private TestBeatSyncedContainer beatContainer; private MasterGameplayClockContainer gameplayClockContainer; @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.UserInterface { gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0) { - Child = beatContainer = new BeatContainer + Child = beatContainer = new TestBeatSyncedContainer { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, @@ -55,13 +55,16 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("Start playback", () => gameplayClockContainer.Start()); } - [Test] - public void TestSeekBackDoesntPlayMidBeat() + [TestCase(false)] + [TestCase(true)] + public void TestDisallowMistimedEventFiring(bool allowMistimed) { int? lastBeatIndex = null; double? lastActuationTime = null; TimingControlPoint lastTimingPoint = null; + AddStep("set mistimed to disallow", () => beatContainer.AllowMistimedEventFiring = allowMistimed); + AddStep("bind event", () => { beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => @@ -79,7 +82,15 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddUntilStep("wait for trigger", () => lastBeatIndex != null); - AddAssert("trigger is near beat length", () => lastActuationTime != null && lastBeatIndex != null && Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength, lastActuationTime.Value, 32)); + + if (!allowMistimed) + { + AddAssert("trigger is near beat length", () => lastActuationTime != null && lastBeatIndex != null && Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength, lastActuationTime.Value, BeatSyncedContainer.MISTIMED_ALLOWANCE)); + } + else + { + AddAssert("trigger is not near beat length", () => lastActuationTime != null && lastBeatIndex != null && !Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength, lastActuationTime.Value, BeatSyncedContainer.MISTIMED_ALLOWANCE)); + } } [Test] @@ -132,10 +143,16 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("bpm is from beatmap", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 60)); } - private class BeatContainer : BeatSyncedContainer + private class TestBeatSyncedContainer : BeatSyncedContainer { private const int flash_layer_height = 150; + public new bool AllowMistimedEventFiring + { + get => base.AllowMistimedEventFiring; + set => base.AllowMistimedEventFiring = value; + } + private readonly InfoString timingPointCount; private readonly InfoString currentTimingPoint; private readonly InfoString beatCount; @@ -148,7 +165,7 @@ namespace osu.Game.Tests.Visual.UserInterface private readonly Box flashLayer; - public BeatContainer() + public TestBeatSyncedContainer() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 55f694f17e..78ba716cca 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.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 System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Audio.Track; @@ -35,6 +36,19 @@ namespace osu.Game.Graphics.Containers /// protected double EarlyActivationMilliseconds; + /// + /// While this container automatically applied an animation delay (meaning any animations inside a implementation will + /// always be correctly timed), the event itself can potentially fire away from the related beat. + /// + /// By setting this to false, cases where the event is to be fired more than from the related beat will be skipped. + /// + protected bool AllowMistimedEventFiring = true; + + /// + /// The maximum deviance from the actual beat that an can fire when is set to false. + /// + public const double MISTIMED_ALLOWANCE = 16; + /// /// The time in milliseconds until the next beat. /// @@ -145,8 +159,13 @@ namespace osu.Game.Graphics.Containers if (timingPoint == lastTimingPoint && beatIndex == lastBeat) return; - using (BeginDelayedSequence(-TimeSinceLastBeat)) - OnNewBeat(beatIndex, timingPoint, effectPoint, track?.CurrentAmplitudes ?? ChannelAmplitudes.Empty); + // as this event is sometimes used for sound triggers where `BeginDelayedSequence` has no effect, avoid firing it if too far away from the beat. + // this can happen after a seek operation. + if (AllowMistimedEventFiring || Math.Abs(TimeSinceLastBeat) < MISTIMED_ALLOWANCE) + { + using (BeginDelayedSequence(-TimeSinceLastBeat)) + OnNewBeat(beatIndex, timingPoint, effectPoint, track?.CurrentAmplitudes ?? ChannelAmplitudes.Empty); + } lastBeat = beatIndex; lastTimingPoint = timingPoint; From efdc8fa8a6cfdebabf5b97ab663e910c9eb3d0f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 19:08:12 +0900 Subject: [PATCH 11/14] Fix incorrect step name Co-authored-by: Henry Lin --- .../Visual/UserInterface/TestSceneBeatSyncedContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 2c5433e4aa..6bd5c7bcd8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.UserInterface double? lastActuationTime = null; TimingControlPoint lastTimingPoint = null; - AddStep("set mistimed to disallow", () => beatContainer.AllowMistimedEventFiring = allowMistimed); + AddStep($"set mistimed to {(allowMistimed ? "allowed" : "disallowed")}", () => beatContainer.AllowMistimedEventFiring = allowMistimed); AddStep("bind event", () => { From db4d64effb1f208b528af2ff4d0bd5a1954d86f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 17 Jul 2021 14:29:18 +0900 Subject: [PATCH 12/14] Rename incorrect step --- .../Visual/UserInterface/TestSceneBeatSyncedContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 6bd5c7bcd8..6b56f339d8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -140,7 +140,7 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddUntilStep("wait for trigger", () => lastBpm != null); - AddAssert("bpm is from beatmap", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 60)); + AddAssert("bpm is default", () => lastBpm != null && Precision.AlmostEquals(lastBpm.Value, 60)); } private class TestBeatSyncedContainer : BeatSyncedContainer From 23ed77f2c6f10135e3b849e5adf96008372b9cd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 17 Jul 2021 14:33:02 +0900 Subject: [PATCH 13/14] Fix test failure under visual tests due to double firing --- .../UserInterface/TestSceneBeatSyncedContainer.cs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 6b56f339d8..e5bcc08924 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -65,19 +65,16 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep($"set mistimed to {(allowMistimed ? "allowed" : "disallowed")}", () => beatContainer.AllowMistimedEventFiring = allowMistimed); - AddStep("bind event", () => + AddStep("Set time before zero", () => { beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => { lastActuationTime = gameplayClockContainer.CurrentTime; lastTimingPoint = timingControlPoint; lastBeatIndex = i; + beatContainer.NewBeat = null; }; - }); - AddStep("Set time before zero", () => - { - lastBeatIndex = null; gameplayClockContainer.Seek(-1000); }); @@ -99,19 +96,14 @@ namespace osu.Game.Tests.Visual.UserInterface int? lastBeatIndex = null; double? lastBpm = null; - AddStep("bind event", () => + AddStep("Set time before zero", () => { beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => { lastBeatIndex = i; lastBpm = timingControlPoint.BPM; }; - }); - AddStep("Set time before zero", () => - { - lastBeatIndex = null; - lastBpm = null; gameplayClockContainer.Seek(-1000); }); From 87d3bd4b9389a3582f15ccfbf054dd8b3fe8e01a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 17 Jul 2021 14:35:43 +0900 Subject: [PATCH 14/14] Fix time until next beat potentially being exactly zero at point of trigger --- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 78ba716cca..6e4901ab1a 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -151,7 +151,7 @@ namespace osu.Game.Graphics.Containers beatIndex--; TimeUntilNextBeat = (timingPoint.Time - currentTrackTime) % beatLength; - if (TimeUntilNextBeat < 0) + if (TimeUntilNextBeat <= 0) TimeUntilNextBeat += beatLength; TimeSinceLastBeat = beatLength - TimeUntilNextBeat;