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

Add a 'ducking' effect to the currently playing track when changing ruleset #28547

Merged
merged 29 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a567515
Apply a ducking effect to the currently playing track when switching …
nekodex Jun 21, 2024
0d11b2b
Replace manual usages of `AudioFilter` with new ducking methods
nekodex Jun 21, 2024
2ffeb1b
Add fallback behaviour for custom rulesets
nekodex Jun 22, 2024
153138c
Use `null` to disable audio filter instead
nekodex Jul 3, 2024
b972632
Change default easing to match prior behaviour
nekodex Jul 3, 2024
d29d114
Match prior ducking behaviour
nekodex Jul 3, 2024
d948193
Change Duck() to be IDisposable and prevent overlapping usages
nekodex Jul 4, 2024
6813980
Merge branch 'master' into audio-ducking-fx
nekodex Jul 4, 2024
753463f
Fix code style
nekodex Jul 4, 2024
82e4e88
Change overlapping `Duck()` usages to be a noop instead of a throw
nekodex Jul 4, 2024
a5077fc
Rename TimedDuck -> DuckMomentarily
nekodex Jul 4, 2024
7f84e37
Merge branch 'music-controller-nullability' into audio-ducking-fx
peppy Jul 5, 2024
0696e2d
Apply nullability to ducking methods
peppy Jul 5, 2024
4528daf
Update resources
peppy Jul 5, 2024
ec4623d
Reduce duck length slightly on toolbar ruleset selector
peppy Jul 5, 2024
0d858ce
Change default easings to `In`/`Out` for all ducking operations
peppy Jul 5, 2024
554740a
Adjust ducking API to use a parameters `record`
peppy Jul 5, 2024
31ed0d2
Merge branch 'master' into audio-ducking-fx
peppy Jul 5, 2024
20ba6ca
Add mention of return type for `Duck` method
peppy Jul 5, 2024
717f7ba
Better support multiple concurrent ducking operations
peppy Jul 5, 2024
65418ac
Add basic test coverage
peppy Jul 5, 2024
7efb4ce
Fix multiple disposals resulting in assert being hit
peppy Jul 5, 2024
5907e0d
Make `DuckDuration` non-zero by default
peppy Jul 5, 2024
2bfa03c
Rename test scene file
frenzibyte Jul 8, 2024
0067450
Change volume parameter to `double`
peppy Jul 8, 2024
aa36a84
Reduce precision requirement for tests
peppy Jul 8, 2024
3650f3c
Allow multiple ducks with same parameters
peppy Jul 8, 2024
784a9ae
Merge branch 'master' into audio-ducking-fx
frenzibyte Jul 8, 2024
2fcc61e
Merge branch 'master' into audio-ducking-fx
frenzibyte Jul 8, 2024
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
30 changes: 17 additions & 13 deletions osu.Game/Collections/ManageCollectionsDialog.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
// 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.

using System;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Audio.Effects;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;

namespace osu.Game.Collections
Expand All @@ -21,11 +21,14 @@ public partial class ManageCollectionsDialog : OsuFocusedOverlayContainer
private const double enter_duration = 500;
private const double exit_duration = 200;

private AudioFilter lowPassFilter = null!;

protected override string PopInSampleName => @"UI/overlay-big-pop-in";
protected override string PopOutSampleName => @"UI/overlay-big-pop-out";

private IDisposable? audioDucker;

[Resolved]
private MusicController? musicController { get; set; }

public ManageCollectionsDialog()
{
Anchor = Anchor.Centre;
Expand All @@ -39,7 +42,7 @@ public ManageCollectionsDialog()
}

[BackgroundDependencyLoader]
private void load(OsuColour colours, AudioManager audio)
private void load(OsuColour colours)
{
Children = new Drawable[]
{
Expand Down Expand Up @@ -110,19 +113,20 @@ private void load(OsuColour colours, AudioManager audio)
},
}
}
},
lowPassFilter = new AudioFilter(audio.TrackMixer)
}
};
}

public override bool IsPresent => base.IsPresent
// Safety for low pass filter potentially getting stuck in applied state due to
// transforms on `this` causing children to no longer be updated.
|| lowPassFilter.IsAttached;
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
audioDucker?.Dispose();
}

protected override void PopIn()
{
lowPassFilter.CutoffTo(300, 100, Easing.OutCubic);
audioDucker = musicController?.Duck(100, 1f, unduckDuration: 100);

this.FadeIn(enter_duration, Easing.OutQuint);
this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutQuint);
}
Expand All @@ -131,7 +135,7 @@ protected override void PopOut()
{
base.PopOut();

lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
audioDucker?.Dispose();

this.FadeOut(exit_duration, Easing.OutQuint);
this.ScaleTo(0.9f, exit_duration);
Expand Down
14 changes: 0 additions & 14 deletions osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@

#nullable disable

using System;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Audio.Effects;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;

Expand Down Expand Up @@ -57,16 +55,13 @@ public DangerousConfirmContainer()
private Sample tickSample;
private Sample confirmSample;
private double lastTickPlaybackTime;
private AudioFilter lowPassFilter = null!;
private bool mouseDown;

[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
tickSample = audio.Samples.Get(@"UI/dialog-dangerous-tick");
confirmSample = audio.Samples.Get(@"UI/dialog-dangerous-select");

AddInternal(lowPassFilter = new AudioFilter(audio.SampleMixer));
}

protected override void LoadComplete()
Expand All @@ -75,15 +70,8 @@ protected override void LoadComplete()
Progress.BindValueChanged(progressChanged, true);
}

protected override void AbortConfirm()
{
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
base.AbortConfirm();
}

protected override void Confirm()
{
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
confirmSample?.Play();
base.Confirm();
}
Expand Down Expand Up @@ -123,8 +111,6 @@ protected override void OnHoverLost(HoverLostEvent e)

private void progressChanged(ValueChangedEvent<double> progress)
{
lowPassFilter.Cutoff = Math.Max(1, (int)(progress.NewValue * AudioFilter.MAX_LOWPASS_CUTOFF * 0.5));

if (progress.NewValue < progress.OldValue)
return;

Expand Down
25 changes: 13 additions & 12 deletions osu.Game/Overlays/DialogOverlay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@

#nullable disable

using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays.Dialog;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Input.Events;
using osu.Game.Audio.Effects;

namespace osu.Game.Overlays
{
Expand All @@ -23,15 +23,16 @@ public partial class DialogOverlay : OsuFocusedOverlayContainer, IDialogOverlay
protected override string PopInSampleName => "UI/dialog-pop-in";
protected override string PopOutSampleName => "UI/dialog-pop-out";

private AudioFilter lowPassFilter;
[Resolved]
private MusicController musicController { get; set; }

public PopupDialog CurrentDialog { get; private set; }

public override bool IsPresent => Scheduler.HasPendingTasks
|| dialogContainer.Children.Count > 0
// Safety for low pass filter potentially getting stuck in applied state due to
// transforms on `this` causing children to no longer be updated.
|| lowPassFilter.IsAttached;
|| dialogContainer.Children.Count > 0;

[CanBeNull]
private IDisposable audioDucker;

public DialogOverlay()
{
Expand All @@ -49,10 +50,10 @@ public DialogOverlay()
Origin = Anchor.Centre;
}

[BackgroundDependencyLoader]
private void load(AudioManager audio)
protected override void Dispose(bool isDisposing)
{
AddInternal(lowPassFilter = new AudioFilter(audio.TrackMixer));
base.Dispose(isDisposing);
audioDucker?.Dispose();
}

public void Push(PopupDialog dialog)
Expand Down Expand Up @@ -105,13 +106,13 @@ void dismiss()

protected override void PopIn()
{
lowPassFilter.CutoffTo(300, 100, Easing.OutCubic);
audioDucker = musicController.Duck(100, 1f, unduckDuration: 100);
}

protected override void PopOut()
{
base.PopOut();
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
audioDucker?.Dispose();

// PopOut gets called initially, but we only want to hide dialog when we have been loaded and are present.
if (IsLoaded && CurrentDialog?.State.Value == Visibility.Visible)
Expand Down
70 changes: 70 additions & 0 deletions osu.Game/Overlays/MusicController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Game.Audio.Effects;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets.Mods;
Expand Down Expand Up @@ -63,6 +65,17 @@ public partial class MusicController : CompositeDrawable
[Resolved]
private RealmAccess realm { get; set; }

private AudioFilter audioDuckFilter;
private readonly BindableDouble audioDuckVolume = new BindableDouble(1);
private bool audioDuckActive;

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

protected override void LoadComplete()
{
base.LoadComplete();
Expand Down Expand Up @@ -243,6 +256,63 @@ public void NextTrack(Action onSuccess = null) => Schedule(() =>
onSuccess?.Invoke();
});

/// <summary>
/// Attenuates the volume and/or filters the currently playing track.
/// </summary>
/// <param name="duration">Duration of the ducking transition, in ms.</param>
/// <param name="duckVolumeTo">Level to drop volume to (1.0 = 100%).</param>
/// <param name="duckCutoffTo">Cutoff frequency to drop `AudioFilter` to. Use `null` to skip filter effect.</param>
/// <param name="easing">Easing for the ducking transition.</param>
/// <param name="unduckDuration">Duration of the unducking transition, in ms.</param>
/// <param name="unduckEasing">Easing for the unducking transition.</param>
public IDisposable Duck(int duration = 0, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, Easing easing = Easing.OutCubic, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic)
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

I feel like cubic easing is too abrupt in its current form. Can you give a plain In/Out a try? It already feels better to me (also a slight reduction to the unduck delay):

diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs
index 8b52c59bae..06845116b1 100644
--- a/osu.Game/Overlays/MusicController.cs
+++ b/osu.Game/Overlays/MusicController.cs
@@ -265,7 +265,7 @@ public void NextTrack(Action onSuccess = null) => Schedule(() =>
         /// <param name="easing">Easing for the ducking transition.</param>
         /// <param name="unduckDuration">Duration of the unducking transition, in ms.</param>
         /// <param name="unduckEasing">Easing for the unducking transition.</param>
-        public IDisposable Duck(int duration = 0, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, Easing easing = Easing.OutCubic, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic)
+        public IDisposable Duck(int duration = 0, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, Easing easing = Easing.Out, int unduckDuration = 500, Easing unduckEasing = Easing.In)
         {
             if (audioDuckActive) return null;
 
@@ -292,7 +292,7 @@ public IDisposable Duck(int duration = 0, float duckVolumeTo = 0.25f, int? duckC
         /// <param name="duckCutoffTo">Cutoff frequency to drop `AudioFilter` to. Use `null` to skip filter effect.</param>
         /// <param name="duckDuration">Duration of the ducking transition, in ms.</param>
         /// <param name="duckEasing">Easing for the ducking transition.</param>
-        public void TimedDuck(int delay, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, int duckDuration = 0, Easing duckEasing = Easing.OutCubic)
+        public void TimedDuck(int delay, int unduckDuration = 500, Easing unduckEasing = Easing.In, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, int duckDuration = 0, Easing duckEasing = Easing.Out)
         {
             if (audioDuckActive) return;
 
diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs
index 7da6b76aaa..07ce1f69ef 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs
@@ -122,7 +122,7 @@ private void playRulesetSelectionSample(ValueChangedEvent<RulesetInfo> r)
 
             rulesetSelectionChannel[r.NewValue] = channel;
             channel.Play();
-            musicController?.TimedDuck(600);
+            musicController?.TimedDuck(500);
         }
 
         public override bool HandleNonPositionalInput => !Current.Disabled && base.HandleNonPositionalInput;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you elaborate on which usage(s) you find to be too abrupt? If it's just the ruleset selector, probably better to change that specific usage rather than the default?

CubicIn/CubicOut matches what the previous implementations (DialogOverlay, etc) used, so they should sound identical to before.

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

I was testing with the ruleset selector, but I think it might be a better default. I'll check the other usages and loop back.

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

I think non-cubic sounds better for the dialog cases too. There's not much difference since the duration is so short, but it's a little less sharp.

{
if (audioDuckActive) return null;

audioDuckActive = true;

Schedule(() =>
{
if (duckCutoffTo.IsNotNull())
audioDuckFilter?.CutoffTo((int)duckCutoffTo, duration, easing);

this.TransformBindableTo(audioDuckVolume, duckVolumeTo, duration, easing);
});

return new InvokeOnDisposal(() => unduck(unduckDuration, unduckEasing));
}

/// <summary>
/// A convenience method that ducks the currently playing track, then after a delay, unducks it.
/// </summary>
/// <param name="delay">Delay after audio is ducked before unducking begins, in ms.</param>
/// <param name="unduckDuration">Duration of the unducking transition, in ms.</param>
/// <param name="unduckEasing">Easing for the unducking transition.</param>
/// <param name="duckVolumeTo">Level to drop volume to (1.0 = 100%).</param>
/// <param name="duckCutoffTo">Cutoff frequency to drop `AudioFilter` to. Use `null` to skip filter effect.</param>
/// <param name="duckDuration">Duration of the ducking transition, in ms.</param>
/// <param name="duckEasing">Easing for the ducking transition.</param>
public void TimedDuck(int delay, int unduckDuration = 500, Easing unduckEasing = Easing.InCubic, float duckVolumeTo = 0.25f, int? duckCutoffTo = 300, int duckDuration = 0, Easing duckEasing = Easing.OutCubic)
nekodex marked this conversation as resolved.
Show resolved Hide resolved
{
if (audioDuckActive) return;

Duck(duckDuration, duckVolumeTo, duckCutoffTo, duckEasing);
Scheduler.AddDelayed(() => unduck(unduckDuration, unduckEasing), delay);
}

private void unduck(int duration, Easing easing)
{
if (!audioDuckActive) return;

audioDuckActive = false;

Schedule(() =>
{
audioDuckFilter?.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, duration, easing);
this.TransformBindableTo(audioDuckVolume, 1, duration, easing);
});
}

private bool next()
{
if (beatmap.Disabled || !AllowTrackControl.Value)
Expand Down
43 changes: 42 additions & 1 deletion osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@

#nullable disable

using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
Expand All @@ -21,14 +25,21 @@ public partial class ToolbarRulesetSelector : RulesetSelector
{
protected Drawable ModeButtonLine { get; private set; }

[Resolved]
private MusicController musicController { get; set; }

private readonly Dictionary<RulesetInfo, Sample> rulesetSelectionSample = new Dictionary<RulesetInfo, Sample>();
private readonly Dictionary<RulesetInfo, SampleChannel> rulesetSelectionChannel = new Dictionary<RulesetInfo, SampleChannel>();
private Sample defaultSelectSample;

public ToolbarRulesetSelector()
{
RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X;
}

[BackgroundDependencyLoader]
private void load()
private void load(AudioManager audio)
{
AddRangeInternal(new[]
{
Expand All @@ -54,6 +65,13 @@ private void load()
}
},
});

foreach (var r in Rulesets.AvailableRulesets)
rulesetSelectionSample[r] = audio.Samples.Get($@"UI/ruleset-select-{r.ShortName}");
nekodex marked this conversation as resolved.
Show resolved Hide resolved

defaultSelectSample = audio.Samples.Get(@"UI/default-select");

Current.ValueChanged += playRulesetSelectionSample;
}

protected override void LoadComplete()
Expand Down Expand Up @@ -84,6 +102,29 @@ private void moveLineToCurrent()
}
}

private void playRulesetSelectionSample(ValueChangedEvent<RulesetInfo> r)
{
// Don't play sample on first setting of value
if (r.OldValue == null)
return;

var channel = rulesetSelectionSample[r.NewValue]?.GetChannel();

// Skip sample choking and ducking for the default/fallback sample
if (channel == null)
{
defaultSelectSample.Play();
return;
}

foreach (var pair in rulesetSelectionChannel)
pair.Value?.Stop();

rulesetSelectionChannel[r.NewValue] = channel;
channel.Play();
musicController?.TimedDuck(600);
}

public override bool HandleNonPositionalInput => !Current.Disabled && base.HandleNonPositionalInput;

public override bool HandlePositionalInput => !Current.Disabled && base.HandlePositionalInput;
Expand Down
Loading
Loading