From b1c7740b8639d18b2d5cba639b3f04522a64a195 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Mon, 31 Jul 2023 01:16:19 -0700 Subject: [PATCH] started reworking audio around Voice API --- src/Audio/AudioDevice.cs | 65 ++- src/Audio/AudioTween.cs | 4 +- src/Audio/AudioTweenManager.cs | 36 +- src/Audio/MasteringVoice.cs | 46 ++ src/Audio/ReverbEffect.cs | 59 +-- src/Audio/{SoundQueue.cs => SoundSequence.cs} | 67 +-- src/Audio/SourceVoice.cs | 123 +++++ src/Audio/StaticSourceVoice.cs | 11 + src/Audio/SubmixVoice.cs | 25 + src/Audio/Voice.cs | 446 ++++++++++++++++++ 10 files changed, 723 insertions(+), 159 deletions(-) create mode 100644 src/Audio/MasteringVoice.cs rename src/Audio/{SoundQueue.cs => SoundSequence.cs} (61%) create mode 100644 src/Audio/SourceVoice.cs create mode 100644 src/Audio/StaticSourceVoice.cs create mode 100644 src/Audio/SubmixVoice.cs create mode 100644 src/Audio/Voice.cs diff --git a/src/Audio/AudioDevice.cs b/src/Audio/AudioDevice.cs index e5a86b0..9ffbdf9 100644 --- a/src/Audio/AudioDevice.cs +++ b/src/Audio/AudioDevice.cs @@ -9,24 +9,15 @@ namespace MoonWorks.Audio { public IntPtr Handle { get; } public byte[] Handle3D { get; } - public IntPtr MasteringVoice { get; } public FAudio.FAudioDeviceDetails DeviceDetails { get; } + private MasteringVoice masteringVoice; + public MasteringVoice MasteringVoice => masteringVoice; + public float CurveDistanceScalar = 1f; public float DopplerScale = 1f; public float SpeedOfSound = 343.5f; - private float masteringVolume = 1f; - public float MasteringVolume - { - get => masteringVolume; - set - { - masteringVolume = value; - FAudio.FAudioVoice_SetVolume(MasteringVoice, masteringVolume, 0); - } - } - private readonly HashSet resources = new HashSet(); private readonly List autoUpdateStreamingSoundReferences = new List(); private readonly List autoFreeStaticSoundInstanceReferences = new List(); @@ -93,26 +84,18 @@ namespace MoonWorks.Audio } /* Init Mastering Voice */ - IntPtr masteringVoice; - - if (FAudio.FAudio_CreateMasteringVoice( - Handle, - out masteringVoice, - FAudio.FAUDIO_DEFAULT_CHANNELS, - FAudio.FAUDIO_DEFAULT_SAMPLERATE, - 0, + var result = MasteringVoice.Create( + this, i, - IntPtr.Zero - ) != 0) + out masteringVoice + ); + + if (!result) { - Logger.LogError("No mastering voice found!"); - FAudio.FAudio_Release(Handle); - Handle = IntPtr.Zero; + Logger.LogError("Audio device creation failed!"); return; } - MasteringVoice = masteringVoice; - /* Init 3D Audio */ Handle3D = new byte[FAudio.F3DAUDIO_HANDLE_BYTESIZE]; @@ -192,7 +175,7 @@ namespace MoonWorks.Audio { if (soundSequenceReferences[i].TryGetTarget(out var soundSequence)) { - soundSequence.Update(); + soundSequence.OnUpdate(); } else { @@ -203,13 +186,13 @@ namespace MoonWorks.Audio AudioTweenManager.Update(elapsedSeconds); } - public void SyncPlay() + public void TriggerSyncGroup(uint syncGroup) { - FAudio.FAudio_CommitChanges(Handle, 1); + FAudio.FAudio_CommitChanges(Handle, syncGroup); } internal void CreateTween( - SoundInstance soundInstance, + Voice voice, AudioTweenProperty property, System.Func easingFunction, float start, @@ -220,7 +203,7 @@ namespace MoonWorks.Audio lock (StateLock) { AudioTweenManager.CreateTween( - soundInstance, + voice, property, easingFunction, start, @@ -232,12 +215,12 @@ namespace MoonWorks.Audio } internal void ClearTweens( - SoundInstance soundReference, + Voice voice, AudioTweenProperty property ) { lock (StateLock) { - AudioTweenManager.ClearTweens(soundReference, property); + AudioTweenManager.ClearTweens(voice, property); } } @@ -286,6 +269,18 @@ namespace MoonWorks.Audio if (disposing) { + // stop all source voices + foreach (var weakReference in resources) + { + var target = weakReference.Target; + + if (target != null && target is SourceVoice voice) + { + voice.Stop(); + } + } + + // destroy all audio resources foreach (var weakReference in resources) { var target = weakReference.Target; @@ -295,10 +290,10 @@ namespace MoonWorks.Audio (target as IDisposable).Dispose(); } } + resources.Clear(); } - FAudio.FAudioVoice_DestroyVoice(MasteringVoice); FAudio.FAudio_Release(Handle); IsDisposed = true; diff --git a/src/Audio/AudioTween.cs b/src/Audio/AudioTween.cs index dc71fef..75b2e9d 100644 --- a/src/Audio/AudioTween.cs +++ b/src/Audio/AudioTween.cs @@ -14,7 +14,7 @@ namespace MoonWorks.Audio internal class AudioTween { - public SoundInstance SoundInstance; + public Voice Voice; public AudioTweenProperty Property; public EasingFunction EasingFunction; public float Time; @@ -51,7 +51,7 @@ namespace MoonWorks.Audio public void Free(AudioTween tween) { - tween.SoundInstance = null; + tween.Voice = null; Tweens.Enqueue(tween); } } diff --git a/src/Audio/AudioTweenManager.cs b/src/Audio/AudioTweenManager.cs index f98adcc..d5b05ee 100644 --- a/src/Audio/AudioTweenManager.cs +++ b/src/Audio/AudioTweenManager.cs @@ -6,7 +6,7 @@ namespace MoonWorks.Audio internal class AudioTweenManager { private AudioTweenPool AudioTweenPool = new AudioTweenPool(); - private readonly Dictionary<(SoundInstance, AudioTweenProperty), AudioTween> AudioTweens = new Dictionary<(SoundInstance, AudioTweenProperty), AudioTween>(); + private readonly Dictionary<(Voice, AudioTweenProperty), AudioTween> AudioTweens = new Dictionary<(Voice, AudioTweenProperty), AudioTween>(); private readonly List DelayedAudioTweens = new List(); public void Update(float elapsedSeconds) @@ -14,7 +14,7 @@ namespace MoonWorks.Audio for (var i = DelayedAudioTweens.Count - 1; i >= 0; i--) { var audioTween = DelayedAudioTweens[i]; - var soundInstance = audioTween.SoundInstance; + var voice = audioTween.Voice; audioTween.Time += elapsedSeconds; @@ -24,23 +24,23 @@ namespace MoonWorks.Audio switch (audioTween.Property) { case AudioTweenProperty.Pan: - audioTween.StartValue = soundInstance.Pan; + audioTween.StartValue = voice.Pan; break; case AudioTweenProperty.Pitch: - audioTween.StartValue = soundInstance.Pitch; + audioTween.StartValue = voice.Pitch; break; case AudioTweenProperty.Volume: - audioTween.StartValue = soundInstance.Volume; + audioTween.StartValue = voice.Volume; break; case AudioTweenProperty.FilterFrequency: - audioTween.StartValue = soundInstance.FilterFrequency; + audioTween.StartValue = voice.FilterFrequency; break; case AudioTweenProperty.Reverb: - audioTween.StartValue = soundInstance.Reverb; + audioTween.StartValue = voice.Reverb; break; } @@ -64,7 +64,7 @@ namespace MoonWorks.Audio } public void CreateTween( - SoundInstance soundInstance, + Voice voice, AudioTweenProperty property, System.Func easingFunction, float start, @@ -73,7 +73,7 @@ namespace MoonWorks.Audio float delayTime ) { var tween = AudioTweenPool.Obtain(); - tween.SoundInstance = soundInstance; + tween.Voice = voice; tween.Property = property; tween.EasingFunction = easingFunction; tween.StartValue = start; @@ -92,21 +92,21 @@ namespace MoonWorks.Audio } } - public void ClearTweens(SoundInstance soundInstance, AudioTweenProperty property) + public void ClearTweens(Voice voice, AudioTweenProperty property) { - AudioTweens.Remove((soundInstance, property)); + AudioTweens.Remove((voice, property)); } private void AddTween( AudioTween audioTween ) { // if a tween with the same sound and property already exists, get rid of it - if (AudioTweens.TryGetValue((audioTween.SoundInstance, audioTween.Property), out var currentTween)) + if (AudioTweens.TryGetValue((audioTween.Voice, audioTween.Property), out var currentTween)) { AudioTweenPool.Free(currentTween); } - AudioTweens[(audioTween.SoundInstance, audioTween.Property)] = audioTween; + AudioTweens[(audioTween.Voice, audioTween.Property)] = audioTween; } private static bool UpdateAudioTween(AudioTween audioTween, float delta) @@ -133,23 +133,23 @@ namespace MoonWorks.Audio switch (audioTween.Property) { case AudioTweenProperty.Pan: - audioTween.SoundInstance.Pan = value; + audioTween.Voice.Pan = value; break; case AudioTweenProperty.Pitch: - audioTween.SoundInstance.Pitch = value; + audioTween.Voice.Pitch = value; break; case AudioTweenProperty.Volume: - audioTween.SoundInstance.Volume = value; + audioTween.Voice.Volume = value; break; case AudioTweenProperty.FilterFrequency: - audioTween.SoundInstance.FilterFrequency = value; + audioTween.Voice.FilterFrequency = value; break; case AudioTweenProperty.Reverb: - audioTween.SoundInstance.Reverb = value; + audioTween.Voice.Reverb = value; break; } diff --git a/src/Audio/MasteringVoice.cs b/src/Audio/MasteringVoice.cs new file mode 100644 index 0000000..78fe624 --- /dev/null +++ b/src/Audio/MasteringVoice.cs @@ -0,0 +1,46 @@ +using System; + +namespace MoonWorks.Audio +{ + public class MasteringVoice : Voice + { + // mastering voice can't pan + public override float Pan => 0; + + internal static bool Create( + AudioDevice device, + uint deviceIndex, + out MasteringVoice masteringVoice + ) { + var result = FAudio.FAudio_CreateMasteringVoice( + device.Handle, + out var handle, + FAudio.FAUDIO_DEFAULT_CHANNELS, + FAudio.FAUDIO_DEFAULT_SAMPLERATE, + 0, + deviceIndex, + IntPtr.Zero + ); + + if (result == 0) + { + masteringVoice = new MasteringVoice(device, handle); + } + else + { + Logger.LogError("Failed to create mastering voice!"); + masteringVoice = null; + } + + return result == 0; + } + + internal MasteringVoice( + AudioDevice device, + IntPtr handle + ) : base(device) + { + Handle = handle; + } + } +} diff --git a/src/Audio/ReverbEffect.cs b/src/Audio/ReverbEffect.cs index 42fab2c..eac7cda 100644 --- a/src/Audio/ReverbEffect.cs +++ b/src/Audio/ReverbEffect.cs @@ -4,53 +4,31 @@ using System.Runtime.InteropServices; namespace MoonWorks.Audio { // sound instances can send their audio to this voice to add reverb - public unsafe class ReverbEffect : AudioResource + public unsafe class ReverbEffect : SubmixVoice { - private IntPtr voice; - public IntPtr Voice => voice; - - public ReverbEffect(AudioDevice audioDevice) : base(audioDevice) + public ReverbEffect(AudioDevice audioDevice) : base(audioDevice, 1, audioDevice.DeviceDetails.OutputFormat.Format.nSamplesPerSec) { /* Init reverb */ - IntPtr reverb; FAudio.FAudioCreateReverb(out reverb, 0); - IntPtr chainPtr; - chainPtr = (nint) NativeMemory.Alloc( - (nuint) Marshal.SizeOf() + var chain = new FAudio.FAudioEffectChain(); + var descriptor = new FAudio.FAudioEffectDescriptor(); + + descriptor.InitialState = 1; + descriptor.OutputChannels = Device.DeviceDetails.OutputFormat.Format.nChannels; + descriptor.pEffect = reverb; + + chain.EffectCount = 1; + chain.pEffectDescriptors = (nint) (&descriptor); + + FAudio.FAudioVoice_SetEffectChain( + Handle, + ref chain ); - FAudio.FAudioEffectChain* reverbChain = (FAudio.FAudioEffectChain*) chainPtr; - reverbChain->EffectCount = 1; - reverbChain->pEffectDescriptors = (nint) NativeMemory.Alloc( - (nuint) Marshal.SizeOf() - ); - - FAudio.FAudioEffectDescriptor* reverbDescriptor = - (FAudio.FAudioEffectDescriptor*) reverbChain->pEffectDescriptors; - - reverbDescriptor->InitialState = 1; - reverbDescriptor->OutputChannels = (uint) ( - (audioDevice.DeviceDetails.OutputFormat.Format.nChannels == 6) ? 6 : 1 - ); - reverbDescriptor->pEffect = reverb; - - FAudio.FAudio_CreateSubmixVoice( - audioDevice.Handle, - out voice, - 1, /* omnidirectional reverb */ - audioDevice.DeviceDetails.OutputFormat.Format.nSamplesPerSec, - 0, - 0, - IntPtr.Zero, - chainPtr - ); FAudio.FAPOBase_Release(reverb); - NativeMemory.Free((void*) reverbChain->pEffectDescriptors); - NativeMemory.Free((void*) chainPtr); - /* Init reverb params */ // Defaults based on FAUDIOFX_I3DL2_PRESET_GENERIC @@ -86,7 +64,7 @@ namespace MoonWorks.Audio fixed (FAudio.FAudioFXReverbParameters* reverbParamsPtr = &reverbParams) { FAudio.FAudioVoice_SetEffectParameters( - voice, + Handle, 0, (nint) reverbParamsPtr, (uint) Marshal.SizeOf(), @@ -94,10 +72,5 @@ namespace MoonWorks.Audio ); } } - - protected override void Destroy() - { - FAudio.FAudioVoice_DestroyVoice(Voice); - } } } diff --git a/src/Audio/SoundQueue.cs b/src/Audio/SoundSequence.cs similarity index 61% rename from src/Audio/SoundQueue.cs rename to src/Audio/SoundSequence.cs index d098ecf..6b8c554 100644 --- a/src/Audio/SoundQueue.cs +++ b/src/Audio/SoundSequence.cs @@ -3,25 +3,25 @@ using System; namespace MoonWorks.Audio { // NOTE: all sounds played with a SoundSequence must have the same audio format! - public class SoundSequence : SoundInstance + public class SoundSequence : SourceVoice { public int NeedSoundThreshold = 0; public delegate void OnSoundNeededFunc(); public OnSoundNeededFunc OnSoundNeeded; - private object StateLock = new object(); - public SoundSequence(AudioDevice device, ushort formatTag, ushort bitsPerSample, ushort blockAlign, ushort channels, uint samplesPerSecond) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond) { device.AddSoundSequenceReference(this); + OnUpdate += Update; } public SoundSequence(AudioDevice device, StaticSound templateSound) : base(device, templateSound.FormatTag, templateSound.BitsPerSample, templateSound.BlockAlign, templateSound.Channels, templateSound.SamplesPerSecond) { device.AddSoundSequenceReference(this); + OnUpdate += Update; } - public void Update() + private void Update() { lock (StateLock) { @@ -31,7 +31,7 @@ namespace MoonWorks.Audio if (NeedSoundThreshold > 0) { FAudio.FAudioSourceVoice_GetState( - Voice, + Handle, out var state, FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED ); @@ -65,66 +65,11 @@ namespace MoonWorks.Audio lock (StateLock) { FAudio.FAudioSourceVoice_SubmitSourceBuffer( - Voice, + Handle, ref sound.Handle, IntPtr.Zero ); } } - - public override void Pause() - { - lock (StateLock) - { - if (State == SoundState.Playing) - { - FAudio.FAudioSourceVoice_Stop(Voice, 0, 0); - State = SoundState.Paused; - } - } - } - - public override void Play() - { - PlayUsingOperationSet(0); - } - - public override void QueueSyncPlay() - { - PlayUsingOperationSet(1); - } - - private void PlayUsingOperationSet(uint operationSet) - { - lock (StateLock) - { - if (State == SoundState.Playing) - { - return; - } - - FAudio.FAudioSourceVoice_Start(Voice, 0, operationSet); - State = SoundState.Playing; - } - } - - public override void Stop() - { - lock (StateLock) - { - FAudio.FAudioSourceVoice_ExitLoop(Voice, 0); - State = SoundState.Stopped; - } - } - - public override void StopImmediate() - { - lock (StateLock) - { - FAudio.FAudioSourceVoice_Stop(Voice, 0, 0); - FAudio.FAudioSourceVoice_FlushSourceBuffers(Voice); - State = SoundState.Stopped; - } - } } } diff --git a/src/Audio/SourceVoice.cs b/src/Audio/SourceVoice.cs new file mode 100644 index 0000000..55c1918 --- /dev/null +++ b/src/Audio/SourceVoice.cs @@ -0,0 +1,123 @@ +using System; + +namespace MoonWorks.Audio +{ + public class SourceVoice : Voice + { + protected FAudio.FAudioWaveFormatEx Format; + + protected object StateLock = new object(); + + public uint BuffersQueued + { + get + { + FAudio.FAudioSourceVoice_GetState( + Handle, + out var state, + FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED + ); + + return state.BuffersQueued; + } + } + + private SoundState state; + public SoundState State + { + get + { + if (BuffersQueued == 0) + { + Stop(); + } + + return state; + } + + internal set + { + state = value; + } + } + + public delegate void OnUpdateFunc(); + public OnUpdateFunc OnUpdate; // called by AudioDevice thread + + public SourceVoice( + AudioDevice device, + ushort formatTag, + ushort bitsPerSample, + ushort blockAlign, + ushort channels, + uint samplesPerSecond + ) : base(device) + { + Format = new FAudio.FAudioWaveFormatEx + { + wFormatTag = formatTag, + wBitsPerSample = bitsPerSample, + nChannels = channels, + nBlockAlign = blockAlign, + nSamplesPerSec = samplesPerSecond, + nAvgBytesPerSec = blockAlign * samplesPerSecond + }; + + FAudio.FAudio_CreateSourceVoice( + device.Handle, + out var Handle, + ref Format, + FAudio.FAUDIO_VOICE_USEFILTER, + FAudio.FAUDIO_DEFAULT_FREQ_RATIO, + IntPtr.Zero, + IntPtr.Zero, // default sends to mastering voice! + IntPtr.Zero + ); + } + + public void Play(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) + { + lock (StateLock) + { + FAudio.FAudioSourceVoice_Start(Handle, 0, syncGroup); + + State = SoundState.Playing; + } + } + + public void Pause(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) + { + lock (StateLock) + { + FAudio.FAudioSourceVoice_Stop(Handle, 0, syncGroup); + + State = SoundState.Paused; + } + } + + public void ExitLoop(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) + { + lock (StateLock) + { + FAudio.FAudioSourceVoice_ExitLoop(Handle, syncGroup); + } + } + + public void Stop(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) + { + lock (StateLock) + { + FAudio.FAudioSourceVoice_Stop(Handle, 0, syncGroup); + FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle); + + State = SoundState.Stopped; + } + } + + protected override unsafe void Destroy() + { + Stop(); + base.Destroy(); + } + } +} diff --git a/src/Audio/StaticSourceVoice.cs b/src/Audio/StaticSourceVoice.cs new file mode 100644 index 0000000..f4f6a7b --- /dev/null +++ b/src/Audio/StaticSourceVoice.cs @@ -0,0 +1,11 @@ +namespace MoonWorks.Audio +{ + public class StaticSourceVoice : SourceVoice + { + public StaticSourceVoice(AudioDevice device, ushort formatTag, ushort bitsPerSample, ushort blockAlign, ushort channels, uint samplesPerSecond) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond) + { + } + + + } +} diff --git a/src/Audio/SubmixVoice.cs b/src/Audio/SubmixVoice.cs new file mode 100644 index 0000000..02ad4ad --- /dev/null +++ b/src/Audio/SubmixVoice.cs @@ -0,0 +1,25 @@ +using System; + +namespace MoonWorks.Audio +{ + public class SubmixVoice : Voice + { + public SubmixVoice( + AudioDevice device, + uint inputChannels, + uint sampleRate + ) : base(device) + { + FAudio.FAudio_CreateSubmixVoice( + device.Handle, + out Handle, + inputChannels, + sampleRate, + 0, + 0, + IntPtr.Zero, + IntPtr.Zero + ); + } + } +} diff --git a/src/Audio/Voice.cs b/src/Audio/Voice.cs new file mode 100644 index 0000000..14ee3a2 --- /dev/null +++ b/src/Audio/Voice.cs @@ -0,0 +1,446 @@ +using System; +using System.Runtime.InteropServices; +using EasingFunction = System.Func; + +namespace MoonWorks.Audio +{ + public abstract class Voice : AudioResource + { + protected IntPtr Handle; + + protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings; + + private ReverbEffect ReverbEffect; + + public bool Is3D { get; protected set; } + + private float pan = 0; + public virtual float Pan + { + get => pan; + internal set + { + value = Math.MathHelper.Clamp(value, -1f, 1f); + if (pan != value) + { + pan = value; + + if (pan < -1f) + { + pan = -1f; + } + if (pan > 1f) + { + pan = 1f; + } + + if (Is3D) { return; } + + SetPanMatrixCoefficients(); + FAudio.FAudioVoice_SetOutputMatrix( + Handle, + Device.MasteringVoice.Handle, + dspSettings.SrcChannelCount, + dspSettings.DstChannelCount, + dspSettings.pMatrixCoefficients, + 0 + ); + } + } + } + + private float pitch = 0; + public float Pitch + { + get => pitch; + internal set + { + value = Math.MathHelper.Clamp(value, -1f, 1f); + if (pitch != value) + { + pitch = value; + UpdatePitch(); + } + } + } + + private float volume = 1; + public float Volume + { + get => volume; + internal set + { + value = Math.MathHelper.Max(0, value); + if (volume != value) + { + volume = value; + FAudio.FAudioVoice_SetVolume(Handle, volume, 0); + } + } + } + + private const float MAX_FILTER_FREQUENCY = 1f; + private const float MAX_FILTER_ONEOVERQ = 1.5f; + + private FAudio.FAudioFilterParameters filterParameters = new FAudio.FAudioFilterParameters + { + Type = FAudio.FAudioFilterType.FAudioLowPassFilter, + Frequency = 1f, + OneOverQ = 1f + }; + + public float FilterFrequency + { + get => filterParameters.Frequency; + internal set + { + value = System.Math.Clamp(value, 0.01f, MAX_FILTER_FREQUENCY); + if (filterParameters.Frequency != value) + { + filterParameters.Frequency = value; + + FAudio.FAudioVoice_SetFilterParameters( + Handle, + ref filterParameters, + 0 + ); + } + } + } + + public float FilterOneOverQ + { + get => filterParameters.OneOverQ; + internal set + { + value = System.Math.Clamp(value, 0.01f, MAX_FILTER_ONEOVERQ); + if (filterParameters.OneOverQ != value) + { + filterParameters.OneOverQ = value; + + FAudio.FAudioVoice_SetFilterParameters( + Handle, + ref filterParameters, + 0 + ); + } + } + } + + private FilterType filterType; + public FilterType FilterType + { + get => filterType; + set + { + if (filterType != value) + { + filterType = value; + + switch (filterType) + { + case FilterType.None: + filterParameters = new FAudio.FAudioFilterParameters + { + Type = FAudio.FAudioFilterType.FAudioLowPassFilter, + Frequency = 1f, + OneOverQ = 1f + }; + break; + + case FilterType.LowPass: + filterParameters.Type = FAudio.FAudioFilterType.FAudioLowPassFilter; + filterParameters.Frequency = 1f; + break; + + case FilterType.BandPass: + filterParameters.Type = FAudio.FAudioFilterType.FAudioBandPassFilter; + break; + + case FilterType.HighPass: + filterParameters.Type = FAudio.FAudioFilterType.FAudioHighPassFilter; + filterParameters.Frequency = 0f; + break; + } + + FAudio.FAudioVoice_SetFilterParameters( + Handle, + ref filterParameters, + 0 + ); + } + } + } + + private float reverb; + public unsafe float Reverb + { + get => reverb; + internal set + { + if (ReverbEffect != null) + { + value = MathF.Max(0, value); + if (reverb != value) + { + reverb = value; + + float* outputMatrix = (float*) dspSettings.pMatrixCoefficients; + outputMatrix[0] = reverb; + if (dspSettings.SrcChannelCount == 2) + { + outputMatrix[1] = reverb; + } + + FAudio.FAudioVoice_SetOutputMatrix( + Handle, + ReverbEffect.Handle, + dspSettings.SrcChannelCount, + 1, + dspSettings.pMatrixCoefficients, + 0 + ); + } + } + + #if DEBUG + if (ReverbEffect == null) + { + Logger.LogWarn("Tried to set reverb value before applying a reverb effect"); + } + #endif + } + } + + public Voice(AudioDevice device) : base(device) { } + + public void SetPan(float targetValue) + { + Pan = targetValue; + Device.ClearTweens(this, AudioTweenProperty.Pan); + } + + public void SetPan(float targetValue, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, 0); + } + + public void SetPan(float targetValue, float delayTime, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, delayTime); + } + + public void SetPitch(float targetValue) + { + Pitch = targetValue; + Device.ClearTweens(this, AudioTweenProperty.Pitch); + } + + public void SetPitch(float targetValue, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.Pitch, easingFunction, Pitch, targetValue, duration, 0); + } + + public void SetPitch(float targetValue, float delayTime, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.Pitch, easingFunction, Pitch, targetValue, duration, delayTime); + } + + public void SetVolume(float targetValue) + { + Volume = targetValue; + Device.ClearTweens(this, AudioTweenProperty.Volume); + } + + public void SetVolume(float targetValue, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.Volume, easingFunction, Volume, targetValue, duration, 0); + } + + public void SetVolume(float targetValue, float delayTime, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.Volume, easingFunction, Volume, targetValue, duration, delayTime); + } + + public void SetFilterFrequency(float targetValue) + { + FilterFrequency = targetValue; + Device.ClearTweens(this, AudioTweenProperty.FilterFrequency); + } + + public void SetFilterFrequency(float targetValue, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.FilterFrequency, easingFunction, FilterFrequency, targetValue, duration, 0); + } + + public void SetFilterFrequency(float targetValue, float delayTime, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.FilterFrequency, easingFunction, FilterFrequency, targetValue, duration, delayTime); + } + + public void SetFilterOneOverQ(float targetValue) + { + FilterOneOverQ = targetValue; + } + + public void SetReverb(float targetValue) + { + Reverb = targetValue; + Device.ClearTweens(this, AudioTweenProperty.Reverb); + } + + public void SetReverb(float targetValue, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, 0); + } + + public void SetReverb(float targetValue, float delayTime, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, delayTime); + } + + public unsafe void SetSends(Voice send) + { + FAudio.FAudioSendDescriptor* sendDesc = stackalloc FAudio.FAudioSendDescriptor[1]; + sendDesc[0].Flags = 0; + sendDesc[0].pOutputVoice = send.Handle; + + var sends = new FAudio.FAudioVoiceSends(); + sends.SendCount = 1; + sends.pSends = (nint) sendDesc; + + FAudio.FAudioVoice_SetOutputVoices( + Handle, + ref sends + ); + } + + public unsafe void SetSends(Voice sendOne, Voice sendTwo) + { + var sendDesc = stackalloc FAudio.FAudioSendDescriptor[2]; + sendDesc[0].Flags = 0; + sendDesc[0].pOutputVoice = sendOne.Handle; + sendDesc[1].Flags = 0; + sendDesc[1].pOutputVoice = sendTwo.Handle; + + var sends = new FAudio.FAudioVoiceSends(); + sends.SendCount = 2; + sends.pSends = (nint) sendDesc; + + FAudio.FAudioVoice_SetOutputVoices( + Handle, + ref sends + ); + } + + public unsafe void SetReverbEffectChain(ReverbEffect reverbEffect) + { + SetSends(Device.MasteringVoice, reverbEffect); + ReverbEffect = reverbEffect; + } + + private unsafe void InitDSPSettings(uint srcChannels) + { + dspSettings = new FAudio.F3DAUDIO_DSP_SETTINGS(); + dspSettings.DopplerFactor = 1f; + dspSettings.SrcChannelCount = srcChannels; + dspSettings.DstChannelCount = Device.DeviceDetails.OutputFormat.Format.nChannels; + + nuint memsize = ( + 4 * + dspSettings.SrcChannelCount * + dspSettings.DstChannelCount + ); + + dspSettings.pMatrixCoefficients = (nint) NativeMemory.Alloc(memsize); + byte* memPtr = (byte*) dspSettings.pMatrixCoefficients; + for (uint i = 0; i < memsize; i += 1) + { + memPtr[i] = 0; + } + + SetPanMatrixCoefficients(); + } + + private void UpdatePitch() + { + float doppler; + float dopplerScale = Device.DopplerScale; + if (!Is3D || dopplerScale == 0.0f) + { + doppler = 1.0f; + } + else + { + doppler = dspSettings.DopplerFactor * dopplerScale; + } + + FAudio.FAudioSourceVoice_SetFrequencyRatio( + Handle, + (float) System.Math.Pow(2.0, pitch) * doppler, + 0 + ); + } + + // Taken from https://github.com/FNA-XNA/FNA/blob/master/src/Audio/SoundEffectInstance.cs + private unsafe void SetPanMatrixCoefficients() + { + /* Two major things to notice: + * 1. The spec assumes any speaker count >= 2 has Front Left/Right. + * 2. Stereo panning is WAY more complicated than you think. + * The main thing is that hard panning does NOT eliminate an + * entire channel; the two channels are blended on each side. + * -flibit + */ + float* outputMatrix = (float*) dspSettings.pMatrixCoefficients; + if (dspSettings.SrcChannelCount == 1) + { + if (dspSettings.DstChannelCount == 1) + { + outputMatrix[0] = 1.0f; + } + else + { + outputMatrix[0] = (pan > 0.0f) ? (1.0f - pan) : 1.0f; + outputMatrix[1] = (pan < 0.0f) ? (1.0f + pan) : 1.0f; + } + } + else + { + if (dspSettings.DstChannelCount == 1) + { + outputMatrix[0] = 1.0f; + outputMatrix[1] = 1.0f; + } + else + { + if (pan <= 0.0f) + { + // Left speaker blends left/right channels + outputMatrix[0] = 0.5f * pan + 1.0f; + outputMatrix[1] = 0.5f * -pan; + // Right speaker gets less of the right channel + outputMatrix[2] = 0.0f; + outputMatrix[3] = pan + 1.0f; + } + else + { + // Left speaker gets less of the left channel + outputMatrix[0] = -pan + 1.0f; + outputMatrix[1] = 0.0f; + // Right speaker blends right/left channels + outputMatrix[2] = 0.5f * pan; + outputMatrix[3] = 0.5f * -pan + 1.0f; + } + } + } + } + + protected unsafe override void Destroy() + { + FAudio.FAudioVoice_DestroyVoice(Handle); + + NativeMemory.Free((void*) dspSettings.pMatrixCoefficients); + } + } +}