using Amphora; using System; using System.Runtime.InteropServices; namespace Susurrus.XAudio2 { /// /// A voice that plays queued buffers of samples in one fixed format. /// public class SourceVoice : XAudio2Voice { [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int FlaggedCall(IntPtr self, uint flags, uint operationSet); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int SubmitCall(IntPtr self, ref XAudio2Buffer buffer, IntPtr bufferWma); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NoArgumentCall(IntPtr self); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void GetStateCall(IntPtr self, out XAudio2VoiceState state, uint flags); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int SetVolumeCall(IntPtr self, float volume, uint operationSet); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int SetSampleRateCall(IntPtr self, uint sampleRate); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int SetFrequencyRatioCall(IntPtr self, float ratio, uint operationSet); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void GetFrequencyRatioCall(IntPtr self, out float ratio); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int SetOutputMatrixCall( IntPtr self, IntPtr destination, uint sourceChannels, uint destinationChannels, float[] levels, uint operationSet); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void GetOutputMatrixCall( IntPtr self, IntPtr destination, uint sourceChannels, uint destinationChannels, float[] levels); private readonly FlaggedCall start; private readonly FlaggedCall stop; private readonly SubmitCall submit; private readonly NoArgumentCall flush; private readonly GetStateCall getState; private readonly SetVolumeCall setVolume; private readonly SetSampleRateCall setSampleRate; private readonly SetFrequencyRatioCall setFrequencyRatio; private readonly GetFrequencyRatioCall getFrequencyRatio; private readonly SetOutputMatrixCall setOutputMatrix; private readonly GetOutputMatrixCall getOutputMatrix; public SourceVoice(IntPtr handle) : base(handle) { this.setFrequencyRatio = Vtable.Bind(handle, VoiceSlots.SetFrequencyRatio); this.getFrequencyRatio = Vtable.Bind(handle, VoiceSlots.GetFrequencyRatio); this.setOutputMatrix = Vtable.Bind(handle, VoiceSlots.SetOutputMatrix); this.getOutputMatrix = Vtable.Bind(handle, VoiceSlots.GetOutputMatrix); this.start = Vtable.Bind(handle, VoiceSlots.Start); this.stop = Vtable.Bind(handle, VoiceSlots.Stop); this.submit = Vtable.Bind(handle, VoiceSlots.SubmitSourceBuffer); this.flush = Vtable.Bind(handle, VoiceSlots.FlushSourceBuffers); this.getState = Vtable.Bind(handle, VoiceSlots.GetState); this.setVolume = Vtable.Bind(handle, VoiceSlots.SetVolume); this.setSampleRate = Vtable.Bind(handle, VoiceSlots.SetSourceSampleRate); } private static Status Check(int hresult, string call) { if (hresult < 0) { return XAudio2Failures.CallFailed.Fail(call, hresult); } return Status.Ok; } public Status Start() { ThrowIfDestroyed(); return Check(start(Handle, flags: 0, operationSet: 0), "IXAudio2SourceVoice::Start"); } public Status Stop() { ThrowIfDestroyed(); return Check(stop(Handle, flags: 0, operationSet: 0), "IXAudio2SourceVoice::Stop"); } /// /// Queues to play after the buffers already queued. Its samples have to stay pinned /// until the voice has played it or been flushed. /// public Status Submit(in XAudio2Buffer buffer) { ThrowIfDestroyed(); XAudio2Buffer copy = buffer; return Check(submit(Handle, ref copy, bufferWma: IntPtr.Zero), "IXAudio2SourceVoice::SubmitSourceBuffer"); } /// /// Drops the queued buffers. The mixing thread does it, so reaches zero a /// moment later. /// public Status Flush() { ThrowIfDestroyed(); return Check(flush(Handle), "IXAudio2SourceVoice::FlushSourceBuffers"); } public XAudio2VoiceState GetState() { ThrowIfDestroyed(); getState(Handle, out XAudio2VoiceState state, flags: 0); return state; } /// /// Returns how many buffers are queued, including the one playing. Cheaper than , /// which also counts the samples played. /// public int GetBuffersQueued() { ThrowIfDestroyed(); getState(Handle, out XAudio2VoiceState state, XAudio2VoiceState.NoSamplesPlayed); return (int)state.BuffersQueued; } /// /// Sets the linear gain, where one leaves the samples as they are. /// public Status SetVolume(float volume) { ThrowIfDestroyed(); Volume.Check(volume, nameof(volume)); return Check(setVolume(Handle, volume, operationSet: 0), "IXAudio2Voice::SetVolume"); } /// /// Changes the rate the voice reads its samples at, which pitches the sound without changing the format. /// public Status SetSampleRate(int sampleRate) { ThrowIfDestroyed(); if (sampleRate <= 0) { throw new ArgumentOutOfRangeException( nameof(sampleRate), sampleRate, "A sample rate must be positive."); } return Check(setSampleRate(Handle, (uint)sampleRate), "IXAudio2SourceVoice::SetSourceSampleRate"); } /// /// Sets the ratio the voice reads its samples at, on top of its sample rate. A ratio of one leaves the pitch /// alone. The voice was made with a highest ratio it can take. /// public Status SetFrequencyRatio(float ratio) { ThrowIfDestroyed(); Pitch.Check(ratio, nameof(ratio)); return Check(setFrequencyRatio(Handle, ratio, operationSet: 0), "IXAudio2SourceVoice::SetFrequencyRatio"); } public float GetFrequencyRatio() { ThrowIfDestroyed(); getFrequencyRatio(Handle, out float ratio); return ratio; } /// /// Sets how much of each channel of the voice goes to each channel of the mastering voice, as a row of levels /// for every destination channel and a column for every source channel. /// public Status SetOutputMatrix(int sourceChannels, int destinationChannels, float[] levels) { ThrowIfDestroyed(); CheckMatrix(sourceChannels, destinationChannels, levels); return Check(setOutputMatrix(Handle, IntPtr.Zero, (uint)sourceChannels, (uint)destinationChannels, levels, operationSet: 0), "IXAudio2Voice::SetOutputMatrix"); } /// /// Reads back the levels the voice sends to the mastering voice, into the start of . /// public void GetOutputMatrix(int sourceChannels, int destinationChannels, float[] levels) { ThrowIfDestroyed(); CheckMatrix(sourceChannels, destinationChannels, levels); getOutputMatrix(Handle, IntPtr.Zero, (uint)sourceChannels, (uint)destinationChannels, levels); } private static void CheckMatrix(int sourceChannels, int destinationChannels, float[] levels) { if (levels == null) { throw new ArgumentNullException(nameof(levels)); } if (sourceChannels < 1 || destinationChannels < 1 || levels.Length < sourceChannels * destinationChannels) { throw new ArgumentException( "The levels need a place for every source channel to every destination channel.", nameof(levels)); } } } }