2019-08-13 13:46:57 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2019-01-24 16:43:03 +08:00
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
using System ;
2018-05-14 16:41:35 +08:00
using System.Collections.Generic ;
2018-04-13 17:19:50 +08:00
using System.Linq ;
2020-08-07 19:51:56 +08:00
using JetBrains.Annotations ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Allocation ;
2020-07-10 15:33:31 +08:00
using osu.Framework.Audio.Track ;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics ;
2020-08-04 20:53:00 +08:00
using osu.Framework.Graphics.Audio ;
using osu.Framework.Graphics.Containers ;
2019-08-13 13:38:49 +08:00
using osu.Framework.Input.Bindings ;
2020-01-09 12:43:44 +08:00
using osu.Framework.Utils ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Threading ;
using osu.Game.Beatmaps ;
2019-08-13 13:38:49 +08:00
using osu.Game.Input.Bindings ;
using osu.Game.Overlays.OSD ;
2019-04-08 18:16:34 +08:00
using osu.Game.Rulesets.Mods ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Overlays
{
2019-08-13 13:29:58 +08:00
/// <summary>
/// Handles playback of the global music track.
/// </summary>
2020-08-05 20:10:38 +08:00
public class MusicController : CompositeDrawable , IKeyBindingHandler < GlobalAction >
2018-04-13 17:19:50 +08:00
{
2019-08-13 13:29:58 +08:00
[Resolved]
private BeatmapManager beatmaps { get ; set ; }
2018-04-13 17:19:50 +08:00
2020-02-17 15:59:35 +08:00
public IBindableList < BeatmapSetInfo > BeatmapSets
{
get
{
if ( LoadState < LoadState . Ready )
throw new InvalidOperationException ( $"{nameof(BeatmapSets)} should not be accessed before the music controller is loaded." ) ;
return beatmapSets ;
}
}
2019-09-18 12:14:33 +08:00
2019-10-24 12:10:17 +08:00
/// <summary>
/// Point in time after which the current track will be restarted on triggering a "previous track" action.
/// </summary>
2019-10-11 17:41:54 +08:00
private const double restart_cutoff_point = 5000 ;
2019-09-18 12:14:33 +08:00
private readonly BindableList < BeatmapSetInfo > beatmapSets = new BindableList < BeatmapSetInfo > ( ) ;
2018-04-13 17:19:50 +08:00
2019-07-10 23:18:19 +08:00
public bool IsUserPaused { get ; private set ; }
2019-07-09 17:32:49 +08:00
2019-08-13 13:29:58 +08: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 18:16:34 +08:00
[Resolved]
2019-08-13 13:29:58 +08:00
private IBindable < WorkingBeatmap > beatmap { get ; set ; }
2019-04-08 18:16:34 +08:00
[Resolved]
2019-04-10 16:13:12 +08:00
private IBindable < IReadOnlyList < Mod > > mods { get ; set ; }
2018-05-23 16:37:39 +08:00
2019-08-13 13:38:49 +08:00
[Resolved(canBeNull: true)]
private OnScreenDisplay onScreenDisplay { get ; set ; }
2020-08-07 19:51:56 +08:00
[NotNull]
2020-08-07 18:43:16 +08:00
public DrawableTrack CurrentTrack { get ; private set ; } = new DrawableTrack ( new TrackVirtual ( 1000 ) ) ;
2020-08-04 20:53:00 +08:00
2020-05-27 15:08:47 +08:00
private IBindable < WeakReference < BeatmapSetInfo > > managerUpdated ;
2020-05-19 15:44:22 +08:00
private IBindable < WeakReference < BeatmapSetInfo > > managerRemoved ;
2018-04-13 17:19:50 +08:00
[BackgroundDependencyLoader]
2019-08-13 13:29:58 +08:00
private void load ( )
2018-04-13 17:19:50 +08:00
{
2020-05-27 15:08:47 +08:00
managerUpdated = beatmaps . ItemUpdated . GetBoundCopy ( ) ;
managerUpdated . BindValueChanged ( beatmapUpdated ) ;
2020-05-19 15:44:22 +08:00
managerRemoved = beatmaps . ItemRemoved . GetBoundCopy ( ) ;
managerRemoved . BindValueChanged ( beatmapRemoved ) ;
2020-02-17 15:59:35 +08:00
2020-07-10 15:33:31 +08:00
beatmapSets . AddRange ( beatmaps . GetAllUsableBeatmapSets ( IncludedDetails . Minimal , true ) . OrderBy ( _ = > RNG . Next ( ) ) ) ;
2020-02-17 15:59:35 +08:00
2020-08-07 22:05:59 +08: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 15:46:54 +08:00
beatmap . BindValueChanged ( beatmapChanged , true ) ;
2019-09-28 09:18:16 +08:00
mods . BindValueChanged ( _ = > ResetTrackAdjustments ( ) , true ) ;
2018-04-13 17:19:50 +08:00
}
2019-08-13 13:29:58 +08:00
/// <summary>
/// Change the position of a <see cref="BeatmapSetInfo"/> in the current playlist.
/// </summary>
/// <param name="beatmapSetInfo">The beatmap to move.</param>
/// <param name="index">The new position.</param>
public void ChangeBeatmapSetPosition ( BeatmapSetInfo beatmapSetInfo , int index )
2018-04-13 17:19:50 +08:00
{
2019-09-18 12:14:33 +08:00
beatmapSets . Remove ( beatmapSetInfo ) ;
beatmapSets . Insert ( index , beatmapSetInfo ) ;
2018-04-13 17:19:50 +08:00
}
2019-08-13 13:38:49 +08:00
/// <summary>
2020-08-04 20:53:00 +08:00
/// Returns whether the beatmap track is playing.
2019-08-13 13:38:49 +08:00
/// </summary>
2020-08-07 19:51:56 +08:00
public bool IsPlaying = > CurrentTrack . IsRunning ;
2020-08-04 20:53:00 +08:00
/// <summary>
/// Returns whether the beatmap track is loaded.
/// </summary>
2020-08-11 11:37:00 +08:00
public bool TrackLoaded = > CurrentTrack . TrackLoaded ;
2019-08-13 13:38:49 +08:00
2020-05-27 15:08:47 +08:00
private void beatmapUpdated ( ValueChangedEvent < WeakReference < BeatmapSetInfo > > weakSet )
2020-02-17 15:59:35 +08:00
{
2020-05-19 15:44:22 +08:00
if ( weakSet . NewValue . TryGetTarget ( out var set ) )
{
Schedule ( ( ) = >
{
2020-05-27 15:08:47 +08:00
beatmapSets . Remove ( set ) ;
beatmapSets . Add ( set ) ;
2020-05-19 15:44:22 +08:00
} ) ;
}
}
2019-06-26 12:18:03 +08:00
2020-05-19 15:44:22 +08:00
private void beatmapRemoved ( ValueChangedEvent < WeakReference < BeatmapSetInfo > > weakSet )
2020-02-17 15:59:35 +08:00
{
2020-05-19 15:44:22 +08:00
if ( weakSet . NewValue . TryGetTarget ( out var set ) )
{
Schedule ( ( ) = >
{
beatmapSets . RemoveAll ( s = > s . ID = = set . ID ) ;
} ) ;
}
}
2018-04-13 17:19:50 +08:00
2019-08-13 13:29:58 +08:00
private ScheduledDelegate seekDelegate ;
2018-04-13 17:19:50 +08:00
2019-08-13 13:29:58 +08:00
public void SeekTo ( double position )
{
seekDelegate ? . Cancel ( ) ;
seekDelegate = Schedule ( ( ) = >
2018-11-03 07:04:30 +08:00
{
2019-08-13 13:29:58 +08:00
if ( ! beatmap . Disabled )
2020-08-07 19:51:56 +08:00
CurrentTrack . Seek ( position ) ;
2019-08-13 13:29:58 +08:00
} ) ;
2018-04-13 17:19:50 +08:00
}
2019-08-19 10:30:04 +08:00
/// <summary>
2020-07-10 17:03:56 +08: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 10:30:04 +08:00
/// </summary>
2020-07-10 17:03:56 +08:00
public void EnsurePlayingSomething ( )
2018-04-13 17:19:50 +08:00
{
2020-07-10 17:03:56 +08:00
if ( IsUserPaused ) return ;
2018-04-13 17:19:50 +08:00
2020-08-07 19:51:56 +08:00
if ( CurrentTrack . IsDummyDevice )
2018-04-13 17:19:50 +08:00
{
2019-08-13 13:38:49 +08:00
if ( beatmap . Disabled )
2020-07-10 17:03:56 +08:00
return ;
2019-08-13 13:38:49 +08:00
2020-07-13 16:28:16 +08:00
NextTrack ( ) ;
2018-04-13 17:19:50 +08:00
}
2020-07-10 17:03:56 +08:00
else if ( ! IsPlaying )
{
Play ( ) ;
}
}
/// <summary>
/// Start playing the current track (if not already playing).
/// </summary>
/// <returns>Whether the operation was successful.</returns>
public bool Play ( bool restart = false )
{
IsUserPaused = false ;
2019-10-10 15:52:51 +08:00
if ( restart )
2020-08-05 20:10:38 +08:00
CurrentTrack . Restart ( ) ;
2019-10-10 15:52:51 +08:00
else if ( ! IsPlaying )
2020-08-05 20:10:38 +08:00
CurrentTrack . Start ( ) ;
2019-10-10 15:52:51 +08:00
return true ;
}
/// <summary>
/// Stop playing the current track and pause at the current position.
/// </summary>
public void Stop ( )
{
2019-10-10 19:12:47 +08:00
IsUserPaused = true ;
2020-08-07 19:51:56 +08:00
if ( CurrentTrack . IsRunning )
2020-08-05 20:10:38 +08:00
CurrentTrack . Stop ( ) ;
2019-10-10 15:52:51 +08:00
}
/// <summary>
/// Toggle pause / play.
/// </summary>
/// <returns>Whether the operation was successful.</returns>
public bool TogglePause ( )
{
2020-08-07 19:51:56 +08:00
if ( CurrentTrack . IsRunning )
2019-10-10 15:52:51 +08:00
Stop ( ) ;
2018-04-13 17:19:50 +08:00
else
2019-10-10 15:52:51 +08:00
Play ( ) ;
2019-08-13 13:38:49 +08:00
return true ;
2018-04-13 17:19:50 +08:00
}
2019-08-13 13:29:58 +08:00
/// <summary>
2020-04-28 10:46:08 +08:00
/// Play the previous track or restart the current track if it's current time below <see cref="restart_cutoff_point"/>.
2019-08-13 13:29:58 +08:00
/// </summary>
2020-04-28 10:46:08 +08:00
public void PreviousTrack ( ) = > Schedule ( ( ) = > prev ( ) ) ;
/// <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 17:19:50 +08:00
{
2020-07-13 16:28:16 +08:00
if ( beatmap . Disabled )
return PreviousTrackResult . None ;
2020-08-07 19:51:56 +08:00
var currentTrackPosition = CurrentTrack . CurrentTime ;
2019-10-11 01:12:36 +08:00
2019-10-11 17:41:54 +08:00
if ( currentTrackPosition > = restart_cutoff_point )
2019-10-11 01:12:36 +08:00
{
SeekTo ( 0 ) ;
2019-10-24 12:10:17 +08:00
return PreviousTrackResult . Restart ;
2019-10-11 01:12:36 +08:00
}
2019-08-13 13:29:58 +08:00
queuedDirection = TrackChangeDirection . Prev ;
2018-05-14 16:41:35 +08:00
2019-09-17 22:08:37 +08:00
var playable = BeatmapSets . TakeWhile ( i = > i . ID ! = current . BeatmapSetInfo . ID ) . LastOrDefault ( ) ? ? BeatmapSets . LastOrDefault ( ) ;
2019-04-01 11:16:05 +08:00
2018-05-14 16:41:35 +08:00
if ( playable ! = null )
2018-05-14 16:45:11 +08:00
{
2019-08-13 13:29:58 +08:00
if ( beatmap is Bindable < WorkingBeatmap > working )
working . Value = beatmaps . GetWorkingBeatmap ( playable . Beatmaps . First ( ) , beatmap . Value ) ;
2019-08-13 13:38:49 +08:00
2020-07-31 21:02:12 +08:00
restartTrack ( ) ;
2019-10-24 12:10:17 +08:00
return PreviousTrackResult . Previous ;
2018-05-14 16:45:11 +08:00
}
2019-08-13 13:38:49 +08:00
2019-10-24 12:10:17 +08:00
return PreviousTrackResult . None ;
2018-04-13 17:19:50 +08:00
}
2019-08-13 13:29:58 +08:00
/// <summary>
/// Play the next random or playlist track.
/// </summary>
2020-04-28 10:46:08 +08:00
public void NextTrack ( ) = > Schedule ( ( ) = > next ( ) ) ;
2019-08-13 13:29:58 +08:00
2020-07-10 15:33:31 +08:00
private bool next ( )
2018-04-13 17:19:50 +08:00
{
2020-07-13 16:28:16 +08:00
if ( beatmap . Disabled )
return false ;
2020-07-10 15:33:31 +08:00
queuedDirection = TrackChangeDirection . Next ;
2018-05-14 16:41:35 +08:00
2020-02-01 01:32:47 +08:00
var playable = BeatmapSets . SkipWhile ( i = > i . ID ! = current . BeatmapSetInfo . ID ) . ElementAtOrDefault ( 1 ) ? ? BeatmapSets . FirstOrDefault ( ) ;
2019-04-01 11:16:05 +08:00
2018-05-14 16:41:35 +08:00
if ( playable ! = null )
2018-05-14 16:45:11 +08:00
{
2019-08-13 13:29:58 +08:00
if ( beatmap is Bindable < WorkingBeatmap > working )
working . Value = beatmaps . GetWorkingBeatmap ( playable . Beatmaps . First ( ) , beatmap . Value ) ;
2020-07-31 19:33:18 +08:00
2020-07-31 21:02:12 +08:00
restartTrack ( ) ;
2019-08-13 13:38:49 +08:00
return true ;
2018-05-14 16:45:11 +08:00
}
2019-08-13 13:38:49 +08:00
return false ;
2018-04-13 17:19:50 +08:00
}
2020-07-31 21:02:12 +08: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 19:51:56 +08:00
Schedule ( ( ) = > CurrentTrack . Restart ( ) ) ;
2020-07-31 21:02:12 +08:00
}
2018-04-13 17:19:50 +08:00
private WorkingBeatmap current ;
2019-08-13 13:29:58 +08:00
private TrackChangeDirection ? queuedDirection ;
2018-04-13 17:19:50 +08:00
2019-02-25 18:29:09 +08:00
private void beatmapChanged ( ValueChangedEvent < WorkingBeatmap > beatmap )
2018-04-13 17:19:50 +08:00
{
2019-08-13 13:29:58 +08:00
TrackChangeDirection direction = TrackChangeDirection . None ;
2018-04-13 17:19:50 +08:00
if ( current ! = null )
{
2019-02-25 18:29:09 +08:00
bool audioEquals = beatmap . NewValue ? . BeatmapInfo ? . AudioEquals ( current . BeatmapInfo ) ? ? false ;
2018-04-13 17:19:50 +08:00
if ( audioEquals )
2019-08-13 13:29:58 +08:00
direction = TrackChangeDirection . None ;
2018-04-13 17:19:50 +08:00
else if ( queuedDirection . HasValue )
{
direction = queuedDirection . Value ;
queuedDirection = null ;
}
else
{
2020-05-05 09:31:11 +08:00
// figure out the best direction based on order in playlist.
2019-09-17 22:08:37 +08:00
var last = BeatmapSets . TakeWhile ( b = > b . ID ! = current . BeatmapSetInfo ? . ID ) . Count ( ) ;
var next = beatmap . NewValue = = null ? - 1 : BeatmapSets . TakeWhile ( b = > b . ID ! = beatmap . NewValue . BeatmapSetInfo ? . ID ) . Count ( ) ;
2018-04-13 17:19:50 +08:00
2019-08-13 13:29:58 +08:00
direction = last > next ? TrackChangeDirection . Prev : TrackChangeDirection . Next ;
2018-04-13 17:19:50 +08:00
}
}
2019-08-13 13:29:58 +08:00
current = beatmap . NewValue ;
2020-08-04 20:53:00 +08:00
2020-08-07 19:51:56 +08:00
if ( CurrentTrack . IsDummyDevice | | ! beatmap . OldValue . BeatmapInfo . AudioEquals ( current ? . BeatmapInfo ) )
2020-08-05 20:30:11 +08:00
changeTrack ( ) ;
TrackChanged ? . Invoke ( current , direction ) ;
ResetTrackAdjustments ( ) ;
queuedDirection = null ;
}
private void changeTrack ( )
{
2020-08-12 00:33:06 +08:00
var lastTrack = CurrentTrack ;
2020-08-07 23:58:04 +08:00
2020-08-17 14:38:16 +08:00
var newTrack = new DrawableTrack ( current . LoadTrack ( ) ) ;
2020-08-12 00:33:06 +08:00
newTrack . Completed + = ( ) = > onTrackCompleted ( current ) ;
CurrentTrack = newTrack ;
// 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 ( ( ) = >
{
lastTrack . Expire ( ) ;
if ( newTrack = = CurrentTrack )
AddInternal ( newTrack ) ;
else
{
// If the track has changed via changeTrack() being called multiple times in a single update, force disposal on the old track.
newTrack . Dispose ( ) ;
}
} ) ;
2018-04-13 17:19:50 +08:00
}
2020-08-05 20:21:08 +08: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 ( ) ;
}
2019-11-15 12:47:14 +08:00
private bool allowRateAdjustments ;
/// <summary>
/// Whether mod rate adjustments are allowed to be applied.
/// </summary>
public bool AllowRateAdjustments
{
get = > allowRateAdjustments ;
set
{
if ( allowRateAdjustments = = value )
return ;
allowRateAdjustments = value ;
ResetTrackAdjustments ( ) ;
}
}
2019-09-28 09:18:16 +08:00
public void ResetTrackAdjustments ( )
2019-04-08 18:16:34 +08:00
{
2020-08-05 20:10:38 +08:00
CurrentTrack . ResetSpeedAdjustments ( ) ;
2019-04-08 18:16:34 +08:00
2019-11-15 12:47:14 +08:00
if ( allowRateAdjustments )
{
2019-12-09 16:34:04 +08:00
foreach ( var mod in mods . Value . OfType < IApplicableToTrack > ( ) )
2020-08-05 20:10:38 +08:00
mod . ApplyToTrack ( CurrentTrack ) ;
2019-11-15 12:47:14 +08:00
}
2019-04-08 18:16:34 +08:00
}
2019-08-13 13:38:49 +08:00
public bool OnPressed ( GlobalAction action )
{
2019-08-14 14:19:21 +08:00
if ( beatmap . Disabled )
return false ;
2019-08-13 13:38:49 +08:00
switch ( action )
{
case GlobalAction . MusicPlay :
if ( TogglePause ( ) )
onScreenDisplay ? . Display ( new MusicControllerToast ( IsPlaying ? "Play track" : "Pause track" ) ) ;
return true ;
case GlobalAction . MusicNext :
2020-04-28 10:46:08 +08:00
if ( next ( ) )
2019-08-13 13:38:49 +08:00
onScreenDisplay ? . Display ( new MusicControllerToast ( "Next track" ) ) ;
return true ;
case GlobalAction . MusicPrev :
2020-04-28 10:46:08 +08:00
switch ( prev ( ) )
2019-10-16 21:11:25 +08:00
{
2019-10-24 12:10:17 +08:00
case PreviousTrackResult . Restart :
2019-10-16 21:11:25 +08:00
onScreenDisplay ? . Display ( new MusicControllerToast ( "Restart track" ) ) ;
2019-10-24 09:00:45 +08:00
break ;
2019-10-16 21:11:25 +08:00
2019-10-24 12:10:17 +08:00
case PreviousTrackResult . Previous :
2019-10-16 21:11:25 +08:00
onScreenDisplay ? . Display ( new MusicControllerToast ( "Previous track" ) ) ;
2019-10-24 09:00:45 +08:00
break ;
2019-10-16 21:11:25 +08:00
}
2019-10-24 09:00:45 +08:00
return true ;
2019-08-13 13:38:49 +08:00
}
return false ;
}
2020-01-22 12:22:34 +08:00
public void OnReleased ( GlobalAction action )
{
}
2019-08-13 13:38:49 +08:00
public class MusicControllerToast : Toast
{
public MusicControllerToast ( string action )
: base ( "Music Playback" , action , string . Empty )
{
}
}
2019-08-13 13:29:58 +08:00
}
2019-06-20 22:40:25 +08:00
2019-08-13 13:29:58 +08:00
public enum TrackChangeDirection
{
None ,
Next ,
Prev
2018-04-13 17:19:50 +08:00
}
2019-10-11 17:41:54 +08:00
2019-10-24 12:10:17 +08:00
public enum PreviousTrackResult
2019-10-11 17:41:54 +08:00
{
None ,
Restart ,
Previous
}
2018-04-13 17:19:50 +08:00
}