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