using Amphora;
using System;
using System.Runtime.InteropServices;
namespace Susurrus.XAudio2
{
///
/// A streamed sound that feeds a source voice through a few buffers that take turns, each refilled once the voice
/// has played it. The buffers are one array, pinned for as long as a voice plays the sound.
///
public class XAudio2StreamedSound : StreamedSound
{
public const int BufferCount = 4;
private readonly byte[] ring;
private readonly int bytesPerFrame;
// Not readonly, since Free clears the handle it is called on.
private GCHandle pin;
private int nextBuffer;
///
/// Gets how many frames each buffer holds, which is about a quarter of a second.
///
public int BufferFrames { get; }
///
/// Gets whether the buffers are pinned, which they are while a voice plays the sound.
///
public bool IsPinned => pin.IsAllocated;
public XAudio2StreamedSound(ISoundStream stream) : base(stream)
{
bytesPerFrame = ChannelCount * StreamFeed.BytesPerSample(stream.Format);
BufferFrames = Math.Max(1, stream.SampleRate / 4);
ring = new byte[BufferCount * BufferFrames * bytesPerFrame];
}
///
/// Holds the sound for a voice, pins the buffers and moves the stream to its start, as does. Call once the voice is done.
///
public new Status Begin(Loop loop)
{
Status status = base.Begin(loop);
pin = GCHandle.Alloc(ring, GCHandleType.Pinned);
nextBuffer = 0;
return status;
}
///
/// Queues a refilled buffer on for each one it has played, until the stream has
/// nothing more to give. A failure ends the feeding.
///
public Status Refill(SourceVoice source)
{
int queued = source.GetBuffersQueued();
while (!IsFed && queued < BufferCount)
{
int offset = nextBuffer * BufferFrames * bytesPerFrame;
StatusOr filled = Feed(new ByteView(ring, offset, offset + BufferFrames * bytesPerFrame));
if (!filled.IsOk)
{
return filled.Status;
}
if (filled.Value == 0)
{
break;
}
XAudio2Buffer buffer = new XAudio2Buffer();
buffer.AudioBytes = (uint)(filled.Value * bytesPerFrame);
buffer.AudioData = pin.AddrOfPinnedObject() + offset;
if (IsFed)
{
buffer.Flags = XAudio2Buffer.EndOfStream;
}
Status status = source.Submit(in buffer);
if (!status.IsOk)
{
IsFed = true;
return status;
}
nextBuffer = (nextBuffer + 1) % BufferCount;
++queued;
}
return Status.Ok;
}
///
/// Unpins the buffers and ends the hold. Call it once the voice is done reading them.
///
public void End()
{
pin.Free();
Release();
}
}
}