osu/osu.Game/Overlays/MusicController.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

413 lines
15 KiB
C#
Raw Normal View History

// 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.
2018-04-13 09:19:50 +00:00
2016-10-24 05:00:06 +00:00
using System;
using System.Collections.Generic;
using System.Linq;
2020-08-07 11:51:56 +00:00
using JetBrains.Annotations;
2016-11-10 22:40:42 +00:00
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
2019-02-21 10:04:31 +00:00
using osu.Framework.Bindables;
2016-10-24 02:50:49 +00:00
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
2021-11-08 08:59:23 +00:00
using osu.Game.Database;
using osu.Game.Rulesets.Mods;
2018-04-13 09:19:50 +00:00
namespace osu.Game.Overlays
{
/// <summary>
/// Handles playback of the global music track.
/// </summary>
public class MusicController : CompositeDrawable
{
[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;
/// <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; }
/// <summary>
/// Fired when the global <see cref="WorkingBeatmap"/> has changed.
/// Includes direction information for display purposes.
/// </summary>
public event Action<WorkingBeatmap, TrackChangeDirection> TrackChanged;
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; }
[Resolved]
private IBindable<IReadOnlyList<Mod>> mods { get; set; }
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));
2021-11-08 08:59:23 +00:00
[Resolved]
private RealmAccess realm { get; set; }
2021-11-08 08:59:23 +00:00
[BackgroundDependencyLoader]
private void load()
{
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);
mods.BindValueChanged(_ => ResetTrackAdjustments(), true);
2016-11-05 16:46:09 +00:00
}
2018-04-13 09:19:50 +00:00
/// <summary>
/// Forcefully reload the current <see cref="WorkingBeatmap"/>'s track from disk.
/// </summary>
public void ReloadCurrentTrack() => changeTrack();
2019-08-13 05:38:49 +00:00
/// <summary>
/// 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;
/// <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
private ScheduledDelegate seekDelegate;
2018-04-13 09:19:50 +00:00
public void SeekTo(double position)
{
seekDelegate?.Cancel();
seekDelegate = Schedule(() =>
2018-11-02 23:04:30 +00:00
{
if (!beatmap.Disabled)
2020-08-07 11:51:56 +00:00
CurrentTrack.Seek(position);
});
2016-10-24 09:23:30 +00:00
}
2018-04-13 09:19:50 +00:00
/// <summary>
/// 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.
/// </summary>
public void EnsurePlayingSomething()
2016-11-11 02:35:58 +00:00
{
if (UserPauseRequested) return;
2018-04-13 09:19:50 +00:00
if (CurrentTrack.IsDummyDevice || beatmap.Value.BeatmapSetInfo.DeletePending)
2017-05-01 04:41:22 +00:00
{
2019-08-13 05:38:49 +00:00
if (beatmap.Disabled)
return;
2019-08-13 05:38:49 +00:00
NextTrack();
2017-05-01 04:41:22 +00:00
}
else if (!IsPlaying)
{
Play();
}
}
/// <summary>
/// Start playing the current track (if not already playing).
/// </summary>
/// <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>
/// <returns>Whether the operation was successful.</returns>
public bool Play(bool restart = false, bool requestedByUser = false)
{
if (requestedByUser)
UserPauseRequested = false;
if (restart)
2020-08-05 12:10:38 +00:00
CurrentTrack.Restart();
else if (!IsPlaying)
2020-08-05 12:10:38 +00:00
CurrentTrack.Start();
return true;
}
/// <summary>
/// Stop playing the current track and pause at the current position.
/// </summary>
/// <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>
public void Stop(bool requestedByUser = false)
{
UserPauseRequested |= requestedByUser;
2020-08-07 11:51:56 +00:00
if (CurrentTrack.IsRunning)
2020-08-05 12:10:38 +00:00
CurrentTrack.Stop();
}
/// <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)
Stop(true);
2017-05-01 04:41:22 +00:00
else
Play(requestedByUser: true);
2019-08-13 05:38:49 +00:00
return true;
2016-10-28 12:44:59 +00:00
}
2018-04-13 09:19:50 +00:00
/// <summary>
/// Play the previous track or restart the current track if it's current time below <see cref="restart_cutoff_point"/>.
/// </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(() =>
{
PreviousTrackResult res = prev();
if (res != PreviousTrackResult.None)
onSuccess?.Invoke(res);
});
/// <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()
2016-10-28 12:08:27 +00:00
{
if (beatmap.Disabled)
return PreviousTrackResult.None;
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
}
queuedDirection = TrackChangeDirection.Prev;
var playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current.BeatmapSetInfo)).LastOrDefault()
?? getBeatmapSets().LastOrDefault();
2019-04-01 03:16:05 +00:00
if (playableSet != null)
2018-05-14 08:45:11 +00:00
{
changeBeatmap(beatmaps.GetWorkingBeatmap(playableSet.Beatmaps.First()));
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;
2016-10-24 07:34:44 +00:00
}
2018-04-13 09:19:50 +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>
/// <returns>A <see cref="ScheduledDelegate"/> of the operation.</returns>
2020-09-08 09:26:13 +00:00
public void NextTrack(Action onSuccess = null) => Schedule(() =>
{
bool res = next();
if (res)
onSuccess?.Invoke();
});
private bool next()
2016-10-24 07:34:44 +00:00
{
if (beatmap.Disabled)
return false;
queuedDirection = TrackChangeDirection.Next;
var playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current.BeatmapSetInfo)).ElementAtOrDefault(1)
?? getBeatmapSets().FirstOrDefault();
var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault();
2019-04-01 03:16:05 +00:00
if (playableBeatmap != null)
2018-05-14 08:45:11 +00:00
{
changeBeatmap(beatmaps.GetWorkingBeatmap(playableBeatmap));
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;
2016-10-24 07:34:44 +00:00
}
2018-04-13 09:19:50 +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());
}
2017-05-01 04:41:22 +00:00
private WorkingBeatmap current;
private TrackChangeDirection? queuedDirection;
2018-04-13 09:19:50 +00:00
private IQueryable<BeatmapSetInfo> getBeatmapSets() => realm.Realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending);
private void beatmapChanged(ValueChangedEvent<WorkingBeatmap> beatmap) => changeBeatmap(beatmap.NewValue);
private void changeBeatmap(WorkingBeatmap newWorking)
2017-05-01 04:41:22 +00:00
{
// This method can potentially be triggered multiple times as it is eagerly fired in next() / prev() to ensure correct execution order
// (changeBeatmap must be called before consumers receive the bindable changed event, which is not the case when the local beatmap bindable is updated directly).
if (newWorking == current)
return;
var lastWorking = current;
TrackChangeDirection direction = TrackChangeDirection.None;
2018-04-13 09:19:50 +00:00
2022-05-20 11:44:07 +00:00
bool audioEquals = newWorking?.BeatmapInfo?.AudioEquals(current?.BeatmapInfo) == true;
2020-08-18 04:01:35 +00:00
if (current != null)
{
if (audioEquals)
direction = TrackChangeDirection.None;
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.
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
direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next;
}
}
2018-04-13 09:19:50 +00:00
current = newWorking;
if (lastWorking == null || !lastWorking.TryTransferTrack(current))
changeTrack();
TrackChanged?.Invoke(current, direction);
ResetTrackAdjustments();
queuedDirection = null;
// 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;
}
private void changeTrack()
{
var queuedTrack = getQueuedTrack();
var lastTrack = CurrentTrack;
CurrentTrack = queuedTrack;
// 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.VolumeTo(0, 500, Easing.Out).Expire();
if (queuedTrack == CurrentTrack)
{
AddInternal(queuedTrack);
queuedTrack.VolumeTo(0).Then().VolumeTo(1, 300, Easing.Out);
}
else
{
// If the track has changed since the call to changeTrack, it is safe to dispose the
// queued track rather than consume it.
queuedTrack.Dispose();
}
});
2017-05-01 04:41:22 +00:00
}
2018-04-13 09:19:50 +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();
}
private bool allowTrackAdjustments;
/// <summary>
/// Whether mod track adjustments are allowed to be applied.
/// </summary>
public bool AllowTrackAdjustments
{
get => allowTrackAdjustments;
set
{
if (allowTrackAdjustments == value)
return;
allowTrackAdjustments = value;
ResetTrackAdjustments();
}
}
private AudioAdjustments modTrackAdjustments;
/// <summary>
/// Resets the adjustments currently applied on <see cref="CurrentTrack"/> and applies the mod adjustments if <see cref="AllowTrackAdjustments"/> is <c>true</c>.
/// </summary>
/// <remarks>
/// Does not reset any adjustments applied directly to the beatmap track.
/// </remarks>
public void ResetTrackAdjustments()
{
// todo: we probably want a helper method rather than this.
CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Balance);
CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Frequency);
CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Tempo);
CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Volume);
if (allowTrackAdjustments)
{
CurrentTrack.BindAdjustments(modTrackAdjustments = new AudioAdjustments());
foreach (var mod in mods.Value.OfType<IApplicableToTrack>())
mod.ApplyToTrack(modTrackAdjustments);
}
}
}
public enum TrackChangeDirection
{
None,
Next,
Prev
}
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
}
}