faux mastering voice + submitting static sounds
parent
e2c85ec728
commit
c2cb83f93f
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
namespace MoonWorks.Audio
|
||||
{
|
||||
public interface IReceivableVoice
|
||||
{
|
||||
public System.IntPtr Handle { get; }
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -59,11 +59,7 @@ namespace MoonWorks.Audio
|
|||
|
||||
lock (StateLock)
|
||||
{
|
||||
FAudio.FAudioSourceVoice_SubmitSourceBuffer(
|
||||
Handle,
|
||||
ref sound.Handle,
|
||||
IntPtr.Zero
|
||||
);
|
||||
Submit(sound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ using System;
|
|||
|
||||
namespace MoonWorks.Audio
|
||||
{
|
||||
public class SubmixVoice : SendableVoice, IReceivableVoice
|
||||
public class SubmixVoice : Voice
|
||||
{
|
||||
public SubmixVoice(
|
||||
AudioDevice device,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue