diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMuted.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMuted.cs index c14dc78f38..e08d66fa31 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMuted.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMuted.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods MuteComboCount = { Value = 0 }, }, PassCondition = () => Beatmap.Value.Track.AggregateVolume.Value == 0.0 && - Player.ChildrenOfType().SingleOrDefault()?.AggregateVolume.Value == 1.0, + Player.ChildrenOfType().SingleOrDefault()?.AggregateVolume.Value == 1.0, }); /// diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 5b121f4673..c58c624f5c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -339,7 +339,7 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - drawableRuleset.Overlays.Add(new Metronome(drawableRuleset.Beatmap.HitObjects.First().StartTime)); + drawableRuleset.Overlays.Add(new MetronomeBeat(drawableRuleset.Beatmap.HitObjects.First().StartTime)); } #endregion diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs new file mode 100644 index 0000000000..de441995b5 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs @@ -0,0 +1,116 @@ +// Copyright (c) ppy Pty Ltd . 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Timing; +using osuTK; + +namespace osu.Game.Tests.Visual.Editing +{ + [TestFixture] + public class TestSceneTapTimingControl : EditorClockTestScene + { + [Cached(typeof(EditorBeatmap))] + [Cached(typeof(IBeatSnapProvider))] + private readonly EditorBeatmap editorBeatmap; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + + [Cached] + private Bindable selectedGroup = new Bindable(); + + private TapTimingControl control; + + public TestSceneTapTimingControl() + { + var playableBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); + + // Ensure time doesn't end while testing + playableBeatmap.BeatmapInfo.Length = 1200000; + + editorBeatmap = new EditorBeatmap(playableBeatmap); + + selectedGroup.Value = editorBeatmap.ControlPointInfo.Groups.First(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap); + Beatmap.Disabled = true; + + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + Width = 400, + Scale = new Vector2(1.5f), + Child = control = new TapTimingControl(), + } + }; + } + + [Test] + public void TestTapThenReset() + { + AddStep("click tap button", () => + { + control.ChildrenOfType() + .Last() + .TriggerClick(); + }); + + AddUntilStep("wait for track playing", () => Clock.IsRunning); + + AddStep("click reset button", () => + { + control.ChildrenOfType() + .First() + .TriggerClick(); + }); + + AddUntilStep("wait for track stopped", () => !Clock.IsRunning); + } + + [Test] + public void TestBasic() + { + AddStep("set low bpm", () => + { + editorBeatmap.ControlPointInfo.TimingPoints.First().BeatLength = 1000; + }); + + AddStep("click tap button", () => + { + control.ChildrenOfType() + .Last() + .TriggerClick(); + }); + + AddSliderStep("BPM", 30, 400, 60, bpm => editorBeatmap.ControlPointInfo.TimingPoints.First().BeatLength = 60000f / bpm); + } + + protected override void Dispose(bool isDisposing) + { + Beatmap.Disabled = false; + base.Dispose(isDisposing); + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index ede89c6096..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,24 +263,7 @@ namespace osu.Game.Tests.Visual.UserInterface }; } - protected override void LoadComplete() - { - base.LoadComplete(); - - Beatmap.BindValueChanged(_ => - { - timingPointCount.Value = 0; - currentTimingPoint.Value = 0; - beatCount.Value = 0; - currentBeat.Value = 0; - beatsPerMinute.Value = 0; - adjustedBeatLength.Value = 0; - timeUntilNextBeat.Value = 0; - timeSinceLastBeat.Value = 0; - }, true); - } - - private List timingPoints => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ToList(); + private List timingPoints => BeatSyncSource.ControlPoints?.TimingPoints.ToList(); private TimingControlPoint getNextTimingPoint(TimingControlPoint current) { @@ -292,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); } @@ -300,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/Beatmaps/IBeatSyncProvider.cs b/osu.Game/Beatmaps/IBeatSyncProvider.cs new file mode 100644 index 0000000000..cc1cfc3cb5 --- /dev/null +++ b/osu.Game/Beatmaps/IBeatSyncProvider.cs @@ -0,0 +1,27 @@ +// 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; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Beatmaps +{ + /// + /// Provides various data sources which allow for synchronising visuals to a known beat. + /// Primarily intended for use with . + /// + [Cached] + public interface IBeatSyncProvider + { + ControlPointInfo? ControlPoints { get; } + + IClock? Clock { get; } + + ChannelAmplitudes? Amplitudes { get; } + } +} diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 2024d18570..953731244d 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -5,9 +5,7 @@ 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.Play; @@ -74,65 +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] - 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; - 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; - beatmap = Beatmap.Value.Beatmap; - } - - 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; } @@ -162,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/UserInterfaceV2/LabelledDrawable.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs index 1e6032c1d0..066e1a7978 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs @@ -41,7 +41,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected const float CONTENT_PADDING_VERTICAL = 10; protected const float CONTENT_PADDING_HORIZONTAL = 15; - protected const float CORNER_RADIUS = 15; + + public const float CORNER_RADIUS = 15; /// /// The component that is being displayed. diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 564cb6c131..666413004a 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,8 +22,10 @@ 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; @@ -52,7 +55,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 +555,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/Metronome.cs b/osu.Game/Rulesets/Mods/MetronomeBeat.cs similarity index 92% rename from osu.Game/Rulesets/Mods/Metronome.cs rename to osu.Game/Rulesets/Mods/MetronomeBeat.cs index b85a341577..149af1e30a 100644 --- a/osu.Game/Rulesets/Mods/Metronome.cs +++ b/osu.Game/Rulesets/Mods/MetronomeBeat.cs @@ -11,14 +11,14 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mods { - public class Metronome : BeatSyncedContainer, IAdjustableAudioComponent + public class MetronomeBeat : BeatSyncedContainer, IAdjustableAudioComponent { private readonly double firstHitTime; private readonly PausableSkinnableSound sample; /// Start time of the first hit object, used for providing a count down. - public Metronome(double firstHitTime) + public MetronomeBeat(double firstHitTime) { this.firstHitTime = firstHitTime; AllowMistimedEventFiring = false; @@ -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/Rulesets/Mods/ModMuted.cs b/osu.Game/Rulesets/Mods/ModMuted.cs index a7d3114f2b..84341faab7 100644 --- a/osu.Game/Rulesets/Mods/ModMuted.cs +++ b/osu.Game/Rulesets/Mods/ModMuted.cs @@ -79,11 +79,11 @@ namespace osu.Game.Rulesets.Mods { if (EnableMetronome.Value) { - Metronome metronome; + MetronomeBeat metronomeBeat; - drawableRuleset.Overlays.Add(metronome = new Metronome(drawableRuleset.Beatmap.HitObjects.First().StartTime)); + drawableRuleset.Overlays.Add(metronomeBeat = new MetronomeBeat(drawableRuleset.Beatmap.HitObjects.First().StartTime)); - metronome.AddAdjustment(AdjustableProperty.Volume, metronomeVolumeAdjust); + metronomeBeat.AddAdjustment(AdjustableProperty.Volume, metronomeVolumeAdjust); } if (AffectsHitSounds.Value) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3a2b195eed..0bb3f51903 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,8 +20,10 @@ 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; @@ -53,7 +56,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 +957,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 new file mode 100644 index 0000000000..4dd7a75d4a --- /dev/null +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -0,0 +1,270 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.Edit.Timing +{ + public class MetronomeDisplay : BeatSyncedContainer + { + private Container swing; + + private OsuSpriteText bpmText; + + private Drawable weight; + private Drawable stick; + + [Resolved] + private OverlayColourProvider overlayColourProvider { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + const float taper = 25; + const float swing_vertical_offset = -23; + const float lower_cover_height = 32; + + var triangleSize = new Vector2(90, 120 + taper); + + Margin = new MarginPadding(10); + + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Container + { + Name = @"Taper adjust", + Masking = true, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Size = new Vector2(triangleSize.X, triangleSize.Y - taper), + Children = new Drawable[] + { + new Triangle + { + Name = @"Main body", + EdgeSmoothness = new Vector2(1), + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Size = triangleSize, + Colour = overlayColourProvider.Background3, + }, + }, + }, + new Circle + { + Name = "Centre marker", + Colour = overlayColourProvider.Background5, + RelativeSizeAxes = Axes.Y, + Width = 2, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Y = -(lower_cover_height + 3), + Height = 0.65f, + }, + swing = new Container + { + Name = @"Swing", + RelativeSizeAxes = Axes.Both, + Y = swing_vertical_offset, + Height = 0.80f, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Children = new[] + { + stick = new Circle + { + Name = @"Stick", + RelativeSizeAxes = Axes.Y, + Colour = overlayColourProvider.Colour2, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Width = 4, + }, + weight = new Container + { + Name = @"Weight", + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Size = new Vector2(10), + Rotation = 180, + RelativePositionAxes = Axes.Y, + Y = 0.4f, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Shear = new Vector2(0.2f, 0), + Colour = overlayColourProvider.Colour1, + EdgeSmoothness = new Vector2(1), + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Shear = new Vector2(-0.2f, 0), + Colour = overlayColourProvider.Colour1, + EdgeSmoothness = new Vector2(1), + }, + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = ColourInfo.GradientVertical(overlayColourProvider.Colour1, overlayColourProvider.Colour0), + RelativeSizeAxes = Axes.Y, + Width = 1, + Height = 0.9f + }, + } + }, + } + }, + new Container + { + Name = @"Taper adjust", + Masking = true, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Size = new Vector2(triangleSize.X, triangleSize.Y - taper), + Children = new Drawable[] + { + new Circle + { + Name = @"Locking wedge", + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Colour = overlayColourProvider.Background1, + Size = new Vector2(8), + } + }, + }, + new Circle + { + Name = @"Swing connection point", + Y = swing_vertical_offset, + Anchor = Anchor.BottomCentre, + Origin = Anchor.Centre, + Colour = overlayColourProvider.Colour0, + Size = new Vector2(8) + }, + new Container + { + Name = @"Lower cover", + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.X, + Masking = true, + Height = lower_cover_height, + Children = new Drawable[] + { + new Triangle + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Size = triangleSize, + Colour = overlayColourProvider.Background2, + EdgeSmoothness = new Vector2(1), + Alpha = 0.8f + }, + } + }, + bpmText = new OsuSpriteText + { + Name = @"BPM display", + Colour = overlayColourProvider.Content1, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Y = -3, + }, + }; + } + + private double beatLength; + + private TimingControlPoint timingPoint; + + private bool isSwinging; + + private readonly BindableInt interpolatedBpm = new BindableInt(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + interpolatedBpm.BindValueChanged(bpm => bpmText.Text = bpm.NewValue.ToLocalisableString()); + } + + protected override void Update() + { + base.Update(); + + if (BeatSyncSource.ControlPoints == null || BeatSyncSource.Clock == null) + return; + + timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(BeatSyncSource.Clock.CurrentTime); + + if (beatLength != timingPoint.BeatLength) + { + beatLength = timingPoint.BeatLength; + + EarlyActivationMilliseconds = timingPoint.BeatLength / 2; + + float bpmRatio = (float)Interpolation.ApplyEasing(Easing.OutQuad, Math.Clamp((timingPoint.BPM - 30) / 480, 0, 1)); + + weight.MoveToY((float)Interpolation.Lerp(0.1f, 0.83f, bpmRatio), 600, Easing.OutQuint); + this.TransformBindableTo(interpolatedBpm, (int)Math.Round(timingPoint.BPM), 600, Easing.OutQuint); + } + + if (BeatSyncSource.Clock?.IsRunning != true && isSwinging) + { + swing.ClearTransforms(true); + + using (swing.BeginDelayedSequence(350)) + { + swing.RotateTo(0, 1000, Easing.OutQuint); + stick.FadeColour(overlayColourProvider.Colour2, 1000, Easing.OutQuint); + } + + isSwinging = false; + } + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + const float angle = 27.5f; + + if (!IsBeatSyncedWithTrack) + return; + + isSwinging = true; + + float currentAngle = swing.Rotation; + float targetAngle = currentAngle > 0 ? -angle : angle; + + swing.RotateTo(targetAngle, beatLength, Easing.InOutQuad); + + if (currentAngle != 0 && Math.Abs(currentAngle - targetAngle) > angle * 1.8f && isSwinging) + { + using (stick.BeginDelayedSequence(beatLength / 2)) + stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint); + } + } + } +} diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs new file mode 100644 index 0000000000..1b0f0a3f5e --- /dev/null +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -0,0 +1,104 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; + +namespace osu.Game.Screens.Edit.Timing +{ + public class TapTimingControl : CompositeDrawable + { + [Resolved] + private EditorClock editorClock { get; set; } + + [Resolved] + private Bindable selectedGroup { get; set; } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider, OsuColour colours) + { + Height = 200; + RelativeSizeAxes = Axes.X; + + CornerRadius = LabelledDrawable.CORNER_RADIUS; + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = colourProvider.Background4, + RelativeSizeAxes = Axes.Both, + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 60), + }, + Content = new[] + { + new Drawable[] + { + new MetronomeDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + Children = new Drawable[] + { + new RoundedButton + { + Text = "Reset", + BackgroundColour = colours.Pink, + RelativeSizeAxes = Axes.X, + Width = 0.3f, + Action = reset, + }, + new RoundedButton + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Text = "Play from start", + RelativeSizeAxes = Axes.X, + BackgroundColour = colourProvider.Background1, + Width = 0.68f, + Action = tap, + } + } + }, + } + } + }, + }; + } + + private void tap() + { + editorClock.Seek(selectedGroup.Value.Time); + editorClock.Start(); + } + + private void reset() + { + editorClock.Stop(); + editorClock.Seek(selectedGroup.Value.Time); + } + } +} diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 13af04cd4b..a5abd96d72 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -19,6 +19,7 @@ namespace osu.Game.Screens.Edit.Timing { Flow.AddRange(new Drawable[] { + new TapTimingControl(), bpmTextEntry = new BPMTextBox(), timeSignature = new LabelledTimeSignature { diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index ea43fb1546..d87d57ec49 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -12,6 +12,7 @@ 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; @@ -27,7 +28,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 +251,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;