namespace Nonemm.Recorder; /// A fixed-size queue of samples between the audio thread that fills it and /// the thread that drains it. When it is full, the oldest samples are dropped. public sealed class SampleRing { private readonly float[] buffer; private readonly object gate = new(); private int start; private int count; public SampleRing(int capacity) { buffer = new float[capacity]; } public int Count { get { lock (gate) { return count; } } } public void Write(ReadOnlySpan samples) { lock (gate) { if (samples.Length > buffer.Length) { samples = samples[^buffer.Length..]; } int overflow = count + samples.Length - buffer.Length; if (overflow > 0) { start = (start + overflow) % buffer.Length; count -= overflow; } int end = (start + count) % buffer.Length; int first = Math.Min(samples.Length, buffer.Length - end); samples[..first].CopyTo(buffer.AsSpan(end)); samples[first..].CopyTo(buffer); count += samples.Length; } } /// Fills `destination` with the oldest samples and returns how many it /// got. The rest of `destination` is not touched. public int Read(Span destination) { lock (gate) { int taken = Math.Min(destination.Length, count); int first = Math.Min(taken, buffer.Length - start); buffer.AsSpan(start, first).CopyTo(destination); buffer.AsSpan(0, taken - first).CopyTo(destination[first..]); start = (start + taken) % buffer.Length; count -= taken; return taken; } } /// Drops the oldest samples until at most `keep` are left. public void TrimTo(int keep) { lock (gate) { int drop = count - keep; if (drop > 0) { start = (start + drop) % buffer.Length; count = keep; } } } public void Clear() { lock (gate) { start = 0; count = 0; } } }