using NAudio.Wave; namespace MeetingAssistant.Recording; internal sealed class RollingAudioBuffer { private readonly TimeSpan retention; private readonly object gate = new(); private readonly Queue 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 byte[] TryExtractWav(TimeSpan start, TimeSpan end) { if (end <= start) { return []; } List 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); }