From c4f2c66fd69f214a6c329a50d902302ec3fb7eee Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 2 Apr 2026 17:12:25 +0900 Subject: [PATCH] Implement background music for ranked play (#37166) Using a bespoke music loop, fades in during the intro: https://github.com/user-attachments/assets/8db7e30c-2a7c-4fd0-8385-fa77a6e0a506 Ducks when hovering cards for previews, with debouncing to smooth it: https://github.com/user-attachments/assets/b3557c08-c675-4591-963c-95866d45107f --- - [x] depends on ppy/osu-resources#414 --------- Co-authored-by: Dean Herbert --- osu.Game/Audio/PreviewTrackManager.cs | 4 + .../Card/RankedPlayCard.SongPreview.cs | 18 +++- .../Components/BackgroundMusicManager.cs | 93 +++++++++++++++++++ .../RankedPlay/Intro/IntroScreen.cs | 18 ---- .../RankedPlay/RankedPlayScreen.cs | 11 +++ osu.Game/osu.Game.csproj | 2 +- 6 files changed, 125 insertions(+), 21 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/BackgroundMusicManager.cs diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs index d3ab86a8a0..851dbecee5 100644 --- a/osu.Game/Audio/PreviewTrackManager.cs +++ b/osu.Game/Audio/PreviewTrackManager.cs @@ -22,6 +22,8 @@ namespace osu.Game.Audio protected TrackManagerPreviewTrack? CurrentTrack; + public readonly BindableBool IsPlayingPreview = new BindableBool(); + public PreviewTrackManager(IAdjustableAudioComponent mainTrackAdjustments) { this.mainTrackAdjustments = mainTrackAdjustments; @@ -47,6 +49,7 @@ namespace osu.Game.Audio CurrentTrack?.Stop(); CurrentTrack = track; mainTrackAdjustments.AddAdjustment(AdjustableProperty.Volume, muteBindable); + IsPlayingPreview.Value = true; }); track.Stopped += () => Schedule(() => @@ -56,6 +59,7 @@ namespace osu.Game.Audio CurrentTrack = null; mainTrackAdjustments.RemoveAdjustment(AdjustableProperty.Volume, muteBindable); + IsPlayingPreview.Value = false; }); return track; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Card/RankedPlayCard.SongPreview.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Card/RankedPlayCard.SongPreview.cs index c1b1a057c5..7454e14926 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Card/RankedPlayCard.SongPreview.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Card/RankedPlayCard.SongPreview.cs @@ -41,6 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card protected override Container Content { get; } private readonly Bindable trackRunning = new BindableBool(); + private readonly Container overlayLayer; private bool shouldBePlaying => Enabled.Value && IsHovered; @@ -114,7 +115,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card overlayLayer.Add(new RippleVisualization(cardColours.Border) { - TrackRunning = trackRunning.GetBoundCopy(), + TrackRunning = { BindTarget = trackRunning } }); if (IsHovered) @@ -124,12 +125,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card protected override bool OnHover(HoverEvent e) { + if (previewTrack != null) + previewTrack.Looping = true; + if (shouldBePlaying) + { startPreviewIfAvailable(); + } return base.OnHover(e); } + protected override void OnHoverLost(HoverLostEvent e) + { + if (previewTrack != null) + previewTrack.Looping = false; + + base.OnHoverLost(e); + } + private void onTrackStarted() => Schedule(() => trackRunning.Value = true); private void onTrackStopped() => Schedule(() => trackRunning.Value = false); @@ -193,7 +207,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card [Resolved] private SongPreviewParticleContainer? particleContainer { get; set; } - public required IBindable TrackRunning { get; init; } + public readonly IBindable TrackRunning = new Bindable(); private readonly Color4 accentColour; private readonly Container rippleContainer; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/BackgroundMusicManager.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/BackgroundMusicManager.cs new file mode 100644 index 0000000000..0296259b52 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/BackgroundMusicManager.cs @@ -0,0 +1,93 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Audio; +using osu.Framework.Graphics.Containers; +using osu.Framework.Threading; +using osu.Game.Audio; +using osu.Game.Overlays; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components +{ + public partial class BackgroundMusicManager : CompositeComponent + { + private const int hover_fade_duration = 250; + + private ScheduledDelegate? globalTrackFadeDelegate; + + private DrawableTrack bgm = null!; + + private Bindable isPlayingPreview = null!; + + [Resolved] + private MusicController musicController { get; set; } = null!; + + [Resolved] + private PreviewTrackManager previewTrackManager { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuGameBase game) + { + AddInternal(bgm = new DrawableTrack(audio.Tracks.Get("rankedplay_bgm.ogg"))); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + isPlayingPreview = previewTrackManager.IsPlayingPreview.GetBoundCopy(); + isPlayingPreview.BindValueChanged(playing => + { + bgm.VolumeTo(playing.NewValue ? 0 : 1, hover_fade_duration); + }); + } + + public void Play() + { + if (bgm.IsRunning) + return; + + const int track_fade_duration = 3000; + + // remove music control from player, to prevent overlapping music + musicController.AllowTrackControl.Value = false; + globalTrackFadeDelegate?.Cancel(); + + // cross-fade if global track is playing something + if (musicController.IsPlaying) + { + var globalTrack = musicController.CurrentTrack; + + globalTrack.VolumeTo(0, track_fade_duration, Easing.OutCubic); + globalTrackFadeDelegate = Scheduler.AddDelayed(() => + { + musicController.Stop(); + globalTrack.VolumeTo(1); + }, track_fade_duration); + } + + bgm.VolumeTo(0) + .VolumeTo(1, track_fade_duration, Easing.InCubic); + + bgm.Looping = true; + bgm.Start(); + } + + public void Stop() + { + globalTrackFadeDelegate?.Cancel(); + + bgm.Stop(); + bgm.Reset(); + + // return control of music to player and reset volume + musicController.AllowTrackControl.Value = true; + musicController.CurrentTrack.Volume.Value = 1; + musicController.EnsurePlayingSomething(); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Intro/IntroScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Intro/IntroScreen.cs index 7cfed62015..a73b6bb24f 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Intro/IntroScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Intro/IntroScreen.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; @@ -15,7 +14,6 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay; -using osu.Game.Overlays; namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro { @@ -36,9 +34,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro [Resolved] private IAPIProvider api { get; set; } = null!; - [Resolved] - private MusicController? musicController { get; set; } - private Sample? windupSample; private Sample? impactSample; private Sample? swooshSample; @@ -81,8 +76,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro private StarRatingSequence? starRatingAnimation; - private IDisposable? duckOperation; - public void PlayIntroSequence(UserWithRating player, UserWithRating opponent, double starRating) { double delay = 0; @@ -95,8 +88,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro vsScreen.Play(ref delay, out double impactDelay); - duckOperation = musicController?.Duck(); - if (windupSample != null) { Scheduler.AddDelayed(() => windupSample?.Play(), impactDelay - windupSample.Length); @@ -113,16 +104,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro { starRatingAnimation?.PopOut(); - duckOperation?.Dispose(); - this.Delay(500).FadeOut(); } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - duckOperation?.Dispose(); - } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayScreen.cs index d8571b9db5..a2f65bbb24 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayScreen.cs @@ -88,6 +88,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay [Cached] private readonly SongPreviewParticleContainer particleContainer; + [Cached] + private BackgroundMusicManager backgroundMusic; + public RankedPlayScreen(MultiplayerRoom room) { this.room = room; @@ -129,6 +132,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay }, overlayContainer = new CardDetailsOverlayContainer(), particleContainer = new SongPreviewParticleContainer(), + backgroundMusic = new BackgroundMusicManager() }; } @@ -224,6 +228,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay { chat.Appear(); + if (stage is RankedPlayStage.GameplayWarmup or RankedPlayStage.Gameplay) + backgroundMusic.Stop(); + else + backgroundMusic.Play(); + switch (stage) { case RankedPlayStage.RoundWarmup when matchInfo.CurrentRound == 1: @@ -278,6 +287,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay public override void OnSuspending(ScreenTransitionEvent e) { chat.Disappear(); + backgroundMusic.Stop(); previewTrackManager.StopAnyPlaying(this); base.OnSuspending(e); @@ -296,6 +306,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay return true; } + backgroundMusic.Stop(); previewTrackManager.StopAnyPlaying(this); client.LeaveRoom().FireAndForget(); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 8850de3a7c..0755aeca14 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -40,7 +40,7 @@ - +