mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 16:52:54 +08:00
Merge pull request #14892 from nekodex/popup-filter-effect
Add a dynamic audio `Filter` effect component and use it with `DialogOverlay` popups
This commit is contained in:
commit
9b404d983c
127
osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
Normal file
127
osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
// 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 ManagedBass.Fx;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Audio.Track;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Audio.Effects;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Audio
|
||||||
|
{
|
||||||
|
public class TestSceneAudioFilter : OsuTestScene
|
||||||
|
{
|
||||||
|
private OsuSpriteText lowpassText;
|
||||||
|
private AudioFilter lowpassFilter;
|
||||||
|
|
||||||
|
private OsuSpriteText highpassText;
|
||||||
|
private AudioFilter highpassFilter;
|
||||||
|
|
||||||
|
private Track track;
|
||||||
|
|
||||||
|
private WaveformTestBeatmap beatmap;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(AudioManager audio)
|
||||||
|
{
|
||||||
|
beatmap = new WaveformTestBeatmap(audio);
|
||||||
|
track = beatmap.LoadTrack();
|
||||||
|
|
||||||
|
Add(new FillFlowContainer
|
||||||
|
{
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
lowpassFilter = new AudioFilter(audio.TrackMixer),
|
||||||
|
highpassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("Play Track", () => track.Start());
|
||||||
|
waitTrackPlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestLowPass()
|
||||||
|
{
|
||||||
|
AddStep("Filter Sweep", () =>
|
||||||
|
{
|
||||||
|
lowpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
|
||||||
|
.CutoffTo(0, 2000, Easing.OutCubic);
|
||||||
|
});
|
||||||
|
|
||||||
|
waitTrackPlay();
|
||||||
|
|
||||||
|
AddStep("Filter Sweep (reverse)", () =>
|
||||||
|
{
|
||||||
|
lowpassFilter.CutoffTo(0).Then()
|
||||||
|
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
|
||||||
|
});
|
||||||
|
|
||||||
|
waitTrackPlay();
|
||||||
|
AddStep("Stop track", () => track.Stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHighPass()
|
||||||
|
{
|
||||||
|
AddStep("Filter Sweep", () =>
|
||||||
|
{
|
||||||
|
highpassFilter.CutoffTo(0).Then()
|
||||||
|
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
|
||||||
|
});
|
||||||
|
|
||||||
|
waitTrackPlay();
|
||||||
|
|
||||||
|
AddStep("Filter Sweep (reverse)", () =>
|
||||||
|
{
|
||||||
|
highpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
|
||||||
|
.CutoffTo(0, 2000, Easing.OutCubic);
|
||||||
|
});
|
||||||
|
|
||||||
|
waitTrackPlay();
|
||||||
|
|
||||||
|
AddStep("Stop track", () => track.Stop());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void waitTrackPlay() => AddWaitStep("Let track play", 10);
|
||||||
|
}
|
||||||
|
}
|
137
osu.Game/Audio/Effects/AudioFilter.cs
Normal file
137
osu.Game/Audio/Effects/AudioFilter.cs
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
using ManagedBass.Fx;
|
||||||
|
using osu.Framework.Audio.Mixing;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Audio.Effects
|
||||||
|
{
|
||||||
|
public class AudioFilter : Component, ITransformableFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum cutoff frequency that can be used with a low-pass filter.
|
||||||
|
/// This is equal to nyquist - 1hz.
|
||||||
|
/// </summary>
|
||||||
|
public const int MAX_LOWPASS_CUTOFF = 22049; // nyquist - 1hz
|
||||||
|
|
||||||
|
private readonly AudioMixer mixer;
|
||||||
|
private readonly BQFParameters filter;
|
||||||
|
private readonly BQFType type;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current cutoff of this filter.
|
||||||
|
/// </summary>
|
||||||
|
public BindableNumber<int> Cutoff { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A Component that implements a BASS FX BiQuad Filter Effect.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mixer">The mixer this effect should be applied to.</param>
|
||||||
|
/// <param name="type">The type of filter (e.g. LowPass, HighPass, etc)</param>
|
||||||
|
public AudioFilter(AudioMixer mixer, BQFType type = BQFType.LowPass)
|
||||||
|
{
|
||||||
|
this.mixer = mixer;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
lFilter = type,
|
||||||
|
fCenter = initialCutoff,
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Don't start attached if this is low-pass or high-pass filter (as they have special auto-attach/detach logic)
|
||||||
|
if (type != BQFType.LowPass && type != BQFType.HighPass)
|
||||||
|
attachFilter();
|
||||||
|
|
||||||
|
Cutoff.ValueChanged += updateFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void attachFilter()
|
||||||
|
{
|
||||||
|
Debug.Assert(!mixer.Effects.Contains(filter));
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
detachFilter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cutoff.OldValue >= MAX_LOWPASS_CUTOFF && cutoff.NewValue < MAX_LOWPASS_CUTOFF)
|
||||||
|
attachFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz.
|
||||||
|
if (type == BQFType.HighPass)
|
||||||
|
{
|
||||||
|
if (cutoff.NewValue <= 1)
|
||||||
|
{
|
||||||
|
detachFilter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cutoff.OldValue <= 1 && cutoff.NewValue > 1)
|
||||||
|
attachFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
var filterIndex = mixer.Effects.IndexOf(filter);
|
||||||
|
if (filterIndex < 0) return;
|
||||||
|
|
||||||
|
if (mixer.Effects[filterIndex] is BQFParameters existingFilter)
|
||||||
|
{
|
||||||
|
existingFilter.fCenter = cutoff.NewValue;
|
||||||
|
|
||||||
|
// required to update effect with new parameters.
|
||||||
|
mixer.Effects[filterIndex] = existingFilter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
if (mixer.Effects.Contains(filter))
|
||||||
|
detachFilter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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));
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,10 @@ using osu.Game.Overlays.Dialog;
|
|||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Audio.Effects;
|
||||||
|
|
||||||
namespace osu.Game.Overlays
|
namespace osu.Game.Overlays
|
||||||
{
|
{
|
||||||
@ -18,6 +21,8 @@ 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 AudioFilter lowPassFilter;
|
||||||
|
|
||||||
public PopupDialog CurrentDialog { get; private set; }
|
public PopupDialog CurrentDialog { get; private set; }
|
||||||
|
|
||||||
public DialogOverlay()
|
public DialogOverlay()
|
||||||
@ -34,6 +39,12 @@ namespace osu.Game.Overlays
|
|||||||
Origin = Anchor.BottomCentre;
|
Origin = Anchor.BottomCentre;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(AudioManager audio)
|
||||||
|
{
|
||||||
|
AddInternal(lowPassFilter = new AudioFilter(audio.TrackMixer));
|
||||||
|
}
|
||||||
|
|
||||||
public void Push(PopupDialog dialog)
|
public void Push(PopupDialog dialog)
|
||||||
{
|
{
|
||||||
if (dialog == CurrentDialog || dialog.State.Value != Visibility.Visible) return;
|
if (dialog == CurrentDialog || dialog.State.Value != Visibility.Visible) return;
|
||||||
@ -71,12 +82,15 @@ namespace osu.Game.Overlays
|
|||||||
{
|
{
|
||||||
base.PopIn();
|
base.PopIn();
|
||||||
this.FadeIn(PopupDialog.ENTER_DURATION, Easing.OutQuint);
|
this.FadeIn(PopupDialog.ENTER_DURATION, Easing.OutQuint);
|
||||||
|
lowPassFilter.CutoffTo(300, 100, Easing.OutCubic);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void PopOut()
|
protected override void PopOut()
|
||||||
{
|
{
|
||||||
base.PopOut();
|
base.PopOut();
|
||||||
|
|
||||||
|
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
|
||||||
|
|
||||||
if (CurrentDialog?.State.Value == Visibility.Visible)
|
if (CurrentDialog?.State.Value == Visibility.Visible)
|
||||||
{
|
{
|
||||||
CurrentDialog.Hide();
|
CurrentDialog.Hide();
|
||||||
|
@ -508,6 +508,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
if (isNewBeatmap || HasUnsavedChanges)
|
if (isNewBeatmap || HasUnsavedChanges)
|
||||||
{
|
{
|
||||||
|
samplePlaybackDisabled.Value = true;
|
||||||
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave, cancelExit));
|
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave, cancelExit));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -756,7 +757,11 @@ namespace osu.Game.Screens.Edit
|
|||||||
ClipboardContent = editorBeatmap.BeatmapInfo.RulesetID == nextBeatmap.RulesetID ? clipboard.Value : string.Empty
|
ClipboardContent = editorBeatmap.BeatmapInfo.RulesetID == nextBeatmap.RulesetID ? clipboard.Value : string.Empty
|
||||||
});
|
});
|
||||||
|
|
||||||
private void cancelExit() => loader?.CancelPendingDifficultySwitch();
|
private void cancelExit()
|
||||||
|
{
|
||||||
|
samplePlaybackDisabled.Value = false;
|
||||||
|
loader?.CancelPendingDifficultySwitch();
|
||||||
|
}
|
||||||
|
|
||||||
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);
|
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user