2021-04-14 18:50:22 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System ;
using System.Linq ;
2022-09-07 18:12:16 +08:00
using osu.Framework.Allocation ;
2021-04-14 18:50:22 +08:00
using osu.Framework.Audio ;
using osu.Framework.Audio.Track ;
using osu.Framework.Bindables ;
using osu.Framework.Graphics ;
2023-12-26 18:20:41 +08:00
using osu.Framework.Logging ;
2021-04-14 18:50:22 +08:00
using osu.Framework.Timing ;
using osu.Game.Beatmaps ;
2022-05-22 21:15:53 +08:00
using osu.Game.Beatmaps.ControlPoints ;
2022-09-07 18:12:16 +08:00
using osu.Game.Overlays ;
2021-04-14 18:50:22 +08:00
namespace osu.Game.Screens.Play
{
2021-04-16 19:47:09 +08:00
/// <summary>
/// A <see cref="GameplayClockContainer"/> which uses a <see cref="WorkingBeatmap"/> as a source.
/// <para>
/// This is the most complete <see cref="GameplayClockContainer"/> which takes into account all user and platform offsets,
/// and provides implementations for user actions such as skipping or adjusting playback rates that may occur during gameplay.
/// </para>
/// </summary>
/// <remarks>
/// This is intended to be used as a single controller for gameplay, or as a reference source for other <see cref="GameplayClockContainer"/>s.
/// </remarks>
2022-09-07 16:38:00 +08:00
public partial class MasterGameplayClockContainer : GameplayClockContainer , IBeatSyncProvider
2021-04-14 18:50:22 +08:00
{
/// <summary>
/// Duration before gameplay start time required before skip button displays.
/// </summary>
public const double MINIMUM_SKIP_TIME = 1000 ;
public readonly BindableNumber < double > UserPlaybackRate = new BindableDouble ( 1 )
{
MinValue = 0.5 ,
MaxValue = 2 ,
Precision = 0.1 ,
} ;
2024-01-03 16:17:01 +08:00
/// <summary>
/// Whether the audio playback rate should be validated.
/// Mostly disabled for tests.
/// </summary>
2024-01-03 18:46:26 +08:00
internal bool ShouldValidatePlaybackRate { get ; init ; } = true ;
2024-01-03 16:17:01 +08:00
2023-12-26 18:20:41 +08:00
/// <summary>
/// Whether the audio playback is within acceptable ranges.
/// Will become false if audio playback is not going as expected.
/// </summary>
public IBindable < bool > PlaybackRateValid = > playbackRateValid ;
private readonly Bindable < bool > playbackRateValid = new Bindable < bool > ( true ) ;
2021-04-14 18:50:22 +08:00
private readonly WorkingBeatmap beatmap ;
Fix `StopUsingBeatmapClock()` applying adjustments to track it was supposed to stop using
- Closes https://github.com/ppy/osu/issues/25248
- Possibly also closes https://github.com/ppy/osu/issues/20475
Regressed in e33486a766044c17c2f254f5e8df6d72b29c341e.
`StopUsingBeatmapClock()` intends to, as the name says, stop operating
on the working beatmap clock to yield its usage to other components on
exit. As part of that it tries to unapply audio adjustments so that
other screens can apply theirs freely instead.
However, the aforementioned commit introduced a bug in this. Previously
to it, `track` was an alias for the `SourceClock`, which could be
mutated in an indirect way via `ChangeSource()` calls. The
aforementioned commit made `track` a `readonly` field, initialised in
constructor, which would _never_ change value. In particular, it would
_always_ be the beatmap track, which meant that
`StopUsingBeatmapClock()` would remove the adjustments from the beatmap
track, but then at the end of the method, _apply them onto that same
track again_.
This was only saved by the fact that clock adjustments are removed again
on disposal of the `MasterGameplayClockContainer()`. This - due to async
disposal pressure - could explain infrequently reported cases wherein
the track would just continue to speed up ad infinitum.
To fix, fully substitute the beatmap track for a virtual track at the
point of calling `StopUsingBeatmapClock()`.
2023-10-27 01:27:50 +08:00
private Track track ;
2022-09-05 22:20:02 +08:00
2022-04-13 13:03:52 +08:00
private readonly double skipTargetTime ;
2022-03-17 22:39:45 +08:00
2022-08-29 18:51:16 +08:00
/// <summary>
/// Stores the time at which the last <see cref="StopGameplayClock"/> call was triggered.
/// This is used to ensure we resume from that precise point in time, ignoring the proceeding frequency ramp.
///
/// Optimally, we'd have gameplay ramp down with the frequency, but I believe this was intentionally disabled
/// to avoid fails occurring after the pause screen has been shown.
///
/// In the future I want to change this.
/// </summary>
2023-10-31 13:47:04 +08:00
internal double? LastStopTime ;
2022-08-29 18:51:16 +08:00
2022-09-07 18:12:16 +08:00
[Resolved]
private MusicController musicController { get ; set ; } = null ! ;
2022-03-17 22:39:45 +08:00
/// <summary>
/// Create a new master gameplay clock container.
/// </summary>
/// <param name="beatmap">The beatmap to be used for time and metadata references.</param>
2022-04-13 13:03:52 +08:00
/// <param name="skipTargetTime">The latest time which should be used when introducing gameplay. Will be used when skipping forward.</param>
2022-04-13 13:09:49 +08:00
public MasterGameplayClockContainer ( WorkingBeatmap beatmap , double skipTargetTime )
2023-09-22 13:26:33 +08:00
: base ( beatmap . Track , applyOffsets : true , requireDecoupling : true )
2021-04-14 18:50:22 +08:00
{
this . beatmap = beatmap ;
2022-04-13 13:03:52 +08:00
this . skipTargetTime = skipTargetTime ;
2022-03-17 19:54:42 +08:00
2022-09-08 05:25:55 +08:00
track = beatmap . Track ;
2022-08-23 17:32:56 +08:00
StartTime = findEarliestStartTime ( ) ;
2022-03-17 22:39:45 +08:00
}
2022-03-17 19:54:42 +08:00
2022-03-18 00:15:17 +08:00
private double findEarliestStartTime ( )
2022-03-17 22:39:45 +08:00
{
2022-03-18 00:15:17 +08:00
// here we are trying to find the time to start playback from the "zero" point.
// generally this is either zero, or some point earlier than zero in the case of storyboards, lead-ins etc.
// start with the originally provided latest time (if before zero).
2022-04-13 13:03:52 +08:00
double time = Math . Min ( 0 , skipTargetTime ) ;
2022-03-17 22:39:45 +08:00
// if a storyboard is present, it may dictate the appropriate start time by having events in negative time space.
// this is commonly used to display an intro before the audio track start.
double? firstStoryboardEvent = beatmap . Storyboard . EarliestEventTime ;
if ( firstStoryboardEvent ! = null )
time = Math . Min ( time , firstStoryboardEvent . Value ) ;
// some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available.
// this is not available as an option in the live editor but can still be applied via .osu editing.
double firstHitObjectTime = beatmap . Beatmap . HitObjects . First ( ) . StartTime ;
if ( beatmap . BeatmapInfo . AudioLeadIn > 0 )
time = Math . Min ( time , firstHitObjectTime - beatmap . BeatmapInfo . AudioLeadIn ) ;
return time ;
2021-04-14 18:50:22 +08:00
}
2022-08-22 18:43:18 +08:00
protected override void StopGameplayClock ( )
2021-04-14 18:50:22 +08:00
{
2023-10-31 13:47:04 +08:00
LastStopTime = GameplayClock . CurrentTime ;
2022-08-29 18:51:16 +08:00
2022-04-11 13:11:23 +08:00
if ( IsLoaded )
2021-06-04 20:58:07 +08:00
{
2022-04-11 13:11:23 +08:00
// During normal operation, the source is stopped after performing a frequency ramp.
2022-08-29 21:04:37 +08:00
this . TransformBindableTo ( GameplayClock . ExternalPauseFrequencyAdjust , 0 , 200 , Easing . Out ) . OnComplete ( _ = >
2021-06-04 20:58:07 +08:00
{
2022-08-22 18:43:18 +08:00
if ( IsPaused . Value )
base . StopGameplayClock ( ) ;
} ) ;
2021-06-04 20:58:07 +08:00
}
2021-04-14 18:50:22 +08:00
else
2022-04-11 13:11:23 +08:00
{
2022-08-22 18:43:18 +08:00
base . StopGameplayClock ( ) ;
2022-04-11 13:11:23 +08:00
// If not yet loaded, we still want to ensure relevant state is correct, as it is used for offset calculations.
2022-08-22 18:43:18 +08:00
GameplayClock . ExternalPauseFrequencyAdjust . Value = 0 ;
2022-04-11 13:11:23 +08:00
// We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment.
// Without doing this, an initial seek may be performed with the wrong offset.
2022-08-18 13:52:47 +08:00
GameplayClock . ProcessFrame ( ) ;
2022-04-11 13:11:23 +08:00
}
2021-04-14 18:50:22 +08:00
}
2022-08-29 18:51:16 +08:00
public override void Seek ( double time )
{
// Safety in case the clock is seeked while stopped.
2023-10-31 13:47:04 +08:00
LastStopTime = null ;
2023-12-26 18:20:41 +08:00
elapsedValidationTime = null ;
2022-08-29 18:51:16 +08:00
base . Seek ( time ) ;
}
protected override void PrepareStart ( )
{
2023-10-31 13:47:04 +08:00
if ( LastStopTime ! = null )
2022-08-29 18:51:16 +08:00
{
2023-10-31 13:47:04 +08:00
Seek ( LastStopTime . Value ) ;
LastStopTime = null ;
2022-08-29 18:51:16 +08:00
}
else
base . PrepareStart ( ) ;
}
2022-08-22 18:43:18 +08:00
protected override void StartGameplayClock ( )
2021-04-20 12:09:49 +08:00
{
2023-10-27 01:38:41 +08:00
addAdjustmentsToTrack ( ) ;
2022-08-22 18:43:18 +08:00
base . StartGameplayClock ( ) ;
if ( IsLoaded )
{
2022-08-29 21:04:37 +08:00
this . TransformBindableTo ( GameplayClock . ExternalPauseFrequencyAdjust , 1 , 200 , Easing . In ) ;
2022-08-22 18:43:18 +08:00
}
else
{
// If not yet loaded, we still want to ensure relevant state is correct, as it is used for offset calculations.
GameplayClock . ExternalPauseFrequencyAdjust . Value = 1 ;
// We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment.
// Without doing this, an initial seek may be performed with the wrong offset.
GameplayClock . ProcessFrame ( ) ;
}
2021-04-20 12:09:49 +08:00
}
2021-04-14 18:50:22 +08:00
/// <summary>
/// Skip forward to the next valid skip point.
/// </summary>
public void Skip ( )
{
2022-08-18 13:52:47 +08:00
if ( GameplayClock . CurrentTime > skipTargetTime - MINIMUM_SKIP_TIME )
2021-04-14 18:50:22 +08:00
return ;
2022-04-13 13:03:52 +08:00
double skipTarget = skipTargetTime - MINIMUM_SKIP_TIME ;
2021-04-14 18:50:22 +08:00
2022-08-18 13:52:47 +08:00
if ( GameplayClock . CurrentTime < 0 & & skipTarget > 6000 )
2021-04-14 18:50:22 +08:00
// double skip exception for storyboards with very long intros
skipTarget = 0 ;
Seek ( skipTarget ) ;
}
/// <summary>
/// Changes the backing clock to avoid using the originally provided track.
/// </summary>
public void StopUsingBeatmapClock ( )
{
2023-10-27 01:38:41 +08:00
removeAdjustmentsFromTrack ( ) ;
2023-09-22 15:18:43 +08:00
Fix `StopUsingBeatmapClock()` applying adjustments to track it was supposed to stop using
- Closes https://github.com/ppy/osu/issues/25248
- Possibly also closes https://github.com/ppy/osu/issues/20475
Regressed in e33486a766044c17c2f254f5e8df6d72b29c341e.
`StopUsingBeatmapClock()` intends to, as the name says, stop operating
on the working beatmap clock to yield its usage to other components on
exit. As part of that it tries to unapply audio adjustments so that
other screens can apply theirs freely instead.
However, the aforementioned commit introduced a bug in this. Previously
to it, `track` was an alias for the `SourceClock`, which could be
mutated in an indirect way via `ChangeSource()` calls. The
aforementioned commit made `track` a `readonly` field, initialised in
constructor, which would _never_ change value. In particular, it would
_always_ be the beatmap track, which meant that
`StopUsingBeatmapClock()` would remove the adjustments from the beatmap
track, but then at the end of the method, _apply them onto that same
track again_.
This was only saved by the fact that clock adjustments are removed again
on disposal of the `MasterGameplayClockContainer()`. This - due to async
disposal pressure - could explain infrequently reported cases wherein
the track would just continue to speed up ad infinitum.
To fix, fully substitute the beatmap track for a virtual track at the
point of calling `StopUsingBeatmapClock()`.
2023-10-27 01:27:50 +08:00
track = new TrackVirtual ( beatmap . Track . Length ) ;
track . Seek ( CurrentTime ) ;
2023-09-22 15:18:43 +08:00
if ( IsRunning )
Fix `StopUsingBeatmapClock()` applying adjustments to track it was supposed to stop using
- Closes https://github.com/ppy/osu/issues/25248
- Possibly also closes https://github.com/ppy/osu/issues/20475
Regressed in e33486a766044c17c2f254f5e8df6d72b29c341e.
`StopUsingBeatmapClock()` intends to, as the name says, stop operating
on the working beatmap clock to yield its usage to other components on
exit. As part of that it tries to unapply audio adjustments so that
other screens can apply theirs freely instead.
However, the aforementioned commit introduced a bug in this. Previously
to it, `track` was an alias for the `SourceClock`, which could be
mutated in an indirect way via `ChangeSource()` calls. The
aforementioned commit made `track` a `readonly` field, initialised in
constructor, which would _never_ change value. In particular, it would
_always_ be the beatmap track, which meant that
`StopUsingBeatmapClock()` would remove the adjustments from the beatmap
track, but then at the end of the method, _apply them onto that same
track again_.
This was only saved by the fact that clock adjustments are removed again
on disposal of the `MasterGameplayClockContainer()`. This - due to async
disposal pressure - could explain infrequently reported cases wherein
the track would just continue to speed up ad infinitum.
To fix, fully substitute the beatmap track for a virtual track at the
point of calling `StopUsingBeatmapClock()`.
2023-10-27 01:27:50 +08:00
track . Start ( ) ;
ChangeSource ( track ) ;
2023-09-22 15:18:43 +08:00
2023-10-27 01:38:41 +08:00
addAdjustmentsToTrack ( ) ;
2021-04-14 18:50:22 +08:00
}
2023-12-26 18:20:41 +08:00
protected override void Update ( )
{
base . Update ( ) ;
checkPlaybackValidity ( ) ;
}
#region Clock validation ( ensure things are running correctly for local gameplay )
private double elapsedGameplayClockTime ;
private double? elapsedValidationTime ;
private int playbackDiscrepancyCount ;
private const int allowed_playback_discrepancies = 5 ;
private void checkPlaybackValidity ( )
{
2024-01-03 16:17:01 +08:00
if ( ! ShouldValidatePlaybackRate )
return ;
2023-12-26 18:20:41 +08:00
if ( GameplayClock . IsRunning )
{
elapsedGameplayClockTime + = GameplayClock . ElapsedFrameTime ;
2023-12-26 23:11:22 +08:00
if ( elapsedValidationTime = = null )
elapsedValidationTime = elapsedGameplayClockTime ;
else
elapsedValidationTime + = GameplayClock . Rate * Time . Elapsed ;
2023-12-26 18:20:41 +08:00
if ( Math . Abs ( elapsedGameplayClockTime - elapsedValidationTime ! . Value ) > 300 )
{
if ( playbackDiscrepancyCount + + > allowed_playback_discrepancies )
{
if ( playbackRateValid . Value )
{
playbackRateValid . Value = false ;
Logger . Log ( "System audio playback is not working as expected. Some online functionality will not work.\n\nPlease check your audio drivers." , level : LogLevel . Important ) ;
}
}
else
{
Logger . Log ( $"Playback discrepancy detected ({playbackDiscrepancyCount} of allowed {allowed_playback_discrepancies}): {elapsedGameplayClockTime:N1} vs {elapsedValidationTime:N1}" ) ;
}
elapsedValidationTime = null ;
}
}
}
#endregion
2021-04-14 18:50:22 +08:00
private bool speedAdjustmentsApplied ;
2023-10-27 01:38:41 +08:00
private void addAdjustmentsToTrack ( )
2021-04-14 18:50:22 +08:00
{
if ( speedAdjustmentsApplied )
return ;
2022-09-07 18:12:16 +08:00
musicController . ResetTrackAdjustments ( ) ;
2022-09-08 16:14:06 +08:00
track . BindAdjustments ( AdjustmentsFromMods ) ;
2022-08-18 13:52:47 +08:00
track . AddAdjustment ( AdjustableProperty . Frequency , GameplayClock . ExternalPauseFrequencyAdjust ) ;
2022-08-18 14:08:09 +08:00
track . AddAdjustment ( AdjustableProperty . Tempo , UserPlaybackRate ) ;
2021-04-14 18:50:22 +08:00
speedAdjustmentsApplied = true ;
}
2023-10-27 01:38:41 +08:00
private void removeAdjustmentsFromTrack ( )
2021-04-14 18:50:22 +08:00
{
if ( ! speedAdjustmentsApplied )
return ;
2022-09-08 16:14:06 +08:00
track . UnbindAdjustments ( AdjustmentsFromMods ) ;
2022-08-18 13:52:47 +08:00
track . RemoveAdjustment ( AdjustableProperty . Frequency , GameplayClock . ExternalPauseFrequencyAdjust ) ;
2022-08-18 14:08:09 +08:00
track . RemoveAdjustment ( AdjustableProperty . Tempo , UserPlaybackRate ) ;
2021-04-14 18:50:22 +08:00
speedAdjustmentsApplied = false ;
}
protected override void Dispose ( bool isDisposing )
{
base . Dispose ( isDisposing ) ;
2023-10-27 01:38:41 +08:00
removeAdjustmentsFromTrack ( ) ;
2021-04-14 18:50:22 +08:00
}
2022-05-22 21:15:53 +08:00
ControlPointInfo IBeatSyncProvider . ControlPoints = > beatmap . Beatmap . ControlPointInfo ;
2022-08-15 18:46:29 +08:00
IClock IBeatSyncProvider . Clock = > this ;
2021-09-17 14:39:03 +08:00
2022-08-02 17:30:25 +08:00
ChannelAmplitudes IHasAmplitudes . CurrentAmplitudes = > beatmap . TrackLoaded ? beatmap . Track . CurrentAmplitudes : ChannelAmplitudes . Empty ;
2021-04-14 18:50:22 +08:00
}
}