Skip to content

Commit b91979d

Browse files
Added demo of using Custom Waveform.
1 parent ad98af2 commit b91979d

File tree

11 files changed

+353
-9
lines changed

11 files changed

+353
-9
lines changed

samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/Nodes/MediaStreamAudioSource.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ public MediaStreamAudioSource(IElement element, SVGEditor.SVGEditor svg) : base(
1616
{
1717
await SetMediaStreamAudioSourceNode(context);
1818
}
19-
return audioNode!;
2019
_ = audioNodeSlim.Release();
20+
return audioNode!;
2121
};
2222

2323
public new float Height
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using KristofferStrube.Blazor.FormulaEditor;
2+
using KristofferStrube.Blazor.FormulaEditor.BooleanExpressions;
3+
using KristofferStrube.Blazor.FormulaEditor.Expressions;
4+
5+
namespace KristofferStrube.Blazor.WebAudio.WasmExample.CustomPeriodicWaves;
6+
7+
public static class ExpressionTemplates
8+
{
9+
public static NumberReturningExpression SineWave(Identifier nIdentifier) => new CasesExpression()
10+
{
11+
Cases = [new() {
12+
Value = new NumericExpression() { Value = 1 },
13+
Condition = new EqualsOperator() { First = new IdentifierExpression() { Value = nIdentifier }, Second = new NumericExpression() { Value = 1 } }
14+
}],
15+
Otherwise = new NumericExpression() { Value = 0 },
16+
};
17+
18+
public static NumberReturningExpression SquareWave(Identifier nIdentifier) => new MultiplicationOperator()
19+
{
20+
First = new FractionOperator()
21+
{
22+
Numerator = new NumericExpression() { Value = 2 },
23+
Denominator = new MultiplicationOperator()
24+
{
25+
First = new IdentifierExpression() { Value = nIdentifier },
26+
Second = new ConstantExpression() { Value = new Constant("π", Math.PI) },
27+
ExplicitOperator = false
28+
}
29+
},
30+
Second = new SubtractionOperator()
31+
{
32+
First = new NumericExpression() { Value = 1 },
33+
Second = new PowerOperator()
34+
{
35+
Value = new NumericExpression() { Value = -1, Parenthesis = true },
36+
Power = new IdentifierExpression() { Value = nIdentifier }
37+
},
38+
Parenthesis = true
39+
},
40+
ExplicitOperator = false
41+
};
42+
43+
public static NumberReturningExpression SawtoothWave(Identifier nIdentifier) => new MultiplicationOperator()
44+
{
45+
First = new PowerOperator()
46+
{
47+
Value = new NumericExpression() { Value = -1, Parenthesis = true },
48+
Power = new AdditionOperator()
49+
{
50+
First = new IdentifierExpression() { Value = nIdentifier },
51+
Second = new NumericExpression() { Value = 1 },
52+
Parenthesis = true
53+
}
54+
},
55+
Second = new FractionOperator()
56+
{
57+
Numerator = new NumericExpression() { Value = 2 },
58+
Denominator = new MultiplicationOperator()
59+
{
60+
First = new IdentifierExpression() { Value = nIdentifier },
61+
Second = new ConstantExpression() { Value = new Constant("π", Math.PI) },
62+
ExplicitOperator = false
63+
}
64+
},
65+
ExplicitOperator = false
66+
};
67+
68+
public static NumberReturningExpression TriangleWave(Identifier nIdentifier) => new FractionOperator()
69+
{
70+
Numerator = new MultiplicationOperator()
71+
{
72+
First = new NumericExpression() { Value = 8 },
73+
Second = new FunctionExpression()
74+
{
75+
Function = new Function("sin", v => Math.Sin(v)),
76+
Input = new FractionOperator()
77+
{
78+
Numerator = new MultiplicationOperator()
79+
{
80+
First = new IdentifierExpression() { Value = nIdentifier },
81+
Second = new ConstantExpression() { Value = new Constant("π", Math.PI) },
82+
ExplicitOperator = false
83+
},
84+
Denominator = new NumericExpression() { Value = 2 }
85+
},
86+
Parenthesis = false
87+
},
88+
ExplicitOperator = false
89+
},
90+
Denominator = new PowerOperator()
91+
{
92+
Value = new MultiplicationOperator()
93+
{
94+
First = new ConstantExpression() { Value = new Constant("π", Math.PI) },
95+
Second = new IdentifierExpression() { Value = nIdentifier },
96+
ExplicitOperator = false,
97+
Parenthesis = true
98+
},
99+
Power = new NumericExpression() { Value = 2 }
100+
}
101+
};
102+
}

samples/KristofferStrube.Blazor.WebAudio.WasmExample/KristofferStrube.Blazor.WebAudio.WasmExample.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.2.2" />
1313
<PackageReference Include="Excubo.Blazor.Canvas" Version="3.2.50" />
1414
<PackageReference Include="KristofferStrube.Blazor.CSSView" Version="0.1.0-alpha.0" />
15+
<PackageReference Include="KristofferStrube.Blazor.FormulaEditor" Version="0.1.0-alpha.2" />
1516
<PackageReference Include="KristofferStrube.Blazor.MediaStreamRecording" Version="0.1.0-alpha.1" />
1617
<PackageReference Include="KristofferStrube.Blazor.SVGEditor" Version="0.3.0" />
1718
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" />

samples/KristofferStrube.Blazor.WebAudio.WasmExample/Pages/Index.razor

Lines changed: 139 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
@page "/"
22
@using KristofferStrube.Blazor.DOM
3+
@using KristofferStrube.Blazor.FormulaEditor
4+
@using KristofferStrube.Blazor.FormulaEditor.Expressions
5+
@using KristofferStrube.Blazor.WebAudio.WasmExample.CustomPeriodicWaves
36
@implements IAsyncDisposable
47
@inject IJSRuntime JSRuntime
58
<PageTitle>WebAudio - Playing Sound</PageTitle>
@@ -27,24 +30,92 @@ Status:
2730
{
2831
<button class="btn btn-warning" @onclick=StopSound>Stop Sound 🔊</button>
2932
}
33+
<EnumSelector @bind-Value=oscillatorType Disabled=@(oscillator is not null) />
3034
}
3135
</div>
36+
@if (oscillatorType is OscillatorType.Custom)
37+
{
38+
<div>
39+
<label for="presets">Custom wave preset formula:</label>
40+
<select id="presets" @bind=@selectedFormulaPreset @bind:after=CustomWavePresetUpdated>
41+
<option value="sine">Sine</option>
42+
<option value="square">Square</option>
43+
<option value="sawtooth">Sawtooth</option>
44+
<option value="triangle">Triangle</option>
45+
</select>
46+
<input type="number" @bind=coefficientSeriesLength @bind:after=CoefficientSeriesLengthUpdated min="0" step="1" />
47+
<br />
48+
<MathEditor @bind-Expression="customFormula" AvailableIdentifiers="identifiers" />
49+
<button class="btn btn-primary" @onclick=CalculateCoefficients>Evaluate formula</button>
50+
<br />
51+
<table>
52+
<thead>
53+
<tr>
54+
<th></th>
55+
@for (int i = 0; i < coefficientSeriesLength; i++)
56+
{
57+
<th><b>@i</b></th>
58+
}
59+
</tr>
60+
</thead>
61+
<tbody>
62+
<tr>
63+
<th>Real Coefficients</th>
64+
@for (int i = 0; i < coefficientSeriesLength; i++)
65+
{
66+
int k = i;
67+
<td><input @bind=@realCoefficients[k] style="width:50px;" /></td>
68+
}
69+
</tr>
70+
<tr>
71+
<th>Imaginary Coefficients</th>
72+
@for (int i = 0; i < coefficientSeriesLength; i++)
73+
{
74+
int k = i;
75+
<td><input @bind=@imagCoefficients[k] style="width:50px;" /></td>
76+
}
77+
</tr>
78+
</tbody>
79+
</table>
80+
</div>
81+
}
3282
<GainSlider GainNode=gainNode />
83+
<TimeDomainPlot Analyser=analyser />
3384

3485
@code {
3586
AudioContext context = default!;
3687
GainNode gainNode = default!;
88+
AnalyserNode analyser = default!;
3789
EventListener<Event> stateChangeListener = default!;
3890
AudioContextState state = AudioContextState.Closed;
3991
OscillatorNode? oscillator;
92+
OscillatorType oscillatorType = OscillatorType.Sine;
93+
PeriodicWave? customWave;
94+
95+
private double n = 0;
96+
private Identifier nIdentifier = default!;
97+
private List<Identifier> identifiers = default!;
98+
NumberReturningExpression customFormula = default!;
99+
private string selectedFormulaPreset = "triangle";
100+
float[] realCoefficients = new float[10];
101+
float[] imagCoefficients = new float[10];
102+
int coefficientSeriesLength = 10;
40103

41104
protected override async Task OnInitializedAsync()
42105
{
106+
nIdentifier = new Identifier("n", () => n);
107+
identifiers = [nIdentifier];
108+
CustomWavePresetUpdated();
109+
CalculateCoefficients();
110+
43111
context = await AudioContext.CreateAsync(JSRuntime);
44112

45113
AudioDestinationNode destination = await context.GetDestinationAsync();
46114
gainNode = await GainNode.CreateAsync(JSRuntime, context, new() { Gain = 0.1f });
115+
analyser = await AnalyserNode.CreateAsync(JSRuntime, context);
116+
47117
await gainNode.ConnectAsync(destination);
118+
await gainNode.ConnectAsync(analyser);
48119

49120
stateChangeListener = await context.AddOnStateChangeEventListener(async (e) =>
50121
{
@@ -56,10 +127,24 @@ Status:
56127

57128
public async Task PlaySound()
58129
{
130+
if (oscillatorType is OscillatorType.Custom)
131+
{
132+
customWave = await PeriodicWave.CreateAsync(JSRuntime, context, new()
133+
{
134+
Real = realCoefficients,
135+
Imag = imagCoefficients,
136+
});
137+
}
138+
else
139+
{
140+
customWave = null;
141+
}
142+
59143
OscillatorOptions oscillatorOptions = new()
60144
{
61-
Type = OscillatorType.Sine,
62-
Frequency = Random.Shared.Next(100, 500)
145+
Type = oscillatorType,
146+
Frequency = 440,
147+
PeriodicWave = customWave
63148
};
64149
oscillator = await OscillatorNode.CreateAsync(JSRuntime, context, oscillatorOptions);
65150
await oscillator.ConnectAsync(gainNode);
@@ -68,15 +153,64 @@ Status:
68153

69154
public async Task StopSound()
70155
{
71-
if (oscillator is null) return;
72-
await oscillator.StopAsync();
73-
oscillator = null;
156+
if (oscillator is not null)
157+
{
158+
await oscillator.StopAsync();
159+
await oscillator.DisconnectAsync();
160+
oscillator = null;
161+
}
162+
if (customWave is not null)
163+
{
164+
await customWave.DisposeAsync();
165+
}
166+
}
167+
168+
public void CustomWavePresetUpdated()
169+
{
170+
customFormula = selectedFormulaPreset switch
171+
{
172+
"sine" => ExpressionTemplates.SineWave(nIdentifier),
173+
"square" => ExpressionTemplates.SquareWave(nIdentifier),
174+
"sawtooth" => ExpressionTemplates.SawtoothWave(nIdentifier),
175+
_ => ExpressionTemplates.TriangleWave(nIdentifier),
176+
};
177+
}
178+
179+
public void CalculateCoefficients()
180+
{
181+
imagCoefficients = new float[coefficientSeriesLength];
182+
for (int i = 1; i < coefficientSeriesLength; i++)
183+
{
184+
n = i;
185+
186+
imagCoefficients[i] = (float)customFormula.Evaluate();
187+
}
188+
realCoefficients = new float[coefficientSeriesLength];
189+
}
190+
191+
public void CoefficientSeriesLengthUpdated()
192+
{
193+
Array.Resize(ref realCoefficients, coefficientSeriesLength);
194+
Array.Resize(ref imagCoefficients, coefficientSeriesLength);
74195
}
75196

76197
public async ValueTask DisposeAsync()
77198
{
78199
await context.RemoveOnStateChangeEventListener(stateChangeListener);
79200
await StopSound();
201+
if (context is not null)
202+
{
203+
await context.DisposeAsync();
204+
}
205+
if (gainNode is not null)
206+
{
207+
await gainNode.DisconnectAsync();
208+
await gainNode.DisposeAsync();
209+
}
210+
if (analyser is not null)
211+
{
212+
await analyser.DisposeAsync();
213+
}
80214
}
81215
}
82216

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@typeparam TEnum where TEnum : Enum
2+
3+
<select @bind=Value @bind:after=OnValueChanged disabled=@Disabled>
4+
@foreach (TEnum option in Enum.GetValues(typeof(TEnum)))
5+
{
6+
<option value="@option">@option</option>
7+
}
8+
</select>
9+
10+
@code {
11+
[Parameter]
12+
public bool Disabled { get; set; } = false;
13+
14+
[Parameter, EditorRequired]
15+
public required TEnum Value { get; set; }
16+
17+
[Parameter, EditorRequired]
18+
public EventCallback<TEnum> ValueChanged { get; set; }
19+
20+
private async Task OnValueChanged()
21+
{
22+
await ValueChanged.InvokeAsync(Value);
23+
}
24+
}

samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/Plot.razor

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,7 @@
1111

1212
[Parameter]
1313
public int Height { get; set; } = 200;
14+
15+
[Parameter]
16+
public string Color { get; set; } = "red";
1417
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@using Excubo.Blazor.Canvas
2+
3+
<Plot Data=@timeDomainMeasurements Height=@Height Color=@Color />

0 commit comments

Comments
 (0)