Skip to content

Commit

Permalink
Added demo of using Custom Waveform.
Browse files Browse the repository at this point in the history
  • Loading branch information
KristofferStrube committed Feb 2, 2025
1 parent ad98af2 commit b91979d
Show file tree
Hide file tree
Showing 11 changed files with 353 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.2.2" />
<PackageReference Include="Excubo.Blazor.Canvas" Version="3.2.50" />
<PackageReference Include="KristofferStrube.Blazor.CSSView" Version="0.1.0-alpha.0" />
<PackageReference Include="KristofferStrube.Blazor.FormulaEditor" Version="0.1.0-alpha.2" />
<PackageReference Include="KristofferStrube.Blazor.MediaStreamRecording" Version="0.1.0-alpha.1" />
<PackageReference Include="KristofferStrube.Blazor.SVGEditor" Version="0.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" />
Expand Down
144 changes: 139 additions & 5 deletions samples/KristofferStrube.Blazor.WebAudio.WasmExample/Pages/Index.razor
Original file line number Diff line number Diff line change
@@ -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
<PageTitle>WebAudio - Playing Sound</PageTitle>
Expand Down Expand Up @@ -27,24 +30,92 @@ Status:
{
<button class="btn btn-warning" @onclick=StopSound>Stop Sound 🔊</button>
}
<EnumSelector @bind-Value=oscillatorType Disabled=@(oscillator is not null) />
}
</div>
@if (oscillatorType is OscillatorType.Custom)
{
<div>
<label for="presets">Custom wave preset formula:</label>
<select id="presets" @bind=@selectedFormulaPreset @bind:after=CustomWavePresetUpdated>
<option value="sine">Sine</option>
<option value="square">Square</option>
<option value="sawtooth">Sawtooth</option>
<option value="triangle">Triangle</option>
</select>
<input type="number" @bind=coefficientSeriesLength @bind:after=CoefficientSeriesLengthUpdated min="0" step="1" />
<br />
<MathEditor @bind-Expression="customFormula" AvailableIdentifiers="identifiers" />
<button class="btn btn-primary" @onclick=CalculateCoefficients>Evaluate formula</button>
<br />
<table>
<thead>
<tr>
<th></th>
@for (int i = 0; i < coefficientSeriesLength; i++)
{
<th><b>@i</b></th>
}
</tr>
</thead>
<tbody>
<tr>
<th>Real Coefficients</th>
@for (int i = 0; i < coefficientSeriesLength; i++)
{
int k = i;
<td><input @bind=@realCoefficients[k] style="width:50px;" /></td>
}
</tr>
<tr>
<th>Imaginary Coefficients</th>
@for (int i = 0; i < coefficientSeriesLength; i++)
{
int k = i;
<td><input @bind=@imagCoefficients[k] style="width:50px;" /></td>
}
</tr>
</tbody>
</table>
</div>
}
<GainSlider GainNode=gainNode />
<TimeDomainPlot Analyser=analyser />

@code {
AudioContext context = default!;
GainNode gainNode = default!;
AnalyserNode analyser = default!;
EventListener<Event> stateChangeListener = default!;
AudioContextState state = AudioContextState.Closed;
OscillatorNode? oscillator;
OscillatorType oscillatorType = OscillatorType.Sine;
PeriodicWave? customWave;

private double n = 0;
private Identifier nIdentifier = default!;
private List<Identifier> 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) =>
{
Expand All @@ -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);
Expand All @@ -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();
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@typeparam TEnum where TEnum : Enum

<select @bind=Value @bind:after=OnValueChanged disabled=@Disabled>
@foreach (TEnum option in Enum.GetValues(typeof(TEnum)))
{
<option value="@option">@option</option>
}
</select>

@code {
[Parameter]
public bool Disabled { get; set; } = false;

[Parameter, EditorRequired]
public required TEnum Value { get; set; }

[Parameter, EditorRequired]
public EventCallback<TEnum> ValueChanged { get; set; }

private async Task OnValueChanged()
{
await ValueChanged.InvokeAsync(Value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@

[Parameter]
public int Height { get; set; } = 200;

[Parameter]
public string Color { get; set; } = "red";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@using Excubo.Blazor.Canvas

<Plot Data=@timeDomainMeasurements Height=@Height Color=@Color />
Loading

0 comments on commit b91979d

Please sign in to comment.