2019-08-13 05:46:57 +00:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2019-01-24 08:43:03 +00:00
// See the LICENCE file in the repository root for full licence text.
2018-04-13 09:19:50 +00:00
using System ;
2018-05-14 08:41:35 +00:00
using System.Collections.Generic ;
2018-04-13 09:19:50 +00:00
using System.Linq ;
2020-08-07 11:51:56 +00:00
using JetBrains.Annotations ;
2018-04-13 09:19:50 +00:00
using osu.Framework.Allocation ;
2020-09-02 11:04:56 +00:00
using osu.Framework.Audio ;
2020-07-10 07:33:31 +00:00
using osu.Framework.Audio.Track ;
2019-02-21 10:04:31 +00:00
using osu.Framework.Bindables ;
2018-04-13 09:19:50 +00:00
using osu.Framework.Graphics ;
2020-08-04 12:53:00 +00:00
using osu.Framework.Graphics.Audio ;
using osu.Framework.Graphics.Containers ;
2018-04-13 09:19:50 +00:00
using osu.Framework.Threading ;
using osu.Game.Beatmaps ;
2021-11-08 08:59:23 +00:00
using osu.Game.Database ;
2019-04-08 10:16:34 +00:00
using osu.Game.Rulesets.Mods ;
2018-04-13 09:19:50 +00:00
namespace osu.Game.Overlays
{
2019-08-13 05:29:58 +00:00
/// <summary>
/// Handles playback of the global music track.
/// </summary>
2020-09-04 07:22:37 +00:00
public class MusicController : CompositeDrawable
2018-04-13 09:19:50 +00:00
{
2019-08-13 05:29:58 +00:00
[Resolved]
private BeatmapManager beatmaps { get ; set ; }
2018-04-13 09:19:50 +00:00
2019-10-24 04:10:17 +00:00
/// <summary>
/// Point in time after which the current track will be restarted on triggering a "previous track" action.
/// </summary>
2019-10-11 09:41:54 +00:00
private const double restart_cutoff_point = 5000 ;
2020-11-02 07:08:59 +00:00
/// <summary>
/// Whether the user has requested the track to be paused. Use <see cref="IsPlaying"/> to determine whether the track is still playing.
/// </summary>
public bool UserPauseRequested { get ; private set ; }
2019-07-09 09:32:49 +00:00
2019-08-13 05:29:58 +00:00
/// <summary>
/// Fired when the global <see cref="WorkingBeatmap"/> has changed.
/// Includes direction information for display purposes.
/// </summary>
public event Action < WorkingBeatmap , TrackChangeDirection > TrackChanged ;
2019-04-08 10:16:34 +00:00
[Resolved]
2019-08-13 05:29:58 +00:00
private IBindable < WorkingBeatmap > beatmap { get ; set ; }
2019-04-08 10:16:34 +00:00
[Resolved]
2019-04-10 08:13:12 +00:00
private IBindable < IReadOnlyList < Mod > > mods { get ; set ; }
2018-05-23 08:37:39 +00:00
2020-08-07 11:51:56 +00:00
[NotNull]
2020-08-07 10:43:16 +00:00
public DrawableTrack CurrentTrack { get ; private set ; } = new DrawableTrack ( new TrackVirtual ( 1000 ) ) ;
2020-08-04 12:53:00 +00:00
2021-11-08 08:59:23 +00:00
[Resolved]
2022-01-24 10:59:58 +00:00
private RealmAccess realm { get ; set ; }
2021-11-08 08:59:23 +00:00
2021-12-22 08:05:00 +00:00
[BackgroundDependencyLoader]
private void load ( )
2018-04-13 09:19:50 +00:00
{
2020-08-07 14:05:59 +00:00
// Todo: These binds really shouldn't be here, but are unlikely to cause any issues for now.
// They are placed here for now since some tests rely on setting the beatmap _and_ their hierarchies inside their load(), which runs before the MusicController's load().
2018-06-07 07:46:54 +00:00
beatmap . BindValueChanged ( beatmapChanged , true ) ;
2019-09-28 01:18:16 +00:00
mods . BindValueChanged ( _ = > ResetTrackAdjustments ( ) , true ) ;
2018-04-13 09:19:50 +00:00
}
2020-09-24 09:55:49 +00:00
/// <summary>
/// Forcefully reload the current <see cref="WorkingBeatmap"/>'s track from disk.
/// </summary>
2020-09-25 03:27:08 +00:00
public void ReloadCurrentTrack ( ) = > changeTrack ( ) ;
2020-09-24 09:55:49 +00:00
2019-08-13 05:38:49 +00:00
/// <summary>
2020-08-04 12:53:00 +00:00
/// Returns whether the beatmap track is playing.
2019-08-13 05:38:49 +00:00
/// </summary>
2020-08-07 11:51:56 +00:00
public bool IsPlaying = > CurrentTrack . IsRunning ;
2020-08-04 12:53:00 +00:00
/// <summary>
/// Returns whether the beatmap track is loaded.
/// </summary>
2020-08-11 03:37:00 +00:00
public bool TrackLoaded = > CurrentTrack . TrackLoaded ;
2019-08-13 05:38:49 +00:00
2019-08-13 05:29:58 +00:00
private ScheduledDelegate seekDelegate ;
2018-04-13 09:19:50 +00:00
2019-08-13 05:29:58 +00:00
public void SeekTo ( double position )
{
seekDelegate ? . Cancel ( ) ;
seekDelegate = Schedule ( ( ) = >
2018-11-02 23:04:30 +00:00
{
2019-08-13 05:29:58 +00:00
if ( ! beatmap . Disabled )
2020-08-07 11:51:56 +00:00
CurrentTrack . Seek ( position ) ;
2019-08-13 05:29:58 +00:00
} ) ;
2018-04-13 09:19:50 +00:00
}
2019-08-19 02:30:04 +00:00
/// <summary>
2020-07-10 09:03:56 +00:00
/// Ensures music is playing, no matter what, unless the user has explicitly paused.
/// This means that if the current beatmap has a virtual track (see <see cref="TrackVirtual"/>) a new beatmap will be selected.
2019-08-19 02:30:04 +00:00
/// </summary>
2020-07-10 09:03:56 +00:00
public void EnsurePlayingSomething ( )
2018-04-13 09:19:50 +00:00
{
2020-11-02 07:08:59 +00:00
if ( UserPauseRequested ) return ;
2018-04-13 09:19:50 +00:00
2020-10-09 04:11:24 +00:00
if ( CurrentTrack . IsDummyDevice | | beatmap . Value . BeatmapSetInfo . DeletePending )
2018-04-13 09:19:50 +00:00
{
2019-08-13 05:38:49 +00:00
if ( beatmap . Disabled )
2020-07-10 09:03:56 +00:00
return ;
2019-08-13 05:38:49 +00:00
2020-07-13 08:28:16 +00:00
NextTrack ( ) ;
2018-04-13 09:19:50 +00:00
}
2020-07-10 09:03:56 +00:00
else if ( ! IsPlaying )
{
Play ( ) ;
}
}
/// <summary>
/// Start playing the current track (if not already playing).
/// </summary>
2020-11-02 06:01:30 +00:00
/// <param name="restart">Whether to restart the track from the beginning.</param>
/// <param name="requestedByUser">
/// Whether the request to play was issued by the user rather than internally.
/// Specifying <c>true</c> will ensure that other methods like <see cref="EnsurePlayingSomething"/>
/// will resume music playback going forward.
/// </param>
2020-07-10 09:03:56 +00:00
/// <returns>Whether the operation was successful.</returns>
2020-11-02 06:01:30 +00:00
public bool Play ( bool restart = false , bool requestedByUser = false )
2020-07-10 09:03:56 +00:00
{
2020-11-02 06:01:30 +00:00
if ( requestedByUser )
2020-11-02 07:08:59 +00:00
UserPauseRequested = false ;
2020-07-10 09:03:56 +00:00
2019-10-10 07:52:51 +00:00
if ( restart )
2020-08-05 12:10:38 +00:00
CurrentTrack . Restart ( ) ;
2019-10-10 07:52:51 +00:00
else if ( ! IsPlaying )
2020-08-05 12:10:38 +00:00
CurrentTrack . Start ( ) ;
2019-10-10 07:52:51 +00:00
return true ;
}
/// <summary>
/// Stop playing the current track and pause at the current position.
/// </summary>
2020-10-31 15:06:53 +00:00
/// <param name="requestedByUser">
/// Whether the request to stop was issued by the user rather than internally.
/// Specifying <c>true</c> will ensure that other methods like <see cref="EnsurePlayingSomething"/>
/// will not resume music playback until the next explicit call to <see cref="Play"/>.
/// </param>
2020-11-02 05:56:50 +00:00
public void Stop ( bool requestedByUser = false )
2019-10-10 07:52:51 +00:00
{
2020-11-02 07:08:59 +00:00
UserPauseRequested | = requestedByUser ;
2020-08-07 11:51:56 +00:00
if ( CurrentTrack . IsRunning )
2020-08-05 12:10:38 +00:00
CurrentTrack . Stop ( ) ;
2019-10-10 07:52:51 +00:00
}
/// <summary>
/// Toggle pause / play.
/// </summary>
/// <returns>Whether the operation was successful.</returns>
public bool TogglePause ( )
{
2020-08-07 11:51:56 +00:00
if ( CurrentTrack . IsRunning )
2020-11-02 05:56:50 +00:00
Stop ( true ) ;
2018-04-13 09:19:50 +00:00
else
2020-11-02 06:01:30 +00:00
Play ( requestedByUser : true ) ;
2019-08-13 05:38:49 +00:00
return true ;
2018-04-13 09:19:50 +00:00
}
2019-08-13 05:29:58 +00:00
/// <summary>
2020-04-28 02:46:08 +00:00
/// Play the previous track or restart the current track if it's current time below <see cref="restart_cutoff_point"/>.
2019-08-13 05:29:58 +00:00
/// </summary>
2020-09-08 09:26:13 +00:00
/// <param name="onSuccess">Invoked when the operation has been performed successfully.</param>
public void PreviousTrack ( Action < PreviousTrackResult > onSuccess = null ) = > Schedule ( ( ) = >
2020-09-04 07:10:14 +00:00
{
PreviousTrackResult res = prev ( ) ;
if ( res ! = PreviousTrackResult . None )
onSuccess ? . Invoke ( res ) ;
} ) ;
2020-04-28 02:46:08 +00:00
/// <summary>
/// Play the previous track or restart the current track if it's current time below <see cref="restart_cutoff_point"/>.
/// </summary>
/// <returns>The <see cref="PreviousTrackResult"/> that indicate the decided action.</returns>
private PreviousTrackResult prev ( )
2018-04-13 09:19:50 +00:00
{
2020-07-13 08:28:16 +00:00
if ( beatmap . Disabled )
return PreviousTrackResult . None ;
2021-10-27 04:04:41 +00:00
double currentTrackPosition = CurrentTrack . CurrentTime ;
2019-10-10 17:12:36 +00:00
2019-10-11 09:41:54 +00:00
if ( currentTrackPosition > = restart_cutoff_point )
2019-10-10 17:12:36 +00:00
{
SeekTo ( 0 ) ;
2019-10-24 04:10:17 +00:00
return PreviousTrackResult . Restart ;
2019-10-10 17:12:36 +00:00
}
2019-08-13 05:29:58 +00:00
queuedDirection = TrackChangeDirection . Prev ;
2018-05-14 08:41:35 +00:00
2022-01-21 19:27:07 +00:00
var playableSet = getBeatmapSets ( ) . AsEnumerable ( ) . TakeWhile ( i = > ! i . Equals ( current . BeatmapSetInfo ) ) . LastOrDefault ( )
? ? getBeatmapSets ( ) . LastOrDefault ( ) ;
2019-04-01 03:16:05 +00:00
2022-01-21 19:27:07 +00:00
if ( playableSet ! = null )
2018-05-14 08:45:11 +00:00
{
2022-01-21 19:27:07 +00:00
changeBeatmap ( beatmaps . GetWorkingBeatmap ( playableSet . Beatmaps . First ( ) ) ) ;
2020-07-31 13:02:12 +00:00
restartTrack ( ) ;
2019-10-24 04:10:17 +00:00
return PreviousTrackResult . Previous ;
2018-05-14 08:45:11 +00:00
}
2019-08-13 05:38:49 +00:00
2019-10-24 04:10:17 +00:00
return PreviousTrackResult . None ;
2018-04-13 09:19:50 +00:00
}
2019-08-13 05:29:58 +00:00
/// <summary>
/// Play the next random or playlist track.
/// </summary>
2020-09-08 09:26:13 +00:00
/// <param name="onSuccess">Invoked when the operation has been performed successfully.</param>
2020-09-04 07:10:14 +00:00
/// <returns>A <see cref="ScheduledDelegate"/> of the operation.</returns>
2020-09-08 09:26:13 +00:00
public void NextTrack ( Action onSuccess = null ) = > Schedule ( ( ) = >
2020-09-04 07:10:14 +00:00
{
bool res = next ( ) ;
if ( res )
onSuccess ? . Invoke ( ) ;
} ) ;
2019-08-13 05:29:58 +00:00
2020-07-10 07:33:31 +00:00
private bool next ( )
2018-04-13 09:19:50 +00:00
{
2020-07-13 08:28:16 +00:00
if ( beatmap . Disabled )
return false ;
2020-07-10 07:33:31 +00:00
queuedDirection = TrackChangeDirection . Next ;
2018-05-14 08:41:35 +00:00
2022-01-21 19:27:07 +00:00
var playableSet = getBeatmapSets ( ) . AsEnumerable ( ) . SkipWhile ( i = > ! i . Equals ( current . BeatmapSetInfo ) ) . ElementAtOrDefault ( 1 )
? ? getBeatmapSets ( ) . FirstOrDefault ( ) ;
2022-01-11 16:18:36 +00:00
var playableBeatmap = playableSet ? . Beatmaps . FirstOrDefault ( ) ;
2019-04-01 03:16:05 +00:00
2022-01-11 12:32:01 +00:00
if ( playableBeatmap ! = null )
2018-05-14 08:45:11 +00:00
{
2022-01-11 12:32:01 +00:00
changeBeatmap ( beatmaps . GetWorkingBeatmap ( playableBeatmap ) ) ;
2020-07-31 13:02:12 +00:00
restartTrack ( ) ;
2019-08-13 05:38:49 +00:00
return true ;
2018-05-14 08:45:11 +00:00
}
2019-08-13 05:38:49 +00:00
return false ;
2018-04-13 09:19:50 +00:00
}
2020-07-31 13:02:12 +00:00
private void restartTrack ( )
{
// if not scheduled, the previously track will be stopped one frame later (see ScheduleAfterChildren logic in GameBase).
// we probably want to move this to a central method for switching to a new working beatmap in the future.
2020-08-07 11:51:56 +00:00
Schedule ( ( ) = > CurrentTrack . Restart ( ) ) ;
2020-07-31 13:02:12 +00:00
}
2018-04-13 09:19:50 +00:00
private WorkingBeatmap current ;
2019-08-13 05:29:58 +00:00
private TrackChangeDirection ? queuedDirection ;
2018-04-13 09:19:50 +00:00
2022-01-21 19:27:07 +00:00
private IQueryable < BeatmapSetInfo > getBeatmapSets ( ) = > realm . Realm . All < BeatmapSetInfo > ( ) . Where ( s = > ! s . DeletePending ) ;
2020-08-21 09:43:58 +00:00
private void beatmapChanged ( ValueChangedEvent < WorkingBeatmap > beatmap ) = > changeBeatmap ( beatmap . NewValue ) ;
private void changeBeatmap ( WorkingBeatmap newWorking )
2018-04-13 09:19:50 +00:00
{
2020-09-04 08:31:54 +00:00
// This method can potentially be triggered multiple times as it is eagerly fired in next() / prev() to ensure correct execution order
2020-09-04 08:50:49 +00:00
// (changeBeatmap must be called before consumers receive the bindable changed event, which is not the case when the local beatmap bindable is updated directly).
2020-09-04 06:45:24 +00:00
if ( newWorking = = current )
return ;
2020-08-21 09:43:58 +00:00
var lastWorking = current ;
2019-08-13 05:29:58 +00:00
TrackChangeDirection direction = TrackChangeDirection . None ;
2018-04-13 09:19:50 +00:00
2020-08-21 09:43:58 +00:00
bool audioEquals = newWorking ? . BeatmapInfo ? . AudioEquals ( current ? . BeatmapInfo ) ? ? false ;
2020-08-18 04:01:35 +00:00
2018-04-13 09:19:50 +00:00
if ( current ! = null )
{
if ( audioEquals )
2019-08-13 05:29:58 +00:00
direction = TrackChangeDirection . None ;
2018-04-13 09:19:50 +00:00
else if ( queuedDirection . HasValue )
{
direction = queuedDirection . Value ;
queuedDirection = null ;
}
else
{
2020-05-05 01:31:11 +00:00
// figure out the best direction based on order in playlist.
2022-01-21 19:27:07 +00:00
int last = getBeatmapSets ( ) . AsEnumerable ( ) . TakeWhile ( b = > ! b . Equals ( current . BeatmapSetInfo ) ) . Count ( ) ;
int next = newWorking = = null ? - 1 : getBeatmapSets ( ) . AsEnumerable ( ) . TakeWhile ( b = > ! b . Equals ( newWorking . BeatmapSetInfo ) ) . Count ( ) ;
2018-04-13 09:19:50 +00:00
2019-08-13 05:29:58 +00:00
direction = last > next ? TrackChangeDirection . Prev : TrackChangeDirection . Next ;
2018-04-13 09:19:50 +00:00
}
}
2020-08-21 09:43:58 +00:00
current = newWorking ;
2020-08-04 12:53:00 +00:00
2020-08-18 04:01:35 +00:00
if ( ! audioEquals | | CurrentTrack . IsDummyDevice )
{
2020-08-05 12:30:11 +00:00
changeTrack ( ) ;
2020-08-18 04:01:35 +00:00
}
else
{
// transfer still valid track to new working beatmap
2020-08-21 09:43:58 +00:00
current . TransferTrack ( lastWorking . Track ) ;
2020-08-18 04:01:35 +00:00
}
2020-08-05 12:30:11 +00:00
TrackChanged ? . Invoke ( current , direction ) ;
ResetTrackAdjustments ( ) ;
queuedDirection = null ;
2020-08-21 09:43:58 +00:00
// this will be a noop if coming from the beatmapChanged event.
// the exception is local operations like next/prev, where we want to complete loading the track before sending out a change.
if ( beatmap . Value ! = current & & beatmap is Bindable < WorkingBeatmap > working )
working . Value = current ;
2020-08-05 12:30:11 +00:00
}
private void changeTrack ( )
{
2021-12-24 09:38:17 +00:00
var queuedTrack = getQueuedTrack ( ) ;
2020-08-11 16:33:06 +00:00
2021-12-24 09:38:17 +00:00
var lastTrack = CurrentTrack ;
2020-08-22 10:44:54 +00:00
CurrentTrack = queuedTrack ;
2020-08-11 16:33:06 +00:00
// At this point we may potentially be in an async context from tests. This is extremely dangerous but we have to make do for now.
// CurrentTrack is immediately updated above for situations where a immediate knowledge about the new track is required,
// but the mutation of the hierarchy is scheduled to avoid exceptions.
Schedule ( ( ) = >
{
2020-09-02 11:04:56 +00:00
lastTrack . VolumeTo ( 0 , 500 , Easing . Out ) . Expire ( ) ;
2020-08-11 16:33:06 +00:00
2020-08-22 10:44:54 +00:00
if ( queuedTrack = = CurrentTrack )
2020-09-02 11:04:56 +00:00
{
2020-08-22 10:44:54 +00:00
AddInternal ( queuedTrack ) ;
2020-09-02 11:04:56 +00:00
queuedTrack . VolumeTo ( 0 ) . Then ( ) . VolumeTo ( 1 , 300 , Easing . Out ) ;
}
2020-08-11 16:33:06 +00:00
else
{
2020-08-22 10:44:54 +00:00
// If the track has changed since the call to changeTrack, it is safe to dispose the
// queued track rather than consume it.
queuedTrack . Dispose ( ) ;
2020-08-11 16:33:06 +00:00
}
} ) ;
2018-04-13 09:19:50 +00:00
}
2021-12-24 09:38:17 +00:00
private DrawableTrack getQueuedTrack ( )
{
// Important to keep this in its own method to avoid inadvertently capturing unnecessary variables in the callback.
// Can lead to leaks.
var queuedTrack = new DrawableTrack ( current . LoadTrack ( ) ) ;
queuedTrack . Completed + = ( ) = > onTrackCompleted ( current ) ;
return queuedTrack ;
}
2020-08-05 12:21:08 +00:00
private void onTrackCompleted ( WorkingBeatmap workingBeatmap )
{
// the source of track completion is the audio thread, so the beatmap may have changed before firing.
if ( current ! = workingBeatmap )
return ;
if ( ! CurrentTrack . Looping & & ! beatmap . Disabled )
NextTrack ( ) ;
}
2021-07-29 07:39:26 +00:00
private bool allowTrackAdjustments ;
2019-11-15 04:47:14 +00:00
/// <summary>
2021-07-29 07:39:26 +00:00
/// Whether mod track adjustments are allowed to be applied.
2019-11-15 04:47:14 +00:00
/// </summary>
2021-07-29 07:39:26 +00:00
public bool AllowTrackAdjustments
2019-11-15 04:47:14 +00:00
{
2021-07-29 07:39:26 +00:00
get = > allowTrackAdjustments ;
2019-11-15 04:47:14 +00:00
set
{
2021-07-29 07:39:26 +00:00
if ( allowTrackAdjustments = = value )
2019-11-15 04:47:14 +00:00
return ;
2021-07-29 07:39:26 +00:00
allowTrackAdjustments = value ;
2019-11-15 04:47:14 +00:00
ResetTrackAdjustments ( ) ;
}
}
2020-09-02 06:23:50 +00:00
/// <summary>
2021-07-29 07:39:26 +00:00
/// Resets the adjustments currently applied on <see cref="CurrentTrack"/> and applies the mod adjustments if <see cref="AllowTrackAdjustments"/> is <c>true</c>.
2020-09-02 06:23:50 +00:00
/// </summary>
/// <remarks>
2021-07-29 07:39:26 +00:00
/// Does not reset any adjustments applied directly to the beatmap track.
2020-09-02 06:23:50 +00:00
/// </remarks>
2019-09-28 01:18:16 +00:00
public void ResetTrackAdjustments ( )
2019-04-08 10:16:34 +00:00
{
2021-07-29 07:39:26 +00:00
CurrentTrack . RemoveAllAdjustments ( AdjustableProperty . Balance ) ;
CurrentTrack . RemoveAllAdjustments ( AdjustableProperty . Frequency ) ;
CurrentTrack . RemoveAllAdjustments ( AdjustableProperty . Tempo ) ;
CurrentTrack . RemoveAllAdjustments ( AdjustableProperty . Volume ) ;
2019-04-08 10:16:34 +00:00
2021-07-29 07:39:26 +00:00
if ( allowTrackAdjustments )
2019-11-15 04:47:14 +00:00
{
2019-12-09 08:34:04 +00:00
foreach ( var mod in mods . Value . OfType < IApplicableToTrack > ( ) )
2020-08-05 12:10:38 +00:00
mod . ApplyToTrack ( CurrentTrack ) ;
2019-11-15 04:47:14 +00:00
}
2019-04-08 10:16:34 +00:00
}
2019-08-13 05:29:58 +00:00
}
2019-06-20 14:40:25 +00:00
2019-08-13 05:29:58 +00:00
public enum TrackChangeDirection
{
None ,
Next ,
Prev
2018-04-13 09:19:50 +00:00
}
2019-10-11 09:41:54 +00:00
2019-10-24 04:10:17 +00:00
public enum PreviousTrackResult
2019-10-11 09:41:54 +00:00
{
None ,
Restart ,
Previous
}
2018-04-13 09:19:50 +00:00
}