From 977b1fd1637cd4e5f72f827a99ad5a26f2f1f1d9 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Fri, 26 Feb 2021 16:17:26 -0800 Subject: [PATCH] initial 3D audio implementation --- AudioDevice.cs | 18 ++++++ AudioEmitter.cs | 135 +++++++++++++++++++++++++++++++++++++++++++ AudioListener.cs | 97 +++++++++++++++++++++++++++++++ AudioResource.cs | 52 +++++++++++++++++ SoundInstance.cs | 103 ++++++++++++++++++--------------- StaticSound.cs | 38 ++---------- StreamingSound.cs | 18 +----- StreamingSoundOgg.cs | 19 +----- 8 files changed, 367 insertions(+), 113 deletions(-) create mode 100644 AudioEmitter.cs create mode 100644 AudioListener.cs create mode 100644 AudioResource.cs diff --git a/AudioDevice.cs b/AudioDevice.cs index 7ba24e3..7b792b1 100644 --- a/AudioDevice.cs +++ b/AudioDevice.cs @@ -17,6 +17,8 @@ namespace MoonWorks.Audio public float SpeedOfSound = 343.5f; internal FAudio.FAudioVoiceSends ReverbSends; + + private readonly List> resources = new List>(); private readonly List> streamingSounds = new List>(); private bool IsDisposed; @@ -216,6 +218,22 @@ namespace MoonWorks.Audio streamingSounds.Add(new WeakReference(instance)); } + internal void AddResourceReference(WeakReference resourceReference) + { + lock (resources) + { + resources.Add(resourceReference); + } + } + + internal void RemoveResourceReference(WeakReference resourceReference) + { + lock (resources) + { + resources.Remove(resourceReference); + } + } + protected virtual void Dispose(bool disposing) { if (!IsDisposed) diff --git a/AudioEmitter.cs b/AudioEmitter.cs new file mode 100644 index 0000000..51a9576 --- /dev/null +++ b/AudioEmitter.cs @@ -0,0 +1,135 @@ +using System; +using System.Runtime.InteropServices; +using MoonWorks.Math; + +namespace MoonWorks.Audio +{ + public class AudioEmitter : AudioResource + { + internal FAudio.F3DAUDIO_EMITTER emitterData; + + public float DopplerScale + { + get + { + return emitterData.DopplerScaler; + } + set + { + if (value < 0.0f) + { + throw new ArgumentOutOfRangeException("AudioEmitter.DopplerScale must be greater than or equal to 0.0f"); + } + emitterData.DopplerScaler = value; + } + } + + public Vector3 Forward + { + get + { + return new Vector3( + emitterData.OrientFront.x, + emitterData.OrientFront.y, + -emitterData.OrientFront.z + ); + } + set + { + emitterData.OrientFront.x = value.X; + emitterData.OrientFront.y = value.Y; + emitterData.OrientFront.z = -value.Z; + } + } + + public Vector3 Position + { + get + { + return new Vector3( + emitterData.Position.x, + emitterData.Position.y, + -emitterData.Position.z + ); + } + set + { + emitterData.Position.x = value.X; + emitterData.Position.y = value.Y; + emitterData.Position.z = -value.Z; + } + } + + + public Vector3 Up + { + get + { + return new Vector3( + emitterData.OrientTop.x, + emitterData.OrientTop.y, + -emitterData.OrientTop.z + ); + } + set + { + emitterData.OrientTop.x = value.X; + emitterData.OrientTop.y = value.Y; + emitterData.OrientTop.z = -value.Z; + } + } + + public Vector3 Velocity + { + get + { + return new Vector3( + emitterData.Velocity.x, + emitterData.Velocity.y, + -emitterData.Velocity.z + ); + } + set + { + emitterData.Velocity.x = value.X; + emitterData.Velocity.y = value.Y; + emitterData.Velocity.z = -value.Z; + } + } + + private static readonly float[] stereoAzimuth = new float[] + { + 0.0f, 0.0f + }; + + private static readonly GCHandle stereoAzimuthHandle = GCHandle.Alloc( + stereoAzimuth, + GCHandleType.Pinned + ); + + public AudioEmitter(AudioDevice device) : base(device) + { + emitterData = new FAudio.F3DAUDIO_EMITTER(); + + DopplerScale = 1f; + Forward = Vector3.Forward; + Position = Vector3.Zero; + Up = Vector3.Up; + Velocity = Vector3.Zero; + + /* Unexposed variables, defaults based on XNA behavior */ + emitterData.pCone = IntPtr.Zero; + emitterData.ChannelCount = 1; + emitterData.ChannelRadius = 1.0f; + emitterData.pChannelAzimuths = stereoAzimuthHandle.AddrOfPinnedObject(); + emitterData.pVolumeCurve = IntPtr.Zero; + emitterData.pLFECurve = IntPtr.Zero; + emitterData.pLPFDirectCurve = IntPtr.Zero; + emitterData.pLPFReverbCurve = IntPtr.Zero; + emitterData.pReverbCurve = IntPtr.Zero; + emitterData.CurveDistanceScaler = 1.0f; + } + + protected override void Destroy() { } + } +} diff --git a/AudioListener.cs b/AudioListener.cs new file mode 100644 index 0000000..67e44ba --- /dev/null +++ b/AudioListener.cs @@ -0,0 +1,97 @@ +using System; +using MoonWorks.Math; + +namespace MoonWorks.Audio +{ + public class AudioListener : AudioResource + { + internal FAudio.F3DAUDIO_LISTENER listenerData; + + public Vector3 Forward + { + get + { + return new Vector3( + listenerData.OrientFront.x, + listenerData.OrientFront.y, + -listenerData.OrientFront.z + ); + } + set + { + listenerData.OrientFront.x = value.X; + listenerData.OrientFront.y = value.Y; + listenerData.OrientFront.z = -value.Z; + } + } + + public Vector3 Position + { + get + { + return new Vector3( + listenerData.Position.x, + listenerData.Position.y, + -listenerData.Position.z + ); + } + set + { + listenerData.Position.x = value.X; + listenerData.Position.y = value.Y; + listenerData.Position.z = -value.Z; + } + } + + + public Vector3 Up + { + get + { + return new Vector3( + listenerData.OrientTop.x, + listenerData.OrientTop.y, + -listenerData.OrientTop.z + ); + } + set + { + listenerData.OrientTop.x = value.X; + listenerData.OrientTop.y = value.Y; + listenerData.OrientTop.z = -value.Z; + } + } + + public Vector3 Velocity + { + get + { + return new Vector3( + listenerData.Velocity.x, + listenerData.Velocity.y, + -listenerData.Velocity.z + ); + } + set + { + listenerData.Velocity.x = value.X; + listenerData.Velocity.y = value.Y; + listenerData.Velocity.z = -value.Z; + } + } + + public AudioListener(AudioDevice device) : base(device) + { + listenerData = new FAudio.F3DAUDIO_LISTENER(); + Forward = Vector3.Forward; + Position = Vector3.Zero; + Up = Vector3.Up; + Velocity = Vector3.Zero; + + /* Unexposed variables, defaults based on XNA behavior */ + listenerData.pCone = IntPtr.Zero; + } + + protected override void Destroy() { } + } +} diff --git a/AudioResource.cs b/AudioResource.cs new file mode 100644 index 0000000..e74166f --- /dev/null +++ b/AudioResource.cs @@ -0,0 +1,52 @@ +using System; + +namespace MoonWorks.Audio +{ + public abstract class AudioResource : IDisposable + { + public AudioDevice Device { get; } + + public bool IsDisposed { get; private set; } + + private WeakReference selfReference; + + public AudioResource(AudioDevice device) + { + Device = device; + + selfReference = new WeakReference(this); + Device.AddResourceReference(selfReference); + } + + protected abstract void Destroy(); + + protected virtual void Dispose(bool disposing) + { + if (!IsDisposed) + { + Destroy(); + + if (selfReference != null) + { + Device.RemoveResourceReference(selfReference); + selfReference = null; + } + + IsDisposed = true; + } + } + + ~AudioResource() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/SoundInstance.cs b/SoundInstance.cs index 7514f1a..bf4c028 100644 --- a/SoundInstance.cs +++ b/SoundInstance.cs @@ -1,11 +1,11 @@ using System; using System.Runtime.InteropServices; +using MoonWorks.Math; namespace MoonWorks.Audio { - public abstract class SoundInstance : IDisposable + public abstract class SoundInstance : AudioResource { - protected AudioDevice Device { get; } internal IntPtr Handle { get; } internal FAudio.FAudioWaveFormatEx Format { get; } public bool Loop { get; } @@ -13,7 +13,6 @@ namespace MoonWorks.Audio protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings; protected bool is3D; - private bool IsDisposed; public abstract SoundState State { get; protected set; } @@ -54,22 +53,8 @@ namespace MoonWorks.Audio get => _pitch; set { - float doppler; - if (!is3D || Device.DopplerScale == 0f) - { - doppler = 1f; - } - else - { - doppler = dspSettings.DopplerFactor * Device.DopplerScale; - } - - _pitch = value; - FAudio.FAudioSourceVoice_SetFrequencyRatio( - Handle, - (float) System.Math.Pow(2.0, _pitch) * doppler, - 0 - ); + _pitch = MathHelper.Clamp(value, -1f, 1f); + UpdatePitch(); } } @@ -182,9 +167,8 @@ namespace MoonWorks.Audio uint samplesPerSecond, bool is3D, bool loop - ) { - Device = device; - + ) : base(device) + { var blockAlign = (ushort)(4 * channels); var format = new FAudio.FAudioWaveFormatEx { @@ -228,6 +212,32 @@ namespace MoonWorks.Audio State = SoundState.Stopped; } + public void Apply3D(AudioListener listener, AudioEmitter emitter) + { + is3D = true; + + emitter.emitterData.CurveDistanceScaler = Device.CurveDistanceScalar; + emitter.emitterData.ChannelCount = dspSettings.SrcChannelCount; + + FAudio.F3DAudioCalculate( + Device.Handle3D, + ref listener.listenerData, + ref emitter.emitterData, + FAudio.F3DAUDIO_CALCULATE_MATRIX | FAudio.F3DAUDIO_CALCULATE_DOPPLER, + ref dspSettings + ); + + UpdatePitch(); + FAudio.FAudioVoice_SetOutputMatrix( + Handle, + Device.MasteringVoice, + dspSettings.SrcChannelCount, + dspSettings.DstChannelCount, + dspSettings.pMatrixCoefficients, + 0 + ); + } + public abstract void Play(); public abstract void Pause(); public abstract void Stop(bool immediate); @@ -257,6 +267,26 @@ namespace MoonWorks.Audio 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() { @@ -311,33 +341,12 @@ namespace MoonWorks.Audio } } - protected virtual void Dispose(bool disposing) + protected override void Destroy() { - if (!IsDisposed) - { - if (disposing) - { - // dispose managed state (managed objects) - Stop(true); - } + Stop(true); - FAudio.FAudioVoice_DestroyVoice(Handle); - Marshal.FreeHGlobal(dspSettings.pMatrixCoefficients); - IsDisposed = true; - } - } - - ~SoundInstance() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: false); - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); + FAudio.FAudioVoice_DestroyVoice(Handle); + Marshal.FreeHGlobal(dspSettings.pMatrixCoefficients); } } } diff --git a/StaticSound.cs b/StaticSound.cs index b27a952..9d03dd8 100644 --- a/StaticSound.cs +++ b/StaticSound.cs @@ -1,12 +1,10 @@ using System; -using System.IO; using System.Runtime.InteropServices; namespace MoonWorks.Audio { - public class StaticSound : IDisposable + public class StaticSound : AudioResource { - internal AudioDevice Device { get; } internal FAudio.FAudioBuffer Handle; public ushort Channels { get; } public uint SamplesPerSecond { get; } @@ -14,8 +12,6 @@ namespace MoonWorks.Audio public uint LoopStart { get; set; } = 0; public uint LoopLength { get; set; } = 0; - private bool IsDisposed; - public static StaticSound LoadOgg(AudioDevice device, string filePath) { var filePointer = FAudio.stb_vorbis_open_filename(filePath, out var error, IntPtr.Zero); @@ -54,9 +50,8 @@ namespace MoonWorks.Audio float[] buffer, uint bufferOffset, /* in floats */ uint bufferLength /* in floats */ - ) { - Device = device; - + ) : base(device) + { Channels = channels; SamplesPerSecond = samplesPerSecond; @@ -79,32 +74,9 @@ namespace MoonWorks.Audio return new StaticSoundInstance(Device, this, false, loop); } - protected virtual void Dispose(bool disposing) + protected override void Destroy() { - if (!IsDisposed) - { - if (disposing) - { - // dispose managed state (managed objects) - } - - Marshal.FreeHGlobal(Handle.pAudioData); - IsDisposed = true; - } - } - - // override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources - ~StaticSound() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: false); - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); + Marshal.FreeHGlobal(Handle.pAudioData); } } } diff --git a/StreamingSound.cs b/StreamingSound.cs index 3909cad..aa46e04 100644 --- a/StreamingSound.cs +++ b/StreamingSound.cs @@ -16,8 +16,6 @@ namespace MoonWorks.Audio public int PendingBufferCount => queuedBuffers.Count; - private bool IsDisposed; - public StreamingSound( AudioDevice device, ushort channels, @@ -172,21 +170,9 @@ namespace MoonWorks.Audio protected abstract void SeekStart(); - protected override void Dispose(bool disposing) + protected override void Destroy() { - if (!IsDisposed) - { - if (disposing) - { - // dispose managed state (managed objects) - Stop(true); - } - - // dispose unmanaged state - IsDisposed = true; - } - - base.Dispose(disposing); + Stop(true); } } } diff --git a/StreamingSoundOgg.cs b/StreamingSoundOgg.cs index 49c9ad9..caf5151 100644 --- a/StreamingSoundOgg.cs +++ b/StreamingSoundOgg.cs @@ -15,8 +15,6 @@ namespace MoonWorks.Audio public override SoundState State { get; protected set; } - private bool IsDisposed; - public static StreamingSoundOgg Load( AudioDevice device, string filePath, @@ -83,22 +81,9 @@ namespace MoonWorks.Audio FAudio.stb_vorbis_seek_start(FileHandle); } - protected override void Dispose(bool disposing) + protected override void Destroy() { - if (!IsDisposed) - { - if (disposing) - { - // dispose managed state (managed objects) - } - - // dispose unmanaged state - FAudio.stb_vorbis_close(FileHandle); - - IsDisposed = true; - } - - base.Dispose(disposing); + FAudio.stb_vorbis_close(FileHandle); } } }