using Amphora; using System; using System.Runtime.InteropServices; namespace Susurrus.XAudio2 { /// /// The voice that mixes every other voice and plays the result on the audio device. /// public class MasteringVoice : XAudio2Voice { // IUnknown::Release. The voice keeps its own reference to an effect, so the creator's is dropped. private const int ReleaseSlot = 2; // The limiter's own defaults. public const uint LimiterReleaseDefault = 6; public const uint LimiterLoudnessDefault = 1000; [StructLayout(LayoutKind.Sequential, Pack = 1)] private struct VoiceDetails { public uint CreationFlags; public uint ActiveFlags; public uint InputChannels; public uint InputSampleRate; } // FXMASTERINGLIMITER_PARAMETERS. [StructLayout(LayoutKind.Sequential, Pack = 1)] private struct LimiterParameters { public uint Release; public uint Loudness; } // XAUDIO2_EFFECT_DESCRIPTOR. [StructLayout(LayoutKind.Sequential, Pack = 1)] private struct EffectDescriptor { public IntPtr Effect; public int InitialState; public uint OutputChannels; } // XAUDIO2_EFFECT_CHAIN. [StructLayout(LayoutKind.Sequential, Pack = 1)] private struct EffectChain { public uint EffectCount; public IntPtr Descriptors; } [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void GetVoiceDetailsCall(IntPtr self, out VoiceDetails details); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int SetEffectChainCall(IntPtr self, ref EffectChain chain); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int EffectSwitchCall(IntPtr self, uint effectIndex, uint operationSet); [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate uint ReleaseCall(IntPtr self); [DllImport("XAudio2_9.dll", ExactSpelling = true, CallingConvention = CallingConvention.StdCall)] private static extern int CreateFX( ref Guid effectClass, out IntPtr effect, ref LimiterParameters initialData, uint initialDataSize); private static Guid MasteringLimiterClass = new Guid("C4137916-2BE1-46FD-8599-441536F49856"); private readonly GetVoiceDetailsCall getVoiceDetails; private readonly SetEffectChainCall setEffectChain; private readonly EffectSwitchCall enableEffect; private readonly EffectSwitchCall disableEffect; private bool hasLimiter; private bool limiterEnabled; public MasteringVoice(IntPtr handle) : base(handle) { this.getVoiceDetails = Vtable.Bind(handle, VoiceSlots.GetVoiceDetails); this.setEffectChain = Vtable.Bind(handle, VoiceSlots.SetEffectChain); this.enableEffect = Vtable.Bind(handle, VoiceSlots.EnableEffect); this.disableEffect = Vtable.Bind(handle, VoiceSlots.DisableEffect); } /// /// Gets whether the voice has a limiter, which adds. /// public bool HasLimiter => hasLimiter; /// /// Gets whether the limiter is working, which is false without one. /// public bool IsLimiting => hasLimiter && limiterEnabled; /// /// Returns how many channels the audio device plays. /// public int GetChannelCount() { ThrowIfDestroyed(); getVoiceDetails(Handle, out VoiceDetails details); return (int)details.InputChannels; } /// /// Puts a limiter on the voice's output, so that many loud sounds at once can't push the mix past full scale /// and clip. It starts working when is true. It fails when this machine's XAudio2 /// has no limiter, and the voice then plays unlimited. /// public Status AttachLimiter(bool enabled, uint release = LimiterReleaseDefault, uint loudness = LimiterLoudnessDefault) { ThrowIfDestroyed(); if (hasLimiter) { throw new InvalidOperationException("The voice already has a limiter."); } LimiterParameters parameters = new LimiterParameters { Release = release, Loudness = loudness }; int hresult; IntPtr effect; try { hresult = CreateFX(ref MasteringLimiterClass, out effect, ref parameters, initialDataSize: (uint)Marshal.SizeOf()); } catch (Exception exception) when (exception is DllNotFoundException || exception is EntryPointNotFoundException || exception is BadImageFormatException) { return XAudio2Failures.Unavailable.Fail(exception.Message); } if (hresult < 0) { return XAudio2Failures.CallFailed.Fail("CreateFX", hresult); } try { EffectDescriptor descriptor = new EffectDescriptor { Effect = effect, InitialState = enabled ? 1 : 0, OutputChannels = (uint)GetChannelCount(), }; unsafe { EffectChain chain = new EffectChain { EffectCount = 1, Descriptors = (IntPtr)(&descriptor) }; hresult = setEffectChain(Handle, ref chain); } } finally { Vtable.Bind(effect, ReleaseSlot)(effect); } if (hresult < 0) { return XAudio2Failures.CallFailed.Fail("IXAudio2Voice::SetEffectChain", hresult); } hasLimiter = true; limiterEnabled = enabled; return Status.Ok; } /// /// Turns the limiter on or off while sound plays. Does nothing if the voice has no limiter. /// public Status SetLimiterEnabled(bool enabled) { ThrowIfDestroyed(); if (!hasLimiter) { return Status.Ok; } int hresult = enabled ? enableEffect(Handle, 0, 0) : disableEffect(Handle, 0, 0); if (hresult < 0) { return XAudio2Failures.CallFailed.Fail("IXAudio2Voice::EnableEffect", hresult); } limiterEnabled = enabled; return Status.Ok; } } }