#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 } }