From c2cb83f93fc674c5be11383d1f83e254b1cae992 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Tue, 1 Aug 2023 18:13:17 -0700 Subject: [PATCH] faux mastering voice + submitting static sounds --- src/Audio/AudioDevice.cs | 99 +++++++---- src/Audio/AudioTweenManager.cs | 20 +-- src/Audio/IReceivableVoice.cs | 7 - src/Audio/MasteringVoice.cs | 43 ----- src/Audio/SendableVoice.cs | 227 -------------------------- src/Audio/SoundSequence.cs | 6 +- src/Audio/SourceVoice.cs | 89 +++++++--- src/Audio/StaticSound.cs | 69 +------- src/Audio/StaticSoundInstance.cs | 141 ---------------- src/Audio/SubmixVoice.cs | 2 +- src/Audio/Voice.cs | 271 +++++++++++++++++++++++++++++-- 11 files changed, 396 insertions(+), 578 deletions(-) delete mode 100644 src/Audio/IReceivableVoice.cs delete mode 100644 src/Audio/MasteringVoice.cs delete mode 100644 src/Audio/SendableVoice.cs delete mode 100644 src/Audio/StaticSoundInstance.cs diff --git a/src/Audio/AudioDevice.cs b/src/Audio/AudioDevice.cs index 18bb9ea..2c39408 100644 --- a/src/Audio/AudioDevice.cs +++ b/src/Audio/AudioDevice.cs @@ -10,8 +10,12 @@ namespace MoonWorks.Audio public byte[] Handle3D { get; } public FAudio.FAudioDeviceDetails DeviceDetails { get; } - private MasteringVoice masteringVoice; - public MasteringVoice MasteringVoice => masteringVoice; + private IntPtr trueMasteringVoice; + + // this is a fun little trick where we use a submix voice as a "faux" mastering voice + // this lets us maintain API consistency for effects like panning and reverb + private SubmixVoice fauxMasteringVoice; + public SubmixVoice MasteringVoice => fauxMasteringVoice; public float CurveDistanceScalar = 1f; public float DopplerScale = 1f; @@ -19,8 +23,8 @@ namespace MoonWorks.Audio private readonly HashSet resources = new HashSet(); private readonly List autoUpdateStreamingSoundReferences = new List(); - private readonly List autoFreeStaticSoundInstanceReferences = new List(); private readonly List> soundSequenceReferences = new List>(); + private readonly List autoFreeSourceVoices = new List(); private AudioTweenManager AudioTweenManager; @@ -85,18 +89,25 @@ namespace MoonWorks.Audio } /* Init Mastering Voice */ - var result = MasteringVoice.Create( - this, + var result = FAudio.FAudio_CreateMasteringVoice( + Handle, + out trueMasteringVoice, + FAudio.FAUDIO_DEFAULT_CHANNELS, + FAudio.FAUDIO_DEFAULT_SAMPLERATE, + 0, i, - out masteringVoice + IntPtr.Zero ); - if (!result) + if (result != 0) { + Logger.LogError("Failed to create a mastering voice!"); Logger.LogError("Audio device creation failed!"); return; } + fauxMasteringVoice = new SubmixVoice(this, FAudio.FAUDIO_DEFAULT_CHANNELS, FAudio.FAUDIO_DEFAULT_SAMPLERATE); + /* Init 3D Audio */ Handle3D = new byte[FAudio.F3DAUDIO_HANDLE_BYTESIZE]; @@ -162,17 +173,6 @@ namespace MoonWorks.Audio } } - for (var i = autoFreeStaticSoundInstanceReferences.Count - 1; i >= 0; i -= 1) - { - var staticSoundInstance = autoFreeStaticSoundInstanceReferences[i]; - - if (staticSoundInstance.State == SoundState.Stopped) - { - staticSoundInstance.Free(); - autoFreeStaticSoundInstanceReferences.RemoveAt(i); - } - } - for (var i = soundSequenceReferences.Count - 1; i >= 0; i -= 1) { if (soundSequenceReferences[i].TryGetTarget(out var soundSequence)) @@ -185,14 +185,61 @@ namespace MoonWorks.Audio } } + for (var i = autoFreeSourceVoices.Count - 1; i >= 0; i -= 1) + { + var voice = autoFreeSourceVoices[i]; + if (voice.BuffersQueued == 0) + { + Return(voice); + autoFreeSourceVoices.RemoveAt(i); + } + } + AudioTweenManager.Update(elapsedSeconds); } + /// + /// Triggers all pending operations with the given syncGroup value. + /// public void TriggerSyncGroup(uint syncGroup) { FAudio.FAudio_CommitChanges(Handle, syncGroup); } + /// + /// Obtains an appropriate source voice from the voice pool. + /// + /// The format that the voice must match. + /// A source voice with the given format. + public SourceVoice Obtain(Format format) + { + lock (StateLock) + { + return VoicePool.Obtain(format); + } + } + + internal void ReturnWhenIdle(SourceVoice voice) + { + lock (StateLock) + { + autoFreeSourceVoices.Add(voice); + } + } + + /// + /// Returns the source voice to the voice pool. + /// + /// + internal void Return(SourceVoice voice) + { + lock (StateLock) + { + voice.Reset(); + VoicePool.Return(voice); + } + } + internal void CreateTween( Voice voice, AudioTweenProperty property, @@ -252,26 +299,11 @@ namespace MoonWorks.Audio autoUpdateStreamingSoundReferences.Add(instance); } - internal void AddAutoFreeStaticSoundInstance(StaticSoundInstance instance) - { - autoFreeStaticSoundInstanceReferences.Add(instance); - } - internal void AddSoundSequenceReference(SoundSequence sequence) { soundSequenceReferences.Add(new WeakReference(sequence)); } - internal SourceVoice ObtainSourceVoice(Format format) - { - return VoicePool.Obtain(format); - } - - internal void ReturnSourceVoice(SourceVoice voice) - { - VoicePool.Return(voice); - } - protected virtual void Dispose(bool disposing) { if (!IsDisposed) @@ -306,6 +338,7 @@ namespace MoonWorks.Audio resources.Clear(); } + FAudio.FAudioVoice_DestroyVoice(trueMasteringVoice); FAudio.FAudio_Release(Handle); IsDisposed = true; diff --git a/src/Audio/AudioTweenManager.cs b/src/Audio/AudioTweenManager.cs index 88d8203..d5b05ee 100644 --- a/src/Audio/AudioTweenManager.cs +++ b/src/Audio/AudioTweenManager.cs @@ -24,10 +24,7 @@ namespace MoonWorks.Audio switch (audioTween.Property) { case AudioTweenProperty.Pan: - if (voice is SendableVoice pannableVoice) - { - audioTween.StartValue = pannableVoice.Pan; - } + audioTween.StartValue = voice.Pan; break; case AudioTweenProperty.Pitch: @@ -43,10 +40,7 @@ namespace MoonWorks.Audio break; case AudioTweenProperty.Reverb: - if (voice is SendableVoice reverbableVoice) - { - audioTween.StartValue = reverbableVoice.Reverb; - } + audioTween.StartValue = voice.Reverb; break; } @@ -139,10 +133,7 @@ namespace MoonWorks.Audio switch (audioTween.Property) { case AudioTweenProperty.Pan: - if (audioTween.Voice is SendableVoice pannableVoice) - { - pannableVoice.Pan = value; - } + audioTween.Voice.Pan = value; break; case AudioTweenProperty.Pitch: @@ -158,10 +149,7 @@ namespace MoonWorks.Audio break; case AudioTweenProperty.Reverb: - if (audioTween.Voice is SendableVoice reverbableVoice) - { - reverbableVoice.Reverb = value; - } + audioTween.Voice.Reverb = value; break; } diff --git a/src/Audio/IReceivableVoice.cs b/src/Audio/IReceivableVoice.cs deleted file mode 100644 index a40abeb..0000000 --- a/src/Audio/IReceivableVoice.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MoonWorks.Audio -{ - public interface IReceivableVoice - { - public System.IntPtr Handle { get; } - } -} diff --git a/src/Audio/MasteringVoice.cs b/src/Audio/MasteringVoice.cs deleted file mode 100644 index e5c23ed..0000000 --- a/src/Audio/MasteringVoice.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; - -namespace MoonWorks.Audio -{ - public class MasteringVoice : Voice, IReceivableVoice - { - 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, device.DeviceDetails.OutputFormat.Format.nChannels, 0) - { - this.handle = handle; - } - } -} diff --git a/src/Audio/SendableVoice.cs b/src/Audio/SendableVoice.cs deleted file mode 100644 index c467584..0000000 --- a/src/Audio/SendableVoice.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using EasingFunction = System.Func; - -namespace MoonWorks.Audio -{ - public unsafe class SendableVoice : Voice - { - private IReceivableVoice OutputVoice; - private ReverbEffect ReverbEffect; - - byte* pMatrixCoefficients; - - protected float pan = 0; - public 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, - OutputVoice.Handle, - SourceChannelCount, - DestinationChannelCount, - (nint) pMatrixCoefficients, - 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*) pMatrixCoefficients; - outputMatrix[0] = reverb; - if (SourceChannelCount == 2) - { - outputMatrix[1] = reverb; - } - - FAudio.FAudioVoice_SetOutputMatrix( - Handle, - ReverbEffect.Handle, - SourceChannelCount, - 1, - (nint) pMatrixCoefficients, - 0 - ); - } - } - - #if DEBUG - if (ReverbEffect == null) - { - Logger.LogWarn("Tried to set reverb value before applying a reverb effect"); - } - #endif - } - } - - public SendableVoice(AudioDevice device, uint sourceChannelCount, uint destinationChannelCount) : base(device, sourceChannelCount, destinationChannelCount) - { - OutputVoice = device.MasteringVoice; - nuint memsize = (nuint) (4 * sourceChannelCount * destinationChannelCount); - pMatrixCoefficients = (byte*) NativeMemory.AllocZeroed(memsize); - SetPanMatrixCoefficients(); - } - - public virtual void SetPan(float targetValue) - { - Pan = targetValue; - Device.ClearTweens(this, AudioTweenProperty.Pan); - } - - public virtual void SetPan(float targetValue, float duration, EasingFunction easingFunction) - { - Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, 0); - } - - public virtual void SetPan(float targetValue, float delayTime, float duration, EasingFunction easingFunction) - { - Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, delayTime); - } - - public virtual void SetReverb(float targetValue) - { - Reverb = targetValue; - Device.ClearTweens(this, AudioTweenProperty.Reverb); - } - - public virtual void SetReverb(float targetValue, float duration, EasingFunction easingFunction) - { - Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, 0); - } - - public virtual void SetReverb(float targetValue, float delayTime, float duration, EasingFunction easingFunction) - { - Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, delayTime); - } - - public unsafe void SetOutputVoice(IReceivableVoice 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 - ); - - OutputVoice = send; - } - - public virtual unsafe void SetReverbEffectChain(ReverbEffect reverbEffect) - { - var sendDesc = stackalloc FAudio.FAudioSendDescriptor[2]; - sendDesc[0].Flags = 0; - sendDesc[0].pOutputVoice = OutputVoice.Handle; - sendDesc[1].Flags = 0; - sendDesc[1].pOutputVoice = reverbEffect.Handle; - - var sends = new FAudio.FAudioVoiceSends(); - sends.SendCount = 2; - sends.pSends = (nint) sendDesc; - - FAudio.FAudioVoice_SetOutputVoices( - Handle, - ref sends - ); - - ReverbEffect = reverbEffect; - } - - // 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*) pMatrixCoefficients; - if (SourceChannelCount == 1) - { - if (DestinationChannelCount == 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 (DestinationChannelCount == 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 override unsafe void Destroy() - { - NativeMemory.Free(pMatrixCoefficients); - base.Destroy(); - } - } -} diff --git a/src/Audio/SoundSequence.cs b/src/Audio/SoundSequence.cs index fdb4155..12c7145 100644 --- a/src/Audio/SoundSequence.cs +++ b/src/Audio/SoundSequence.cs @@ -59,11 +59,7 @@ namespace MoonWorks.Audio lock (StateLock) { - FAudio.FAudioSourceVoice_SubmitSourceBuffer( - Handle, - ref sound.Handle, - IntPtr.Zero - ); + Submit(sound); } } } diff --git a/src/Audio/SourceVoice.cs b/src/Audio/SourceVoice.cs index 15462a2..696b884 100644 --- a/src/Audio/SourceVoice.cs +++ b/src/Audio/SourceVoice.cs @@ -2,7 +2,10 @@ using System; namespace MoonWorks.Audio { - public class SourceVoice : SendableVoice + /// + /// Emits audio from submitted audio buffers. + /// + public class SourceVoice : Voice { private Format format; public Format Format => format; @@ -55,7 +58,7 @@ namespace MoonWorks.Audio FAudio.FAudio_CreateSourceVoice( device.Handle, - out var Handle, + out handle, ref fAudioFormat, FAudio.FAUDIO_VOICE_USEFILTER, FAudio.FAUDIO_DEFAULT_FREQ_RATIO, @@ -65,6 +68,11 @@ namespace MoonWorks.Audio ); } + /// + /// Starts consumption and processing of audio by the voice. + /// Delivers the result to any connected submix or mastering voice. + /// + /// Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called. public void Play(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) { lock (StateLock) @@ -75,6 +83,11 @@ namespace MoonWorks.Audio } } + /// + /// Pauses playback. + /// All source buffers that are queued on the voice and the current cursor position are preserved. + /// + /// Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called. public void Pause(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) { lock (StateLock) @@ -85,6 +98,11 @@ namespace MoonWorks.Audio } } + /// + /// Stops looping the voice when it reaches the end of the current loop region. + /// If the cursor for the voice is not in a loop region, ExitLoop does nothing. + /// + /// Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called. public void ExitLoop(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) { lock (StateLock) @@ -93,6 +111,10 @@ namespace MoonWorks.Audio } } + /// + /// Stops playback and removes all pending audio buffers from the voice queue. + /// + /// Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called. public void Stop(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW) { lock (StateLock) @@ -104,7 +126,32 @@ namespace MoonWorks.Audio } } - public void SubmitBuffer(FAudio.FAudioBuffer buffer) + /// + /// Adds a static sound to the voice queue. + /// The voice processes and plays back the buffers in its queue in the order that they were submitted. + /// + /// The sound to submit to the voice. + /// Designates that the voice will loop the submitted buffer. + public void Submit(StaticSound sound, bool loop = false) + { + if (loop) + { + sound.Buffer.LoopCount = FAudio.FAUDIO_LOOP_INFINITE; + } + else + { + sound.Buffer.LoopCount = 0; + } + + Submit(sound.Buffer); + } + + /// + /// Adds an FAudio buffer to the voice queue. + /// The voice processes and plays back the buffers in its queue in the order that they were submitted. + /// + /// The buffer to submit to the voice. + public void Submit(FAudio.FAudioBuffer buffer) { FAudio.FAudioSourceVoice_SubmitSourceBuffer( Handle, @@ -113,31 +160,27 @@ namespace MoonWorks.Audio ); } - // FIXME: maybe this is bad - // NOTE: SourceVoices obtained this way will be returned to the voice pool when stopped! - public static SourceVoice ObtainSourceVoice(AudioDevice device, Format format) + public void Submit(StreamingSound streamingSound) { - return device.ObtainSourceVoice(format); + } - // intended for short-lived sound effects - public static SourceVoice PlayStaticSound(AudioDevice device, StaticSound sound, SubmixVoice sendVoice = null) + /// + /// Designates that this source voice will return to the voice pool once all its buffers are exhausted. + /// + public void ReturnWhenIdle() { - var voice = ObtainSourceVoice(device, sound.Format); + Device.ReturnWhenIdle(this); + } - if (sendVoice == null) - { - voice.SetOutputVoice(device.MasteringVoice); - } - else - { - voice.SetOutputVoice(sendVoice); - } - - voice.SubmitBuffer(sound.Handle); - voice.Play(); - - return voice; + /// + /// Returns this source voice to the voice pool. + /// + public void Return() + { + Stop(); + Reset(); + Device.Return(this); } protected override unsafe void Destroy() diff --git a/src/Audio/StaticSound.cs b/src/Audio/StaticSound.cs index 3c92377..9c29142 100644 --- a/src/Audio/StaticSound.cs +++ b/src/Audio/StaticSound.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; @@ -7,17 +6,11 @@ namespace MoonWorks.Audio { public class StaticSound : AudioResource { - internal FAudio.FAudioBuffer Handle; + internal FAudio.FAudioBuffer Buffer; private Format format; public Format Format => format; - public uint LoopStart { get; set; } = 0; - public uint LoopLength { get; set; } = 0; - - private Stack AvailableInstances = new Stack(); - private HashSet UsedInstances = new HashSet(); - private bool OwnsBuffer; public static unsafe StaticSound LoadOgg(AudioDevice device, string filePath) @@ -253,10 +246,9 @@ namespace MoonWorks.Audio uint bufferLengthInBytes, bool ownsBuffer) : base(device) { - // TODO: should we wrap the format struct to make it nicer? this.format = format; - Handle = new FAudio.FAudioBuffer + Buffer = new FAudio.FAudioBuffer { Flags = FAudio.FAUDIO_END_OF_STREAM, pContext = IntPtr.Zero, @@ -269,66 +261,11 @@ namespace MoonWorks.Audio OwnsBuffer = ownsBuffer; } - /// - /// Gets a sound instance from the pool. - /// NOTE: If AutoFree is false, you will have to call StaticSoundInstance.Free() yourself or leak the instance! - /// - public StaticSoundInstance GetInstance(bool autoFree = true) - { - StaticSoundInstance instance; - - lock (AvailableInstances) - { - if (AvailableInstances.Count == 0) - { - AvailableInstances.Push(new StaticSoundInstance(Device, this)); - } - - instance = AvailableInstances.Pop(); - } - - instance.AutoFree = autoFree; - - lock (UsedInstances) - { - UsedInstances.Add(instance); - } - - return instance; - } - - internal void FreeInstance(StaticSoundInstance instance) - { - instance.Reset(); - - lock (UsedInstances) - { - UsedInstances.Remove(instance); - } - - lock (AvailableInstances) - { - AvailableInstances.Push(instance); - } - } - protected override unsafe void Destroy() { - foreach (var instance in UsedInstances) - { - instance.Free(); - } - - foreach (var instance in AvailableInstances) - { - instance.Dispose(); - } - - AvailableInstances.Clear(); - if (OwnsBuffer) { - NativeMemory.Free((void*) Handle.pAudioData); + NativeMemory.Free((void*) Buffer.pAudioData); } } } diff --git a/src/Audio/StaticSoundInstance.cs b/src/Audio/StaticSoundInstance.cs deleted file mode 100644 index e4dff7b..0000000 --- a/src/Audio/StaticSoundInstance.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System; - -namespace MoonWorks.Audio -{ - public class StaticSoundInstance : SoundInstance - { - public StaticSound Parent { get; } - - public bool Loop { get; set; } - - private SoundState _state = SoundState.Stopped; - public override SoundState State - { - get - { - FAudio.FAudioSourceVoice_GetState( - Voice, - out var state, - FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED - ); - if (state.BuffersQueued == 0) - { - StopImmediate(); - } - - return _state; - } - - protected set - { - _state = value; - } - } - - public bool AutoFree { get; internal set; } - - internal StaticSoundInstance( - AudioDevice device, - StaticSound parent - ) : base(device, parent.FormatTag, parent.BitsPerSample, parent.BlockAlign, parent.Channels, parent.SamplesPerSecond) - { - Parent = parent; - } - - public override void Play() - { - PlayUsingOperationSet(0); - } - - public override void QueueSyncPlay() - { - PlayUsingOperationSet(1); - } - - private void PlayUsingOperationSet(uint operationSet) - { - if (State == SoundState.Playing) - { - return; - } - - if (Loop) - { - Parent.Handle.LoopCount = 255; - Parent.Handle.LoopBegin = Parent.LoopStart; - Parent.Handle.LoopLength = Parent.LoopLength; - } - else - { - Parent.Handle.LoopCount = 0; - Parent.Handle.LoopBegin = 0; - Parent.Handle.LoopLength = 0; - } - - FAudio.FAudioSourceVoice_SubmitSourceBuffer( - Voice, - ref Parent.Handle, - IntPtr.Zero - ); - - FAudio.FAudioSourceVoice_Start(Voice, 0, operationSet); - State = SoundState.Playing; - - if (AutoFree) - { - Device.AddAutoFreeStaticSoundInstance(this); - } - } - - public override void Pause() - { - if (State == SoundState.Playing) - { - FAudio.FAudioSourceVoice_Stop(Voice, 0, 0); - State = SoundState.Paused; - } - } - - public override void Stop() - { - FAudio.FAudioSourceVoice_ExitLoop(Voice, 0); - State = SoundState.Stopped; - } - - public override void StopImmediate() - { - FAudio.FAudioSourceVoice_Stop(Voice, 0, 0); - FAudio.FAudioSourceVoice_FlushSourceBuffers(Voice); - State = SoundState.Stopped; - } - - public void Seek(uint sampleFrame) - { - if (State == SoundState.Playing) - { - FAudio.FAudioSourceVoice_Stop(Voice, 0, 0); - FAudio.FAudioSourceVoice_FlushSourceBuffers(Voice); - } - - Parent.Handle.PlayBegin = sampleFrame; - } - - // Call this when you no longer need the sound instance. - // If AutoFree is set, this will automatically be called when the sound instance stops playing. - // If the sound isn't stopped when you call this, things might get weird! - public void Free() - { - Parent.FreeInstance(this); - } - - internal void Reset() - { - Pan = 0; - Pitch = 0; - Volume = 1; - Loop = false; - Is3D = false; - FilterType = FilterType.None; - } - } -} diff --git a/src/Audio/SubmixVoice.cs b/src/Audio/SubmixVoice.cs index 338e08e..580256a 100644 --- a/src/Audio/SubmixVoice.cs +++ b/src/Audio/SubmixVoice.cs @@ -2,7 +2,7 @@ using System; namespace MoonWorks.Audio { - public class SubmixVoice : SendableVoice, IReceivableVoice + public class SubmixVoice : Voice { public SubmixVoice( AudioDevice device, diff --git a/src/Audio/Voice.cs b/src/Audio/Voice.cs index d098186..771e5fa 100644 --- a/src/Audio/Voice.cs +++ b/src/Audio/Voice.cs @@ -1,9 +1,10 @@ using System; +using System.Runtime.InteropServices; using EasingFunction = System.Func; namespace MoonWorks.Audio { - public abstract class Voice : AudioResource + public abstract unsafe class Voice : AudioResource { protected IntPtr handle; public IntPtr Handle => handle; @@ -11,6 +12,11 @@ namespace MoonWorks.Audio public uint SourceChannelCount { get; } public uint DestinationChannelCount { get; } + private SubmixVoice OutputVoice; + private ReverbEffect ReverbEffect; + + byte* pMatrixCoefficients; + public bool Is3D { get; protected set; } private float dopplerFactor; @@ -27,21 +33,6 @@ namespace MoonWorks.Audio } } - 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 { @@ -57,6 +48,21 @@ namespace MoonWorks.Audio } } + 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 const float MAX_FILTER_FREQUENCY = 1f; private const float MAX_FILTER_ONEOVERQ = 1.5f; @@ -150,10 +156,89 @@ namespace MoonWorks.Audio } } + protected float pan = 0; + public 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, + OutputVoice.Handle, + SourceChannelCount, + DestinationChannelCount, + (nint) pMatrixCoefficients, + 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*) pMatrixCoefficients; + outputMatrix[0] = reverb; + if (SourceChannelCount == 2) + { + outputMatrix[1] = reverb; + } + + FAudio.FAudioVoice_SetOutputMatrix( + Handle, + ReverbEffect.Handle, + SourceChannelCount, + 1, + (nint) pMatrixCoefficients, + 0 + ); + } + } + + #if DEBUG + if (ReverbEffect == null) + { + Logger.LogWarn("Tried to set reverb value before applying a reverb effect"); + } + #endif + } + } + public Voice(AudioDevice device, uint sourceChannelCount, uint destinationChannelCount) : base(device) { SourceChannelCount = sourceChannelCount; DestinationChannelCount = destinationChannelCount; + OutputVoice = device.MasteringVoice; + nuint memsize = 4 * sourceChannelCount * destinationChannelCount; + pMatrixCoefficients = (byte*) NativeMemory.AllocZeroed(memsize); + SetPanMatrixCoefficients(); } public void SetPitch(float targetValue) @@ -209,6 +294,159 @@ namespace MoonWorks.Audio FilterOneOverQ = targetValue; } + public virtual void SetPan(float targetValue) + { + Pan = targetValue; + Device.ClearTweens(this, AudioTweenProperty.Pan); + } + + public virtual void SetPan(float targetValue, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, 0); + } + + public virtual void SetPan(float targetValue, float delayTime, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, delayTime); + } + + public virtual void SetReverb(float targetValue) + { + Reverb = targetValue; + Device.ClearTweens(this, AudioTweenProperty.Reverb); + } + + public virtual void SetReverb(float targetValue, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, 0); + } + + public virtual void SetReverb(float targetValue, float delayTime, float duration, EasingFunction easingFunction) + { + Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, delayTime); + } + + public unsafe void SetOutputVoice(SubmixVoice send) + { + OutputVoice = send; + + if (ReverbEffect != null) + { + SetReverbEffectChain(ReverbEffect); + } + else + { + 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 SetReverbEffectChain(ReverbEffect reverbEffect) + { + var sendDesc = stackalloc FAudio.FAudioSendDescriptor[2]; + sendDesc[0].Flags = 0; + sendDesc[0].pOutputVoice = OutputVoice.Handle; + sendDesc[1].Flags = 0; + sendDesc[1].pOutputVoice = reverbEffect.Handle; + + var sends = new FAudio.FAudioVoiceSends(); + sends.SendCount = 2; + sends.pSends = (nint) sendDesc; + + FAudio.FAudioVoice_SetOutputVoices( + Handle, + ref sends + ); + + ReverbEffect = reverbEffect; + } + + public void RemoveReverbEffectChain() + { + if (ReverbEffect != null) + { + ReverbEffect = null; + reverb = 0; + SetOutputVoice(OutputVoice); + } + } + + // 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*) pMatrixCoefficients; + if (SourceChannelCount == 1) + { + if (DestinationChannelCount == 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 (DestinationChannelCount == 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; + } + } + } + } + + public virtual void Reset() + { + RemoveReverbEffectChain(); + Volume = 1; + Pan = 0; + Pitch = 0; + FilterType = FilterType.None; + FilterFrequency = 1; + FilterOneOverQ = 1; + SetOutputVoice(Device.MasteringVoice); + } + private void UpdatePitch() { float doppler; @@ -231,6 +469,7 @@ namespace MoonWorks.Audio protected unsafe override void Destroy() { + NativeMemory.Free(pMatrixCoefficients); FAudio.FAudioVoice_DestroyVoice(Handle); } }