namespace ytLive.Services.Audio;
///
/// A thread-safe float ring buffer for the live audio path (TASK 9): NAudio
/// raises capture chunks on its own threads, the mixer's live loop drains on
/// its own. Writes overwrite the OLDEST samples when full so the buffer never
/// grows unbounded — the stream can't stall on capture jitter, it just skips
/// the stale tail.
///
public sealed class AudioRingBuffer
{
private readonly object _sync = new();
private readonly float[] _buffer;
private int _head;
private int _count;
public AudioRingBuffer(int capacity)
{
if (capacity <= 0)
throw new ArgumentOutOfRangeException(nameof(capacity));
_buffer = new float[capacity];
}
public int Count
{
get
{
lock (_sync)
{
return _count;
}
}
}
/// Appends a chunk, dropping the oldest samples if the buffer is full.
public void Write(float[] samples)
{
if (samples.Length == 0)
return;
lock (_sync)
{
if (samples.Length >= _buffer.Length)
{
Array.Copy(samples, samples.Length - _buffer.Length, _buffer, 0, _buffer.Length);
_head = 0;
_count = _buffer.Length;
return;
}
if (_count + samples.Length > _buffer.Length)
{
// Evict the oldest samples to make room; the incoming write then
// overwrites them as it wraps. Head stays put — only writing
// advances it.
var drop = _count + samples.Length - _buffer.Length;
_count -= drop;
}
var writeAt = _head;
var first = Math.Min(samples.Length, _buffer.Length - writeAt);
Array.Copy(samples, 0, _buffer, writeAt, first);
if (first < samples.Length)
Array.Copy(samples, first, _buffer, 0, samples.Length - first);
_head = (writeAt + samples.Length) % _buffer.Length;
_count += samples.Length;
}
}
/// Reads up to samples, removing them;
/// returns the number actually read (fewer on underrun).
public int Read(float[] destination, int count)
{
lock (_sync)
{
var n = Math.Min(count, _count);
var tail = (_head - _count + _buffer.Length) % _buffer.Length;
var first = Math.Min(n, _buffer.Length - tail);
Array.Copy(_buffer, tail, destination, 0, first);
if (first < n)
Array.Copy(_buffer, 0, destination, first, n - first);
_count -= n;
return n;
}
}
public void Clear()
{
lock (_sync)
{
_head = 0;
_count = 0;
}
}
}