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