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