diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/AudioEditor.razor b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/AudioEditor.razor index 7bdeb72..9a76c09 100644 --- a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/AudioEditor.razor +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/AudioEditor.razor @@ -42,6 +42,11 @@ } } + protected override async Task OnParametersSetAsync() + { + await base.OnParametersSetAsync(); + } + protected List SupportedElements { get; set; } = new() { new(typeof(G), element => element.TagName == "G"), @@ -50,6 +55,7 @@ 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(MediaStreamAudioSource), element => element.TagName is "RECT" && element.GetAttribute("data-elementtype") == "media-stream-audio-source"), new(typeof(Connector), element => element.TagName is "LINE" && element.GetAttribute("data-elementtype") == "connector"), }; @@ -58,6 +64,7 @@ new(typeof(AddNewConnectorMenuItem), (_,data) => data is Port { Ingoing: false }), new(typeof(AddNewAudioDestinationMenuItem), (_,_) => true), new(typeof(AddNewOscillatorMenuItem), (_,_) => true), + new(typeof(AddNewMediaStreamAudioSourceMenuItem), (_,_) => true), new(typeof(AddNewGainMenuItem), (_,_) => true), new(typeof(AddNewAnalyserMenuItem), (_,_) => true), new(typeof(AddNewBiquadFilterMenuItem), (_,_) => true), @@ -81,6 +88,10 @@ { await audioSource.StopAsync(); } + else if (node is MediaStreamAudioSource mediaStreamAudioSourceNode) + { + await mediaStreamAudioSourceNode.StopAsync(await mediaStreamAudioSourceNode.AudioNode(AudioContext)); + } } } diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/Connector.cs b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/Connector.cs index 1b5a996..223de0b 100644 --- a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/Connector.cs +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/Connector.cs @@ -20,36 +20,43 @@ public Connector(IElement element, SVGEditor.SVGEditor svg) : base(element, svg) public override string StateRepresentation => base.StateRepresentation + IsHovered.ToString(); + private (Node node, ulong port)? from; public (Node node, ulong port)? From { get { - var fromNode = (Node?)SVG.Elements.FirstOrDefault(e => e is Node && e.Id == Element.GetAttribute("data-from-node")); - ulong fromPort = (ulong)Element.GetAttributeOrZero("data-from-port"); - _ = fromNode?.OutgoingConnectors.Add((this, fromPort)); - if (fromNode is null) return null; - if (To is { } to) + if (from is null) { - QueuedTasks.Enqueue(async context => await (await fromNode.AudioNode(context)).ConnectAsync(await to.node.AudioNode(context), fromPort, to.port)); + var fromNode = (Node?)SVG.Elements.FirstOrDefault(e => e is Node && e.Id == Element.GetAttribute("data-from-node")); + ulong fromPort = (ulong)Element.GetAttributeOrZero("data-from-port"); + _ = fromNode?.OutgoingConnectors.Add((this, fromPort)); + if (fromNode is null) return null; + if (To is { } to) + { + QueuedTasks.Enqueue(async context => await (await fromNode.AudioNode(context)).ConnectAsync(await to.node.AudioNode(context), fromPort, to.port)); + } + from = (fromNode, fromPort); } - return (fromNode, fromPort); + return from; } set { - if (From is { } from) + if (from is { } previousValue) { - _ = from.node.OutgoingConnectors.Remove((this, from.port)); - QueuedTasks.Enqueue(async context => await (await from.node.AudioNode(context)).DisconnectAsync(from.port)); + _ = previousValue.node.OutgoingConnectors.Remove((this, previousValue.port)); + QueuedTasks.Enqueue(async context => await (await previousValue.node.AudioNode(context)).DisconnectAsync(previousValue.port)); } if (value is null) { _ = Element.RemoveAttribute("data-from-node"); _ = Element.RemoveAttribute("data-from-port"); + from = null; } else { Element.SetAttribute("data-from-node", value.Value.node.Id); Element.SetAttribute("data-from-port", value.Value.port.ToString()); + from = value; _ = value.Value.node.OutgoingConnectors.Add((this, value.Value.port)); if (To is { } to) { @@ -60,35 +67,41 @@ public Connector(IElement element, SVGEditor.SVGEditor svg) : base(element, svg) } } - + private (Node node, ulong port)? to; public (Node node, ulong port)? To { get { - var toNode = (Node?)SVG.Elements.FirstOrDefault(e => e is Node && e.Id == Element.GetAttribute("data-to-node")); - ulong toPort = (ulong)Element.GetAttributeOrZero("data-to-port"); - _ = toNode?.OutgoingConnectors.Add((this, toPort)); - return toNode is null ? null : (toNode, toPort); + if (to is null) + { + var toNode = (Node?)SVG.Elements.FirstOrDefault(e => e is Node && e.Id == Element.GetAttribute("data-to-node")); + ulong toPort = (ulong)Element.GetAttributeOrZero("data-to-port"); + _ = toNode?.OutgoingConnectors.Add((this, toPort)); + to = toNode is null ? null : (toNode, toPort); + } + return to; } set { - if (To is { } to) + if (to is { } previousValue) { - _ = to.node.IngoingConnectors.Remove((this, to.port)); - if (From is { } from) + _ = previousValue.node.IngoingConnectors.Remove((this, previousValue.port)); + if (from is { } previouvFromValue) { - QueuedTasks.Enqueue(async context => await (await from.node.AudioNode(context)).DisconnectAsync(from.port)); + QueuedTasks.Enqueue(async context => await (await previouvFromValue.node.AudioNode(context)).DisconnectAsync(previouvFromValue.port)); } } if (value is null) { _ = Element.RemoveAttribute("data-to-node"); _ = Element.RemoveAttribute("data-to-port"); + to = null; } else { Element.SetAttribute("data-to-node", value.Value.node.Id); Element.SetAttribute("data-to-port", value.Value.port.ToString()); + to = value; _ = value.Value.node.IngoingConnectors.Add((this, value.Value.port)); if (From is { } from) { diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/MenuItems/AddNewMediaStreamAudioSourceMenuItem.razor b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/MenuItems/AddNewMediaStreamAudioSourceMenuItem.razor new file mode 100644 index 0000000..8a8684a --- /dev/null +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/MenuItems/AddNewMediaStreamAudioSourceMenuItem.razor @@ -0,0 +1,9 @@ +@using BlazorContextMenu +
🎤
New MediaStream Source
+@code { + [CascadingParameter] + public required SVGEditor.SVGEditor SVGEditor { get; set; } + + [Parameter] + public required object Data { get; set; } +} \ No newline at end of file diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/NodeEditors/AnalyserEditor.razor b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/NodeEditors/AnalyserEditor.razor index 68a29eb..6e58056 100644 --- a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/NodeEditors/AnalyserEditor.razor +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/NodeEditors/AnalyserEditor.razor @@ -43,6 +43,8 @@ @code { + private AnalyserNode? analyser; + private byte[] timeDomainMeasurements = Array.Empty(); [CascadingParameter] @@ -52,30 +54,31 @@ { await base.OnAfterRenderAsync(firstRender); - if (!firstRender) return; - - AnalyserNode analyser = ((AnalyserNode)await SVGElement.AudioNode(AudioContext)); + if (analyser is null && AudioContext is not null) + { + analyser = (AnalyserNode)await SVGElement.AudioNode(AudioContext); - int bufferLength = (int)await analyser.GetFrequencyBinCountAsync(); - Uint8Array timeDomainDataArray = await Uint8Array.CreateAsync(AudioContext.JSRuntime, bufferLength); - Uint8Array frequencyDataArray = await Uint8Array.CreateAsync(AudioContext.JSRuntime, bufferLength); + int bufferLength = (int)await analyser.GetFrequencyBinCountAsync(); + Uint8Array timeDomainDataArray = await Uint8Array.CreateAsync(AudioContext.JSRuntime, bufferLength); + Uint8Array frequencyDataArray = await Uint8Array.CreateAsync(AudioContext.JSRuntime, bufferLength); - SVGElement.Running = true; + SVGElement.Running = true; - while (SVGElement.Running) - { - try + while (SVGElement.Running) { - await analyser.GetByteTimeDomainDataAsync(timeDomainDataArray); + try + { + await analyser.GetByteTimeDomainDataAsync(timeDomainDataArray); - timeDomainMeasurements = await timeDomainDataArray.GetByteArrayAsync(); - await Task.Delay(10); - SVGElement._stateRepresentation = ""; - StateHasChanged(); - } - catch (Exception) - { - + timeDomainMeasurements = await timeDomainDataArray.GetByteArrayAsync(); + await Task.Delay(10); + SVGElement._stateRepresentation = ""; + StateHasChanged(); + } + catch (Exception) + { + + } } } } diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/NodeEditors/GainEditor.razor b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/NodeEditors/GainEditor.razor index 5ebf29e..ffe69a8 100644 --- a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/NodeEditors/GainEditor.razor +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/NodeEditors/GainEditor.razor @@ -28,7 +28,7 @@
@if (GainAudioParam is not null) { - + } diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/NodeEditors/MediaStreamAudioSourceEditor.razor b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/NodeEditors/MediaStreamAudioSourceEditor.razor new file mode 100644 index 0000000..ba2a631 --- /dev/null +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/NodeEditors/MediaStreamAudioSourceEditor.razor @@ -0,0 +1,68 @@ +@using BlazorContextMenu +@using KristofferStrube.Blazor.SVGEditor.Extensions +@inherits NodeEditor + + + + + + + + @if (SVGElement.AudioOptions.Count > 0) + { + + } + + + + + + + + + +@code { + private MediaStreamAudioSourceNode? mediaStreamAudioSourceNode; + + [CascadingParameter] + public required AudioContext AudioContext { get; set; } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (mediaStreamAudioSourceNode is null && AudioContext is not null) + { + mediaStreamAudioSourceNode = (MediaStreamAudioSourceNode)await SVGElement.AudioNode(AudioContext); + StateHasChanged(); + } + } + + protected async Task SetMediaStreamAudioSourceNode() + { + await SVGElement.SetMediaStreamAudioSourceNode(AudioContext); + StateHasChanged(); + } +} \ No newline at end of file diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/NodeEditors/NodeEditor.razor b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/NodeEditors/NodeEditor.razor index c99935d..95f23d1 100644 --- a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/NodeEditors/NodeEditor.razor +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/NodeEditors/NodeEditor.razor @@ -7,7 +7,7 @@ @inherits ShapeEditor @code { - public bool ChildContentIsNoninteractive => SVGElement.Selected && SVGElement.SVG.EditMode is EditMode.Move; + public bool ChildContentIsNoninteractive => SVGElement.SVG.EditMode is EditMode.Move && SVGElement.Selected || SVGElement.SVG.EditMode is EditMode.Add or EditMode.Move; public void SelectPort(int port) { diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/Nodes/BiquadFilter.cs b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/Nodes/BiquadFilter.cs index 9a5ce40..00ba03d 100644 --- a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/Nodes/BiquadFilter.cs +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/Nodes/BiquadFilter.cs @@ -27,7 +27,6 @@ public BiquadFilter(IElement element, SVGEditor.SVGEditor svg) : base(element, s return audioNode; }; - public new float Height { get => 280; @@ -41,7 +40,7 @@ public BiquadFilterType? Type { get { - return Element.GetAttribute("data-type") is { } value ? Deserialize(value) : null; + return Element.GetAttribute("data-type") is { } value ? Deserialize($"\"{value}\"") : null; } set { @@ -51,7 +50,7 @@ public BiquadFilterType? Type } else { - Element.SetAttribute("data-type", Serialize(value.Value)); + Element.SetAttribute("data-type", Serialize(value.Value)[1..^1]); } Changed?.Invoke(this); } @@ -61,17 +60,17 @@ public float? Q { get { - return Element.GetAttribute("data-q") is { } value ? float.Parse(value, CultureInfo.InvariantCulture) : null; + return Element.GetAttribute("data-Q") is { } value ? float.Parse(value, CultureInfo.InvariantCulture) : null; } set { if (value is null) { - _ = Element.RemoveAttribute("data-q"); + _ = Element.RemoveAttribute("data-Q"); } else { - Element.SetAttribute("data-q", value.Value.AsString()); + Element.SetAttribute("data-v", value.Value.AsString()); } Changed?.Invoke(this); } @@ -101,17 +100,17 @@ public float? Frequency { get { - return Element.GetAttribute("data-frequnecy") is { } value ? float.Parse(value, CultureInfo.InvariantCulture) : null; + return Element.GetAttribute("data-frequency") is { } value ? float.Parse(value, CultureInfo.InvariantCulture) : null; } set { if (value is null) { - _ = Element.RemoveAttribute("data-frequnecy"); + _ = Element.RemoveAttribute("data-frequency"); } else { - Element.SetAttribute("data-frequnecy", value.Value.AsString()); + Element.SetAttribute("data-frequency", value.Value.AsString()); } Changed?.Invoke(this); } diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/Nodes/MediaStreamAudioSource.cs b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/Nodes/MediaStreamAudioSource.cs new file mode 100644 index 0000000..3b94ae6 --- /dev/null +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/AudioEditor/Nodes/MediaStreamAudioSource.cs @@ -0,0 +1,98 @@ +using AngleSharp.Dom; +using KristofferStrube.Blazor.MediaCaptureStreams; +using KristofferStrube.Blazor.WebAudio.WasmExample.AudioEditor.NodeEditors; + +namespace KristofferStrube.Blazor.WebAudio.WasmExample.AudioEditor; + +public class MediaStreamAudioSource : Node +{ + public MediaStreamAudioSource(IElement element, SVGEditor.SVGEditor svg) : base(element, svg) { } + + private AudioNode? audioNode; + public override Func> AudioNode => async (context) => + { + if (audioNode is null) + { + await SetMediaStreamAudioSourceNode(context); + } + return audioNode!; + }; + + public async Task SetMediaStreamAudioSourceNode(AudioContext context) + { + + MediaDevicesService mediaDevicesService = new(context.JSRuntime); + MediaDevices mediaDevices = await mediaDevicesService.GetMediaDevicesAsync(); + + MediaTrackConstraints mediaTrackConstraints = new() + { + AutoGainControl = false, + DeviceId = SelectedAudioSource is null ? null : new ConstrainDomString(SelectedAudioSource) + }; + MediaStream mediaStream = await mediaDevices.GetUserMediaAsync(new MediaStreamConstraints() { Audio = mediaTrackConstraints }); + + MediaDeviceInfo[] deviceInfos = await mediaDevices.EnumerateDevicesAsync(); + AudioOptions.Clear(); + foreach (MediaDeviceInfo device in deviceInfos) + { + if (await device.GetKindAsync() is MediaDeviceKind.AudioInput) + { + AudioOptions.Add((await device.GetLabelAsync(), await device.GetDeviceIdAsync())); + } + } + + AudioNode? oldAudioNode = audioNode; + audioNode = await context.CreateMediaStreamSourceAsync(mediaStream); + + await StopAsync(oldAudioNode); + if (oldAudioNode is not null) + { + foreach ((Connector connector, ulong port) in OutgoingConnectors.ToList()) + { + connector.From = (this, port); + } + } + } + + public string? SelectedAudioSource { get; set; } + public List<(string label, string id)> AudioOptions { get; set; } = new(); + + public override Type Presenter => typeof(MediaStreamAudioSourceEditor); + + public static new void AddNew(SVGEditor.SVGEditor SVG) + { + IElement element = SVG.Document.CreateElement("RECT"); + element.SetAttribute("data-elementtype", "media-stream-audio-source"); + + MediaStreamAudioSource node = new(element, SVG) + { + Changed = SVG.UpdateInput, + Stroke = "#28B6F6", + StrokeWidth = "2", + Height = 100, + Width = 250, + }; + + (node.X, node.Y) = SVG.LocalDetransform(SVG.LastRightClick); + + SVG.ClearSelectedShapes(); + SVG.SelectShape(node); + SVG.AddElement(node); + } + + public override async void BeforeBeingRemoved() + { + await StopAsync(audioNode); + } + + public async Task StopAsync(AudioNode? audioNodeToStop) + { + if (audioNodeToStop is not null) + { + await audioNodeToStop!.DisconnectAsync(); + MediaStream oldMediaStream = await ((MediaStreamAudioSourceNode)audioNodeToStop).GetMediaStreamAsync(); + MediaStreamTrack[] oldAudioTracks = await oldMediaStream.GetAudioTracksAsync(); + await oldAudioTracks.First().StopAsync(); + } + } +} diff --git a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/AudioParamSlider.razor b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/AudioParamSlider.razor index 294d275..e274018 100644 --- a/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/AudioParamSlider.razor +++ b/samples/KristofferStrube.Blazor.WebAudio.WasmExample/Shared/AudioParamSlider.razor @@ -10,10 +10,10 @@ max="@Max" step="@(StepSize.ToString(CultureInfo.InvariantCulture))" style="width:min(200px, calc(100%-40px));" /> - + + style="border: none; outline: none; background: none;max-width:100%;" /> diff --git a/src/KristofferStrube.Blazor.WebAudio/AudioNodes/MediaStreamAudioSourceNode.cs b/src/KristofferStrube.Blazor.WebAudio/AudioNodes/MediaStreamAudioSourceNode.cs index 9f42484..f5af35a 100644 --- a/src/KristofferStrube.Blazor.WebAudio/AudioNodes/MediaStreamAudioSourceNode.cs +++ b/src/KristofferStrube.Blazor.WebAudio/AudioNodes/MediaStreamAudioSourceNode.cs @@ -1,4 +1,5 @@ using KristofferStrube.Blazor.MediaCaptureStreams; +using KristofferStrube.Blazor.WebAudio.Extensions; using Microsoft.JSInterop; namespace KristofferStrube.Blazor.WebAudio; @@ -20,10 +21,35 @@ public class MediaStreamAudioSourceNode : AudioNode return Task.FromResult(new MediaStreamAudioSourceNode(jSRuntime, jSReference)); } + /// + /// Creates a using the standard constructor. + /// + /// An instance. + /// The this new will be associated with. + /// Optional initial parameter value for this . + /// A new instance of a . + public static async Task CreateAsync(IJSRuntime jSRuntime, BaseAudioContext context, MediaStreamAudioSourceOptions options) + { + IJSObjectReference helper = await jSRuntime.GetHelperAsync(); + IJSObjectReference jSInstance = await helper.InvokeAsync("constructMediaStreamAudioSourceNode", context, options); + return new MediaStreamAudioSourceNode(jSRuntime, jSInstance); + } + /// /// Constructs a wrapper instance for a given JS Instance of a . /// /// An instance. /// A JS reference to an existing . protected MediaStreamAudioSourceNode(IJSRuntime jSRuntime, IJSObjectReference jSReference) : base(jSRuntime, jSReference) { } + + /// + /// Gets the used when constructing this + /// + /// + public async Task GetMediaStreamAsync() + { + IJSObjectReference helper = await webAudioHelperTask.Value; + IJSObjectReference jSInstance = await helper.InvokeAsync("getAttribute", JSReference, "mediaStream"); + return await MediaStream.CreateAsync(JSRuntime, jSInstance); + } } diff --git a/src/KristofferStrube.Blazor.WebAudio/Options/MediaStreamAudioSourceOptions.cs b/src/KristofferStrube.Blazor.WebAudio/Options/MediaStreamAudioSourceOptions.cs new file mode 100644 index 0000000..4c88670 --- /dev/null +++ b/src/KristofferStrube.Blazor.WebAudio/Options/MediaStreamAudioSourceOptions.cs @@ -0,0 +1,17 @@ +using KristofferStrube.Blazor.MediaCaptureStreams; +using System.Text.Json.Serialization; + +namespace KristofferStrube.Blazor.WebAudio; + +/// +/// This specifies the options to use in constructing an . +/// +/// See the API definition here. +public class MediaStreamAudioSourceOptions +{ + /// + /// The media stream that will act as a source. + /// + [JsonPropertyName("mediaStream")] + public required MediaStream MediaStream { get; set; } +} \ No newline at end of file