using System;
using System.Runtime.InteropServices;
namespace Susurrus.XAudio2
{
///
/// A sound whose samples a voice reads in place while the mixing thread plays them. The sound keeps its own copy,
/// and pins it for as long as any voice holds the sound.
///
public class XAudio2Sound : Sound
{
private readonly byte[] samples;
// Not readonly, since Free clears the handle it is called on.
private GCHandle pin;
///
/// Gets how many bytes of samples the sound has.
///
public int ByteCount => samples.Length;
///
/// Gets how many frames of samples the sound has.
///
public int FrameCount { get; }
///
/// Gets the number of frames per second the sound plays at its natural speed.
///
public int SampleRate { get; }
public SampleFormat Format { get; }
///
/// Gets how many voices are reading the samples.
///
public int HoldCount { get; private set; }
///
/// Gets whether the samples are pinned, which they are while the sound is held.
///
public bool IsPinned => pin.IsAllocated;
private static DecodedSound CheckNotNull(DecodedSound decoded)
{
if (decoded == null)
{
throw new ArgumentNullException(nameof(decoded));
}
return decoded;
}
///
/// Copies the samples of , so the caller can drop or reuse its own.
///
public XAudio2Sound(DecodedSound decoded) : base(
CheckNotNull(decoded).Duration, decoded.ChannelCount)
{
SampleRate = decoded.SampleRate;
Format = decoded.Format;
FrameCount = decoded.FrameCount;
samples = new byte[decoded.Samples.Length];
decoded.Samples.CopyTo(samples, 0);
}
// Rounds, so a duration that was itself rounded down to a tick converts back to the frame it came from.
private uint FrameAt(TimeSpan time)
{
long frame = (time.Ticks * SampleRate + TimeSpan.TicksPerSecond / 2) / TimeSpan.TicksPerSecond;
return (uint)Math.Min(frame, FrameCount);
}
///
/// Holds the sound for a voice, which pins the samples, and returns a buffer of all of them to queue. A buffer
/// given a repeats its region until the voice is stopped. Every call needs a once the voice is done with the buffer.
///
public XAudio2Buffer AcquireBuffer(Loop loop = default)
{
loop.CheckAgainst(Duration, nameof(loop));
if (HoldCount == 0)
{
pin = GCHandle.Alloc(samples, GCHandleType.Pinned);
}
++HoldCount;
XAudio2Buffer returnMe = new XAudio2Buffer();
returnMe.Flags = XAudio2Buffer.EndOfStream;
returnMe.AudioBytes = (uint)samples.Length;
returnMe.AudioData = pin.AddrOfPinnedObject();
if (loop.IsLooping)
{
uint begin = FrameAt(loop.Start);
returnMe.LoopBegin = begin;
returnMe.LoopLength = FrameAt(loop.EndIn(Duration)) - begin;
returnMe.LoopCount = XAudio2Buffer.LoopInfinite;
}
return returnMe;
}
///
/// Ends a hold, and unpins the samples when it was the last. Throws if there is no hold to end.
///
public void ReleaseBuffer()
{
if (HoldCount == 0)
{
throw new InvalidOperationException("The sound isn't held, so there is no hold to release.");
}
--HoldCount;
if (HoldCount == 0)
{
pin.Free();
}
}
}
}