Files
meeting-assistant/MeetingAssistant/Recording/RollingAudioBuffer.cs
codex 7777b349a5
PR and Push Build/Test / build-and-test (push) Successful in 6m37s
Archive summary refinements and profile switching
2026-05-27 23:11:21 +02:00

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);
}