Public Access
119 lines
3.4 KiB
C#
119 lines
3.4 KiB
C#
using NAudio.Wave;
|
|
|
|
namespace MeetingAssistant.Recording;
|
|
|
|
internal sealed class RollingAudioBuffer
|
|
{
|
|
private readonly TimeSpan retention;
|
|
private readonly object gate = new();
|
|
private readonly Queue<TimedAudioChunk> chunks = new();
|
|
private TimeSpan position;
|
|
|
|
public RollingAudioBuffer(TimeSpan retention)
|
|
{
|
|
this.retention = retention > TimeSpan.Zero ? retention : TimeSpan.FromMinutes(10);
|
|
}
|
|
|
|
public void Append(AudioChunk chunk)
|
|
{
|
|
if (chunk.Pcm.Length == 0 || chunk.SampleRate <= 0 || chunk.Channels <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (gate)
|
|
{
|
|
var start = position;
|
|
var end = start + chunk.Duration;
|
|
chunks.Enqueue(new TimedAudioChunk(start, end, chunk));
|
|
position = end;
|
|
Prune(end - retention);
|
|
}
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
lock (gate)
|
|
{
|
|
chunks.Clear();
|
|
position = TimeSpan.Zero;
|
|
}
|
|
}
|
|
|
|
public byte[] TryExtractWav(TimeSpan start, TimeSpan end)
|
|
{
|
|
if (end <= start)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
List<TimedAudioChunk> matchingChunks;
|
|
lock (gate)
|
|
{
|
|
matchingChunks = chunks
|
|
.Where(chunk => chunk.End > start && chunk.Start < end)
|
|
.ToList();
|
|
}
|
|
|
|
if (matchingChunks.Count == 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var format = new WaveFormat(
|
|
matchingChunks[0].Chunk.SampleRate,
|
|
16,
|
|
matchingChunks[0].Chunk.Channels);
|
|
using var output = new MemoryStream();
|
|
using (var writer = new WaveFileWriter(output, format))
|
|
{
|
|
foreach (var timedChunk in matchingChunks)
|
|
{
|
|
if (timedChunk.Chunk.SampleRate != format.SampleRate || timedChunk.Chunk.Channels != format.Channels)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var blockAlign = format.BlockAlign;
|
|
var copyStart = Max(start, timedChunk.Start);
|
|
var copyEnd = Min(end, timedChunk.End);
|
|
var firstFrame = (int)Math.Floor((copyStart - timedChunk.Start).TotalSeconds * format.SampleRate);
|
|
var afterLastFrame = (int)Math.Ceiling((copyEnd - timedChunk.Start).TotalSeconds * format.SampleRate);
|
|
var byteOffset = Clamp(firstFrame * blockAlign, 0, timedChunk.Chunk.Pcm.Length);
|
|
var byteCount = Clamp(afterLastFrame * blockAlign, byteOffset, timedChunk.Chunk.Pcm.Length) - byteOffset;
|
|
if (byteCount > 0)
|
|
{
|
|
writer.Write(timedChunk.Chunk.Pcm, byteOffset, byteCount);
|
|
}
|
|
}
|
|
}
|
|
|
|
return output.ToArray();
|
|
}
|
|
|
|
private void Prune(TimeSpan cutoff)
|
|
{
|
|
while (chunks.Count > 0 && chunks.Peek().End < cutoff)
|
|
{
|
|
chunks.Dequeue();
|
|
}
|
|
}
|
|
|
|
private static TimeSpan Max(TimeSpan left, TimeSpan right)
|
|
{
|
|
return left >= right ? left : right;
|
|
}
|
|
|
|
private static TimeSpan Min(TimeSpan left, TimeSpan right)
|
|
{
|
|
return left <= right ? left : right;
|
|
}
|
|
|
|
private static int Clamp(int value, int min, int max)
|
|
{
|
|
return Math.Min(Math.Max(value, min), max);
|
|
}
|
|
|
|
private sealed record TimedAudioChunk(TimeSpan Start, TimeSpan End, AudioChunk Chunk);
|
|
}
|