Skip to content

Commit

Permalink
Added BiquadFilter to AudioEditor demo.
Browse files Browse the repository at this point in the history
  • Loading branch information
KristofferStrube committed Oct 12, 2023
1 parent 730a113 commit 42f5da8
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
new(typeof(AudioDestination), element => element.TagName is "RECT" && element.GetAttribute("data-elementtype") == "audio-destination"),
new(typeof(Gain), element => element.TagName is "RECT" && element.GetAttribute("data-elementtype") == "gain"),
new(typeof(Analyser), element => element.TagName is "RECT" && element.GetAttribute("data-elementtype") == "analyser"),
new(typeof(BiquadFilter), element => element.TagName is "RECT" && element.GetAttribute("data-elementtype") == "biquad-filter"),
new(typeof(Connector), element => element.TagName is "LINE" && element.GetAttribute("data-elementtype") == "connector"),
};

Expand All @@ -60,6 +61,7 @@
new(typeof(AddNewOscillatorMenuItem), (_,_) => true),
new(typeof(AddNewGainMenuItem), (_,_) => true),
new(typeof(AddNewAnalyserMenuItem), (_,_) => true),
new(typeof(AddNewBiquadFilterMenuItem), (_,_) => true),
};

[Parameter]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ internal static class FloatExtensions
{
internal static string AsString(this float f)
{
return Math.Round(f, 9).ToString(CultureInfo.InvariantCulture);
return Math.Round(f, 4).ToString(CultureInfo.InvariantCulture);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@using BlazorContextMenu
<Item OnClick="_ => BiquadFilter.AddNew(SVGEditor)"><div class="icon">🅱</div> New Biquad Filter</Item>
@code {
[CascadingParameter]
public required SVGEditor.SVGEditor SVGEditor { get; set; }

[Parameter]
public required object Data { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
@using BlazorContextMenu
@using KristofferStrube.Blazor.SVGEditor.Extensions
@inherits NodeEditor<BiquadFilter>

<ContextMenuTrigger MenuId="SVGMenu" WrapperTag="g" Data=@SVGElement MouseButtonTrigger="SVGElement.ShouldTriggerContextMenu ? MouseButtonTrigger.Right : (MouseButtonTrigger)4">
<g transform="translate(@SVGElement.SVG.Translate.x.AsString() @SVGElement.SVG.Translate.y.AsString()) scale(@SVGElement.SVG.Scale.AsString())">
<rect @ref=ElementReference
@onfocusin="FocusElement"
@onfocusout="UnfocusElement"
@onpointerdown="SelectAsync"
@onkeyup="KeyUp"
tabindex="@(SVGElement.IsChildElement ? -1 : 0)"
x=@SVGElement.X.AsString()
y=@SVGElement.Y.AsString()
width=@SVGElement.Width.AsString()
height=@SVGElement.Height.AsString()
stroke="@SVGElement.Stroke"
stroke-width="@SVGElement.StrokeWidth"
stroke-linecap="@SVGElement.StrokeLinecap.AsString()"
stroke-linejoin="@SVGElement.StrokeLinejoin.AsString()"
stroke-dasharray="@SVGElement.StrokeDasharray"
stroke-dashoffset="@SVGElement.StrokeDashoffset.AsString()"
fill="@SVGElement.Fill"
style="filter:brightness(@(SVGElement.Selected ? "0.9" : "1"))">
</rect>
<foreignObject @onpointermove:stopPropagation=!ChildContentIsNoninteractive x="@((SVGElement.X+10).AsString())" y="@((SVGElement.Y+10).AsString())" height="260" width="200" style="border:solid @(SVGElement.StrokeWidth)px @(SVGElement.Stroke);padding:2;pointer-events:@(ChildContentIsNoninteractive ? "none" : "inherit");touch-action:@(ChildContentIsNoninteractive ? "none" : "inherit");">
Biquad Filter
<br />
<select @bind=SVGElement.Type @bind:after="SetType">
<option value="@BiquadFilterType.Lowpass" title="A lowpass filter allows frequencies below the cutoff frequency to pass through and attenuates frequencies above the cutoff.">Lowpass</option>
<option value="@BiquadFilterType.Highpass" title="A highpass filter is the opposite of a lowpass filter. Frequencies above the cutoff frequency are passed through, but frequencies below the cutoff are attenuated.">Highpass</option>
<option value="@BiquadFilterType.Bandpass" title="A bandpass filter allows a range of frequencies to pass through and attenuates the frequencies below and above this frequency range.">Bandpass</option>
<option value="@BiquadFilterType.Lowshelf" title="The lowshelf filter allows all frequencies through, but adds a boost (or attenuation) to the lower frequencies.">Lowshelf</option>
<option value="@BiquadFilterType.Highshelf" title="The highshelf filter is the opposite of the lowshelf filter and allows all frequencies through, but adds a boost to the higher frequencies.">Highshelf</option
<option value="@BiquadFilterType.Peaking" title="The peaking filter allows all frequencies through, but adds a boost (or attenuation) to a range of frequencies.">Peaking</option>
<option value="@BiquadFilterType.Notch" title="The notch filter (also known as a band-stop or band-rejection filter) is the opposite of a bandpass filter.">Notch</option>
<option value="@BiquadFilterType.Allpass" title="An allpass filter allows all frequencies through, but changes the phase relationship between the various frequencies.">Allpass</option>
</select>
<br />
@if (QAudioParam is not null && SVGElement.Type
is BiquadFilterType.Lowpass
or BiquadFilterType.Highpass
or BiquadFilterType.Bandpass
or BiquadFilterType.Peaking
or BiquadFilterType.Notch
or BiquadFilterType.Allpass)
{
<AudioParamSlider AudioParam="QAudioParam" Label="Q" Min="0" Max="2000" StepSize="1" UpdateCallback="f => SVGElement.Q = f" />
}
@if (DetuneAudioParam is not null)
{
<AudioParamSlider AudioParam="DetuneAudioParam" Label="Detune" Min="0" Max="100" StepSize="1" UpdateCallback="f => SVGElement.Detune = f" />
}
@if (FrequencyAudioParam is not null)
{
<AudioParamSlider AudioParam="FrequencyAudioParam" Label="Frequency" Min="0" Max="2000" StepSize="1" UpdateCallback="f => SVGElement.Frequency = f" />
}
@if (GainAudioParam is not null && SVGElement.Type
is BiquadFilterType.Lowshelf
or BiquadFilterType.Highshelf
or BiquadFilterType.Peaking)
{
<AudioParamSlider AudioParam="GainAudioParam" Label="Gain" Min="0" Max="200" StepSize="1" UpdateCallback="f => SVGElement.Gain = f" />
}
</foreignObject>
<ContextMenuTrigger MenuId="SVGMenu" WrapperTag="g" Data=@(new Port(SVGElement, 0, true) { Node = SVGElement, PortNumber = 0 }) MouseButtonTrigger="SVGElement.ShouldTriggerContextMenu ? MouseButtonTrigger.Right : (MouseButtonTrigger)4">
<circle @onpointerdown="() => SelectPort(0)" cx=@((SVGElement.X).AsString()) cy="@((SVGElement.Y+20).AsString())" r="10" fill="grey">

</circle>
</ContextMenuTrigger>
<ContextMenuTrigger MenuId="SVGMenu" WrapperTag="g" Data=@(new Port(SVGElement, 0, false)) MouseButtonTrigger="SVGElement.ShouldTriggerContextMenu ? MouseButtonTrigger.Right : (MouseButtonTrigger)4">
<circle cx=@((SVGElement.X+SVGElement.Width).AsString()) cy="@((SVGElement.Y+20).AsString())" r="10" fill="grey">

</circle>
</ContextMenuTrigger>
</g>
</ContextMenuTrigger>

@code {
private AudioParam? QAudioParam;
private AudioParam? DetuneAudioParam;
private AudioParam? FrequencyAudioParam;
private AudioParam? GainAudioParam;

[CascadingParameter]
public required AudioContext AudioContext { get; set; }

protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);

if (AudioContext is not null)
{
if (QAudioParam is null)
{
QAudioParam = await ((BiquadFilterNode)(await SVGElement.AudioNode(AudioContext))).GetQAsync();
}
if (DetuneAudioParam is null)
{
DetuneAudioParam = await ((BiquadFilterNode)(await SVGElement.AudioNode(AudioContext))).GetDetuneAsync();
}
if (FrequencyAudioParam is null)
{
FrequencyAudioParam = await ((BiquadFilterNode)(await SVGElement.AudioNode(AudioContext))).GetFrequencyAsync();
}
if (GainAudioParam is null)
{
GainAudioParam = await ((BiquadFilterNode)(await SVGElement.AudioNode(AudioContext))).GetGainAsync();
StateHasChanged();
}
}
}

private async Task SetType()
{
if (SVGElement.Type is {} type)
{
await ((BiquadFilterNode)await SVGElement.AudioNode(AudioContext)).SetTypeAsync(type);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using AngleSharp.Dom;
using KristofferStrube.Blazor.WebAudio.WasmExample.AudioEditor.NodeEditors;
using System.Globalization;
using static System.Text.Json.JsonSerializer;

namespace KristofferStrube.Blazor.WebAudio.WasmExample.AudioEditor;

public class BiquadFilter : Node
{
public BiquadFilter(IElement element, SVGEditor.SVGEditor svg) : base(element, svg) { }

private AudioNode? audioNode;
public override Func<AudioContext, Task<AudioNode>> AudioNode => async (context) =>
{
if (audioNode is null)
{
BiquadFilterOptions options = new();
options.Type = Type ?? options.Type;
options.Q = Q ?? options.Q;
options.Detune = Detune ?? options.Detune;
options.Frequency = Frequency ?? options.Frequency;
options.Gain = Gain ?? options.Gain;

BiquadFilterNode oscillator = await BiquadFilterNode.CreateAsync(context.JSRuntime, context, options);
audioNode = oscillator;
}
return audioNode;
};


public new float Height
{
get => 280;
set
{
base.Height = 280;
}
}

public BiquadFilterType? Type
{
get
{
return Element.GetAttribute("data-type") is { } value ? Deserialize<BiquadFilterType>(value) : null;
}
set
{
if (value is null)
{
_ = Element.RemoveAttribute("data-type");
}
else
{
Element.SetAttribute("data-type", Serialize(value.Value));
}
Changed?.Invoke(this);
}
}

public float? Q
{
get
{
return Element.GetAttribute("data-q") is { } value ? float.Parse(value, CultureInfo.InvariantCulture) : null;
}
set
{
if (value is null)
{
_ = Element.RemoveAttribute("data-q");
}
else
{
Element.SetAttribute("data-q", value.Value.AsString());
}
Changed?.Invoke(this);
}
}

public float? Detune
{
get
{
return Element.GetAttribute("data-detune") is { } value ? float.Parse(value, CultureInfo.InvariantCulture) : null;
}
set
{
if (value is null)
{
_ = Element.RemoveAttribute("data-detune");
}
else
{
Element.SetAttribute("data-detune", value.Value.AsString());
}
Changed?.Invoke(this);
}
}

public float? Frequency
{
get
{
return Element.GetAttribute("data-frequnecy") is { } value ? float.Parse(value, CultureInfo.InvariantCulture) : null;
}
set
{
if (value is null)
{
_ = Element.RemoveAttribute("data-frequnecy");
}
else
{
Element.SetAttribute("data-frequnecy", value.Value.AsString());
}
Changed?.Invoke(this);
}
}

public float? Gain
{
get
{
return Element.GetAttribute("data-gain") is { } value ? float.Parse(value, CultureInfo.InvariantCulture) : null;
}
set
{
if (value is null)
{
_ = Element.RemoveAttribute("data-gain");
}
else
{
Element.SetAttribute("data-gain", value.Value.AsString());
}
Changed?.Invoke(this);
}
}

public override Type Presenter => typeof(BiquadFilterEditor);

public static new void AddNew(SVGEditor.SVGEditor SVG)
{
IElement element = SVG.Document.CreateElement("RECT");
element.SetAttribute("data-elementtype", "biquad-filter");

BiquadFilter node = new(element, SVG)
{
Changed = SVG.UpdateInput,
Stroke = "#FFEE58",
StrokeWidth = "2",
Height = 280,
Width = 250,
};

(node.X, node.Y) = SVG.LocalDetransform(SVG.LastRightClick);

SVG.ClearSelectedShapes();
SVG.SelectShape(node);
SVG.AddElement(node);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,4 @@ public float? GainValue
SVG.SelectShape(node);
SVG.AddElement(node);
}

public override async void BeforeBeingRemoved()
{
if (audioNode is OscillatorNode { } oscillator)
{
await oscillator.StopAsync();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public async Task<AudioParam> GetDetuneAsync()
public async Task<AudioParam> GetQAsync()
{
IJSObjectReference helper = await webAudioHelperTask.Value;
IJSObjectReference jSInstance = await helper.InvokeAsync<IJSObjectReference>("getAttribute", JSReference, "q");
IJSObjectReference jSInstance = await helper.InvokeAsync<IJSObjectReference>("getAttribute", JSReference, "Q");
return await AudioParam.CreateAsync(JSRuntime, jSInstance);
}

Expand Down

0 comments on commit 42f5da8

Please sign in to comment.