Show More
Commit Description:
Various UI improvements.
Commit Description:
Various UI improvements.
File last commit:
Show/Diff file:
Action:
FNA/src/Audio/SoundEffectInstance.cs
632 lines | 12.8 KiB | text/x-csharp | CSharpLexer
#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
}
}