Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shuffle playback order in global playlist by default #29917

Merged
merged 5 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public void TestMusicNavigationActions()
{
Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null!;

AddStep("disable shuffle", () => Game.MusicController.Shuffle.Value = false);

// ensure we have at least two beatmaps available to identify the direction the music controller navigated to.
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5);

Expand Down
83 changes: 80 additions & 3 deletions osu.Game/Overlays/MusicController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
Expand All @@ -13,8 +14,10 @@
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Audio.Effects;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Rulesets.Mods;

Expand Down Expand Up @@ -43,6 +46,8 @@ public partial class MusicController : CompositeDrawable
/// </summary>
public readonly BindableBool AllowTrackControl = new BindableBool(true);

public readonly BindableBool Shuffle = new BindableBool(true);

/// <summary>
/// Fired when the global <see cref="WorkingBeatmap"/> has changed.
/// Includes direction information for display purposes.
Expand All @@ -66,12 +71,18 @@ public partial class MusicController : CompositeDrawable

private AudioFilter audioDuckFilter = null!;

private readonly Bindable<RandomSelectAlgorithm> randomSelectAlgorithm = new Bindable<RandomSelectAlgorithm>();
private readonly List<BeatmapSetInfo> previousRandomSets = new List<BeatmapSetInfo>();
private int randomHistoryDirection;

[BackgroundDependencyLoader]
private void load(AudioManager audio)
private void load(AudioManager audio, OsuConfigManager configManager)
{
AddInternal(audioDuckFilter = new AudioFilter(audio.TrackMixer));
audio.Tracks.AddAdjustment(AdjustableProperty.Volume, audioDuckVolume);
sampleVolume = audio.VolumeSample.GetBoundCopy();

configManager.BindWith(OsuSetting.RandomSelectAlgorithm, randomSelectAlgorithm);
}

protected override void LoadComplete()
Expand Down Expand Up @@ -238,8 +249,15 @@ private PreviousTrackResult prev(bool allowProtectedTracks)

queuedDirection = TrackChangeDirection.Prev;

var playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Protected || allowProtectedTracks)
BeatmapSetInfo? playableSet;

if (Shuffle.Value)
playableSet = getNextRandom(-1, allowProtectedTracks);
else
{
playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Protected || allowProtectedTracks)
?? getBeatmapSets().AsEnumerable().LastOrDefault(s => !s.Protected || allowProtectedTracks);
}

if (playableSet != null)
{
Expand Down Expand Up @@ -327,8 +345,15 @@ private bool next(bool allowProtectedTracks)

queuedDirection = TrackChangeDirection.Next;

var playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo) || (i.Protected && !allowProtectedTracks)).ElementAtOrDefault(1)
BeatmapSetInfo? playableSet;

if (Shuffle.Value)
playableSet = getNextRandom(1, allowProtectedTracks);
else
{
playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo) || (i.Protected && !allowProtectedTracks)).ElementAtOrDefault(1)
?? getBeatmapSets().AsEnumerable().FirstOrDefault(i => !i.Protected || allowProtectedTracks);
}

var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault();

Expand All @@ -342,6 +367,58 @@ private bool next(bool allowProtectedTracks)
return false;
}

private BeatmapSetInfo? getNextRandom(int direction, bool allowProtectedTracks)
{
BeatmapSetInfo result;

var possibleSets = getBeatmapSets().AsEnumerable().Where(s => !s.Protected || allowProtectedTracks).ToArray();

if (possibleSets.Length == 0)
return null;

// 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);
result = previousRandomSets[^1];
previousRandomSets.RemoveAt(previousRandomSets.Count - 1);
randomHistoryDirection += direction;
return result;
}

// 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:
result = possibleSets[RNG.Next(possibleSets.Length)];
break;

case RandomSelectAlgorithm.RandomPermutation:
var notYetPlayedSets = possibleSets.Except(previousRandomSets).ToArray();

if (notYetPlayedSets.Length == 0)
{
notYetPlayedSets = possibleSets;
previousRandomSets.Clear();
randomHistoryDirection = 0;
}

result = notYetPlayedSets[RNG.Next(notYetPlayedSets.Length)];
break;

default:
throw new ArgumentOutOfRangeException(nameof(randomSelectAlgorithm), randomSelectAlgorithm.Value, "Unsupported random select algorithm");
}

previousRandomSets.Add(result);
randomHistoryDirection += direction;
return result;
}

private void restartTrack()
{
// if not scheduled, the previously track will be stopped one frame later (see ScheduleAfterChildren logic in GameBase).
Expand Down
12 changes: 12 additions & 0 deletions osu.Game/Overlays/NowPlayingOverlay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public partial class NowPlayingOverlay : OsuFocusedOverlayContainer, INamedOverl
private IconButton prevButton = null!;
private IconButton playButton = null!;
private IconButton nextButton = null!;
private MusicIconButton shuffleButton = null!;
private IconButton playlistButton = null!;

private ScrollingTextContainer title = null!, artist = null!;
Expand All @@ -69,6 +70,7 @@ public partial class NowPlayingOverlay : OsuFocusedOverlayContainer, INamedOverl
private OsuColour colours { get; set; } = null!;

private Bindable<bool> allowTrackControl = null!;
private readonly BindableBool shuffle = new BindableBool(true);

public NowPlayingOverlay()
{
Expand Down Expand Up @@ -162,6 +164,13 @@ private void load()
Action = () => musicController.NextTrack(),
Icon = FontAwesome.Solid.StepForward,
},
shuffleButton = new MusicIconButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Action = shuffle.Toggle,
Icon = FontAwesome.Solid.Random,
}
}
},
playlistButton = new MusicIconButton
Expand Down Expand Up @@ -227,6 +236,9 @@ protected override void LoadComplete()
allowTrackControl = musicController.AllowTrackControl.GetBoundCopy();
allowTrackControl.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledStates), true);

shuffle.BindTo(musicController.Shuffle);
shuffle.BindValueChanged(s => shuffleButton.FadeColour(s.NewValue ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true);

musicController.TrackChanged += trackChanged;
trackChanged(beatmap.Value);
}
Expand Down
Loading