|
|
#region License
|
|
|
/* FNA - XNA4 Reimplementation for Desktop Platforms
|
|
|
* Copyright 2009-2020 Ethan Lee and the MonoGame Team
|
|
|
*
|
|
|
* Released under the Microsoft Public License.
|
|
|
* See LICENSE for details.
|
|
|
*/
|
|
|
#endregion
|
|
|
|
|
|
#region Using Statements
|
|
|
using System;
|
|
|
using System.Runtime.InteropServices;
|
|
|
#endregion
|
|
|
|
|
|
namespace Microsoft.Xna.Framework.Audio
|
|
|
{
|
|
|
// http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.audio.soundeffectinstance.aspx
|
|
|
public class SoundEffectInstance : IDisposable
|
|
|
{
|
|
|
#region Public Properties
|
|
|
|
|
|
public bool IsDisposed
|
|
|
{
|
|
|
get;
|
|
|
protected set;
|
|
|
}
|
|
|
|
|
|
private bool INTERNAL_looped = false;
|
|
|
public virtual bool IsLooped
|
|
|
{
|
|
|
get
|
|
|
{
|
|
|
return INTERNAL_looped;
|
|
|
}
|
|
|
set
|
|
|
{
|
|
|
if (hasStarted)
|
|
|
{
|
|
|
throw new InvalidOperationException();
|
|
|
}
|
|
|
INTERNAL_looped = value;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private float INTERNAL_pan = 0.0f;
|
|
|
public float Pan
|
|
|
{
|
|
|
get
|
|
|
{
|
|
|
return INTERNAL_pan;
|
|
|
}
|
|
|
set
|
|
|
{
|
|
|
if (IsDisposed)
|
|
|
{
|
|
|
throw new ObjectDisposedException(
|
|
|
"SoundEffectInstance"
|
|
|
);
|
|
|
}
|
|
|
|
|
|
if (value > 1.0f || value < -1.0f)
|
|
|
{
|
|
|
throw new ArgumentOutOfRangeException("value");
|
|
|
}
|
|
|
INTERNAL_pan = value;
|
|
|
if (is3D)
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
SetPanMatrixCoefficients();
|
|
|
if (handle != IntPtr.Zero)
|
|
|
{
|
|
|
FAudio.FAudioVoice_SetOutputMatrix(
|
|
|
handle,
|
|
|
SoundEffect.Device().MasterVoice,
|
|
|
dspSettings.SrcChannelCount,
|
|
|
dspSettings.DstChannelCount,
|
|
|
dspSettings.pMatrixCoefficients,
|
|
|
0
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private float INTERNAL_pitch = 0.0f;
|
|
|
public float Pitch
|
|
|
{
|
|
|
get
|
|
|
{
|
|
|
return INTERNAL_pitch;
|
|
|
}
|
|
|
set
|
|
|
{
|
|
|
INTERNAL_pitch = MathHelper.Clamp(value, -1.0f, 1.0f);
|
|
|
if (handle != IntPtr.Zero)
|
|
|
{
|
|
|
UpdatePitch();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private SoundState INTERNAL_state = SoundState.Stopped;
|
|
|
public SoundState State
|
|
|
{
|
|
|
get
|
|
|
{
|
|
|
if ( !isDynamic &&
|
|
|
handle != IntPtr.Zero &&
|
|
|
INTERNAL_state == SoundState.Playing )
|
|
|
{
|
|
|
FAudio.FAudioVoiceState state;
|
|
|
FAudio.FAudioSourceVoice_GetState(
|
|
|
handle,
|
|
|
out state,
|
|
|
FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED
|
|
|
);
|
|
|
if (state.BuffersQueued == 0)
|
|
|
{
|
|
|
Stop(true);
|
|
|
}
|
|
|
}
|
|
|
return INTERNAL_state;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private float INTERNAL_volume = 1.0f;
|
|
|
public float Volume
|
|
|
{
|
|
|
get
|
|
|
{
|
|
|
return INTERNAL_volume;
|
|
|
}
|
|
|
set
|
|
|
{
|
|
|
INTERNAL_volume = value;
|
|
|
if (handle != IntPtr.Zero)
|
|
|
{
|
|
|
FAudio.FAudioVoice_SetVolume(
|
|
|
handle,
|
|
|
INTERNAL_volume,
|
|
|
0
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
#region Internal Variables
|
|
|
|
|
|
internal IntPtr handle;
|
|
|
internal bool isDynamic;
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
#region Private Variables
|
|
|
|
|
|
private SoundEffect parentEffect;
|
|
|
private WeakReference selfReference;
|
|
|
private bool hasStarted;
|
|
|
private bool is3D;
|
|
|
private bool usingReverb;
|
|
|
private FAudio.F3DAUDIO_DSP_SETTINGS dspSettings;
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
#region Internal Constructor
|
|
|
|
|
|
internal SoundEffectInstance(SoundEffect parent = null)
|
|
|
{
|
|
|
SoundEffect.Device();
|
|
|
|
|
|
selfReference = new WeakReference(this, true);
|
|
|
parentEffect = parent;
|
|
|
isDynamic = this is DynamicSoundEffectInstance;
|
|
|
hasStarted = false;
|
|
|
is3D = false;
|
|
|
usingReverb = false;
|
|
|
INTERNAL_state = SoundState.Stopped;
|
|
|
|
|
|
if (!isDynamic)
|
|
|
{
|
|
|
InitDSPSettings(parentEffect.format.nChannels);
|
|
|
}
|
|
|
if (parentEffect != null)
|
|
|
{
|
|
|
parentEffect.Instances.Add(selfReference);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
#region Destructor
|
|
|
|
|
|
~SoundEffectInstance()
|
|
|
{
|
|
|
if (!IsDisposed && State == SoundState.Playing)
|
|
|
{
|
|
|
// STOP LEAKING YOUR INSTANCES, ARGH
|
|
|
GC.ReRegisterForFinalize(this);
|
|
|
return;
|
|
|
}
|
|
|
Dispose();
|
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
#region Public Methods
|
|
|
|
|
|
public void Dispose()
|
|
|
{
|
|
|
Dispose(true);
|
|
|
GC.SuppressFinalize(this);
|
|
|
}
|
|
|
|
|
|
public void Apply3D(AudioListener listener, AudioEmitter emitter)
|
|
|
{
|
|
|
if (listener == null)
|
|
|
{
|
|
|
throw new ArgumentNullException("listener");
|
|
|
}
|
|
|
if (emitter == null)
|
|
|
{
|
|
|
throw new ArgumentNullException("emitter");
|
|
|
}
|
|
|
if (IsDisposed)
|
|
|
{
|
|
|
throw new ObjectDisposedException(
|
|
|
"SoundEffectInstance"
|
|
|
);
|
|
|
}
|
|
|
|
|
|
is3D = true;
|
|
|
SoundEffect.FAudioContext dev = SoundEffect.Device();
|
|
|
emitter.emitterData.CurveDistanceScaler = dev.CurveDistanceScaler;
|
|
|
emitter.emitterData.ChannelCount = dspSettings.SrcChannelCount;
|
|
|
FAudio.F3DAudioCalculate(
|
|
|
dev.Handle3D,
|
|
|
ref listener.listenerData,
|
|
|
ref emitter.emitterData,
|
|
|
(
|
|
|
FAudio.F3DAUDIO_CALCULATE_MATRIX |
|
|
|
FAudio.F3DAUDIO_CALCULATE_DOPPLER
|
|
|
),
|
|
|
ref dspSettings
|
|
|
);
|
|
|
if (handle != IntPtr.Zero)
|
|
|
{
|
|
|
UpdatePitch();
|
|
|
FAudio.FAudioVoice_SetOutputMatrix(
|
|
|
handle,
|
|
|
SoundEffect.Device().MasterVoice,
|
|
|
dspSettings.SrcChannelCount,
|
|
|
dspSettings.DstChannelCount,
|
|
|
dspSettings.pMatrixCoefficients,
|
|
|
0
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public void Apply3D(AudioListener[] listeners, AudioEmitter emitter)
|
|
|
{
|
|
|
if (listeners == null)
|
|
|
{
|
|
|
throw new ArgumentNullException("listeners");
|
|
|
}
|
|
|
if (listeners.Length == 1)
|
|
|
{
|
|
|
Apply3D(listeners[0], emitter);
|
|
|
return;
|
|
|
}
|
|
|
throw new NotSupportedException("Only one listener is supported.");
|
|
|
}
|
|
|
|
|
|
public virtual void Play()
|
|
|
{
|
|
|
if (State == SoundState.Playing)
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
if (State == SoundState.Paused)
|
|
|
{
|
|
|
/* Just resume the existing handle */
|
|
|
FAudio.FAudioSourceVoice_Start(handle, 0, 0);
|
|
|
INTERNAL_state = SoundState.Playing;
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
SoundEffect.FAudioContext dev = SoundEffect.Device();
|
|
|
|
|
|
/* Create handle */
|
|
|
FAudio.FAudioWaveFormatEx fmt = isDynamic ?
|
|
|
(this as DynamicSoundEffectInstance).format :
|
|
|
parentEffect.format;
|
|
|
FAudio.FAudio_CreateSourceVoice(
|
|
|
dev.Handle,
|
|
|
out handle,
|
|
|
ref fmt,
|
|
|
FAudio.FAUDIO_VOICE_USEFILTER,
|
|
|
FAudio.FAUDIO_DEFAULT_FREQ_RATIO,
|
|
|
IntPtr.Zero,
|
|
|
IntPtr.Zero,
|
|
|
IntPtr.Zero
|
|
|
);
|
|
|
if (handle == IntPtr.Zero)
|
|
|
{
|
|
|
return; /* What */
|
|
|
}
|
|
|
|
|
|
/* Apply current properties */
|
|
|
FAudio.FAudioVoice_SetVolume(handle, INTERNAL_volume, 0);
|
|
|
UpdatePitch();
|
|
|
if (is3D || Pan != 0.0f)
|
|
|
{
|
|
|
FAudio.FAudioVoice_SetOutputMatrix(
|
|
|
handle,
|
|
|
SoundEffect.Device().MasterVoice,
|
|
|
dspSettings.SrcChannelCount,
|
|
|
dspSettings.DstChannelCount,
|
|
|
dspSettings.pMatrixCoefficients,
|
|
|
0
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/* For static effects, submit the buffer now */
|
|
|
if (isDynamic)
|
|
|
{
|
|
|
(this as DynamicSoundEffectInstance).QueueInitialBuffers();
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
if (IsLooped)
|
|
|
{
|
|
|
parentEffect.handle.LoopCount = 255;
|
|
|
parentEffect.handle.LoopBegin = parentEffect.loopStart;
|
|
|
parentEffect.handle.LoopLength = parentEffect.loopLength;
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
parentEffect.handle.LoopCount = 0;
|
|
|
parentEffect.handle.LoopBegin = 0;
|
|
|
parentEffect.handle.LoopLength = 0;
|
|
|
}
|
|
|
FAudio.FAudioSourceVoice_SubmitSourceBuffer(
|
|
|
handle,
|
|
|
ref parentEffect.handle,
|
|
|
IntPtr.Zero
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/* Play, finally. */
|
|
|
FAudio.FAudioSourceVoice_Start(handle, 0, 0);
|
|
|
INTERNAL_state = SoundState.Playing;
|
|
|
hasStarted = true;
|
|
|
}
|
|
|
|
|
|
public void Pause()
|
|
|
{
|
|
|
if (handle != IntPtr.Zero && State == SoundState.Playing)
|
|
|
{
|
|
|
FAudio.FAudioSourceVoice_Stop(handle, 0, 0);
|
|
|
INTERNAL_state = SoundState.Paused;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public void Resume()
|
|
|
{
|
|
|
SoundState state = State; // Triggers a query, update
|
|
|
if (handle == IntPtr.Zero)
|
|
|
{
|
|
|
// XNA4 just plays if we've not started yet.
|
|
|
Play();
|
|
|
}
|
|
|
else if (state == SoundState.Paused)
|
|
|
{
|
|
|
FAudio.FAudioSourceVoice_Start(handle, 0, 0);
|
|
|
INTERNAL_state = SoundState.Playing;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public void Stop()
|
|
|
{
|
|
|
Stop(true);
|
|
|
}
|
|
|
|
|
|
public void Stop(bool immediate)
|
|
|
{
|
|
|
if (handle == IntPtr.Zero)
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (immediate)
|
|
|
{
|
|
|
FAudio.FAudioSourceVoice_Stop(handle, 0, 0);
|
|
|
FAudio.FAudioSourceVoice_FlushSourceBuffers(handle);
|
|
|
FAudio.FAudioVoice_DestroyVoice(handle);
|
|
|
handle = IntPtr.Zero;
|
|
|
usingReverb = false;
|
|
|
INTERNAL_state = SoundState.Stopped;
|
|
|
|
|
|
if (isDynamic)
|
|
|
{
|
|
|
FrameworkDispatcher.Streams.Remove(
|
|
|
this as DynamicSoundEffectInstance
|
|
|
);
|
|
|
(this as DynamicSoundEffectInstance).ClearBuffers();
|
|
|
}
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
if (isDynamic)
|
|
|
{
|
|
|
throw new InvalidOperationException();
|
|
|
}
|
|
|
FAudio.FAudioSourceVoice_ExitLoop(handle, 0);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
#region Protected Methods
|
|
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
|
{
|
|
|
if (!IsDisposed)
|
|
|
{
|
|
|
Stop(true);
|
|
|
if (parentEffect != null)
|
|
|
{
|
|
|
parentEffect.Instances.Remove(selfReference);
|
|
|
}
|
|
|
selfReference = null;
|
|
|
Marshal.FreeHGlobal(dspSettings.pMatrixCoefficients);
|
|
|
IsDisposed = true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
#region Internal Methods
|
|
|
|
|
|
internal void InitDSPSettings(uint srcChannels)
|
|
|
{
|
|
|
dspSettings = new FAudio.F3DAUDIO_DSP_SETTINGS();
|
|
|
dspSettings.DopplerFactor = 1.0f;
|
|
|
dspSettings.SrcChannelCount = srcChannels;
|
|
|
dspSettings.DstChannelCount = SoundEffect.Device().DeviceDetails.OutputFormat.Format.nChannels;
|
|
|
|
|
|
int memsize = (
|
|
|
4 *
|
|
|
(int) dspSettings.SrcChannelCount *
|
|
|
(int) dspSettings.DstChannelCount
|
|
|
);
|
|
|
dspSettings.pMatrixCoefficients = Marshal.AllocHGlobal(memsize);
|
|
|
unsafe
|
|
|
{
|
|
|
byte* memPtr = (byte*) dspSettings.pMatrixCoefficients;
|
|
|
for (int i = 0; i < memsize; i += 1)
|
|
|
{
|
|
|
memPtr[i] = 0;
|
|
|
}
|
|
|
}
|
|
|
SetPanMatrixCoefficients();
|
|
|
}
|
|
|
|
|
|
internal unsafe void INTERNAL_applyReverb(float rvGain)
|
|
|
{
|
|
|
if (handle == IntPtr.Zero)
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (!usingReverb)
|
|
|
{
|
|
|
SoundEffect.Device().AttachReverb(handle);
|
|
|
usingReverb = true;
|
|
|
}
|
|
|
|
|
|
// Re-using this float array...
|
|
|
float* outputMatrix = (float*) dspSettings.pMatrixCoefficients;
|
|
|
outputMatrix[0] = rvGain;
|
|
|
if (dspSettings.SrcChannelCount == 2)
|
|
|
{
|
|
|
outputMatrix[1] = rvGain;
|
|
|
}
|
|
|
FAudio.FAudioVoice_SetOutputMatrix(
|
|
|
handle,
|
|
|
SoundEffect.Device().ReverbVoice,
|
|
|
dspSettings.SrcChannelCount,
|
|
|
1,
|
|
|
dspSettings.pMatrixCoefficients,
|
|
|
0
|
|
|
);
|
|
|
}
|
|
|
|
|
|
internal void INTERNAL_applyLowPassFilter(float cutoff)
|
|
|
{
|
|
|
if (handle == IntPtr.Zero)
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
FAudio.FAudioFilterParameters p = new FAudio.FAudioFilterParameters();
|
|
|
p.Type = FAudio.FAudioFilterType.FAudioLowPassFilter;
|
|
|
p.Frequency = cutoff;
|
|
|
p.OneOverQ = 1.0f;
|
|
|
FAudio.FAudioVoice_SetFilterParameters(
|
|
|
handle,
|
|
|
ref p,
|
|
|
0
|
|
|
);
|
|
|
}
|
|
|
|
|
|
internal void INTERNAL_applyHighPassFilter(float cutoff)
|
|
|
{
|
|
|
if (handle == IntPtr.Zero)
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
FAudio.FAudioFilterParameters p = new FAudio.FAudioFilterParameters();
|
|
|
p.Type = FAudio.FAudioFilterType.FAudioHighPassFilter;
|
|
|
p.Frequency = cutoff;
|
|
|
p.OneOverQ = 1.0f;
|
|
|
FAudio.FAudioVoice_SetFilterParameters(
|
|
|
handle,
|
|
|
ref p,
|
|
|
0
|
|
|
);
|
|
|
}
|
|
|
|
|
|
internal void INTERNAL_applyBandPassFilter(float center)
|
|
|
{
|
|
|
if (handle == IntPtr.Zero)
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
FAudio.FAudioFilterParameters p = new FAudio.FAudioFilterParameters();
|
|
|
p.Type = FAudio.FAudioFilterType.FAudioBandPassFilter;
|
|
|
p.Frequency = center;
|
|
|
p.OneOverQ = 1.0f;
|
|
|
FAudio.FAudioVoice_SetFilterParameters(
|
|
|
handle,
|
|
|
ref p,
|
|
|
0
|
|
|
);
|
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
#region Private Methods
|
|
|
|
|
|
private void UpdatePitch()
|
|
|
{
|
|
|
float doppler;
|
|
|
float dopplerScale = SoundEffect.Device().DopplerScale;
|
|
|
if (!is3D || dopplerScale == 0.0f)
|
|
|
{
|
|
|
doppler = 1.0f;
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
doppler = dspSettings.DopplerFactor * dopplerScale;
|
|
|
}
|
|
|
|
|
|
FAudio.FAudioSourceVoice_SetFrequencyRatio(
|
|
|
handle,
|
|
|
(float) Math.Pow(2.0, INTERNAL_pitch) * doppler,
|
|
|
0
|
|
|
);
|
|
|
}
|
|
|
|
|
|
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.
|
|
|
* Aside from that, XNA is pretty naive about the output matrix.
|
|
|
* -flibit
|
|
|
*/
|
|
|
float* outputMatrix = (float*) dspSettings.pMatrixCoefficients;
|
|
|
if (dspSettings.SrcChannelCount == 1)
|
|
|
{
|
|
|
if (dspSettings.DstChannelCount == 1)
|
|
|
{
|
|
|
outputMatrix[0] = 1.0f;
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
outputMatrix[0] = (INTERNAL_pan > 0.0f) ? (1.0f - INTERNAL_pan) : 1.0f;
|
|
|
outputMatrix[1] = (INTERNAL_pan < 0.0f) ? (1.0f + INTERNAL_pan) : 1.0f;
|
|
|
}
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
if (dspSettings.DstChannelCount == 1)
|
|
|
{
|
|
|
outputMatrix[0] = 1.0f;
|
|
|
outputMatrix[1] = 1.0f;
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
if (INTERNAL_pan <= 0.0f)
|
|
|
{
|
|
|
// Left speaker blends left/right channels
|
|
|
outputMatrix[0] = 0.5f * INTERNAL_pan + 1.0f;
|
|
|
outputMatrix[1] = 0.5f * -INTERNAL_pan;
|
|
|
// Right speaker gets less of the right channel
|
|
|
outputMatrix[2] = 0.0f;
|
|
|
outputMatrix[3] = INTERNAL_pan + 1.0f;
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
// Left speaker gets less of the left channel
|
|
|
outputMatrix[0] = -INTERNAL_pan + 1.0f;
|
|
|
outputMatrix[1] = 0.0f;
|
|
|
// Right speaker blends right/left channels
|
|
|
outputMatrix[2] = 0.5f * INTERNAL_pan;
|
|
|
outputMatrix[3] = 0.5f * -INTERNAL_pan + 1.0f;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
}
|
|
|
}
|
|
|
|