From b91979d04374feb5574164cb802f8a1e0aa42b66 Mon Sep 17 00:00:00 2001 From: KristofferStrube Date: Sun, 2 Feb 2025 14:48:36 +0100 Subject: [PATCH] Added demo of using Custom Waveform. --- .../Nodes/MediaStreamAudioSource.cs | 2 +- .../ExpressionTemplates.cs | 102 +++++++++++++ ...rStrube.Blazor.WebAudio.WasmExample.csproj | 1 + .../Pages/Index.razor | 144 +++++++++++++++++- .../Shared/EnumSelector.razor | 24 +++ .../Shared/Plot.razor | 3 + .../Shared/TimeDomainPlot.razor | 3 + .../Shared/TimeDomainPlot.razor.cs | 58 +++++++ .../wwwroot/index.html | 2 +- .../PeriodicWave.cs | 19 ++- .../KristofferStrube.Blazor.WebAudio.js | 4 + 11 files changed, 353 insertions(+), 9 deletions(-) create mode 100644 samples/KristofferStrube.Blazor.WebAudio.WasmExample/CustomPeriodicWaves/ExpressionTemplates.cs create mode 100644 samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/EnumSelector.razor create mode 100644 samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/TimeDomainPlot.razor create mode 100644 samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/TimeDomainPlot.razor.cs diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/Nodes/MediaStreamAudioSource.cs b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/Nodes/MediaStreamAudioSource.cs index 8415997..d0c89e1 100644 --- a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/Nodes/MediaStreamAudioSource.cs +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/Nodes/MediaStreamAudioSource.cs @@ -16,8 +16,8 @@ public MediaStreamAudioSource(IElement element, SVGEditor.SVGEditor svg) : base( { await SetMediaStreamAudioSourceNode(context); } - return audioNode!; _ = audioNodeSlim.Release(); + return audioNode!; }; public new float Height diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/CustomPeriodicWaves/ExpressionTemplates.cs b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/CustomPeriodicWaves/ExpressionTemplates.cs new file mode 100644 index 0000000..9b41c1c --- /dev/null +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/CustomPeriodicWaves/ExpressionTemplates.cs @@ -0,0 +1,102 @@ +using KristofferStrube.Blazor.FormulaEditor; +using KristofferStrube.Blazor.FormulaEditor.BooleanExpressions; +using KristofferStrube.Blazor.FormulaEditor.Expressions; + +namespace KristofferStrube.Blazor.WebAudio.WasmExample.CustomPeriodicWaves; + +public static class ExpressionTemplates +{ + public static NumberReturningExpression SineWave(Identifier nIdentifier) => new CasesExpression() + { + Cases = [new() { + Value = new NumericExpression() { Value = 1 }, + Condition = new EqualsOperator() { First = new IdentifierExpression() { Value = nIdentifier }, Second = new NumericExpression() { Value = 1 } } + }], + Otherwise = new NumericExpression() { Value = 0 }, + }; + + public static NumberReturningExpression SquareWave(Identifier nIdentifier) => new MultiplicationOperator() + { + First = new FractionOperator() + { + Numerator = new NumericExpression() { Value = 2 }, + Denominator = new MultiplicationOperator() + { + First = new IdentifierExpression() { Value = nIdentifier }, + Second = new ConstantExpression() { Value = new Constant("π", Math.PI) }, + ExplicitOperator = false + } + }, + Second = new SubtractionOperator() + { + First = new NumericExpression() { Value = 1 }, + Second = new PowerOperator() + { + Value = new NumericExpression() { Value = -1, Parenthesis = true }, + Power = new IdentifierExpression() { Value = nIdentifier } + }, + Parenthesis = true + }, + ExplicitOperator = false + }; + + public static NumberReturningExpression SawtoothWave(Identifier nIdentifier) => new MultiplicationOperator() + { + First = new PowerOperator() + { + Value = new NumericExpression() { Value = -1, Parenthesis = true }, + Power = new AdditionOperator() + { + First = new IdentifierExpression() { Value = nIdentifier }, + Second = new NumericExpression() { Value = 1 }, + Parenthesis = true + } + }, + Second = new FractionOperator() + { + Numerator = new NumericExpression() { Value = 2 }, + Denominator = new MultiplicationOperator() + { + First = new IdentifierExpression() { Value = nIdentifier }, + Second = new ConstantExpression() { Value = new Constant("π", Math.PI) }, + ExplicitOperator = false + } + }, + ExplicitOperator = false + }; + + public static NumberReturningExpression TriangleWave(Identifier nIdentifier) => new FractionOperator() + { + Numerator = new MultiplicationOperator() + { + First = new NumericExpression() { Value = 8 }, + Second = new FunctionExpression() + { + Function = new Function("sin", v => Math.Sin(v)), + Input = new FractionOperator() + { + Numerator = new MultiplicationOperator() + { + First = new IdentifierExpression() { Value = nIdentifier }, + Second = new ConstantExpression() { Value = new Constant("π", Math.PI) }, + ExplicitOperator = false + }, + Denominator = new NumericExpression() { Value = 2 } + }, + Parenthesis = false + }, + ExplicitOperator = false + }, + Denominator = new PowerOperator() + { + Value = new MultiplicationOperator() + { + First = new ConstantExpression() { Value = new Constant("π", Math.PI) }, + Second = new IdentifierExpression() { Value = nIdentifier }, + ExplicitOperator = false, + Parenthesis = true + }, + Power = new NumericExpression() { Value = 2 } + } + }; +} diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/KristofferStrube.Blazor.WebAudio.WasmExample.csproj b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/KristofferStrube.Blazor.WebAudio.WasmExample.csproj index 5a19890..012f6ec 100644 --- a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/KristofferStrube.Blazor.WebAudio.WasmExample.csproj +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/KristofferStrube.Blazor.WebAudio.WasmExample.csproj @@ -12,6 +12,7 @@ + diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Pages/Index.razor b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Pages/Index.razor index d7afc78..58f2451 100644 --- a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Pages/Index.razor +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Pages/Index.razor @@ -1,5 +1,8 @@ @page "/" @using KristofferStrube.Blazor.DOM +@using KristofferStrube.Blazor.FormulaEditor +@using KristofferStrube.Blazor.FormulaEditor.Expressions +@using KristofferStrube.Blazor.WebAudio.WasmExample.CustomPeriodicWaves @implements IAsyncDisposable @inject IJSRuntime JSRuntime WebAudio - Playing Sound @@ -27,24 +30,92 @@ Status: { } + } +@if (oscillatorType is OscillatorType.Custom) +{ +
+ + + +
+ + +
+ + + + + @for (int i = 0; i < coefficientSeriesLength; i++) + { + + } + + + + + + @for (int i = 0; i < coefficientSeriesLength; i++) + { + int k = i; + + } + + + + @for (int i = 0; i < coefficientSeriesLength; i++) + { + int k = i; + + } + + +
@i
Real Coefficients
Imaginary Coefficients
+
+} + @code { AudioContext context = default!; GainNode gainNode = default!; + AnalyserNode analyser = default!; EventListener stateChangeListener = default!; AudioContextState state = AudioContextState.Closed; OscillatorNode? oscillator; + OscillatorType oscillatorType = OscillatorType.Sine; + PeriodicWave? customWave; + + private double n = 0; + private Identifier nIdentifier = default!; + private List identifiers = default!; + NumberReturningExpression customFormula = default!; + private string selectedFormulaPreset = "triangle"; + float[] realCoefficients = new float[10]; + float[] imagCoefficients = new float[10]; + int coefficientSeriesLength = 10; protected override async Task OnInitializedAsync() { + nIdentifier = new Identifier("n", () => n); + identifiers = [nIdentifier]; + CustomWavePresetUpdated(); + CalculateCoefficients(); + context = await AudioContext.CreateAsync(JSRuntime); AudioDestinationNode destination = await context.GetDestinationAsync(); gainNode = await GainNode.CreateAsync(JSRuntime, context, new() { Gain = 0.1f }); + analyser = await AnalyserNode.CreateAsync(JSRuntime, context); + await gainNode.ConnectAsync(destination); + await gainNode.ConnectAsync(analyser); stateChangeListener = await context.AddOnStateChangeEventListener(async (e) => { @@ -56,10 +127,24 @@ Status: public async Task PlaySound() { + if (oscillatorType is OscillatorType.Custom) + { + customWave = await PeriodicWave.CreateAsync(JSRuntime, context, new() + { + Real = realCoefficients, + Imag = imagCoefficients, + }); + } + else + { + customWave = null; + } + OscillatorOptions oscillatorOptions = new() { - Type = OscillatorType.Sine, - Frequency = Random.Shared.Next(100, 500) + Type = oscillatorType, + Frequency = 440, + PeriodicWave = customWave }; oscillator = await OscillatorNode.CreateAsync(JSRuntime, context, oscillatorOptions); await oscillator.ConnectAsync(gainNode); @@ -68,15 +153,64 @@ Status: public async Task StopSound() { - if (oscillator is null) return; - await oscillator.StopAsync(); - oscillator = null; + if (oscillator is not null) + { + await oscillator.StopAsync(); + await oscillator.DisconnectAsync(); + oscillator = null; + } + if (customWave is not null) + { + await customWave.DisposeAsync(); + } + } + + public void CustomWavePresetUpdated() + { + customFormula = selectedFormulaPreset switch + { + "sine" => ExpressionTemplates.SineWave(nIdentifier), + "square" => ExpressionTemplates.SquareWave(nIdentifier), + "sawtooth" => ExpressionTemplates.SawtoothWave(nIdentifier), + _ => ExpressionTemplates.TriangleWave(nIdentifier), + }; + } + + public void CalculateCoefficients() + { + imagCoefficients = new float[coefficientSeriesLength]; + for (int i = 1; i < coefficientSeriesLength; i++) + { + n = i; + + imagCoefficients[i] = (float)customFormula.Evaluate(); + } + realCoefficients = new float[coefficientSeriesLength]; + } + + public void CoefficientSeriesLengthUpdated() + { + Array.Resize(ref realCoefficients, coefficientSeriesLength); + Array.Resize(ref imagCoefficients, coefficientSeriesLength); } public async ValueTask DisposeAsync() { await context.RemoveOnStateChangeEventListener(stateChangeListener); await StopSound(); + if (context is not null) + { + await context.DisposeAsync(); + } + if (gainNode is not null) + { + await gainNode.DisconnectAsync(); + await gainNode.DisposeAsync(); + } + if (analyser is not null) + { + await analyser.DisposeAsync(); + } } } diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/EnumSelector.razor b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/EnumSelector.razor new file mode 100644 index 0000000..9cfabd1 --- /dev/null +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/EnumSelector.razor @@ -0,0 +1,24 @@ +@typeparam TEnum where TEnum : Enum + + + +@code { + [Parameter] + public bool Disabled { get; set; } = false; + + [Parameter, EditorRequired] + public required TEnum Value { get; set; } + + [Parameter, EditorRequired] + public EventCallback ValueChanged { get; set; } + + private async Task OnValueChanged() + { + await ValueChanged.InvokeAsync(Value); + } +} diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/Plot.razor b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/Plot.razor index 165eab9..049f14a 100644 --- a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/Plot.razor +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/Plot.razor @@ -11,4 +11,7 @@ [Parameter] public int Height { get; set; } = 200; + + [Parameter] + public string Color { get; set; } = "red"; } diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/TimeDomainPlot.razor b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/TimeDomainPlot.razor new file mode 100644 index 0000000..b81c8f3 --- /dev/null +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/TimeDomainPlot.razor @@ -0,0 +1,3 @@ +@using Excubo.Blazor.Canvas + + \ No newline at end of file diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/TimeDomainPlot.razor.cs b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/TimeDomainPlot.razor.cs new file mode 100644 index 0000000..492c916 --- /dev/null +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/TimeDomainPlot.razor.cs @@ -0,0 +1,58 @@ +using KristofferStrube.Blazor.WebIDL; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; + +namespace KristofferStrube.Blazor.WebAudio.WasmExample.Shared; + +public partial class TimeDomainPlot : ComponentBase, IDisposable +{ + private bool running; + private byte[] timeDomainMeasurements = Array.Empty(); + + [Inject] + public required IJSRuntime JSRuntime { get; set; } + + [Parameter, EditorRequired] + public AnalyserNode? Analyser { get; set; } + + [Parameter] + public int Height { get; set; } = 200; + + [Parameter] + public string Color { get; set; } = "red"; + + protected override async Task OnAfterRenderAsync(bool _) + { + if (running || Analyser is null) + { + return; + } + + running = true; + + int bufferLength = (int)await Analyser.GetFrequencyBinCountAsync(); + Uint8Array timeDomainDataArray = await Uint8Array.CreateAsync(JSRuntime, bufferLength); + + while (running) + { + await Analyser.GetByteTimeDomainDataAsync(timeDomainDataArray); + try + { + timeDomainMeasurements = await timeDomainDataArray.GetAsArrayAsync(); + } + catch + { + // If others try to retrieve a byte array at the same time Blazor will fail, so we simply just do nothing if it fails. + } + + await Task.Delay(20); + StateHasChanged(); + } + } + + public void Dispose() + { + running = false; + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/wwwroot/index.html b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/wwwroot/index.html index 62643e0..1d399e3 100644 --- a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/wwwroot/index.html +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/wwwroot/index.html @@ -52,7 +52,7 @@ - + diff --git a/src/KristofferStrube.Blazor.WebAudio/PeriodicWave.cs b/src/KristofferStrube.Blazor.WebAudio/PeriodicWave.cs index 7f104b4..95c9cc0 100644 --- a/src/KristofferStrube.Blazor.WebAudio/PeriodicWave.cs +++ b/src/KristofferStrube.Blazor.WebAudio/PeriodicWave.cs @@ -1,4 +1,5 @@ -using KristofferStrube.Blazor.WebIDL; +using KristofferStrube.Blazor.WebAudio.Extensions; +using KristofferStrube.Blazor.WebIDL; using Microsoft.JSInterop; namespace KristofferStrube.Blazor.WebAudio; @@ -22,8 +23,22 @@ public static Task CreateAsync(IJSRuntime jSRuntime, IJSObjectRefe return Task.FromResult(new PeriodicWave(jSRuntime, jSReference, options)); } + /// + /// Creates a using the standard constructor. + /// + /// An instance. + /// The this new will be associated with. + /// Initial parameter value for this . + /// A new instance of a . + public static async Task CreateAsync(IJSRuntime jSRuntime, BaseAudioContext context, PeriodicWaveOptions options) + { + IJSObjectReference helper = await jSRuntime.GetHelperAsync(); + IJSObjectReference jSInstance = await helper.InvokeAsync("constructPeriodicWave", context, options); + return new PeriodicWave(jSRuntime, jSInstance, new() { DisposesJSReference = true }); + } + /// - public PeriodicWave(IJSRuntime jSRuntime, IJSObjectReference jSReference, CreationOptions options) : base(jSRuntime, jSReference, options) + protected PeriodicWave(IJSRuntime jSRuntime, IJSObjectReference jSReference, CreationOptions options) : base(jSRuntime, jSReference, options) { } } diff --git a/src/KristofferStrube.Blazor.WebAudio/wwwroot/KristofferStrube.Blazor.WebAudio.js b/src/KristofferStrube.Blazor.WebAudio/wwwroot/KristofferStrube.Blazor.WebAudio.js index 994c285..3adb184 100644 --- a/src/KristofferStrube.Blazor.WebAudio/wwwroot/KristofferStrube.Blazor.WebAudio.js +++ b/src/KristofferStrube.Blazor.WebAudio/wwwroot/KristofferStrube.Blazor.WebAudio.js @@ -78,6 +78,10 @@ export function constructPannerNode(context, options) { return new PannerNode(context, options); } +export function constructPeriodicWave(context, options) { + return new PeriodicWave(context, options); +} + export function constructAudioWorkletNode(context, name, options) { return new AudioWorkletNode(context, name, options); }