faux mastering voice + submitting static sounds

pull/50/head
cosmonaut 2023-08-01 18:13:17 -07:00
parent e2c85ec728
commit c2cb83f93f
11 changed files with 396 additions and 578 deletions

View File

@ -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<WeakReference> resources = new HashSet<WeakReference>();
private readonly List<StreamingSound> autoUpdateStreamingSoundReferences = new List<StreamingSound>();
private readonly List<StaticSoundInstance> autoFreeStaticSoundInstanceReferences = new List<StaticSoundInstance>();
private readonly List<WeakReference<SoundSequence>> soundSequenceReferences = new List<WeakReference<SoundSequence>>();
private readonly List<SourceVoice> autoFreeSourceVoices = new List<SourceVoice>();
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);
}
/// <summary>
/// Triggers all pending operations with the given syncGroup value.
/// </summary>
public void TriggerSyncGroup(uint syncGroup)
{
FAudio.FAudio_CommitChanges(Handle, syncGroup);
}
/// <summary>
/// Obtains an appropriate source voice from the voice pool.
/// </summary>
/// <param name="format">The format that the voice must match.</param>
/// <returns>A source voice with the given format.</returns>
public SourceVoice Obtain(Format format)
{
lock (StateLock)
{
return VoicePool.Obtain(format);
}
}
internal void ReturnWhenIdle(SourceVoice voice)
{
lock (StateLock)
{
autoFreeSourceVoices.Add(voice);
}
}
/// <summary>
/// Returns the source voice to the voice pool.
/// </summary>
/// <param name="voice"></param>
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<SoundSequence>(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;

View File

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

View File

@ -1,7 +0,0 @@
namespace MoonWorks.Audio
{
public interface IReceivableVoice
{
public System.IntPtr Handle { get; }
}
}

View File

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

View File

@ -1,227 +0,0 @@
using System;
using System.Runtime.InteropServices;
using EasingFunction = System.Func<float, float>;
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();
}
}
}

View File

@ -59,11 +59,7 @@ namespace MoonWorks.Audio
lock (StateLock)
{
FAudio.FAudioSourceVoice_SubmitSourceBuffer(
Handle,
ref sound.Handle,
IntPtr.Zero
);
Submit(sound);
}
}
}

View File

@ -2,7 +2,10 @@ using System;
namespace MoonWorks.Audio
{
public class SourceVoice : SendableVoice
/// <summary>
/// Emits audio from submitted audio buffers.
/// </summary>
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
);
}
/// <summary>
/// Starts consumption and processing of audio by the voice.
/// Delivers the result to any connected submix or mastering voice.
/// </summary>
/// <param name="syncGroup">Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called.</param>
public void Play(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW)
{
lock (StateLock)
@ -75,6 +83,11 @@ namespace MoonWorks.Audio
}
}
/// <summary>
/// Pauses playback.
/// All source buffers that are queued on the voice and the current cursor position are preserved.
/// </summary>
/// <param name="syncGroup">Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called.</param>
public void Pause(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW)
{
lock (StateLock)
@ -85,6 +98,11 @@ namespace MoonWorks.Audio
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="syncGroup">Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called.</param>
public void ExitLoop(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW)
{
lock (StateLock)
@ -93,6 +111,10 @@ namespace MoonWorks.Audio
}
}
/// <summary>
/// Stops playback and removes all pending audio buffers from the voice queue.
/// </summary>
/// <param name="syncGroup">Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called.</param>
public void Stop(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW)
{
lock (StateLock)
@ -104,7 +126,32 @@ namespace MoonWorks.Audio
}
}
public void SubmitBuffer(FAudio.FAudioBuffer buffer)
/// <summary>
/// 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.
/// </summary>
/// <param name="sound">The sound to submit to the voice.</param>
/// <param name="loop">Designates that the voice will loop the submitted buffer.</param>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="buffer">The buffer to submit to the voice.</param>
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)
/// <summary>
/// Designates that this source voice will return to the voice pool once all its buffers are exhausted.
/// </summary>
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;
/// <summary>
/// Returns this source voice to the voice pool.
/// </summary>
public void Return()
{
Stop();
Reset();
Device.Return(this);
}
protected override unsafe void Destroy()

View File

@ -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<StaticSoundInstance> AvailableInstances = new Stack<StaticSoundInstance>();
private HashSet<StaticSoundInstance> UsedInstances = new HashSet<StaticSoundInstance>();
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;
}
/// <summary>
/// Gets a sound instance from the pool.
/// NOTE: If AutoFree is false, you will have to call StaticSoundInstance.Free() yourself or leak the instance!
/// </summary>
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);
}
}
}

View File

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

View File

@ -2,7 +2,7 @@ using System;
namespace MoonWorks.Audio
{
public class SubmixVoice : SendableVoice, IReceivableVoice
public class SubmixVoice : Voice
{
public SubmixVoice(
AudioDevice device,

View File

@ -1,9 +1,10 @@
using System;
using System.Runtime.InteropServices;
using EasingFunction = System.Func<float, float>;
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);
}
}