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

Fix results screen sounds persisting after exit #31742

Merged
merged 3 commits into from
Feb 3, 2025

Conversation

smoogipoo
Copy link
Contributor

@smoogipoo smoogipoo commented Jan 31, 2025

Resolves #31740

I probably uncovered about 4 issues down multiple implementations, and this is the best/simplest that I've come up with for now.

Testing:

diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
index 319a87fdfc..2a4f499faf 100644
--- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
@@ -4,6 +4,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Threading;
 using osu.Framework.Allocation;
 using osu.Framework.Audio;
 using osu.Framework.Bindables;
@@ -122,7 +123,7 @@ public partial class AccuracyCircle : CompositeDrawable
         public AccuracyCircle(ScoreInfo score, bool withFlair = false)
         {
             this.score = score;
-            this.withFlair = withFlair;
+            this.withFlair = true;
 
             ScoreProcessor scoreProcessor = score.Ruleset.CreateInstance().CreateScoreProcessor();
             accuracyX = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.X);
@@ -339,6 +340,7 @@ protected override void LoadComplete()
                         {
                             Schedule(() =>
                             {
+                                rankApplauseSound!.Looping = true;
                                 rankApplauseSound!.VolumeTo(applause_volume);
                                 rankApplauseSound!.Play();
                             });
@@ -464,5 +466,12 @@ private double inverseEasing(Easing easing, double targetValue)
 
             return test;
         }
+
+        protected override void Dispose(bool isDisposing)
+        {
+            Thread.Sleep(10000);
+
+            base.Dispose(isDisposing);
+        }
     }
 }
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index 95dbfb2712..2ab186abcf 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -333,8 +333,8 @@ public override bool OnExiting(ScreenExitEvent e)
             // HitObject references from HitEvent.
             Score?.HitEvents.Clear();
 
-            if (!skipExitTransition)
-                this.FadeOut(100);
+            // if (!skipExitTransition)
+            //     this.FadeOut(100);
 
             audioContainer.Volume.Value = 0;
             return false;

Q: Why fade out immediately instead of over 100ms or so?

Primarily, I ran into ppy/osu-framework#6518. In most cases it works fine but there's the odd case where the sample remains audible and feels very bad.

I judge the immediate fade out to be acceptable here as it's masked by the screen exiting sound. To my ears at least...

Q: Why not use ISamplePlaybackDisabler?

Note that this will also result in immediate fade out as above.

For AccuracyCircle it'll work fine (with few changes), however there are other components involved in this screen that don't use or can't easily be made to use PausableSkinnableSound because they shouldn't be skinnable. Without this, some sounds pause but others continue for a little while which feels bad to me.
We don't really have a PausableDrawableSample that responds to this interface right now for non-skinnable cases, and it's not immediately obvious what its semantics would be.

Using AudioContainer means that all sounds are muted at the same time.

Q: What about DrawableAudioMixer?

I think this will also work, but I don't want to deal with nested mixers right now.

@peppy
Copy link
Member

peppy commented Feb 2, 2025

I think it would be best to let other quick samples play out rather than potentially get clipped to zero volume on exiting the screen.

Given how nested the playback of applause it, the easiest way to fix this might be to just move playback into ResultsScreen itself. It would remove the panning effect but I dunno how physically correct having the applause pan with the panel movement is in the first place, so I'd be happy to lose that.

10 minute mock-up:

diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
index 319a87fdfc..dc95647538 100644
--- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
@@ -2,7 +2,6 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
-using System.Collections.Generic;
 using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Audio;
@@ -101,7 +100,6 @@ public partial class AccuracyCircle : CompositeDrawable
         private PoolableSkinnableSample? badgeMaxSound;
         private PoolableSkinnableSample? swooshUpSound;
         private PoolableSkinnableSample? rankImpactSound;
-        private PoolableSkinnableSample? rankApplauseSound;
 
         private readonly Bindable<double> tickPlaybackRate = new Bindable<double>();
 
@@ -119,6 +117,9 @@ public partial class AccuracyCircle : CompositeDrawable
         private readonly bool isFailedSDueToMisses;
         private RankText failedSRankText = null!;
 
+        [Resolved]
+        private ResultsScreen? results { get; set; }
+
         public AccuracyCircle(ScoreInfo score, bool withFlair = false)
         {
             this.score = score;
@@ -197,15 +198,9 @@ private void load()
 
             if (withFlair)
             {
-                var applauseSamples = new List<string> { applauseSampleName };
-                if (score.Rank >= ScoreRank.B)
-                    // when rank is B or higher, play legacy applause sample on legacy skins.
-                    applauseSamples.Insert(0, @"applause");
-
                 AddRangeInternal(new Drawable[]
                 {
                     rankImpactSound = new PoolableSkinnableSample(new SampleInfo(impactSampleName)),
-                    rankApplauseSound = new PoolableSkinnableSample(new SampleInfo(applauseSamples.ToArray())),
                     scoreTickSound = new PoolableSkinnableSample(new SampleInfo(@"Results/score-tick")),
                     badgeTickSound = new PoolableSkinnableSample(new SampleInfo(@"Results/badge-dink")),
                     badgeMaxSound = new PoolableSkinnableSample(new SampleInfo(@"Results/badge-dink-max")),
@@ -333,16 +328,9 @@ protected override void LoadComplete()
                         });
 
                         const double applause_pre_delay = 545f;
-                        const double applause_volume = 0.8f;
 
                         using (BeginDelayedSequence(applause_pre_delay))
-                        {
-                            Schedule(() =>
-                            {
-                                rankApplauseSound!.VolumeTo(applause_volume);
-                                rankApplauseSound!.Play();
-                            });
-                        }
+                            Schedule(() => results?.PlayApplause(score.Rank));
                     }
                 }
 
@@ -384,34 +372,6 @@ protected override void Update()
             }
         }
 
-        private string applauseSampleName
-        {
-            get
-            {
-                switch (score.Rank)
-                {
-                    default:
-                    case ScoreRank.D:
-                        return @"Results/applause-d";
-
-                    case ScoreRank.C:
-                        return @"Results/applause-c";
-
-                    case ScoreRank.B:
-                        return @"Results/applause-b";
-
-                    case ScoreRank.A:
-                        return @"Results/applause-a";
-
-                    case ScoreRank.S:
-                    case ScoreRank.SH:
-                    case ScoreRank.X:
-                    case ScoreRank.XH:
-                        return @"Results/applause-s";
-                }
-            }
-        }
-
         private string impactSampleName
         {
             get
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index 5e91171051..34e51dc73d 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -17,6 +17,7 @@
 using osu.Framework.Input.Bindings;
 using osu.Framework.Input.Events;
 using osu.Framework.Screens;
+using osu.Game.Audio;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
 using osu.Game.Graphics.UserInterface;
@@ -29,10 +30,12 @@
 using osu.Game.Screens.Play;
 using osu.Game.Screens.Ranking.Expanded.Accuracy;
 using osu.Game.Screens.Ranking.Statistics;
+using osu.Game.Skinning;
 using osuTK;
 
 namespace osu.Game.Screens.Ranking
 {
+    [Cached]
     public abstract partial class ResultsScreen : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>
     {
         protected const float BACKGROUND_BLUR = 20;
@@ -263,6 +266,64 @@ protected override void Update()
             }
         }
 
+        #region Applause
+
+        public void PlayApplause(ScoreRank rank)
+        {
+            const double applause_volume = 0.8f;
+
+            if (!this.IsCurrentScreen())
+                return;
+
+            rankApplauseSound?.Dispose();
+
+            var applauseSamples = new List<string>();
+
+            if (rank >= ScoreRank.B)
+                // when rank is B or higher, play legacy applause sample on legacy skins.
+                applauseSamples.Insert(0, @"applause");
+
+            switch (rank)
+            {
+                default:
+                case ScoreRank.D:
+                    applauseSamples.Add(@"Results/applause-d");
+                    break;
+
+                case ScoreRank.C:
+                    applauseSamples.Add(@"Results/applause-c");
+                    break;
+
+                case ScoreRank.B:
+                    applauseSamples.Add(@"Results/applause-b");
+                    break;
+
+                case ScoreRank.A:
+                    applauseSamples.Add(@"Results/applause-a");
+                    break;
+
+                case ScoreRank.S:
+                case ScoreRank.SH:
+                case ScoreRank.X:
+                case ScoreRank.XH:
+                    applauseSamples.Add(@"Results/applause-s");
+                    break;
+            }
+
+            LoadComponentAsync(rankApplauseSound = new PoolableSkinnableSample(new SampleInfo(applauseSamples.ToArray())), s =>
+            {
+                if (!this.IsCurrentScreen() || s != rankApplauseSound)
+                    return;
+
+                rankApplauseSound.VolumeTo(applause_volume);
+                rankApplauseSound.Play();
+            });
+        }
+
+        private PoolableSkinnableSample? rankApplauseSound;
+
+        #endregion
+
         /// <summary>
         /// Performs a fetch/refresh of scores to be displayed.
         /// </summary>
@@ -330,6 +391,8 @@ public override bool OnExiting(ScreenExitEvent e)
 
             if (!skipExitTransition)
                 this.FadeOut(100);
+
+            rankApplauseSound?.Stop();
             return false;
         }
 

Copy link
Member

@peppy peppy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as commented

@smoogipoo
Copy link
Contributor Author

What's the difference between letting other quick samples play out and not clip, and letting this one clip?

@peppy
Copy link
Member

peppy commented Feb 3, 2025

The real killer with applause samples is that many popular skins these days have full songs as applause.mp3 that can last minutes... so a precedent has been set for it not being limited to the length of the original sample (which is arguably quite long already).

With the approach of moving it to the results screen, we could also do a fade on applause rather than immediately cut it off for bonus points.

@smoogipoo
Copy link
Contributor Author

I suppose, but I mentioned the reason why it's not a fade in the OP. And in that case I would expect everything to fade uniformly.

I will apply your change regardless.

@smoogipoo
Copy link
Contributor Author

Applied. To be clear, this is what I'm referring to by the above - how a similar situation will behave now which isn't an issue with my original changes:

2025-02-03.14-14-32.mp4

@peppy
Copy link
Member

peppy commented Feb 3, 2025

Hmm, that one is also pretty long. With the AudioContainer method, what's the reason for being unable to fade out? Is there a chance the fade out just doesn't play?

@smoogipoo
Copy link
Contributor Author

what's the reason for being unable to fade out

That's been answered in the OP. ppy/osu-framework#6518

@peppy
Copy link
Member

peppy commented Feb 3, 2025

I'm guessing fixing that framework side is not going to be as simple as running one final Update loop? 😮‍💨

@smoogipoo
Copy link
Contributor Author

smoogipoo commented Feb 3, 2025

Who knows. Anything can be hacked if you put your mind to it.

Would you like me to switch gears and try to understand whether lifetimes in o!f are completely broken? This was intended as a simple/quick change, and in general I agree with the sentiment that the applause is a special case skinned to be minutes in length.

@peppy
Copy link
Member

peppy commented Feb 3, 2025

Yeah let's go with this for now. This sound-on-screen-exit isn't going to be limited to just the results screen. I can imagine us wanting to use the AudioContainer method to fade all screen sounds at a global OsuScreen level. Will make a note of that in the framework thread.

@peppy peppy merged commit b9f840c into ppy:master Feb 3, 2025
8 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Applause audio won't stop after the result screen is closed
2 participants