Skip to content

Commit

Permalink
Added Recording demo.
Browse files Browse the repository at this point in the history
  • Loading branch information
KristofferStrube committed Aug 26, 2024
1 parent cf0a8a8 commit 2c424be
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 4 deletions.
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.MediaStreamRecording" Version="0.1.0-alpha.1" />
<PackageReference Include="KristofferStrube.Blazor.SVGEditor" Version="0.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.3" PrivateAssets="all" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ else
<h3>Time Domain Data</h3>
<Plot Data="timeDomainMeasurements" />
<h3>Frequency Data</h3>
<span>Peak frequency: @Math.Round(peakFrequency, 0) Hz</span>
<Plot Data="frequencyMeasurements" />
<h3>Amplitude Data</h3>
<AmplitudePlot Analyser="analyser" />
Expand Down Expand Up @@ -53,7 +54,7 @@ else
private MediaStream? mediaStream;
private List<(string label, string id)> audioOptions = new();
private string? selectedAudioSource;

private double peakFrequency = 0;

async Task OpenAudio()
{
Expand Down Expand Up @@ -99,6 +100,9 @@ else
var timeDomainDataArray = await Uint8Array.CreateAsync(JSRuntime, bufferLength);
var frequencyDataArray = await Uint8Array.CreateAsync(JSRuntime, bufferLength);

var sampleRate = await context.GetSampleRateAsync();
var fftSize = await analyser.GetFftSizeAsync();

makeMeasurements = true;
while (makeMeasurements)
{
Expand All @@ -107,6 +111,10 @@ else

timeDomainMeasurements = await timeDomainDataArray.GetAsArrayAsync();
frequencyMeasurements = await frequencyDataArray.GetAsArrayAsync();

var largestFrequencyIndex = frequencyMeasurements.ToList().IndexOf(frequencyMeasurements.Max());
peakFrequency = largestFrequencyIndex * sampleRate / fftSize;

await Task.Delay(1);
StateHasChanged();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
@page "/RecordMediaStream"
@using KristofferStrube.Blazor.DOM
@using KristofferStrube.Blazor.FileAPI
@using KristofferStrube.Blazor.MediaCaptureStreams
@using KristofferStrube.Blazor.MediaStreamRecording
@using KristofferStrube.Blazor.WebIDL
@using KristofferStrube.Blazor.WebIDL.Exceptions
@implements IAsyncDisposable
@inject IJSRuntime JSRuntime
@inject IMediaDevicesService MediaDevicesService
<PageTitle>WebAudio - Record MediaStream</PageTitle>
<h2>Record MediaStream</h2>

<p>
On this page we open a <code>MediaStream</code> using the <a href="https://github.com/KristofferStrube/Blazor.MediaCaptureStreams">Blazor.MediaCaptureStreams</a> library
and and record it using a <code>MediaRecorder</code> from the <a href="https://github.com/KristofferStrube/Blazor.MediaStreamRecording">Blazor.MediaStreamRecording</a> library.

Once the recording is done we analyze the data using an <code>AnalyserNode</code> to find its most prominent frequency and then make it possible to play the sound at another playback rate in order to match some input frequency.

</p>

@if (error is { } errorMessage)
{
<p style="color: red;">@errorMessage</p>
}
else if (peakFrequencyCount > 0)
{
<span>Average Max Peak: @(Math.Round(peakFrequencySum / peakFrequencyCount, 0)) Hz</span>
<Plot Data="frequencyMeasurements" />
}
else if (audioBuffer is not null)
{
<button class="btn btn-primary" @onclick="AnalyseFrequency">Analyze</button>
}
else if (mediaStream is null)
{
<button class="btn btn-primary" @onclick="OpenAudio">Load Audio</button>
}
else
{
<AmplitudePlot Analyser="analyser" Color="@(recording ? "#F00" : "#000")" />
if (!recording)
{
<button class="btn btn-primary" @onclick="Record">Record</button>

@if (audioOptions.Count > 0)
{
<label for="audioSource">Audio Source</label>
<select id="audioSource" @bind=selectedAudioSource @bind:after="OpenAudio">
@foreach (var option in audioOptions)
{
<option value="@option.id" selected="@(option.id == selectedAudioSource)">@option.label</option>
}
</select>
}
}
else
{
<button class="btn btn-danger" @onclick="StopRecording">Stop Record</button>
}
}

@code {
private AudioContext? context;
private AnalyserNode? analyser;
private MediaDevices? mediaDevices;
private string? error;
private byte[] frequencyMeasurements = Array.Empty<byte>();
private bool makeMeasurements = false;
private MediaStream? mediaStream;
private List<(string label, string id)> audioOptions = new();
private string? selectedAudioSource;
private double peakFrequencySum = 0;
private double peakFrequencyCount = 0;
bool recording = false;

MediaRecorder? recorder;
EventListener<BlobEvent>? dataAvailableEventListener;
List<Blob> blobsRecorded = new();
AudioBuffer? audioBuffer;

async Task OpenAudio()
{
await StopAudioTrack();

try
{
if (context is null)
{
context = await AudioContext.CreateAsync(JSRuntime);
}
if (mediaDevices is null)
{
mediaDevices = await MediaDevicesService.GetMediaDevicesAsync();
}

MediaTrackConstraints mediaTrackConstraints = new MediaTrackConstraints
{
EchoCancellation = true,
NoiseSuppression = true,
AutoGainControl = false,
DeviceId = selectedAudioSource is null ? null : new ConstrainDomString(selectedAudioSource)
};
mediaStream = await mediaDevices.GetUserMediaAsync(new MediaStreamConstraints() { Audio = mediaTrackConstraints });

var deviceInfos = await mediaDevices.EnumerateDevicesAsync();
audioOptions.Clear();
foreach (var device in deviceInfos)
{
if (await device.GetKindAsync() is MediaDeviceKind.AudioInput)
{
audioOptions.Add((await device.GetLabelAsync(), await device.GetDeviceIdAsync()));
}
}

analyser = await context.CreateAnalyserAsync();
await using MediaStreamAudioSourceNode mediaStreamAudioSourceNode = await context.CreateMediaStreamSourceAsync(mediaStream);
await mediaStreamAudioSourceNode.ConnectAsync(analyser);
}
catch (WebIDLException ex)
{
error = $"{ex.GetType().Name}: {ex.Message}";
}
catch (Exception ex)
{
error = $"An unexpected error of type '{ex.GetType().Name}' happened.";
}
StateHasChanged();
}

async Task Record()
{
if (mediaStream is null)
return;

recording = true;
StateHasChanged();

// List to collect each recording part.
blobsRecorded.Clear();

// Create new MediaRecorder from some existing MediaStream.
recorder = await MediaRecorder.CreateAsync(JSRuntime, mediaStream);

// Add event listener for when each data part is available.
dataAvailableEventListener =
await EventListener<BlobEvent>.CreateAsync(JSRuntime, async (BlobEvent e) =>
{
Blob blob = await e.GetDataAsync();
blobsRecorded.Add(blob);
});
await recorder.AddOnDataAvailableEventListenerAsync(dataAvailableEventListener);

// Starts Recording
await recorder.StartAsync();
}

async Task StopRecording()
{
if (recorder is null || context is null)
return;

recording = false;

// Stops recording
await recorder.StopAsync();

// Combines and collects the total audio data.
await using Blob combinedBlob = await Blob.CreateAsync(JSRuntime, [.. blobsRecorded]);

byte[] audioData = await combinedBlob.ArrayBufferAsync();
audioBuffer = await context.DecodeAudioDataAsync(audioData);

// Dispose of blob parts created while recording.
foreach (Blob blob in blobsRecorded)
await blob.DisposeAsync();

await StopAudioTrack();
}

async Task AnalyseFrequency()
{
if (context is null || audioBuffer is null)
return;

await using AudioBufferSourceNode sourceNode = await AudioBufferSourceNode.CreateAsync(JSRuntime, context, new()
{
Buffer = audioBuffer,
PlaybackRate = 2
});

analyser = await context.CreateAnalyserAsync();
await using AudioDestinationNode destination = await context.GetDestinationAsync();
await sourceNode.ConnectAsync(analyser);
await analyser.ConnectAsync(destination);

int bufferLength = (int)await analyser.GetFrequencyBinCountAsync();
var frequencyDataArray = await Uint8Array.CreateAsync(JSRuntime, bufferLength);

var sampleRate = await context.GetSampleRateAsync();
var fftSize = await analyser.GetFftSizeAsync();

await using EventListener<Event> endedListener = await EventListener<Event>.CreateAsync(JSRuntime, _ =>
{
makeMeasurements = false;
});
await sourceNode.AddOnEndedEventListenerAsync(endedListener);

await sourceNode.StartAsync();

makeMeasurements = true;
while (makeMeasurements)
{
await analyser.GetByteFrequencyDataAsync(frequencyDataArray);

frequencyMeasurements = await frequencyDataArray.GetAsArrayAsync();

byte largestMeasurement = frequencyMeasurements.Max();
var largestFrequencyIndex = frequencyMeasurements.ToList().IndexOf(largestMeasurement);
peakFrequencySum += largestFrequencyIndex * sampleRate / fftSize * largestMeasurement;
peakFrequencyCount += largestMeasurement;
await Task.Delay(1);
StateHasChanged();
}
}

async Task StopAudioTrack()
{
makeMeasurements = false;
if (mediaStream is null) return;
var audioTrack = (await mediaStream.GetAudioTracksAsync()).FirstOrDefault();
if (audioTrack is not null)
{
await audioTrack.StopAsync();
}
if (analyser is not null)
{
await analyser.DisposeAsync();
}
}

public async ValueTask DisposeAsync()
{
await StopAudioTrack();
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public partial class AmplitudePlot : ComponentBase, IDisposable
[Parameter]
public int Width { get; set; } = 180;

[Parameter]
public string Color { get; set; } = "#000";

protected override async Task OnAfterRenderAsync(bool _)
{
if (running || Analyser is null)
Expand All @@ -50,7 +53,7 @@ protected override async Task OnAfterRenderAsync(bool _)

byte[] reading = await timeDomainData.GetAsArrayAsync();

double amplitude = reading.Max(r => Math.Abs(r - 128)) / 128.0;
double amplitude = reading.Average(r => Math.Abs(r - 128)) / 128.0;

await using (Context2D context = await canvas.GetContext2DAsync())
{
Expand All @@ -63,7 +66,7 @@ protected override async Task OnAfterRenderAsync(bool _)
await context.FillAndStrokeStyles.FillStyleAsync($"#fff");
await context.FillRectAsync(i * 10, 0, 10, Height * 10);

await context.FillAndStrokeStyles.FillStyleAsync($"#000");
await context.FillAndStrokeStyles.FillStyleAsync(Color);
await context.FillRectAsync(i * 10, (Height * 10 / 2.0) - (amplitude * Height * 10), 10, amplitude * 2 * Height * 10);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="AnalyzeMediaStream">
<span class="oi oi-microphone" aria-hidden="true"></span> Analyze MediaStream
<svg class="bi" viewBox="0 0 16 16">
<polyline stroke="white" stroke-width="2" fill="none" points="2,8 4,13 6,3 9,9 11,5 13,6 14,2" stroke-linejoin="round" stroke-linecap="round"></polyline>
</svg> Analyze MediaStream
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="RecordMediaStream">
<span class="oi oi-microphone" aria-hidden="true"></span> Record MediaStream
</NavLink>
</div>
<div class="nav-item px-3">
Expand Down

0 comments on commit 2c424be

Please sign in to comment.