// 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.Collections.Generic; using System.Linq; using NUnit.Framework; 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.Rulesets.Osu; using osuTK.Graphics; namespace osu.Game.Tests.Visual.UserInterface { [TestFixture] public class TestSceneBeatSyncedContainer : OsuTestScene { private BeatContainer beatContainer; private DecoupleableInterpolatingFramedClock decoupledClock; [SetUpSteps] public void SetUpSteps() { AddStep("Set beatmap", () => { 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()); } [Test] public void TestFirstBeatAtFirstTimingPoint() { 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_height = 150; private readonly InfoString timingPointCount; private readonly InfoString currentTimingPoint; private readonly InfoString beatCount; private readonly InfoString currentBeat; private readonly InfoString beatsPerMinute; private readonly InfoString adjustedBeatLength; private readonly InfoString timeUntilNextBeat; private readonly InfoString timeSinceLastBeat; private readonly InfoString currentTime; private readonly Box flashLayer; public BeatContainer() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; Children = new Drawable[] { new Container { Name = @"Info Layer", Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Bottom = flash_layer_height }, Children = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, Colour = Color4.Black.Opacity(150), }, new FillFlowContainer { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, AutoSizeAxes = Axes.Both, 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)"), currentBeat = new InfoString(@"Current beat"), beatsPerMinute = new InfoString(@"BPM"), adjustedBeatLength = new InfoString(@"Adjusted beat length"), timeUntilNextBeat = new InfoString(@"Time until next beat"), timeSinceLastBeat = new InfoString(@"Time since last beat"), } } } }, new Container { Name = @"Color indicator", Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, RelativeSizeAxes = Axes.X, Height = flash_layer_height, Children = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, Colour = Color4.Black, }, flashLayer = new Box { RelativeSizeAxes = Axes.Both, Colour = Color4.White, Alpha = 0, } } } }; Beatmap.ValueChanged += delegate { timingPointCount.Value = 0; currentTimingPoint.Value = 0; beatCount.Value = 0; currentBeat.Value = 0; beatsPerMinute.Value = 0; adjustedBeatLength.Value = 0; timeUntilNextBeat.Value = 0; timeSinceLastBeat.Value = 0; }; } private List timingPoints => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ToList(); private TimingControlPoint getNextTimingPoint(TimingControlPoint current) { if (timingPoints[^1] == current) return current; int index = timingPoints.IndexOf(current); // -1 means that this is a "default beat" return index == -1 ? current : timingPoints[index + 1]; } private int calculateBeatCount(TimingControlPoint current) { if (timingPoints.Count == 0) return 0; if (timingPoints[^1] == current) return (int)Math.Ceiling((Clock.CurrentTime - current.Time) / current.BeatLength); return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength); } protected override void Update() { 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); timingPointCount.Value = timingPoints.Count; currentTimingPoint.Value = timingPoints.IndexOf(timingPoint); beatCount.Value = calculateBeatCount(timingPoint); currentBeat.Value = beatIndex; beatsPerMinute.Value = 60000 / timingPoint.BeatLength; adjustedBeatLength.Value = timingPoint.BeatLength; flashLayer.FadeOutFromOne(timingPoint.BeatLength / 4); NewBeat?.Invoke(beatIndex, timingPoint, effectPoint, amplitudes); } } private class InfoString : FillFlowContainer { private const int text_size = 20; private const int margin = 7; private readonly OsuSpriteText valueText; public double Value { set => valueText.Text = $"{value:0.##}"; } public InfoString(string header) { AutoSizeAxes = Axes.Both; Direction = FillDirection.Horizontal; Add(new OsuSpriteText { Text = header + @": ", Font = OsuFont.GetFont(size: text_size) }); Add(valueText = new OsuSpriteText { Font = OsuFont.GetFont(size: text_size) }); Margin = new MarginPadding(margin); } } } }