// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable using System; using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Containers; 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). /// /// This container will also trigger beat events when the beat matching clock is paused at 's BPM. /// public class BeatSyncedContainer : Container { private int lastBeat; private TimingControlPoint lastTimingPoint; /// /// The amount of time before a beat we should fire . /// This allows for adding easing to animations that may be synchronised to the beat. /// 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. /// public double TimeUntilNextBeat { get; private set; } /// /// The time in milliseconds since the last beat /// public double TimeSinceLastBeat { get; private set; } /// /// How many beats per beatlength to trigger. Defaults to 1. /// public int Divisor { get; set; } = 1; /// /// An optional minimum beat length. Any beat length below this will be multiplied by two until valid. /// public double MinimumBeatLength { get; set; } /// /// Whether this container is currently tracking a beatmap's timing data. /// 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) { } protected override void Update() { TimingControlPoint timingPoint; EffectControlPoint effectPoint; IsBeatSyncedWithTrack = BeatSyncSource.Clock?.IsRunning == true && BeatSyncSource.ControlPoints != null; double currentTrackTime; if (IsBeatSyncedWithTrack) { Debug.Assert(BeatSyncSource.ControlPoints != null); Debug.Assert(BeatSyncSource.Clock != null); 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; } double beatLength = timingPoint.BeatLength / Divisor; while (beatLength < MinimumBeatLength) beatLength *= 2; int beatIndex = (int)((currentTrackTime - timingPoint.Time) / beatLength) - (effectPoint.OmitFirstBarLine ? 1 : 0); // The beats before the start of the first control point are off by 1, this should do the trick if (currentTrackTime < timingPoint.Time) beatIndex--; TimeUntilNextBeat = (timingPoint.Time - currentTrackTime) % beatLength; if (TimeUntilNextBeat <= 0) TimeUntilNextBeat += beatLength; TimeSinceLastBeat = beatLength - TimeUntilNextBeat; if (ReferenceEquals(timingPoint, lastTimingPoint) && beatIndex == lastBeat) return; // 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, BeatSyncSource.Amplitudes ?? ChannelAmplitudes.Empty); } lastBeat = beatIndex; lastTimingPoint = timingPoint; } } }