1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-14 16:37:26 +08:00

Merge pull request #15073 from peppy/fix-player-loader-low-pass

Improve `PlayerLoader` audio and visual transitions
This commit is contained in:
Dan Balasescu 2021-10-13 17:01:49 +09:00 committed by GitHub
commit d88daf0cc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 185 additions and 127 deletions

View File

@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
@ -18,16 +19,19 @@ namespace osu.Game.Tests.Visual.Audio
{ {
public class TestSceneAudioFilter : OsuTestScene public class TestSceneAudioFilter : OsuTestScene
{ {
private OsuSpriteText lowpassText; private OsuSpriteText lowPassText;
private AudioFilter lowpassFilter; private AudioFilter lowPassFilter;
private OsuSpriteText highpassText; private OsuSpriteText highPassText;
private AudioFilter highpassFilter; private AudioFilter highPassFilter;
private Track track; private Track track;
private WaveformTestBeatmap beatmap; private WaveformTestBeatmap beatmap;
private OsuSliderBar<int> lowPassSlider;
private OsuSliderBar<int> highPassSlider;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio) private void load(AudioManager audio)
{ {
@ -38,53 +42,89 @@ namespace osu.Game.Tests.Visual.Audio
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
lowpassFilter = new AudioFilter(audio.TrackMixer), lowPassFilter = new AudioFilter(audio.TrackMixer),
highpassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass), highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
lowpassText = new OsuSpriteText lowPassText = new OsuSpriteText
{ {
Padding = new MarginPadding(20), Padding = new MarginPadding(20),
Text = $"Low Pass: {lowpassFilter.Cutoff.Value}hz", Text = $"Low Pass: {lowPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40) Font = new FontUsage(size: 40)
}, },
new OsuSliderBar<int> lowPassSlider = new OsuSliderBar<int>
{ {
Width = 500, Width = 500,
Height = 50, Height = 50,
Padding = new MarginPadding(20), Padding = new MarginPadding(20),
Current = { BindTarget = lowpassFilter.Cutoff } Current = new BindableInt
{
MinValue = 0,
MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
}
}, },
highpassText = new OsuSpriteText highPassText = new OsuSpriteText
{ {
Padding = new MarginPadding(20), Padding = new MarginPadding(20),
Text = $"High Pass: {highpassFilter.Cutoff.Value}hz", Text = $"High Pass: {highPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40) Font = new FontUsage(size: 40)
}, },
new OsuSliderBar<int> highPassSlider = new OsuSliderBar<int>
{ {
Width = 500, Width = 500,
Height = 50, Height = 50,
Padding = new MarginPadding(20), Padding = new MarginPadding(20),
Current = { BindTarget = highpassFilter.Cutoff } Current = new BindableInt
{
MinValue = 0,
MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
}
} }
} }
}); });
lowpassFilter.Cutoff.ValueChanged += e => lowpassText.Text = $"Low Pass: {e.NewValue}hz";
highpassFilter.Cutoff.ValueChanged += e => highpassText.Text = $"High Pass: {e.NewValue}hz"; lowPassSlider.Current.ValueChanged += e =>
{
lowPassText.Text = $"Low Pass: {e.NewValue}hz";
lowPassFilter.Cutoff = e.NewValue;
};
highPassSlider.Current.ValueChanged += e =>
{
highPassText.Text = $"High Pass: {e.NewValue}hz";
highPassFilter.Cutoff = e.NewValue;
};
} }
#region Overrides of Drawable
protected override void Update()
{
base.Update();
highPassSlider.Current.Value = highPassFilter.Cutoff;
lowPassSlider.Current.Value = lowPassFilter.Cutoff;
}
#endregion
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
AddStep("Play Track", () => track.Start()); AddStep("Play Track", () => track.Start());
AddStep("Reset filters", () =>
{
lowPassFilter.Cutoff = AudioFilter.MAX_LOWPASS_CUTOFF;
highPassFilter.Cutoff = 0;
});
waitTrackPlay(); waitTrackPlay();
} }
[Test] [Test]
public void TestLowPass() public void TestLowPassSweep()
{ {
AddStep("Filter Sweep", () => AddStep("Filter Sweep", () =>
{ {
lowpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then() lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic); .CutoffTo(0, 2000, Easing.OutCubic);
}); });
@ -92,7 +132,7 @@ namespace osu.Game.Tests.Visual.Audio
AddStep("Filter Sweep (reverse)", () => AddStep("Filter Sweep (reverse)", () =>
{ {
lowpassFilter.CutoffTo(0).Then() lowPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic); .CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
}); });
@ -101,11 +141,11 @@ namespace osu.Game.Tests.Visual.Audio
} }
[Test] [Test]
public void TestHighPass() public void TestHighPassSweep()
{ {
AddStep("Filter Sweep", () => AddStep("Filter Sweep", () =>
{ {
highpassFilter.CutoffTo(0).Then() highPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic); .CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
}); });
@ -113,7 +153,7 @@ namespace osu.Game.Tests.Visual.Audio
AddStep("Filter Sweep (reverse)", () => AddStep("Filter Sweep (reverse)", () =>
{ {
highpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then() highPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic); .CutoffTo(0, 2000, Easing.OutCubic);
}); });

View File

@ -4,7 +4,6 @@
using System.Diagnostics; using System.Diagnostics;
using ManagedBass.Fx; using ManagedBass.Fx;
using osu.Framework.Audio.Mixing; using osu.Framework.Audio.Mixing;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
namespace osu.Game.Audio.Effects namespace osu.Game.Audio.Effects
@ -21,10 +20,25 @@ namespace osu.Game.Audio.Effects
private readonly BQFParameters filter; private readonly BQFParameters filter;
private readonly BQFType type; private readonly BQFType type;
private bool isAttached;
private int cutoff;
/// <summary> /// <summary>
/// The current cutoff of this filter. /// The cutoff frequency of this filter.
/// </summary> /// </summary>
public BindableNumber<int> Cutoff { get; } public int Cutoff
{
get => cutoff;
set
{
if (value == cutoff)
return;
cutoff = value;
updateFilter(cutoff);
}
}
/// <summary> /// <summary>
/// A Component that implements a BASS FX BiQuad Filter Effect. /// A Component that implements a BASS FX BiQuad Filter Effect.
@ -36,102 +50,96 @@ namespace osu.Game.Audio.Effects
this.mixer = mixer; this.mixer = mixer;
this.type = type; this.type = type;
int initialCutoff;
switch (type)
{
case BQFType.HighPass:
initialCutoff = 1;
break;
case BQFType.LowPass:
initialCutoff = MAX_LOWPASS_CUTOFF;
break;
default:
initialCutoff = 500; // A default that should ensure audio remains audible for other filters.
break;
}
Cutoff = new BindableNumber<int>(initialCutoff)
{
MinValue = 1,
MaxValue = MAX_LOWPASS_CUTOFF
};
filter = new BQFParameters filter = new BQFParameters
{ {
lFilter = type, lFilter = type,
fCenter = initialCutoff,
fBandwidth = 0, fBandwidth = 0,
fQ = 0.7f // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0) // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0)
fQ = 0.7f
}; };
// Don't start attached if this is low-pass or high-pass filter (as they have special auto-attach/detach logic) Cutoff = getInitialCutoff(type);
if (type != BQFType.LowPass && type != BQFType.HighPass)
attachFilter();
Cutoff.ValueChanged += updateFilter;
} }
private void attachFilter() private int getInitialCutoff(BQFType type)
{ {
Debug.Assert(!mixer.Effects.Contains(filter)); switch (type)
mixer.Effects.Add(filter);
}
private void detachFilter()
{
Debug.Assert(mixer.Effects.Contains(filter));
mixer.Effects.Remove(filter);
}
private void updateFilter(ValueChangedEvent<int> cutoff)
{
// Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz.
if (type == BQFType.LowPass)
{ {
if (cutoff.NewValue >= MAX_LOWPASS_CUTOFF) case BQFType.HighPass:
{ return 1;
detachFilter();
return;
}
if (cutoff.OldValue >= MAX_LOWPASS_CUTOFF && cutoff.NewValue < MAX_LOWPASS_CUTOFF) case BQFType.LowPass:
attachFilter(); return MAX_LOWPASS_CUTOFF;
default:
return 500; // A default that should ensure audio remains audible for other filters.
}
}
private void updateFilter(int newValue)
{
switch (type)
{
case BQFType.LowPass:
// Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz.
if (newValue >= MAX_LOWPASS_CUTOFF)
{
ensureDetached();
return;
}
break;
// Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz.
case BQFType.HighPass:
if (newValue <= 1)
{
ensureDetached();
return;
}
break;
} }
// Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz. ensureAttached();
if (type == BQFType.HighPass)
{
if (cutoff.NewValue <= 1)
{
detachFilter();
return;
}
if (cutoff.OldValue <= 1 && cutoff.NewValue > 1)
attachFilter();
}
var filterIndex = mixer.Effects.IndexOf(filter); var filterIndex = mixer.Effects.IndexOf(filter);
if (filterIndex < 0) return; if (filterIndex < 0) return;
if (mixer.Effects[filterIndex] is BQFParameters existingFilter) if (mixer.Effects[filterIndex] is BQFParameters existingFilter)
{ {
existingFilter.fCenter = cutoff.NewValue; existingFilter.fCenter = newValue;
// required to update effect with new parameters. // required to update effect with new parameters.
mixer.Effects[filterIndex] = existingFilter; mixer.Effects[filterIndex] = existingFilter;
} }
} }
private void ensureAttached()
{
if (isAttached)
return;
Debug.Assert(!mixer.Effects.Contains(filter));
mixer.Effects.Add(filter);
isAttached = true;
}
private void ensureDetached()
{
if (!isAttached)
return;
Debug.Assert(mixer.Effects.Contains(filter));
mixer.Effects.Remove(filter);
isAttached = false;
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
ensureDetached();
if (mixer.Effects.Contains(filter))
detachFilter();
} }
} }
} }

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms; using osu.Framework.Graphics.Transforms;
@ -12,7 +11,7 @@ namespace osu.Game.Audio.Effects
/// <summary> /// <summary>
/// The filter cutoff. /// The filter cutoff.
/// </summary> /// </summary>
BindableNumber<int> Cutoff { get; } int Cutoff { get; set; }
} }
public static class FilterableAudioComponentExtensions public static class FilterableAudioComponentExtensions
@ -40,7 +39,7 @@ namespace osu.Game.Audio.Effects
public static TransformSequence<T> CutoffTo<T, TEasing>(this T component, int newCutoff, double duration, TEasing easing) public static TransformSequence<T> CutoffTo<T, TEasing>(this T component, int newCutoff, double duration, TEasing easing)
where T : class, ITransformableFilter, IDrawable where T : class, ITransformableFilter, IDrawable
where TEasing : IEasingFunction where TEasing : IEasingFunction
=> component.TransformBindableTo(component.Cutoff, newCutoff, duration, easing); => component.TransformTo(nameof(component.Cutoff), newCutoff, duration, easing);
/// <summary> /// <summary>
/// Smoothly adjusts filter cutoff over time. /// Smoothly adjusts filter cutoff over time.
@ -49,6 +48,6 @@ namespace osu.Game.Audio.Effects
public static TransformSequence<T> CutoffTo<T, TEasing>(this TransformSequence<T> sequence, int newCutoff, double duration, TEasing easing) public static TransformSequence<T> CutoffTo<T, TEasing>(this TransformSequence<T> sequence, int newCutoff, double duration, TEasing easing)
where T : class, ITransformableFilter, IDrawable where T : class, ITransformableFilter, IDrawable
where TEasing : IEasingFunction where TEasing : IEasingFunction
=> sequence.Append(o => o.TransformBindableTo(o.Cutoff, newCutoff, duration, easing)); => sequence.Append(o => o.TransformTo(nameof(o.Cutoff), newCutoff, duration, easing));
} }
} }

View File

@ -35,6 +35,8 @@ namespace osu.Game.Screens.Play
{ {
protected const float BACKGROUND_BLUR = 15; protected const float BACKGROUND_BLUR = 15;
private const double content_out_duration = 300;
public override bool HideOverlaysOnEnter => hideOverlays; public override bool HideOverlaysOnEnter => hideOverlays;
public override bool DisallowExternalBeatmapRulesetChanges => true; public override bool DisallowExternalBeatmapRulesetChanges => true;
@ -135,36 +137,39 @@ namespace osu.Game.Screens.Play
muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce); muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce);
batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce); batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce);
InternalChild = (content = new LogoTrackingContainer InternalChildren = new Drawable[]
{ {
Anchor = Anchor.Centre, (content = new LogoTrackingContainer
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
}).WithChildren(new Drawable[]
{
MetadataInfo = new BeatmapMetadataDisplay(Beatmap.Value, Mods, content.LogoFacade)
{ {
Alpha = 0,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, RelativeSizeAxes = Axes.Both,
PlayerSettings = new FillFlowContainer<PlayerSettingsGroup> }).WithChildren(new Drawable[]
{ {
Anchor = Anchor.TopRight, MetadataInfo = new BeatmapMetadataDisplay(Beatmap.Value, Mods, content.LogoFacade)
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 20),
Margin = new MarginPadding(25),
Children = new PlayerSettingsGroup[]
{ {
VisualSettings = new VisualSettings(), Alpha = 0,
new InputSettings() Anchor = Anchor.Centre,
} Origin = Anchor.Centre,
}, },
idleTracker = new IdleTracker(750), PlayerSettings = new FillFlowContainer<PlayerSettingsGroup>
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 20),
Margin = new MarginPadding(25),
Children = new PlayerSettingsGroup[]
{
VisualSettings = new VisualSettings(),
new InputSettings()
}
},
idleTracker = new IdleTracker(750),
}),
lowPassFilter = new AudioFilter(audio.TrackMixer) lowPassFilter = new AudioFilter(audio.TrackMixer)
}); };
if (Beatmap.Value.BeatmapInfo.EpilepsyWarning) if (Beatmap.Value.BeatmapInfo.EpilepsyWarning)
{ {
@ -195,7 +200,6 @@ namespace osu.Game.Screens.Play
epilepsyWarning.DimmableBackground = b; epilepsyWarning.DimmableBackground = b;
}); });
lowPassFilter.CutoffTo(500, 100, Easing.OutCubic);
Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
content.ScaleTo(0.7f); content.ScaleTo(0.7f);
@ -240,15 +244,15 @@ namespace osu.Game.Screens.Play
public override bool OnExiting(IScreen next) public override bool OnExiting(IScreen next)
{ {
cancelLoad(); cancelLoad();
contentOut();
content.ScaleTo(0.7f, 150, Easing.InQuint); // Ensure the screen doesn't expire until all the outwards fade operations have completed.
this.FadeOut(150); this.Delay(content_out_duration).FadeOut();
ApplyToBackground(b => b.IgnoreUserSettings.Value = true); ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
BackgroundBrightnessReduction = false; BackgroundBrightnessReduction = false;
Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
return base.OnExiting(next); return base.OnExiting(next);
} }
@ -344,6 +348,7 @@ namespace osu.Game.Screens.Play
content.FadeInFromZero(400); content.FadeInFromZero(400);
content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer); content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer);
lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint);
ApplyToBackground(b => b?.FadeColour(Color4.White, 800, Easing.OutQuint)); ApplyToBackground(b => b?.FadeColour(Color4.White, 800, Easing.OutQuint));
} }
@ -353,8 +358,9 @@ namespace osu.Game.Screens.Play
// Ensure the logo is no longer tracking before we scale the content // Ensure the logo is no longer tracking before we scale the content
content.StopTracking(); content.StopTracking();
content.ScaleTo(0.7f, 300, Easing.InQuint); content.ScaleTo(0.7f, content_out_duration * 2, Easing.OutQuint);
content.FadeOut(250); content.FadeOut(content_out_duration, Easing.OutQuint);
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, content_out_duration);
} }
private void pushWhenLoaded() private void pushWhenLoaded()
@ -381,7 +387,7 @@ namespace osu.Game.Screens.Play
contentOut(); contentOut();
TransformSequence<PlayerLoader> pushSequence = this.Delay(250); TransformSequence<PlayerLoader> pushSequence = this.Delay(content_out_duration);
// only show if the warning was created (i.e. the beatmap needs it) // only show if the warning was created (i.e. the beatmap needs it)
// and this is not a restart of the map (the warning expires after first load). // and this is not a restart of the map (the warning expires after first load).
@ -400,6 +406,11 @@ namespace osu.Game.Screens.Play
}) })
.Delay(EpilepsyWarning.FADE_DURATION); .Delay(EpilepsyWarning.FADE_DURATION);
} }
else
{
// This goes hand-in-hand with the restoration of low pass filter in contentOut().
this.TransformBindableTo(volumeAdjustment, 0, content_out_duration, Easing.OutCubic);
}
pushSequence.Schedule(() => pushSequence.Schedule(() =>
{ {