From eabf57828276ce8ce2b2ad16305e54df271b34ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 22 May 2022 22:15:53 +0900 Subject: [PATCH] Use interface to convey beat sync information --- .../TestSceneBeatSyncedContainer.cs | 22 +++++-- .../Containers/BeatSyncedContainer.cs | 65 ++++--------------- .../Graphics/Containers/IBeatSyncProvider.cs | 26 ++++++++ osu.Game/OsuGameBase.cs | 10 ++- osu.Game/Rulesets/Mods/MetronomeBeat.cs | 2 +- osu.Game/Screens/Edit/Editor.cs | 10 ++- .../Screens/Edit/Timing/MetronomeDisplay.cs | 12 ++-- .../Play/MasterGameplayClockContainer.cs | 8 ++- osu.Game/Screens/Play/Player.cs | 9 ++- 9 files changed, 96 insertions(+), 68 deletions(-) create mode 100644 osu.Game/Graphics/Containers/IBeatSyncProvider.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 1881f6d718..3cbb7daf51 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Audio.Track; @@ -82,11 +83,15 @@ namespace osu.Game.Tests.Visual.UserInterface 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)); + 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)); + AddAssert("trigger is not near beat length", + () => lastActuationTime != null && lastBeatIndex != null && !Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength, + lastActuationTime.Value, BeatSyncedContainer.MISTIMED_ALLOWANCE)); } } @@ -258,7 +263,7 @@ namespace osu.Game.Tests.Visual.UserInterface }; } - private List timingPoints => Beatmap.ControlPointInfo.TimingPoints.ToList(); + private List timingPoints => BeatSyncSource.ControlPoints?.TimingPoints.ToList(); private TimingControlPoint getNextTimingPoint(TimingControlPoint current) { @@ -275,7 +280,11 @@ namespace osu.Game.Tests.Visual.UserInterface if (timingPoints.Count == 0) return 0; if (timingPoints[^1] == current) - return (int)Math.Ceiling((BeatSyncClock.CurrentTime - current.Time) / current.BeatLength); + { + Debug.Assert(BeatSyncSource.Clock != null); + + return (int)Math.Ceiling((BeatSyncSource.Clock.CurrentTime - current.Time) / current.BeatLength); + } return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength); } @@ -283,9 +292,12 @@ namespace osu.Game.Tests.Visual.UserInterface protected override void Update() { base.Update(); + + Debug.Assert(BeatSyncSource.Clock != null); + timeUntilNextBeat.Value = TimeUntilNextBeat; timeSinceLastBeat.Value = TimeSinceLastBeat; - currentTime.Value = BeatSyncClock.CurrentTime; + currentTime.Value = BeatSyncSource.Clock.CurrentTime; } public Action NewBeat; diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index bd46a20434..953731244d 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -5,12 +5,9 @@ using System; 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.Edit; using osu.Game.Screens.Play; namespace osu.Game.Graphics.Containers @@ -75,74 +72,38 @@ namespace osu.Game.Graphics.Containers /// protected bool IsBeatSyncedWithTrack { get; private set; } + [Resolved] + protected IBeatSyncProvider BeatSyncSource { get; private set; } + protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { } - [Resolved] - private IBindable beatmap { get; set; } - - [Resolved(canBeNull: true)] - protected GameplayClock GameplayClock { get; private set; } - - [Resolved(canBeNull: true)] - protected EditorBeatmap EditorBeatmap { get; private set; } - - [Resolved(canBeNull: true)] - protected EditorClock EditorClock { get; private set; } - - protected IBeatmap Beatmap => EditorBeatmap ?? beatmap?.Value.Beatmap; - - protected IClock BeatSyncClock - { - get - { - if (EditorClock != null) - return EditorClock; - - if (GameplayClock != null) - return GameplayClock; - - if (beatmap.Value.TrackLoaded) - return beatmap.Value.Track; - - return null; - } - } - protected override void Update() { - ITrack track = null; - TimingControlPoint timingPoint; EffectControlPoint effectPoint; - IClock clock = BeatSyncClock; + IsBeatSyncedWithTrack = BeatSyncSource.Clock?.IsRunning == true; - if (clock == null) - return; - - double currentTrackTime = clock.CurrentTime + EarlyActivationMilliseconds; - - if (beatmap.Value.TrackLoaded && beatmap.Value.BeatmapLoaded) - { - track = beatmap.Value.Track; - } - - IsBeatSyncedWithTrack = beatmap != null && clock.IsRunning && track?.Length > 0; + double currentTrackTime; if (IsBeatSyncedWithTrack) { - Debug.Assert(beatmap != null); + Debug.Assert(BeatSyncSource.ControlPoints != null); + Debug.Assert(BeatSyncSource.Clock != null); - timingPoint = Beatmap.ControlPointInfo.TimingPointAt(currentTrackTime); - effectPoint = Beatmap.ControlPointInfo.EffectPointAt(currentTrackTime); + currentTrackTime = BeatSyncSource.Clock.CurrentTime + EarlyActivationMilliseconds; + + timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(currentTrackTime); + effectPoint = BeatSyncSource.ControlPoints.EffectPointAt(currentTrackTime); } else { // 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 + EarlyActivationMilliseconds; + timingPoint = TimingControlPoint.DEFAULT; effectPoint = EffectControlPoint.DEFAULT; } @@ -172,7 +133,7 @@ namespace osu.Game.Graphics.Containers if (AllowMistimedEventFiring || Math.Abs(TimeSinceLastBeat) < MISTIMED_ALLOWANCE) { using (BeginDelayedSequence(-TimeSinceLastBeat)) - OnNewBeat(beatIndex, timingPoint, effectPoint, track?.CurrentAmplitudes ?? ChannelAmplitudes.Empty); + OnNewBeat(beatIndex, timingPoint, effectPoint, BeatSyncSource.Amplitudes ?? ChannelAmplitudes.Empty); } lastBeat = beatIndex; diff --git a/osu.Game/Graphics/Containers/IBeatSyncProvider.cs b/osu.Game/Graphics/Containers/IBeatSyncProvider.cs new file mode 100644 index 0000000000..d5be50297c --- /dev/null +++ b/osu.Game/Graphics/Containers/IBeatSyncProvider.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Timing; +using osu.Game.Beatmaps.ControlPoints; + +namespace osu.Game.Graphics.Containers +{ + /// + /// Provides various data sources which allow for synchronising visuals to a known beat. + /// Primarily intended for use with . + /// + [Cached(typeof(IBeatSyncProvider))] + public interface IBeatSyncProvider + { + ControlPointInfo? ControlPoints { get; } + + IClock? Clock { get; } + + ChannelAmplitudes? Amplitudes { get; } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 2e4758a134..52052efd5d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Development; using osu.Framework.Extensions; @@ -21,12 +22,15 @@ using osu.Framework.Input; using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Framework.Timing; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Formats; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Input; using osu.Game.Input.Bindings; @@ -52,7 +56,7 @@ namespace osu.Game /// Unlike , this class will not load any kind of UI, allowing it to be used /// for provide dependencies to test cases without interfering with them. /// - public partial class OsuGameBase : Framework.Game, ICanAcceptFiles + public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider { public const string OSU_PROTOCOL = "osu://"; @@ -552,5 +556,9 @@ namespace osu.Game if (Host != null) Host.ExceptionThrown -= onExceptionThrown; } + + ControlPointInfo IBeatSyncProvider.ControlPoints => Beatmap.Value.Beatmap.ControlPointInfo; + IClock IBeatSyncProvider.Clock => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track : (IClock)null; + ChannelAmplitudes? IBeatSyncProvider.Amplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : (ChannelAmplitudes?)null; } } diff --git a/osu.Game/Rulesets/Mods/MetronomeBeat.cs b/osu.Game/Rulesets/Mods/MetronomeBeat.cs index c7a8b02130..149af1e30a 100644 --- a/osu.Game/Rulesets/Mods/MetronomeBeat.cs +++ b/osu.Game/Rulesets/Mods/MetronomeBeat.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mods int timeSignature = timingPoint.TimeSignature.Numerator; // play metronome from one measure before the first object. - if (BeatSyncClock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature) + if (BeatSyncSource.Clock?.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature) return; sample.Frequency.Value = beatIndex % timeSignature == 0 ? 1 : 0.5f; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3a2b195eed..a50a70374b 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -8,6 +8,7 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -19,11 +20,14 @@ using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Framework.Timing; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; @@ -53,7 +57,7 @@ namespace osu.Game.Screens.Edit { [Cached(typeof(IBeatSnapProvider))] [Cached] - public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider, ISamplePlaybackDisabler + public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider, ISamplePlaybackDisabler, IBeatSyncProvider { public override float BackgroundParallaxAmount => 0.1f; @@ -954,5 +958,9 @@ namespace osu.Game.Screens.Edit public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime); public int BeatDivisor => beatDivisor.Value; + + ControlPointInfo IBeatSyncProvider.ControlPoints => editorBeatmap.ControlPointInfo; + IClock IBeatSyncProvider.Clock => clock; + ChannelAmplitudes? IBeatSyncProvider.Amplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : (ChannelAmplitudes?)null; } } diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index b59865ac1a..7b0b8440e6 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -31,9 +31,6 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private OverlayColourProvider overlayColourProvider { get; set; } - [Resolved] - private EditorBeatmap editorBeatmap { get; set; } - [BackgroundDependencyLoader] private void load() { @@ -216,7 +213,10 @@ namespace osu.Game.Screens.Edit.Timing { base.Update(); - timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(BeatSyncClock.CurrentTime); + if (BeatSyncSource.ControlPoints == null || BeatSyncSource.Clock == null) + return; + + timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(BeatSyncSource.Clock.CurrentTime); if (beatLength != timingPoint.BeatLength) { @@ -230,7 +230,7 @@ namespace osu.Game.Screens.Edit.Timing this.TransformBindableTo(interpolatedBpm, (int)Math.Round(timingPoint.BPM), 600, Easing.OutQuint); } - if (BeatSyncClock?.IsRunning != true && isSwinging) + if (BeatSyncSource.Clock?.IsRunning != true && isSwinging) { swing.ClearTransforms(true); @@ -258,7 +258,7 @@ namespace osu.Game.Screens.Edit.Timing float currentAngle = swing.Rotation; float targetAngle = currentAngle > 0 ? -angle : angle; - swing.RotateTo(targetAngle, beatLength, Easing.InOutQuad); + swing.RotateTo(targetAngle, beatLength, Easing.InOutSine); if (currentAngle != 0 && Math.Abs(currentAngle - targetAngle) > angle * 1.8f && isSwinging) { diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index ea43fb1546..4ca5541362 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -12,8 +12,10 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Graphics.Containers; namespace osu.Game.Screens.Play { @@ -27,7 +29,7 @@ namespace osu.Game.Screens.Play /// /// This is intended to be used as a single controller for gameplay, or as a reference source for other s. /// - public class MasterGameplayClockContainer : GameplayClockContainer + public class MasterGameplayClockContainer : GameplayClockContainer, IBeatSyncProvider { /// /// Duration before gameplay start time required before skip button displays. @@ -250,6 +252,10 @@ namespace osu.Game.Screens.Play removeSourceClockAdjustments(); } + ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.Beatmap.ControlPointInfo; + IClock IBeatSyncProvider.Clock => GameplayClock; + ChannelAmplitudes? IBeatSyncProvider.Amplitudes => beatmap.TrackLoaded ? beatmap.Track.CurrentAmplitudes : (ChannelAmplitudes?)null; + private class HardwareCorrectionOffsetClock : FramedOffsetClock { private readonly BindableDouble pauseRateAdjust; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 51c1e6b43b..cbd9b03c32 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; @@ -16,8 +17,10 @@ using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; +using osu.Framework.Timing; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.IO.Archives; @@ -38,7 +41,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.Play { [Cached] - public abstract class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler, ILocalUserPlayInfo + public abstract class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler, ILocalUserPlayInfo, IBeatSyncProvider { /// /// The delay upon completion of the beatmap before displaying the results screen. @@ -1108,5 +1111,9 @@ namespace osu.Game.Screens.Play IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled; IBindable ILocalUserPlayInfo.IsPlaying => LocalUserPlaying; + + ControlPointInfo IBeatSyncProvider.ControlPoints => Beatmap.Value.Beatmap.ControlPointInfo; + IClock IBeatSyncProvider.Clock => GameplayClockContainer.GameplayClock; + ChannelAmplitudes? IBeatSyncProvider.Amplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : (ChannelAmplitudes?)null; } }