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 ;
2024-09-18 19:51:45 +08:00
using System.Diagnostics ;
2018-04-13 17:19:50 +08:00
using System.Linq ;
using osu.Framework.Allocation ;
2020-09-02 19:04:56 +08:00
using osu.Framework.Audio ;
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 ;
2022-06-27 17:41:27 +08:00
using osu.Framework.Logging ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Threading ;
2024-09-18 19:51:45 +08:00
using osu.Framework.Utils ;
2024-06-22 00:36:30 +08:00
using osu.Game.Audio.Effects ;
2018-04-13 17:19:50 +08:00
using osu.Game.Beatmaps ;
2024-09-18 19:51:45 +08:00
using osu.Game.Configuration ;
2021-11-08 16:59:23 +08:00
using osu.Game.Database ;
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>
2022-11-24 13:32:20 +08:00
public partial class MusicController : CompositeDrawable
2018-04-13 17:19:50 +08:00
{
2019-08-13 13:29:58 +08:00
[Resolved]
2024-07-05 10:55:27 +08:00
private BeatmapManager beatmaps { get ; set ; } = null ! ;
2018-04-13 17:19:50 +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 ;
2020-11-02 15:08:59 +08: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 17:32:49 +08:00
2023-07-25 19:00:18 +08:00
/// <summary>
2023-07-30 12:52:58 +08:00
/// Whether user control of the global track should be allowed.
2023-07-25 19:00:18 +08:00
/// </summary>
public readonly BindableBool AllowTrackControl = new BindableBool ( true ) ;
2024-09-18 19:51:45 +08:00
public readonly BindableBool Shuffle = new BindableBool ( true ) ;
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>
2024-07-05 10:55:27 +08:00
public event Action < WorkingBeatmap , TrackChangeDirection > ? TrackChanged ;
2019-08-13 13:29:58 +08:00
2019-04-08 18:16:34 +08:00
[Resolved]
2024-07-05 10:55:27 +08:00
private IBindable < WorkingBeatmap > beatmap { get ; set ; } = null ! ;
2019-04-08 18:16:34 +08:00
[Resolved]
2024-07-05 10:55:27 +08:00
private IBindable < IReadOnlyList < Mod > > mods { get ; set ; } = null ! ;
2018-05-23 16:37:39 +08:00
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
2024-10-09 17:47:31 +08:00
[Resolved]
private RealmAccess realm { get ; set ; } = null ! ;
2024-10-09 05:08:18 +08:00
2024-08-19 13:46:36 +08:00
private BindableNumber < double > sampleVolume = null ! ;
2024-06-22 00:36:30 +08:00
private readonly BindableDouble audioDuckVolume = new BindableDouble ( 1 ) ;
2024-07-05 11:58:18 +08:00
2024-07-05 17:12:40 +08:00
private AudioFilter audioDuckFilter = null ! ;
2024-06-22 00:36:30 +08:00
2024-09-18 19:51:45 +08:00
private readonly Bindable < RandomSelectAlgorithm > randomSelectAlgorithm = new Bindable < RandomSelectAlgorithm > ( ) ;
2024-10-09 17:52:58 +08:00
private readonly List < Live < BeatmapSetInfo > > previousRandomSets = new List < Live < BeatmapSetInfo > > ( ) ;
2024-09-18 19:51:45 +08:00
private int randomHistoryDirection ;
2024-10-11 18:40:45 +08:00
private int lastRandomTrackDirection ;
2024-09-18 19:51:45 +08:00
2024-06-22 00:36:30 +08:00
[BackgroundDependencyLoader]
2024-10-09 17:47:31 +08:00
private void load ( AudioManager audio , OsuConfigManager configManager )
2024-06-22 00:36:30 +08:00
{
AddInternal ( audioDuckFilter = new AudioFilter ( audio . TrackMixer ) ) ;
audio . Tracks . AddAdjustment ( AdjustableProperty . Volume , audioDuckVolume ) ;
2024-08-19 13:46:36 +08:00
sampleVolume = audio . VolumeSample . GetBoundCopy ( ) ;
2024-09-18 19:51:45 +08:00
configManager . BindWith ( OsuSetting . RandomSelectAlgorithm , randomSelectAlgorithm ) ;
2024-06-22 00:36:30 +08:00
}
2022-09-26 14:42:37 +08:00
protected override void LoadComplete ( )
2018-04-13 17:19:50 +08:00
{
2022-09-26 14:42:37 +08:00
base . LoadComplete ( ) ;
2024-07-05 10:55:27 +08:00
beatmap . BindValueChanged ( b = >
{
if ( b . NewValue ! = null )
changeBeatmap ( b . NewValue ) ;
} , true ) ;
2019-09-28 09:18:16 +08:00
mods . BindValueChanged ( _ = > ResetTrackAdjustments ( ) , true ) ;
2018-04-13 17:19:50 +08:00
}
2020-09-24 17:55:49 +08:00
/// <summary>
/// Forcefully reload the current <see cref="WorkingBeatmap"/>'s track from disk.
/// </summary>
2022-08-01 15:30:45 +08:00
public void ReloadCurrentTrack ( )
{
2024-07-05 10:55:27 +08:00
if ( current = = null )
return ;
2022-08-01 15:30:45 +08:00
changeTrack ( ) ;
TrackChanged ? . Invoke ( current , TrackChangeDirection . None ) ;
}
2020-09-24 17:55:49 +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
2024-07-05 10:55:27 +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
{
2024-08-23 17:21:31 +08:00
if ( ! AllowTrackControl . Value )
2023-07-25 19:00:18 +08:00
return ;
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-11-02 15:08:59 +08:00
if ( UserPauseRequested ) return ;
2018-04-13 17:19:50 +08:00
2020-10-09 12:11:24 +08:00
if ( CurrentTrack . IsDummyDevice | | beatmap . Value . BeatmapSetInfo . DeletePending )
2018-04-13 17:19:50 +08:00
{
2023-07-25 19:00:18 +08:00
if ( beatmap . Disabled | | ! AllowTrackControl . Value )
2020-07-10 17:03:56 +08:00
return ;
2019-08-13 13:38:49 +08:00
2022-06-27 19:40:02 +08:00
Logger . Log ( $"{nameof(MusicController)} skipping next track to {nameof(EnsurePlayingSomething)}" ) ;
2024-07-17 18:02:42 +08:00
NextTrack ( allowProtectedTracks : true ) ;
2018-04-13 17:19:50 +08:00
}
2020-07-10 17:03:56 +08:00
else if ( ! IsPlaying )
{
2022-06-27 17:41:27 +08:00
Logger . Log ( $"{nameof(MusicController)} starting playback to {nameof(EnsurePlayingSomething)}" ) ;
2020-07-10 17:03:56 +08:00
Play ( ) ;
}
}
/// <summary>
/// Start playing the current track (if not already playing).
/// </summary>
2020-11-02 14:01:30 +08: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 17:03:56 +08:00
/// <returns>Whether the operation was successful.</returns>
2020-11-02 14:01:30 +08:00
public bool Play ( bool restart = false , bool requestedByUser = false )
2020-07-10 17:03:56 +08:00
{
2023-07-25 19:00:18 +08:00
if ( requestedByUser & & ! AllowTrackControl . Value )
return false ;
2020-11-02 14:01:30 +08:00
if ( requestedByUser )
2020-11-02 15:08:59 +08:00
UserPauseRequested = false ;
2020-07-10 17:03:56 +08:00
2019-10-10 15:52:51 +08:00
if ( restart )
2022-07-06 15:32:53 +08:00
CurrentTrack . RestartAsync ( ) ;
2019-10-10 15:52:51 +08:00
else if ( ! IsPlaying )
2022-07-06 15:32:53 +08:00
CurrentTrack . StartAsync ( ) ;
2019-10-10 15:52:51 +08:00
return true ;
}
/// <summary>
/// Stop playing the current track and pause at the current position.
/// </summary>
2020-10-31 23:06:53 +08: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 13:56:50 +08:00
public void Stop ( bool requestedByUser = false )
2019-10-10 15:52:51 +08:00
{
2023-07-25 19:00:18 +08:00
if ( requestedByUser & & ! AllowTrackControl . Value )
return ;
2020-11-02 15:08:59 +08:00
UserPauseRequested | = requestedByUser ;
2020-08-07 19:51:56 +08:00
if ( CurrentTrack . IsRunning )
2022-07-06 15:32:53 +08:00
CurrentTrack . StopAsync ( ) ;
2019-10-10 15:52:51 +08:00
}
/// <summary>
/// Toggle pause / play.
/// </summary>
/// <returns>Whether the operation was successful.</returns>
public bool TogglePause ( )
{
2023-07-25 19:00:18 +08:00
if ( ! AllowTrackControl . Value )
return false ;
2020-08-07 19:51:56 +08:00
if ( CurrentTrack . IsRunning )
2020-11-02 13:56:50 +08:00
Stop ( true ) ;
2018-04-13 17:19:50 +08:00
else
2020-11-02 14:01:30 +08:00
Play ( requestedByUser : true ) ;
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-09-08 17:26:13 +08:00
/// <param name="onSuccess">Invoked when the operation has been performed successfully.</param>
2024-07-17 18:02:42 +08:00
/// <param name="allowProtectedTracks">Whether to include <see cref="BeatmapSetInfo.Protected"/> beatmap sets when navigating.</param>
public void PreviousTrack ( Action < PreviousTrackResult > ? onSuccess = null , bool allowProtectedTracks = false ) = > Schedule ( ( ) = >
2020-09-04 15:10:14 +08:00
{
2024-07-17 18:02:42 +08:00
PreviousTrackResult res = prev ( allowProtectedTracks ) ;
2020-09-04 15:10:14 +08:00
if ( res ! = PreviousTrackResult . None )
onSuccess ? . Invoke ( res ) ;
} ) ;
2020-04-28 10:46:08 +08:00
/// <summary>
/// Play the previous track or restart the current track if it's current time below <see cref="restart_cutoff_point"/>.
/// </summary>
2024-07-17 18:02:42 +08:00
/// <param name="allowProtectedTracks">Whether to include <see cref="BeatmapSetInfo.Protected"/> beatmap sets when navigating.</param>
2020-04-28 10:46:08 +08:00
/// <returns>The <see cref="PreviousTrackResult"/> that indicate the decided action.</returns>
2024-07-17 18:02:42 +08:00
private PreviousTrackResult prev ( bool allowProtectedTracks )
2018-04-13 17:19:50 +08:00
{
2023-07-25 19:00:18 +08:00
if ( beatmap . Disabled | | ! AllowTrackControl . Value )
2020-07-13 16:28:16 +08:00
return PreviousTrackResult . None ;
2021-10-27 12:04:41 +08:00
double 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
2024-10-09 17:52:58 +08:00
Live < BeatmapSetInfo > ? playableSet ;
2024-09-18 19:51:45 +08:00
if ( Shuffle . Value )
playableSet = getNextRandom ( - 1 , allowProtectedTracks ) ;
else
{
2024-10-09 17:52:58 +08:00
playableSet = getBeatmapSets ( ) . TakeWhile ( i = > ! i . Value . Equals ( current ? . BeatmapSetInfo ) ) . LastOrDefault ( s = > ! s . Value . Protected | | allowProtectedTracks )
? ? getBeatmapSets ( ) . LastOrDefault ( s = > ! s . Value . Protected | | allowProtectedTracks ) ;
2024-09-18 19:51:45 +08:00
}
2019-04-01 11:16:05 +08:00
2022-01-22 03:27:07 +08:00
if ( playableSet ! = null )
2018-05-14 16:45:11 +08:00
{
2024-10-09 17:52:58 +08:00
changeBeatmap ( beatmaps . GetWorkingBeatmap ( playableSet . Value . Beatmaps . First ( ) ) ) ;
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-09-08 17:26:13 +08:00
/// <param name="onSuccess">Invoked when the operation has been performed successfully.</param>
2024-07-17 18:02:42 +08:00
/// <param name="allowProtectedTracks">Whether to include <see cref="BeatmapSetInfo.Protected"/> beatmap sets when navigating.</param>
2020-09-04 15:10:14 +08:00
/// <returns>A <see cref="ScheduledDelegate"/> of the operation.</returns>
2024-07-17 18:02:42 +08:00
public void NextTrack ( Action ? onSuccess = null , bool allowProtectedTracks = false ) = > Schedule ( ( ) = >
2020-09-04 15:10:14 +08:00
{
2024-07-17 18:02:42 +08:00
bool res = next ( allowProtectedTracks ) ;
2020-09-04 15:10:14 +08:00
if ( res )
onSuccess ? . Invoke ( ) ;
} ) ;
2019-08-13 13:29:58 +08:00
2024-07-05 17:12:40 +08:00
private readonly List < DuckParameters > duckOperations = new List < DuckParameters > ( ) ;
2024-06-22 00:36:30 +08:00
/// <summary>
2024-07-05 17:12:40 +08:00
/// Applies ducking, attenuating the volume and/or low-pass cutoff of the currently playing track to make headroom for effects (or just to apply an effect).
2024-06-22 00:36:30 +08:00
/// </summary>
2024-07-05 17:12:40 +08:00
/// <returns>A <see cref="IDisposable"/> which will restore the duck operation when disposed.</returns>
public IDisposable Duck ( DuckParameters ? parameters = null )
2024-06-22 00:36:30 +08:00
{
2024-08-19 13:46:36 +08:00
// Don't duck if samples have no volume, it sounds weird.
if ( sampleVolume . Value = = 0 )
return new InvokeOnDisposal ( ( ) = > { } ) ;
2024-07-05 13:41:50 +08:00
parameters ? ? = new DuckParameters ( ) ;
2024-07-05 17:12:40 +08:00
duckOperations . Add ( parameters ) ;
2024-07-04 13:23:35 +08:00
2024-07-05 17:12:40 +08:00
DuckParameters volumeOperation = duckOperations . MinBy ( p = > p . DuckVolumeTo ) ! ;
DuckParameters lowPassOperation = duckOperations . MinBy ( p = > p . DuckCutoffTo ) ! ;
2024-07-03 12:47:41 +08:00
2024-07-05 17:12:40 +08:00
audioDuckFilter . CutoffTo ( lowPassOperation . DuckCutoffTo , lowPassOperation . DuckDuration , lowPassOperation . DuckEasing ) ;
this . TransformBindableTo ( audioDuckVolume , volumeOperation . DuckVolumeTo , volumeOperation . DuckDuration , volumeOperation . DuckEasing ) ;
2024-06-22 00:36:30 +08:00
2024-07-05 13:41:50 +08:00
return new InvokeOnDisposal ( restoreDucking ) ;
2024-07-05 17:12:40 +08:00
void restoreDucking ( ) = > Schedule ( ( ) = >
2024-07-05 13:41:50 +08:00
{
2024-07-05 17:36:40 +08:00
if ( ! duckOperations . Remove ( parameters ) )
return ;
2024-07-05 13:41:50 +08:00
2024-07-05 17:12:40 +08:00
DuckParameters ? restoreVolumeOperation = duckOperations . MinBy ( p = > p . DuckVolumeTo ) ;
DuckParameters ? restoreLowPassOperation = duckOperations . MinBy ( p = > p . DuckCutoffTo ) ;
2024-07-05 13:41:50 +08:00
2024-07-05 17:12:40 +08:00
// If another duck operation is in the list, restore ducking to its level, else reset back to defaults.
audioDuckFilter . CutoffTo ( restoreLowPassOperation ? . DuckCutoffTo ? ? AudioFilter . MAX_LOWPASS_CUTOFF , parameters . RestoreDuration , parameters . RestoreEasing ) ;
this . TransformBindableTo ( audioDuckVolume , restoreVolumeOperation ? . DuckVolumeTo ? ? 1 , parameters . RestoreDuration , parameters . RestoreEasing ) ;
} ) ;
2024-06-22 00:36:30 +08:00
}
/// <summary>
2024-07-05 13:41:50 +08:00
/// A convenience method that ducks the currently playing track, then after a delay, restores automatically.
2024-06-22 00:36:30 +08:00
/// </summary>
2024-07-05 13:41:50 +08:00
/// <param name="delayUntilRestore">A delay in milliseconds which defines how long to delay restoration after ducking completes.</param>
/// <param name="parameters">Parameters defining the ducking operation.</param>
public void DuckMomentarily ( double delayUntilRestore , DuckParameters ? parameters = null )
2024-06-22 00:36:30 +08:00
{
2024-08-19 13:46:36 +08:00
// Don't duck if samples have no volume, it sounds weird.
if ( sampleVolume . Value = = 0 )
return ;
2024-07-05 13:41:50 +08:00
parameters ? ? = new DuckParameters ( ) ;
2024-07-04 13:23:35 +08:00
2024-07-05 17:12:40 +08:00
IDisposable duckOperation = Duck ( parameters ) ;
2024-07-04 13:23:35 +08:00
2024-07-05 13:41:50 +08:00
Scheduler . AddDelayed ( ( ) = > duckOperation . Dispose ( ) , delayUntilRestore ) ;
2024-06-22 00:36:30 +08:00
}
2024-07-17 18:02:42 +08:00
private bool next ( bool allowProtectedTracks )
2018-04-13 17:19:50 +08:00
{
2023-07-25 19:00:18 +08:00
if ( beatmap . Disabled | | ! AllowTrackControl . Value )
2020-07-13 16:28:16 +08:00
return false ;
2020-07-10 15:33:31 +08:00
queuedDirection = TrackChangeDirection . Next ;
2018-05-14 16:41:35 +08:00
2024-10-09 17:52:58 +08:00
Live < BeatmapSetInfo > ? playableSet ;
2024-09-18 19:51:45 +08:00
if ( Shuffle . Value )
playableSet = getNextRandom ( 1 , allowProtectedTracks ) ;
else
{
2024-10-09 17:52:58 +08:00
playableSet = getBeatmapSets ( ) . SkipWhile ( i = > ! i . Value . Equals ( current ? . BeatmapSetInfo ) )
. Where ( i = > ! i . Value . Protected | | allowProtectedTracks )
2024-10-01 16:19:59 +08:00
. ElementAtOrDefault ( 1 )
2024-10-09 17:52:58 +08:00
? ? getBeatmapSets ( ) . FirstOrDefault ( i = > ! i . Value . Protected | | allowProtectedTracks ) ;
2024-09-18 19:51:45 +08:00
}
2022-01-22 03:27:07 +08:00
2024-10-09 17:52:58 +08:00
var playableBeatmap = playableSet ? . Value . Beatmaps . FirstOrDefault ( ) ;
2019-04-01 11:16:05 +08:00
2022-01-11 20:32:01 +08:00
if ( playableBeatmap ! = null )
2018-05-14 16:45:11 +08:00
{
2022-01-11 20:32:01 +08:00
changeBeatmap ( beatmaps . GetWorkingBeatmap ( playableBeatmap ) ) ;
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
}
2024-10-09 17:52:58 +08:00
private Live < BeatmapSetInfo > ? getNextRandom ( int direction , bool allowProtectedTracks )
2024-09-18 19:51:45 +08:00
{
2024-10-11 18:40:45 +08:00
try
2024-09-18 19:51:45 +08:00
{
2024-10-11 18:40:45 +08:00
Live < BeatmapSetInfo > result ;
2024-09-18 19:51:45 +08:00
2024-10-14 21:28:32 +08:00
var possibleSets = getBeatmapSets ( ) . Where ( s = > ! s . Value . Protected | | allowProtectedTracks ) . ToList ( ) ;
2024-09-18 19:51:45 +08:00
2024-10-14 21:28:32 +08:00
if ( possibleSets . Count = = 0 )
2024-10-11 18:40:45 +08:00
return null ;
2024-09-18 19:51:45 +08:00
2024-10-14 21:28:32 +08:00
// if there is only one possible set left, play it, even if it is the same as the current track.
// looping is preferable over playing nothing.
if ( possibleSets . Count = = 1 )
return possibleSets . Single ( ) ;
// now that we actually know there is a choice, do not allow the current track to be played again.
possibleSets . RemoveAll ( s = > s . Value . Equals ( current ? . BeatmapSetInfo ) ) ;
2024-10-11 18:40:45 +08:00
// condition below checks if the signs of `randomHistoryDirection` and `direction` are opposite and not zero.
// if that is the case, it means that the user had previously chosen next track `randomHistoryDirection` times and wants to go back,
// or that the user had previously chosen previous track `randomHistoryDirection` times and wants to go forward.
// in both cases, it means that we have a history of previous random selections that we can rewind.
if ( randomHistoryDirection * direction < 0 )
{
Debug . Assert ( Math . Abs ( randomHistoryDirection ) = = previousRandomSets . Count ) ;
// if the user has been shuffling backwards and now going forwards (or vice versa),
// the topmost item from history needs to be discarded because it's the *current* track.
if ( direction * lastRandomTrackDirection < 0 )
2024-09-18 19:51:45 +08:00
{
2024-10-11 18:40:45 +08:00
previousRandomSets . RemoveAt ( previousRandomSets . Count - 1 ) ;
randomHistoryDirection + = direction ;
2024-09-18 19:51:45 +08:00
}
2024-10-11 18:45:14 +08:00
if ( previousRandomSets . Count > 0 )
{
result = previousRandomSets [ ^ 1 ] ;
previousRandomSets . RemoveAt ( previousRandomSets . Count - 1 ) ;
return result ;
}
2024-10-11 18:40:45 +08:00
}
// if the early-return above didn't cover it, it means that we have no history to fall back on
// and need to actually choose something random.
switch ( randomSelectAlgorithm . Value )
{
case RandomSelectAlgorithm . Random :
2024-10-14 21:28:32 +08:00
result = possibleSets [ RNG . Next ( possibleSets . Count ) ] ;
2024-10-11 18:40:45 +08:00
break ;
case RandomSelectAlgorithm . RandomPermutation :
2024-10-14 21:28:32 +08:00
var notYetPlayedSets = possibleSets . Except ( previousRandomSets ) . ToList ( ) ;
2024-10-11 18:40:45 +08:00
2024-10-14 21:28:32 +08:00
if ( notYetPlayedSets . Count = = 0 )
2024-10-11 18:40:45 +08:00
{
notYetPlayedSets = possibleSets ;
previousRandomSets . Clear ( ) ;
randomHistoryDirection = 0 ;
}
2024-10-14 21:28:32 +08:00
result = notYetPlayedSets [ RNG . Next ( notYetPlayedSets . Count ) ] ;
2024-10-11 18:40:45 +08:00
break ;
default :
throw new ArgumentOutOfRangeException ( nameof ( randomSelectAlgorithm ) , randomSelectAlgorithm . Value , "Unsupported random select algorithm" ) ;
}
previousRandomSets . Add ( result ) ;
return result ;
}
finally
{
randomHistoryDirection + = direction ;
lastRandomTrackDirection = direction ;
2024-09-18 19:51:45 +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.
2022-07-06 15:32:53 +08:00
Schedule ( ( ) = > CurrentTrack . RestartAsync ( ) ) ;
2020-07-31 21:02:12 +08:00
}
2024-07-05 10:55:27 +08:00
private WorkingBeatmap ? current ;
2019-08-13 13:29:58 +08:00
private TrackChangeDirection ? queuedDirection ;
2018-04-13 17:19:50 +08:00
2024-10-09 17:52:58 +08:00
private IEnumerable < Live < BeatmapSetInfo > > getBeatmapSets ( ) = > realm . Realm . All < BeatmapSetInfo > ( ) . Where ( s = > ! s . DeletePending )
. AsEnumerable ( )
. Select ( s = > new RealmLive < BeatmapSetInfo > ( s , realm ) ) ;
2022-01-22 03:27:07 +08:00
2020-08-21 17:43:58 +08:00
private void changeBeatmap ( WorkingBeatmap newWorking )
2018-04-13 17:19:50 +08:00
{
2020-09-04 16:31:54 +08: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 16:50:49 +08: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 14:45:24 +08:00
if ( newWorking = = current )
return ;
2020-08-21 17:43:58 +08:00
var lastWorking = current ;
2019-08-13 13:29:58 +08:00
TrackChangeDirection direction = TrackChangeDirection . None ;
2018-04-13 17:19:50 +08:00
2024-07-05 10:55:27 +08:00
bool audioEquals = newWorking . BeatmapInfo ? . AudioEquals ( current ? . BeatmapInfo ) = = true ;
2020-08-18 12:01:35 +08:00
2018-04-13 17:19:50 +08:00
if ( current ! = null )
{
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.
2024-10-09 17:52:58 +08:00
int last = getBeatmapSets ( ) . TakeWhile ( b = > ! b . Value . Equals ( current . BeatmapSetInfo ) ) . Count ( ) ;
int next = getBeatmapSets ( ) . TakeWhile ( b = > ! b . Value . Equals ( newWorking . BeatmapSetInfo ) ) . 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
}
}
2020-08-21 17:43:58 +08:00
current = newWorking ;
2020-08-04 20:53:00 +08:00
2022-05-20 19:43:07 +08:00
if ( lastWorking = = null | | ! lastWorking . TryTransferTrack ( current ) )
2020-08-05 20:30:11 +08:00
changeTrack ( ) ;
TrackChanged ? . Invoke ( current , direction ) ;
ResetTrackAdjustments ( ) ;
queuedDirection = null ;
2020-08-21 17:43:58 +08: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 20:30:11 +08:00
}
private void changeTrack ( )
{
2021-12-24 17:38:17 +08:00
var queuedTrack = getQueuedTrack ( ) ;
2020-08-12 00:33:06 +08:00
2021-12-24 17:38:17 +08:00
var lastTrack = CurrentTrack ;
2023-06-20 17:35:51 +08:00
lastTrack . Completed - = onTrackCompleted ;
2020-08-22 18:44:54 +08:00
CurrentTrack = queuedTrack ;
2020-08-12 00:33:06 +08: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 19:04:56 +08:00
lastTrack . VolumeTo ( 0 , 500 , Easing . Out ) . Expire ( ) ;
2020-08-12 00:33:06 +08:00
2020-08-22 18:44:54 +08:00
if ( queuedTrack = = CurrentTrack )
2020-09-02 19:04:56 +08:00
{
2020-08-22 18:44:54 +08:00
AddInternal ( queuedTrack ) ;
2020-09-02 19:04:56 +08:00
queuedTrack . VolumeTo ( 0 ) . Then ( ) . VolumeTo ( 1 , 300 , Easing . Out ) ;
}
2020-08-12 00:33:06 +08:00
else
{
2020-08-22 18:44:54 +08: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-12 00:33:06 +08:00
}
} ) ;
2018-04-13 17:19:50 +08:00
}
2021-12-24 17:38:17 +08: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.
2024-07-05 10:55:27 +08:00
var queuedTrack = new DrawableTrack ( current ! . LoadTrack ( ) ) ;
2023-06-20 17:35:51 +08:00
queuedTrack . Completed + = onTrackCompleted ;
2021-12-24 17:38:17 +08:00
return queuedTrack ;
}
2023-06-20 17:35:51 +08:00
private void onTrackCompleted ( )
2020-08-05 20:21:08 +08:00
{
2023-07-25 19:00:18 +08:00
if ( ! CurrentTrack . Looping & & ! beatmap . Disabled & & AllowTrackControl . Value )
2024-07-17 18:02:42 +08:00
NextTrack ( allowProtectedTracks : true ) ;
2020-08-05 20:21:08 +08:00
}
2023-07-25 18:58:23 +08:00
private bool applyModTrackAdjustments ;
2019-11-15 12:47:14 +08:00
/// <summary>
2021-07-29 15:39:26 +08:00
/// Whether mod track adjustments are allowed to be applied.
2019-11-15 12:47:14 +08:00
/// </summary>
2023-07-25 18:58:23 +08:00
public bool ApplyModTrackAdjustments
2019-11-15 12:47:14 +08:00
{
2023-07-25 18:58:23 +08:00
get = > applyModTrackAdjustments ;
2019-11-15 12:47:14 +08:00
set
{
2023-07-25 18:58:23 +08:00
if ( applyModTrackAdjustments = = value )
2019-11-15 12:47:14 +08:00
return ;
2023-07-25 18:58:23 +08:00
applyModTrackAdjustments = value ;
2019-11-15 12:47:14 +08:00
ResetTrackAdjustments ( ) ;
}
}
2024-07-05 10:55:27 +08:00
private AudioAdjustments ? modTrackAdjustments ;
2022-05-10 23:02:32 +08:00
2020-09-02 14:23:50 +08:00
/// <summary>
2023-07-25 18:58:23 +08:00
/// Resets the adjustments currently applied on <see cref="CurrentTrack"/> and applies the mod adjustments if <see cref="ApplyModTrackAdjustments"/> is <c>true</c>.
2020-09-02 14:23:50 +08:00
/// </summary>
/// <remarks>
2021-07-29 15:39:26 +08:00
/// Does not reset any adjustments applied directly to the beatmap track.
2020-09-02 14:23:50 +08:00
/// </remarks>
2019-09-28 09:18:16 +08:00
public void ResetTrackAdjustments ( )
2019-04-08 18:16:34 +08:00
{
2022-05-10 23:02:32 +08:00
// todo: we probably want a helper method rather than this.
2021-07-29 15:39:26 +08:00
CurrentTrack . RemoveAllAdjustments ( AdjustableProperty . Balance ) ;
CurrentTrack . RemoveAllAdjustments ( AdjustableProperty . Frequency ) ;
CurrentTrack . RemoveAllAdjustments ( AdjustableProperty . Tempo ) ;
CurrentTrack . RemoveAllAdjustments ( AdjustableProperty . Volume ) ;
2019-04-08 18:16:34 +08:00
2023-07-25 18:58:23 +08:00
if ( applyModTrackAdjustments )
2019-11-15 12:47:14 +08:00
{
2022-05-11 01:46:31 +08:00
CurrentTrack . BindAdjustments ( modTrackAdjustments = new AudioAdjustments ( ) ) ;
2022-05-10 23:02:32 +08:00
2022-05-11 01:49:15 +08:00
foreach ( var mod in mods . Value . OfType < IApplicableToTrack > ( ) )
2022-05-10 23:02:32 +08:00
mod . ApplyToTrack ( modTrackAdjustments ) ;
2019-11-15 12:47:14 +08:00
}
2019-04-08 18:16:34 +08:00
}
2019-08-13 13:29:58 +08:00
}
2019-06-20 22:40:25 +08:00
2024-07-05 17:12:40 +08:00
public class DuckParameters
2024-07-05 13:41:50 +08:00
{
/// <summary>
/// The duration of the ducking transition in milliseconds.
2024-07-05 17:38:24 +08:00
/// Defaults to 100 ms.
2024-07-05 13:41:50 +08:00
/// </summary>
2024-07-05 17:38:24 +08:00
public double DuckDuration = 100 ;
2024-07-05 13:41:50 +08:00
/// <summary>
/// The final volume which should be reached during ducking, when 0 is silent and 1 is original volume.
/// Defaults to 25%.
/// </summary>
2024-07-08 12:47:04 +08:00
public double DuckVolumeTo = 0.25 ;
2024-07-05 13:41:50 +08:00
/// <summary>
2024-07-05 17:12:40 +08:00
/// The low-pass cutoff frequency which should be reached during ducking. If not required, set to <see cref="AudioFilter.MAX_LOWPASS_CUTOFF"/>.
2024-07-05 13:41:50 +08:00
/// Defaults to 300 Hz.
/// </summary>
2024-07-05 17:12:40 +08:00
public int DuckCutoffTo = 300 ;
2024-07-05 13:41:50 +08:00
/// <summary>
/// The easing curve to be applied during ducking.
/// Defaults to <see cref="Easing.Out"/>.
/// </summary>
public Easing DuckEasing = Easing . Out ;
/// <summary>
/// The duration of the restoration transition in milliseconds.
/// Defaults to 500 ms.
/// </summary>
public double RestoreDuration = 500 ;
/// <summary>
/// The easing curve to be applied during restoration.
/// Defaults to <see cref="Easing.In"/>.
/// </summary>
public Easing RestoreEasing = Easing . In ;
}
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
}