initial 3D audio implementation

pull/14/head
cosmonaut 2021-02-26 16:17:26 -08:00
parent 7cc14ea819
commit 5eb5027bf1
8 changed files with 367 additions and 113 deletions

View File

@ -17,6 +17,8 @@ namespace MoonWorks.Audio
public float SpeedOfSound = 343.5f;
internal FAudio.FAudioVoiceSends ReverbSends;
private readonly List<WeakReference<AudioResource>> resources = new List<WeakReference<AudioResource>>();
private readonly List<WeakReference<StreamingSound>> streamingSounds = new List<WeakReference<StreamingSound>>();
private bool IsDisposed;
@ -216,6 +218,22 @@ namespace MoonWorks.Audio
streamingSounds.Add(new WeakReference<StreamingSound>(instance));
}
internal void AddResourceReference(WeakReference<AudioResource> resourceReference)
{
lock (resources)
{
resources.Add(resourceReference);
}
}
internal void RemoveResourceReference(WeakReference<AudioResource> resourceReference)
{
lock (resources)
{
resources.Remove(resourceReference);
}
}
protected virtual void Dispose(bool disposing)
{
if (!IsDisposed)

135
src/Audio/AudioEmitter.cs Normal file
View File

@ -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() { }
}
}

View File

@ -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() { }
}
}

View File

@ -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<AudioResource> selfReference;
public AudioResource(AudioDevice device)
{
Device = device;
selfReference = new WeakReference<AudioResource>(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);
}
}
}

View File

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

View File

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

View File

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

View File

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