Refactor Filter to behave closer to a Transformable

This commit is contained in:
Jamie Taylor 2021-10-02 01:21:55 +09:00
parent 1304b55c41
commit 2a4a376b87
No known key found for this signature in database
GPG Key ID: 2ACFA8B6370B8C8C
4 changed files with 175 additions and 93 deletions

View File

@ -6,74 +6,77 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Audio.Effects; using osu.Game.Audio.Effects;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Tests.Visual.Audio namespace osu.Game.Tests.Visual.Audio
{ {
public class TestSceneFilter : OsuTestScene public class TestSceneFilter : OsuTestScene
{ {
[Resolved]
private AudioManager audio { get; set; }
private WorkingBeatmap testBeatmap; private WorkingBeatmap testBeatmap;
private Filter lowPassFilter;
private Filter highPassFilter; private OsuSpriteText lowpassText;
private Filter bandPassFilter; private OsuSpriteText highpassText;
private Filter lowpassFilter;
private Filter highpassFilter;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(AudioManager audio)
{ {
testBeatmap = new WaveformTestBeatmap(audio); testBeatmap = new WaveformTestBeatmap(audio);
AddRange(new Drawable[] lowpassFilter = new Filter(audio.TrackMixer);
highpassFilter = new Filter(audio.TrackMixer, BQFType.HighPass);
Add(new FillFlowContainer
{ {
lowPassFilter = new Filter(audio.TrackMixer) Children = new Drawable[]
{ {
FilterType = BQFType.LowPass, lowpassText = new OsuSpriteText
SweepCutoffStart = 2000, {
SweepCutoffEnd = 150, Padding = new MarginPadding(20),
SweepDuration = 1000 Text = $"Low Pass: {lowpassFilter.Cutoff.Value}hz",
}, Font = new FontUsage(size: 40)
highPassFilter = new Filter(audio.TrackMixer) },
{ new OsuSliderBar<int>
FilterType = BQFType.HighPass, {
SweepCutoffStart = 150, Width = 500,
SweepCutoffEnd = 2000, Height = 50,
SweepDuration = 1000 Padding = new MarginPadding(20),
}, Current = { BindTarget = lowpassFilter.Cutoff }
bandPassFilter = new Filter(audio.TrackMixer) },
{ highpassText = new OsuSpriteText
FilterType = BQFType.BandPass, {
SweepCutoffStart = 150, Padding = new MarginPadding(20),
SweepCutoffEnd = 20000, Text = $"High Pass: {highpassFilter.Cutoff.Value}hz",
SweepDuration = 1000 Font = new FontUsage(size: 40)
}, },
new OsuSliderBar<int>
{
Width = 500,
Height = 50,
Padding = new MarginPadding(20),
Current = { BindTarget = highpassFilter.Cutoff }
}
}
}); });
lowpassFilter.Cutoff.ValueChanged += e => lowpassText.Text = $"Low Pass: {e.NewValue}hz";
highpassFilter.Cutoff.ValueChanged += e => highpassText.Text = $"High Pass: {e.NewValue}hz";
} }
[Test] [Test]
public void TestLowPass() public void TestLowPass() => testFilter(lowpassFilter, lowpassFilter.MaxCutoff, 0);
{
testFilter(lowPassFilter);
}
[Test] [Test]
public void TestHighPass() public void TestHighPass() => testFilter(highpassFilter, 0, highpassFilter.MaxCutoff);
{
testFilter(highPassFilter);
}
[Test] private void testFilter(Filter filter, int cutoffFrom, int cutoffTo)
public void TestBandPass()
{
testFilter(bandPassFilter);
}
private void testFilter(Filter filter)
{ {
Add(filter);
AddStep("Prepare Track", () => AddStep("Prepare Track", () =>
{ {
testBeatmap = new WaveformTestBeatmap(audio);
testBeatmap.LoadTrack(); testBeatmap.LoadTrack();
}); });
AddStep("Play Track", () => AddStep("Play Track", () =>
@ -81,14 +84,19 @@ namespace osu.Game.Tests.Visual.Audio
testBeatmap.Track.Start(); testBeatmap.Track.Start();
}); });
AddWaitStep("Let track play", 10); AddWaitStep("Let track play", 10);
AddStep("Enable Filter", filter.Enable); AddStep("Filter Sweep", () =>
AddWaitStep("Let track play", 10);
AddStep("Disable Filter", filter.Disable);
AddWaitStep("Let track play", 10);
AddStep("Stop Track", () =>
{ {
testBeatmap.Track.Stop(); filter.CutoffTo(cutoffFrom).Then()
.CutoffTo(cutoffTo, 2000, cutoffFrom > cutoffTo ? Easing.OutCubic : Easing.InCubic);
}); });
AddWaitStep("Let track play", 10);
AddStep("Filter Sweep (reverse)", () =>
{
filter.CutoffTo(cutoffTo).Then()
.CutoffTo(cutoffFrom, 2000, cutoffTo > cutoffFrom ? Easing.OutCubic : Easing.InCubic);
});
AddWaitStep("Let track play", 10);
AddStep("Stop track", () => testBeatmap.Track.Stop());
} }
} }
} }

View File

@ -8,66 +8,87 @@ using osu.Framework.Graphics;
namespace osu.Game.Audio.Effects namespace osu.Game.Audio.Effects
{ {
public class Filter : Component public class Filter : Component, ITransformableFilter
{ {
public BQFType FilterType = BQFType.LowPass; public readonly int MaxCutoff;
public float SweepCutoffStart = 2000;
public float SweepCutoffEnd = 150;
public float SweepDuration = 100;
public Easing SweepEasing = Easing.None;
public bool IsActive { get; private set; }
private readonly Bindable<float> filterFreq = new Bindable<float>();
private readonly AudioMixer mixer; private readonly AudioMixer mixer;
private BQFParameters filter; private readonly BQFParameters filter;
private readonly BQFType type;
public BindableNumber<int> Cutoff { get; }
/// <summary> /// <summary>
/// A BiQuad filter that performs a filter-sweep when toggled on or off. /// A BiQuad filter that performs a filter-sweep when toggled on or off.
/// </summary> /// </summary>
/// <param name="mixer">The mixer this effect should be attached to.</param> /// <param name="mixer">The mixer this effect should be attached to.</param>
public Filter(AudioMixer mixer) /// <param name="type">The type of filter (e.g. LowPass, HighPass, etc)</param>
public Filter(AudioMixer mixer, BQFType type = BQFType.LowPass)
{ {
this.mixer = mixer; this.mixer = mixer;
} this.type = type;
public void Enable() var initialCutoff = 1;
{
attachFilter();
this.TransformBindableTo(filterFreq, SweepCutoffEnd, SweepDuration, SweepEasing);
}
public void Disable() // These max cutoff values are a work-around for BASS' BiQuad filters behaving weirdly when approaching nyquist.
{ // Note that these values assume a sample rate of 44100 (as per BassAudioMixer in osu.Framework)
this.TransformBindableTo(filterFreq, SweepCutoffStart, SweepDuration, SweepEasing).OnComplete(_ => detachFilter()); // See also https://www.un4seen.com/forum/?topic=19542.0 for more information.
} switch (type)
{
case BQFType.HighPass:
MaxCutoff = 21968; // beyond this value, the high-pass cuts out
break;
private void attachFilter() case BQFType.LowPass:
{ MaxCutoff = initialCutoff = 14000; // beyond (roughly) this value, the low-pass filter audibly wraps/reflects
if (IsActive) return; break;
case BQFType.BandPass:
MaxCutoff = 16000; // beyond (roughly) this value, the band-pass filter audibly wraps/reflects
break;
default:
MaxCutoff = 22050; // default to nyquist for other filter types, TODO: handle quirks of other filter types
break;
}
Cutoff = new BindableNumber<int>
{
MinValue = 1,
MaxValue = MaxCutoff
};
filter = new BQFParameters filter = new BQFParameters
{ {
lFilter = FilterType, lFilter = type,
fCenter = filterFreq.Value = SweepCutoffStart fCenter = initialCutoff
}; };
mixer.Effects.Add(filter); attachFilter();
filterFreq.ValueChanged += updateFilter;
IsActive = true; Cutoff.ValueChanged += updateFilter;
Cutoff.Value = initialCutoff;
} }
private void detachFilter() private void attachFilter() => mixer.Effects.Add(filter);
{
if (!IsActive) return;
filterFreq.ValueChanged -= updateFilter; private void detachFilter() => mixer.Effects.Remove(filter);
mixer.Effects.Remove(filter);
IsActive = false;
}
private void updateFilter(ValueChangedEvent<float> cutoff) private void updateFilter(ValueChangedEvent<int> cutoff)
{ {
// This is another workaround for quirks in BASS' BiQuad filters.
// Because the cutoff can't be set above ~14khz (i.e. outside of human hearing range) without the aforementioned wrapping/reflecting quirk occuring, we instead
// remove the effect from the mixer when the cutoff is at maximum so that a LowPass filter isn't always attenuating high frequencies just by existing.
if (type == BQFType.LowPass)
{
if (cutoff.NewValue >= MaxCutoff)
{
detachFilter();
return;
}
if (cutoff.OldValue >= MaxCutoff && cutoff.NewValue < MaxCutoff)
attachFilter();
}
var filterIndex = mixer.Effects.IndexOf(filter); var filterIndex = mixer.Effects.IndexOf(filter);
if (filterIndex < 0) return; if (filterIndex < 0) return;

View File

@ -0,0 +1,54 @@
// 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 osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
namespace osu.Game.Audio.Effects
{
public interface ITransformableFilter
{
/// <summary>
/// The filter cutoff.
/// </summary>
BindableNumber<int> Cutoff { get; }
}
public static class FilterableAudioComponentExtensions
{
/// <summary>
/// Smoothly adjusts filter cutoff over time.
/// </summary>
/// <returns>A <see cref="TransformSequence{T}"/> to which further transforms can be added.</returns>
public static TransformSequence<T> CutoffTo<T>(this T component, int newCutoff, double duration = 0, Easing easing = Easing.None)
where T : class, ITransformableFilter, IDrawable
=> component.CutoffTo(newCutoff, duration, new DefaultEasingFunction(easing));
/// <summary>
/// Smoothly adjusts filter cutoff over time.
/// </summary>
/// <returns>A <see cref="TransformSequence{T}"/> to which further transforms can be added.</returns>
public static TransformSequence<T> CutoffTo<T>(this TransformSequence<T> sequence, int newCutoff, double duration = 0, Easing easing = Easing.None)
where T : class, ITransformableFilter, IDrawable
=> sequence.CutoffTo(newCutoff, duration, new DefaultEasingFunction(easing));
/// <summary>
/// Smoothly adjusts filter cutoff over time.
/// </summary>
/// <returns>A <see cref="TransformSequence{T}"/> to which further transforms can be added.</returns>
public static TransformSequence<T> CutoffTo<T, TEasing>(this T component, int newCutoff, double duration, TEasing easing)
where T : class, ITransformableFilter, IDrawable
where TEasing : IEasingFunction
=> component.TransformBindableTo(component.Cutoff, newCutoff, duration, easing);
/// <summary>
/// Smoothly adjusts filter cutoff over time.
/// </summary>
/// <returns>A <see cref="TransformSequence{T}"/> to which further transforms can be added.</returns>
public static TransformSequence<T> CutoffTo<T, TEasing>(this TransformSequence<T> sequence, int newCutoff, double duration, TEasing easing)
where T : class, ITransformableFilter, IDrawable
where TEasing : IEasingFunction
=> sequence.Append(o => o.TransformBindableTo(o.Cutoff, newCutoff, duration, easing));
}
}

View File

@ -21,7 +21,7 @@ namespace osu.Game.Overlays
protected override string PopInSampleName => "UI/dialog-pop-in"; protected override string PopInSampleName => "UI/dialog-pop-in";
protected override string PopOutSampleName => "UI/dialog-pop-out"; protected override string PopOutSampleName => "UI/dialog-pop-out";
private Filter filter; private Filter lpFilter;
public PopupDialog CurrentDialog { get; private set; } public PopupDialog CurrentDialog { get; private set; }
@ -42,7 +42,7 @@ namespace osu.Game.Overlays
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio) private void load(AudioManager audio)
{ {
AddInternal(filter = new Filter(audio.TrackMixer)); AddInternal(lpFilter = new Filter(audio.TrackMixer));
} }
public void Push(PopupDialog dialog) public void Push(PopupDialog dialog)
@ -82,15 +82,13 @@ namespace osu.Game.Overlays
{ {
base.PopIn(); base.PopIn();
this.FadeIn(PopupDialog.ENTER_DURATION, Easing.OutQuint); this.FadeIn(PopupDialog.ENTER_DURATION, Easing.OutQuint);
filter.Enable(); lpFilter.CutoffTo(2000).Then().CutoffTo(150, 100, Easing.OutCubic);
} }
protected override void PopOut() protected override void PopOut()
{ {
base.PopOut(); base.PopOut();
filter.Disable();
if (CurrentDialog?.State.Value == Visibility.Visible) if (CurrentDialog?.State.Value == Visibility.Visible)
{ {
CurrentDialog.Hide(); CurrentDialog.Hide();
@ -98,6 +96,7 @@ namespace osu.Game.Overlays
} }
this.FadeOut(PopupDialog.EXIT_DURATION, Easing.InSine); this.FadeOut(PopupDialog.EXIT_DURATION, Easing.InSine);
lpFilter.CutoffTo(2000, 100, Easing.InCubic).Then().CutoffTo(lpFilter.MaxCutoff);
} }
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)