mirror of
https://github.com/ppy/osu.git
synced 2025-03-20 05:47:45 +08:00
Refactor Filter to behave closer to a Transformable
This commit is contained in:
parent
1304b55c41
commit
2a4a376b87
@ -6,74 +6,77 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Audio.Effects;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Audio
|
||||
{
|
||||
public class TestSceneFilter : OsuTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private AudioManager audio { get; set; }
|
||||
|
||||
private WorkingBeatmap testBeatmap;
|
||||
private Filter lowPassFilter;
|
||||
private Filter highPassFilter;
|
||||
private Filter bandPassFilter;
|
||||
|
||||
private OsuSpriteText lowpassText;
|
||||
private OsuSpriteText highpassText;
|
||||
private Filter lowpassFilter;
|
||||
private Filter highpassFilter;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(AudioManager 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,
|
||||
SweepCutoffStart = 2000,
|
||||
SweepCutoffEnd = 150,
|
||||
SweepDuration = 1000
|
||||
},
|
||||
highPassFilter = new Filter(audio.TrackMixer)
|
||||
{
|
||||
FilterType = BQFType.HighPass,
|
||||
SweepCutoffStart = 150,
|
||||
SweepCutoffEnd = 2000,
|
||||
SweepDuration = 1000
|
||||
},
|
||||
bandPassFilter = new Filter(audio.TrackMixer)
|
||||
{
|
||||
FilterType = BQFType.BandPass,
|
||||
SweepCutoffStart = 150,
|
||||
SweepCutoffEnd = 20000,
|
||||
SweepDuration = 1000
|
||||
},
|
||||
lowpassText = new OsuSpriteText
|
||||
{
|
||||
Padding = new MarginPadding(20),
|
||||
Text = $"Low Pass: {lowpassFilter.Cutoff.Value}hz",
|
||||
Font = new FontUsage(size: 40)
|
||||
},
|
||||
new OsuSliderBar<int>
|
||||
{
|
||||
Width = 500,
|
||||
Height = 50,
|
||||
Padding = new MarginPadding(20),
|
||||
Current = { BindTarget = lowpassFilter.Cutoff }
|
||||
},
|
||||
highpassText = new OsuSpriteText
|
||||
{
|
||||
Padding = new MarginPadding(20),
|
||||
Text = $"High Pass: {highpassFilter.Cutoff.Value}hz",
|
||||
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]
|
||||
public void TestLowPass()
|
||||
{
|
||||
testFilter(lowPassFilter);
|
||||
}
|
||||
public void TestLowPass() => testFilter(lowpassFilter, lowpassFilter.MaxCutoff, 0);
|
||||
|
||||
[Test]
|
||||
public void TestHighPass()
|
||||
{
|
||||
testFilter(highPassFilter);
|
||||
}
|
||||
public void TestHighPass() => testFilter(highpassFilter, 0, highpassFilter.MaxCutoff);
|
||||
|
||||
[Test]
|
||||
public void TestBandPass()
|
||||
{
|
||||
testFilter(bandPassFilter);
|
||||
}
|
||||
|
||||
private void testFilter(Filter filter)
|
||||
private void testFilter(Filter filter, int cutoffFrom, int cutoffTo)
|
||||
{
|
||||
Add(filter);
|
||||
AddStep("Prepare Track", () =>
|
||||
{
|
||||
testBeatmap = new WaveformTestBeatmap(audio);
|
||||
testBeatmap.LoadTrack();
|
||||
});
|
||||
AddStep("Play Track", () =>
|
||||
@ -81,14 +84,19 @@ namespace osu.Game.Tests.Visual.Audio
|
||||
testBeatmap.Track.Start();
|
||||
});
|
||||
AddWaitStep("Let track play", 10);
|
||||
AddStep("Enable Filter", filter.Enable);
|
||||
AddWaitStep("Let track play", 10);
|
||||
AddStep("Disable Filter", filter.Disable);
|
||||
AddWaitStep("Let track play", 10);
|
||||
AddStep("Stop Track", () =>
|
||||
AddStep("Filter Sweep", () =>
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,66 +8,87 @@ using osu.Framework.Graphics;
|
||||
|
||||
namespace osu.Game.Audio.Effects
|
||||
{
|
||||
public class Filter : Component
|
||||
public class Filter : Component, ITransformableFilter
|
||||
{
|
||||
public BQFType FilterType = BQFType.LowPass;
|
||||
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>();
|
||||
public readonly int MaxCutoff;
|
||||
private readonly AudioMixer mixer;
|
||||
private BQFParameters filter;
|
||||
private readonly BQFParameters filter;
|
||||
private readonly BQFType type;
|
||||
|
||||
public BindableNumber<int> Cutoff { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A BiQuad filter that performs a filter-sweep when toggled on or off.
|
||||
/// </summary>
|
||||
/// <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.type = type;
|
||||
|
||||
public void Enable()
|
||||
{
|
||||
attachFilter();
|
||||
this.TransformBindableTo(filterFreq, SweepCutoffEnd, SweepDuration, SweepEasing);
|
||||
}
|
||||
var initialCutoff = 1;
|
||||
|
||||
public void Disable()
|
||||
{
|
||||
this.TransformBindableTo(filterFreq, SweepCutoffStart, SweepDuration, SweepEasing).OnComplete(_ => detachFilter());
|
||||
}
|
||||
// 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)
|
||||
// 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()
|
||||
{
|
||||
if (IsActive) return;
|
||||
case BQFType.LowPass:
|
||||
MaxCutoff = initialCutoff = 14000; // beyond (roughly) this value, the low-pass filter audibly wraps/reflects
|
||||
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
|
||||
{
|
||||
lFilter = FilterType,
|
||||
fCenter = filterFreq.Value = SweepCutoffStart
|
||||
lFilter = type,
|
||||
fCenter = initialCutoff
|
||||
};
|
||||
|
||||
mixer.Effects.Add(filter);
|
||||
filterFreq.ValueChanged += updateFilter;
|
||||
IsActive = true;
|
||||
attachFilter();
|
||||
|
||||
Cutoff.ValueChanged += updateFilter;
|
||||
Cutoff.Value = initialCutoff;
|
||||
}
|
||||
|
||||
private void detachFilter()
|
||||
{
|
||||
if (!IsActive) return;
|
||||
private void attachFilter() => mixer.Effects.Add(filter);
|
||||
|
||||
filterFreq.ValueChanged -= updateFilter;
|
||||
mixer.Effects.Remove(filter);
|
||||
IsActive = false;
|
||||
}
|
||||
private void detachFilter() => mixer.Effects.Remove(filter);
|
||||
|
||||
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);
|
||||
if (filterIndex < 0) return;
|
||||
|
||||
|
54
osu.Game/Audio/Effects/ITransformableFilter.cs
Normal file
54
osu.Game/Audio/Effects/ITransformableFilter.cs
Normal 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));
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ namespace osu.Game.Overlays
|
||||
protected override string PopInSampleName => "UI/dialog-pop-in";
|
||||
protected override string PopOutSampleName => "UI/dialog-pop-out";
|
||||
|
||||
private Filter filter;
|
||||
private Filter lpFilter;
|
||||
|
||||
public PopupDialog CurrentDialog { get; private set; }
|
||||
|
||||
@ -42,7 +42,7 @@ namespace osu.Game.Overlays
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
AddInternal(filter = new Filter(audio.TrackMixer));
|
||||
AddInternal(lpFilter = new Filter(audio.TrackMixer));
|
||||
}
|
||||
|
||||
public void Push(PopupDialog dialog)
|
||||
@ -82,15 +82,13 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
base.PopIn();
|
||||
this.FadeIn(PopupDialog.ENTER_DURATION, Easing.OutQuint);
|
||||
filter.Enable();
|
||||
lpFilter.CutoffTo(2000).Then().CutoffTo(150, 100, Easing.OutCubic);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
base.PopOut();
|
||||
|
||||
filter.Disable();
|
||||
|
||||
if (CurrentDialog?.State.Value == Visibility.Visible)
|
||||
{
|
||||
CurrentDialog.Hide();
|
||||
@ -98,6 +96,7 @@ namespace osu.Game.Overlays
|
||||
}
|
||||
|
||||
this.FadeOut(PopupDialog.EXIT_DURATION, Easing.InSine);
|
||||
lpFilter.CutoffTo(2000, 100, Easing.InCubic).Then().CutoffTo(lpFilter.MaxCutoff);
|
||||
}
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
|
Loading…
x
Reference in New Issue
Block a user