Wander/FNA/src/Audio/Cue.cs

1463 lines
34 KiB
C#

#region License
/* FNA - XNA4 Reimplementation for Desktop Platforms
* Copyright 2009-2018 Ethan Lee and the MonoGame Team
*
* Released under the Microsoft Public License.
* See LICENSE for details.
*/
#endregion
#region Using Statements
using System;
using System.Collections.Generic;
using System.Diagnostics;
#endregion
namespace Microsoft.Xna.Framework.Audio
{
// http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.audio.cue.aspx
public sealed class Cue : IDisposable
{
#region Public Properties
// FIXME: What is this...? -flibit
public bool IsCreated
{
get;
private set;
}
public bool IsDisposed
{
get;
private set;
}
public bool IsPaused
{
get
{
return ( !INTERNAL_timer.IsRunning &&
INTERNAL_timer.ElapsedTicks > 0 );
}
}
public bool IsPlaying
{
get
{
return ( INTERNAL_timer.IsRunning ||
INTERNAL_timer.ElapsedTicks > 0 ) && !IsStopping;
}
}
// FIXME: Is this just to load WaveBank tracks? -flibit
public bool IsPrepared
{
get;
private set;
}
// FIXME: Is this just to load WaveBank tracks? -flibit
public bool IsPreparing
{
get;
private set;
}
public bool IsStopped
{
get;
private set;
}
public bool IsStopping
{
get
{
return ( INTERNAL_fadeMode == FadeMode.FadeOut ||
INTERNAL_fadeMode == FadeMode.ReleaseRpc );
}
}
public string Name
{
get;
private set;
}
#endregion
#region Internal Properties
private ulong elapsedFrames;
internal bool JustStarted
{
get
{
return elapsedFrames < 2;
}
}
#endregion
#region Private Variables
private AudioEngine INTERNAL_baseEngine;
// Cue information parsed from the SoundBank
private List<string> INTERNAL_waveBankNames;
private CueData INTERNAL_data;
// Current sound and its events
private XACTSoundInstance INTERNAL_activeSound;
private Dictionary<SoundEffectInstance, PlayWaveEventInstance>
INTERNAL_playWaveEventBySound;
// Used for event timestamps
private Stopwatch INTERNAL_timer;
// Sound list
private List<SoundEffectInstance> INTERNAL_instancePool;
private List<double> INTERNAL_instanceVolumes;
private List<short> INTERNAL_instancePitches;
// RPC data list
private List<float> INTERNAL_rpcTrackVolumes;
private List<float> INTERNAL_rpcTrackPitches;
private ushort INTERNAL_maxRpcReleaseTime;
// Events can control volume/pitch as well!
internal double eventVolume;
internal float eventPitch;
// User-controlled sounds require a bit more trickery.
private bool INTERNAL_userControlledPlaying;
private float INTERNAL_controlledValue;
// 3D audio variables
private bool INTERNAL_isPositional;
private AudioListener INTERNAL_listener;
private AudioEmitter INTERNAL_emitter;
// XACT instance variables
private List<Variable> INTERNAL_variables;
// Category managing this Cue, and whether or not it's user-managed
private AudioCategory INTERNAL_category;
private bool INTERNAL_isManaged;
// Fading
private enum FadeMode
{
None,
FadeOut,
FadeIn,
ReleaseRpc
}
private long INTERNAL_fadeStart;
private long INTERNAL_fadeEnd;
private FadeMode INTERNAL_fadeMode = FadeMode.None;
#endregion
#region Private Static Random Number Generator
private static Random random = new Random();
#endregion
#region Disposing Event
public event EventHandler<EventArgs> Disposing;
#endregion
#region Internal Constructor
internal Cue(
AudioEngine audioEngine,
List<string> waveBankNames,
string name,
CueData data,
bool managed
) {
INTERNAL_baseEngine = audioEngine;
INTERNAL_waveBankNames = waveBankNames;
Name = name;
INTERNAL_data = data;
IsPrepared = false;
IsPreparing = true;
INTERNAL_maxRpcReleaseTime = 0;
foreach (XACTSound curSound in data.Sounds)
{
/* Determine the release times per track, if any, to be used to extend
* the sound when playing the release.
*/
{
ushort maxReleaseMS = 0;
// Loop over tracks.
for (int i = 0; i < curSound.RPCCodes.Count; i += 1)
{
// Loop over curves.
foreach (uint curCode in curSound.RPCCodes[i])
{
RPC curRPC = INTERNAL_baseEngine.INTERNAL_getRPC(curCode);
if (!INTERNAL_baseEngine.INTERNAL_isGlobalVariable(curRPC.Variable))
{
// Only release times applied to volume are considered.
if (curRPC.Variable.Equals("ReleaseTime") && curRPC.Parameter == RPCParameter.Volume)
{
maxReleaseMS = Math.Max((ushort)curRPC.LastPoint.X, maxReleaseMS);
}
}
}
}
// Keep track of the maximum release time to extend the sound.
INTERNAL_maxRpcReleaseTime = maxReleaseMS;
}
}
IsPrepared = true;
IsPreparing = false;
IsStopped = false;
INTERNAL_isManaged = managed;
INTERNAL_category = INTERNAL_baseEngine.INTERNAL_initCue(
this,
data.Category
);
eventVolume = 0.0;
eventPitch = 0.0f;
INTERNAL_userControlledPlaying = false;
INTERNAL_isPositional = false;
INTERNAL_playWaveEventBySound =
new Dictionary<SoundEffectInstance, PlayWaveEventInstance>();
INTERNAL_timer = new Stopwatch();
INTERNAL_instancePool = new List<SoundEffectInstance>();
INTERNAL_instanceVolumes = new List<double>();
INTERNAL_instancePitches = new List<short>();
INTERNAL_rpcTrackVolumes = new List<float>();
INTERNAL_rpcTrackPitches = new List<float>();
}
#endregion
#region Destructor
~Cue()
{
Dispose();
}
#endregion
#region Public Dispose Method
public void Dispose()
{
if (!IsDisposed)
{
if (Disposing != null)
{
Disposing.Invoke(this, null);
}
if (INTERNAL_instancePool != null)
{
foreach (SoundEffectInstance sfi in INTERNAL_instancePool)
{
sfi.Dispose();
}
INTERNAL_instancePool.Clear();
INTERNAL_instanceVolumes.Clear();
INTERNAL_instancePitches.Clear();
INTERNAL_rpcTrackVolumes.Clear();
INTERNAL_rpcTrackPitches.Clear();
}
KillCue();
IsDisposed = true;
// IXACTCue* no longer exists, these should all be false
IsStopped = false;
IsCreated = false;
IsPrepared = false;
}
}
#endregion
#region Public Methods
public void Apply3D(AudioListener listener, AudioEmitter emitter)
{
if ((IsPlaying || IsStopping) && !INTERNAL_isPositional)
{
throw new InvalidOperationException("Apply3D call after Play!");
}
if (listener == null)
{
throw new ArgumentNullException("listener");
}
if (emitter == null)
{
throw new ArgumentNullException("emitter");
}
INTERNAL_listener = listener;
INTERNAL_emitter = emitter;
// Set Apply3D-related Variables
Vector3 emitterToListener = listener.Position - emitter.Position;
float distance = emitterToListener.Length();
SetVariable("Distance", distance);
SetVariable(
"DopplerPitchScalar",
INTERNAL_calculateDoppler(emitterToListener, distance)
);
SetVariable(
"OrientationAngle",
MathHelper.ToDegrees((float) Math.Acos(
Vector3.Dot(
emitterToListener / distance, // Direction...
listener.Forward
) // Slope...
)) // Angle!
);
INTERNAL_isPositional = true;
}
public float GetVariable(string name)
{
if (String.IsNullOrEmpty(name))
{
throw new ArgumentNullException("name");
}
if (name.Equals("NumCueInstances"))
{
return INTERNAL_category.INTERNAL_cueInstanceCount(Name);
}
foreach (Variable curVar in INTERNAL_variables)
{
if (name.Equals(curVar.Name))
{
return curVar.GetValue();
}
}
throw new ArgumentException("Instance variable not found!");
}
public void Pause()
{
if (IsPlaying)
{
INTERNAL_timer.Stop();
foreach (SoundEffectInstance sfi in INTERNAL_instancePool)
{
sfi.Pause();
}
}
}
public void Play()
{
if (IsPlaying || IsStopping)
{
throw new InvalidOperationException("Cue already playing!");
}
// Instance limiting
if (INTERNAL_category.INTERNAL_cueInstanceCount(Name) >= INTERNAL_data.InstanceLimit)
{
if (INTERNAL_data.MaxCueBehavior == MaxInstanceBehavior.Fail)
{
return; // Just ignore us...
}
else if (INTERNAL_data.MaxCueBehavior == MaxInstanceBehavior.Queue)
{
throw new NotImplementedException("Cue Queueing not handled!");
}
else if (INTERNAL_data.MaxCueBehavior == MaxInstanceBehavior.ReplaceOldest)
{
if (!INTERNAL_category.INTERNAL_removeOldestCue(Name))
{
return; // Just ignore us...
}
}
else if (INTERNAL_data.MaxCueBehavior == MaxInstanceBehavior.ReplaceQuietest)
{
if (!INTERNAL_category.INTERNAL_removeQuietestCue(Name))
{
return; // Just ignore us...
}
}
else if (INTERNAL_data.MaxCueBehavior == MaxInstanceBehavior.ReplaceLowestPriority)
{
// FIXME: Priority?
if (!INTERNAL_category.INTERNAL_removeOldestCue(Name))
{
return; // Just ignore us...
}
}
}
if (!INTERNAL_category.INTERNAL_addCue(this))
{
Debug.Assert(false);
return;
}
elapsedFrames = 0;
INTERNAL_timer.Start();
if (INTERNAL_data.FadeInMS > 0)
{
INTERNAL_startFadeIn(INTERNAL_data.FadeInMS);
}
if (!INTERNAL_calculateNextSound())
{
return;
}
INTERNAL_activeSound.InitializeClips();
IsPrepared = false;
}
public void Resume()
{
if (IsPaused)
{
INTERNAL_timer.Start();
foreach (SoundEffectInstance sfi in INTERNAL_instancePool)
{
sfi.Resume();
}
}
}
public void SetVariable(string name, float value)
{
if (String.IsNullOrEmpty(name))
{
throw new ArgumentNullException("name");
}
foreach (Variable curVar in INTERNAL_variables)
{
if (name.Equals(curVar.Name))
{
if (curVar.IsGlobal)
{
throw new ArgumentException("Global variables cannot be set on a cue instance!");
}
if (!curVar.IsPublic)
{
throw new ArgumentException("Private variables cannot be set!");
}
if (curVar.IsReadOnly)
{
throw new ArgumentException("Readonly variables cannot be set!");
}
curVar.SetValue(value);
return;
}
}
throw new ArgumentException("Instance variable not found!");
}
public void Stop(AudioStopOptions options)
{
if (IsPlaying || IsStopping)
{
if (!IsPaused)
{
if (options == AudioStopOptions.AsAuthored)
{
if (INTERNAL_data.FadeOutMS > 0)
{
INTERNAL_startFadeOut(INTERNAL_data.FadeOutMS);
return;
}
else if (INTERNAL_maxRpcReleaseTime > 0)
{
INTERNAL_startReleaseRpc(INTERNAL_maxRpcReleaseTime);
return;
}
}
}
foreach (SoundEffectInstance sfi in INTERNAL_instancePool)
{
sfi.Stop();
sfi.Dispose();
}
INTERNAL_instancePool.Clear();
INTERNAL_instanceVolumes.Clear();
INTERNAL_instancePitches.Clear();
INTERNAL_rpcTrackVolumes.Clear();
INTERNAL_rpcTrackPitches.Clear();
INTERNAL_userControlledPlaying = false;
KillCue();
IsStopped = true;
// If this is a managed Cue, we're done here.
if (INTERNAL_isManaged)
{
Dispose();
}
}
}
#endregion
#region Internal Methods
internal bool INTERNAL_update()
{
// If we're not running, save some instructions...
if (!INTERNAL_timer.IsRunning)
{
return true;
}
elapsedFrames += 1;
// User control updates
if (INTERNAL_data.IsUserControlled)
{
string varName = INTERNAL_data.UserControlVariable;
if ( INTERNAL_userControlledPlaying &&
(INTERNAL_baseEngine.INTERNAL_isGlobalVariable(varName) ?
!MathHelper.WithinEpsilon(INTERNAL_controlledValue, INTERNAL_baseEngine.GetGlobalVariable(varName)) :
!MathHelper.WithinEpsilon(INTERNAL_controlledValue, GetVariable(INTERNAL_data.UserControlVariable))) )
{
// TODO: Crossfading
foreach (SoundEffectInstance sfi in INTERNAL_instancePool)
{
sfi.Stop();
sfi.Dispose();
}
INTERNAL_instancePool.Clear();
INTERNAL_instanceVolumes.Clear();
INTERNAL_instancePitches.Clear();
INTERNAL_rpcTrackVolumes.Clear();
INTERNAL_rpcTrackPitches.Clear();
if (!INTERNAL_calculateNextSound())
{
// Nothing to play, bail.
return true;
}
INTERNAL_activeSound.InitializeClips();
INTERNAL_timer.Stop();
INTERNAL_timer.Reset();
INTERNAL_timer.Start();
}
if (INTERNAL_activeSound == null)
{
return INTERNAL_userControlledPlaying;
}
}
// Trigger events for each track
foreach (XACTClipInstance clip in INTERNAL_activeSound.Clips)
{
// Play events when the timestamp has been hit.
for (int i = 0; i < clip.Events.Count; i += 1)
{
EventInstance evt = clip.Events[i];
if ( !evt.Played &&
INTERNAL_timer.ElapsedMilliseconds > evt.Timestamp )
{
evt.Apply(
this,
null,
INTERNAL_timer.ElapsedMilliseconds / 1000.0f
);
}
}
}
// Clear out sound effect instances as they finish
for (int i = 0; i < INTERNAL_instancePool.Count; i += 1)
{
if (INTERNAL_instancePool[i].State == SoundState.Stopped)
{
// Get the event that spawned this instance...
PlayWaveEventInstance evtInstance =
INTERNAL_playWaveEventBySound[INTERNAL_instancePool[i]];
double prevVolume = INTERNAL_instanceVolumes[i];
short prevPitch = INTERNAL_instancePitches[i];
// Then delete all the guff
INTERNAL_playWaveEventBySound.Remove(INTERNAL_instancePool[i]);
INTERNAL_instancePool[i].Dispose();
INTERNAL_instancePool.RemoveAt(i);
INTERNAL_instanceVolumes.RemoveAt(i);
INTERNAL_instancePitches.RemoveAt(i);
INTERNAL_rpcTrackVolumes.RemoveAt(i);
INTERNAL_rpcTrackPitches.RemoveAt(i);
// Increment the loop counter, try to get another loop
evtInstance.LoopCount += 1;
PlayWave(evtInstance, prevVolume, prevPitch);
// Removed a wave, have to step back...
i -= 1;
}
}
// Fade in/out
float fadePerc = 1.0f;
if (INTERNAL_fadeMode != FadeMode.None)
{
if (INTERNAL_fadeMode == FadeMode.FadeOut)
{
if (INTERNAL_category.crossfadeType == CrossfadeType.Linear)
{
fadePerc = (
INTERNAL_fadeEnd -
(
INTERNAL_timer.ElapsedMilliseconds -
INTERNAL_fadeStart
)
) / (float) INTERNAL_fadeEnd;
}
else
{
throw new NotImplementedException("Unhandled CrossfadeType!");
}
if (fadePerc <= 0.0f)
{
Stop(AudioStopOptions.Immediate);
INTERNAL_fadeMode = FadeMode.None;
return false;
}
}
else if (INTERNAL_fadeMode == FadeMode.FadeIn)
{
if (INTERNAL_category.crossfadeType == CrossfadeType.Linear)
{
fadePerc = INTERNAL_timer.ElapsedMilliseconds / (float) INTERNAL_fadeEnd;
}
else
{
throw new NotImplementedException("Unhandled CrossfadeType!");
}
if (fadePerc > 1.0f)
{
fadePerc = 1.0f;
INTERNAL_fadeMode = FadeMode.None;
}
}
else if (INTERNAL_fadeMode == FadeMode.ReleaseRpc)
{
float releasePerc = (
INTERNAL_timer.ElapsedMilliseconds -
INTERNAL_fadeStart
) / (float) INTERNAL_maxRpcReleaseTime;
if (releasePerc > 1.0f)
{
Stop(AudioStopOptions.Immediate);
INTERNAL_fadeMode = FadeMode.None;
return false;
}
}
else
{
throw new NotImplementedException("Unsupported FadeMode!");
}
}
// If everything has been played and finished, we're done here.
if (INTERNAL_instancePool.Count == 0)
{
bool allPlayed = true;
foreach (XACTClipInstance clipInstance in INTERNAL_activeSound.Clips)
{
foreach (EventInstance evt in clipInstance.Events)
{
if (!evt.Played)
{
allPlayed = false;
break;
}
}
}
if (allPlayed)
{
// If this is managed, we're done completely.
if (INTERNAL_isManaged)
{
Dispose();
}
else
{
KillCue();
}
if (INTERNAL_userControlledPlaying)
{
// We're "still" "playing" right now...
return true;
}
IsStopped = true;
return false;
}
}
// RPC updates
float rpcVolume = 0.0f;
float rpcPitch = 0.0f;
float hfGain = 1.0f;
float lfGain = 1.0f;
for (int i = 0; i < INTERNAL_activeSound.Sound.RPCCodes.Count; i += 1)
{
// Are we processing an RPC targeting the sound itself rather than a track?
bool isSoundRpc = i == 0 && INTERNAL_activeSound.Sound.HasSoundRpcs;
// If there is an RPC targeting the sound instance itself, it is handled in rpcVolume/rpcPitch, and the first track is at i-1.
int trackRpcIndex = INTERNAL_activeSound.Sound.HasSoundRpcs ? i - 1 : i;
// If this RPC Code is for a track that is not active yet, we have nothing to do.
if (trackRpcIndex >= INTERNAL_instancePool.Count)
{
// FIXME: This presumes that tracks start in order, which doesn't have to be true.
break;
}
if (!isSoundRpc)
{
INTERNAL_rpcTrackVolumes[trackRpcIndex] = 0.0f;
INTERNAL_rpcTrackPitches[trackRpcIndex] = 0.0f;
}
foreach (uint curCode in INTERNAL_activeSound.Sound.RPCCodes[i])
{
RPC curRPC = INTERNAL_baseEngine.INTERNAL_getRPC(curCode);
float result;
if (!INTERNAL_baseEngine.INTERNAL_isGlobalVariable(curRPC.Variable))
{
float variableValue;
if (curRPC.Variable.Equals("AttackTime"))
{
PlayWaveEvent playWaveEvent =
(PlayWaveEvent) INTERNAL_activeSound.Sound.INTERNAL_clips[trackRpcIndex].Events[0];
long elapsedFromPlay = INTERNAL_timer.ElapsedMilliseconds
- playWaveEvent.Timestamp;
variableValue = elapsedFromPlay;
}
else if (curRPC.Variable.Equals("ReleaseTime"))
{
if (INTERNAL_fadeMode == FadeMode.ReleaseRpc)
{
long elapsedFromStop = INTERNAL_timer.ElapsedMilliseconds - INTERNAL_fadeStart;
variableValue = elapsedFromStop;
}
else
{
variableValue = 0.0f;
}
}
else
{
variableValue = GetVariable(curRPC.Variable);
}
result = curRPC.CalculateRPC(variableValue);
}
else
{
// It's a global variable we're looking for!
result = curRPC.CalculateRPC(
INTERNAL_baseEngine.GetGlobalVariable(
curRPC.Variable
)
);
}
if (curRPC.Parameter == RPCParameter.Volume)
{
// If this RPC targets the sound instance itself then apply to the dedicated variable.
if (isSoundRpc)
{
rpcVolume += result;
}
else
{
INTERNAL_rpcTrackVolumes[trackRpcIndex] += result;
}
}
else if (curRPC.Parameter == RPCParameter.Pitch)
{
float pitch = result;
if (isSoundRpc)
{
rpcPitch += pitch;
}
else
{
INTERNAL_rpcTrackPitches[trackRpcIndex] += pitch;
}
}
else if (curRPC.Parameter == RPCParameter.FilterFrequency)
{
// FIXME: Just listening to the last RPC!
float hf = result / 20000.0f;
float lf = 1.0f - hf;
if (isSoundRpc)
{
hfGain = hf;
lfGain = lf;
}
else
{
throw new NotImplementedException("Per-track filter RPCs!");
}
}
else
{
throw new NotImplementedException(
"RPC Parameter Type: " + curRPC.Parameter.ToString()
);
}
}
}
// Sound effect instance updates
for (int i = 0; i < INTERNAL_instancePool.Count; i += 1)
{
/* The final volume should be the combination of the
* authored volume, category volume, RPC sound/track
* volumes, event volumes, and fade.
*/
INTERNAL_instancePool[i].Volume = XACTCalculator.CalculateAmplitudeRatio(
INTERNAL_instanceVolumes[i] +
rpcVolume +
INTERNAL_rpcTrackVolumes[i] +
eventVolume
) * INTERNAL_category.INTERNAL_volume.Value * fadePerc;
/* The final pitch should be the combination of the
* authored pitch, RPC sound/track pitches, and event
* pitch.
*
* XACT uses -1200 to 1200 (+/- 12 semitones),
* XNA uses -1.0f to 1.0f (+/- 1 octave).
*/
INTERNAL_instancePool[i].Pitch = (
INTERNAL_instancePitches[i] +
rpcPitch +
INTERNAL_rpcTrackPitches[i] +
eventPitch
) / 1200.0f;
/* The final filter is determined by the instance's filter type,
* in addition to our calculation of the HF/LF gain values.
*/
byte fType = INTERNAL_instancePool[i].FilterType;
if (fType == 0xFF)
{
// No-op, no filter!
}
else if (fType == 0)
{
INTERNAL_instancePool[i].INTERNAL_applyLowPassFilter(hfGain);
}
else if (fType == 1)
{
INTERNAL_instancePool[i].INTERNAL_applyHighPassFilter(lfGain);
}
else if (fType == 2)
{
INTERNAL_instancePool[i].INTERNAL_applyBandPassFilter(hfGain, lfGain);
}
else
{
throw new InvalidOperationException("Unhandled filter type!");
}
// Update 3D position, if applicable
if (INTERNAL_isPositional)
{
INTERNAL_instancePool[i].Apply3D(
INTERNAL_listener,
INTERNAL_emitter
);
}
}
return true;
}
internal void INTERNAL_genVariables(List<Variable> cueVariables)
{
INTERNAL_variables = cueVariables;
}
internal float INTERNAL_calculateVolume()
{
float retval = 0.0f;
for (int i = 0; i < INTERNAL_activeSound.Sound.RPCCodes.Count; i += 1)
foreach (uint curCode in INTERNAL_activeSound.Sound.RPCCodes[i])
{
RPC curRPC = INTERNAL_baseEngine.INTERNAL_getRPC(curCode);
if (curRPC.Parameter != RPCParameter.Volume)
{
continue;
}
float result;
if (!INTERNAL_baseEngine.INTERNAL_isGlobalVariable(curRPC.Variable))
{
float variableValue;
if (curRPC.Variable.Equals("AttackTime"))
{
long elapsedFromPlay = INTERNAL_timer.ElapsedMilliseconds;
variableValue = elapsedFromPlay;
}
else if (curRPC.Variable.Equals("ReleaseTime"))
{
if (INTERNAL_fadeMode == FadeMode.ReleaseRpc)
{
long elapsedFromStop = INTERNAL_timer.ElapsedMilliseconds - INTERNAL_fadeStart;
variableValue = elapsedFromStop;
}
else
{
variableValue = 0.0f;
}
}
else
{
variableValue = GetVariable(curRPC.Variable);
}
result = curRPC.CalculateRPC(variableValue);
}
else
{
// It's a global variable we're looking for!
result = curRPC.CalculateRPC(
INTERNAL_baseEngine.GetGlobalVariable(
curRPC.Variable
)
);
}
retval += result;
}
return retval;
}
internal void INTERNAL_startFadeIn(ushort ms)
{
// start is not used, since it's always 0 anyway -flibit
INTERNAL_fadeEnd = ms;
INTERNAL_fadeMode = FadeMode.FadeIn;
}
internal void INTERNAL_startFadeOut(ushort ms)
{
if (INTERNAL_fadeMode == FadeMode.FadeOut)
{
return; // Already in the middle of something...
}
INTERNAL_fadeStart = INTERNAL_timer.ElapsedMilliseconds;
INTERNAL_fadeEnd = ms;
INTERNAL_fadeMode = FadeMode.FadeOut;
INTERNAL_category.INTERNAL_moveToDying(this);
}
internal void INTERNAL_startReleaseRpc(ushort ms)
{
INTERNAL_fadeStart = INTERNAL_timer.ElapsedMilliseconds;
INTERNAL_fadeEnd = ms;
INTERNAL_fadeMode = FadeMode.ReleaseRpc;
}
#endregion
#region Private Methods
private bool INTERNAL_calculateNextSound()
{
if (INTERNAL_activeSound != null)
{
INTERNAL_activeSound.Dispose(
INTERNAL_baseEngine,
INTERNAL_waveBankNames
);
INTERNAL_activeSound = null;
}
INTERNAL_playWaveEventBySound.Clear();
// Pick a sound based on a Cue instance variable
if (INTERNAL_data.IsUserControlled)
{
INTERNAL_userControlledPlaying = true;
if (INTERNAL_baseEngine.INTERNAL_isGlobalVariable(INTERNAL_data.UserControlVariable))
{
INTERNAL_controlledValue = INTERNAL_baseEngine.GetGlobalVariable(
INTERNAL_data.UserControlVariable
);
}
else
{
INTERNAL_controlledValue = GetVariable(
INTERNAL_data.UserControlVariable
);
}
for (int i = 0; i < INTERNAL_data.Probabilities.Length / 2; i += 1)
{
if ( INTERNAL_controlledValue <= INTERNAL_data.Probabilities[i, 0] &&
INTERNAL_controlledValue >= INTERNAL_data.Probabilities[i, 1] )
{
INTERNAL_activeSound = INTERNAL_data.Sounds[i].GenInstance(
INTERNAL_baseEngine,
INTERNAL_waveBankNames
);
return true;
}
}
/* This should only happen when the
* UserControlVariable is none of the sound
* probabilities, in which case we are just
* silent. But, we are still claiming to be
* "playing" in the meantime.
* -flibit
*/
return false;
}
// Randomly pick a sound
double max = 0.0;
for (int i = 0; i < INTERNAL_data.Probabilities.GetLength(0); i += 1)
{
max += INTERNAL_data.Probabilities[i, 0] - INTERNAL_data.Probabilities[i, 1];
}
double next = random.NextDouble() * max;
for (int i = INTERNAL_data.Probabilities.GetLength(0) - 1; i >= 0; i -= 1)
{
if (next > max - (INTERNAL_data.Probabilities[i, 0] - INTERNAL_data.Probabilities[i, 1]))
{
INTERNAL_activeSound = INTERNAL_data.Sounds[i].GenInstance(
INTERNAL_baseEngine,
INTERNAL_waveBankNames
);
break;
}
max -= INTERNAL_data.Probabilities[i, 0] - INTERNAL_data.Probabilities[i, 1];
}
return true;
}
private void KillCue()
{
INTERNAL_timer.Stop();
INTERNAL_timer.Reset();
if (INTERNAL_activeSound != null)
{
if (INTERNAL_waveBankNames.Count > 0) // AKA !SoundBank.IsDisposed
{
INTERNAL_activeSound.Dispose(
INTERNAL_baseEngine,
INTERNAL_waveBankNames
);
}
INTERNAL_activeSound = null;
}
INTERNAL_category.INTERNAL_removeActiveCue(this);
}
internal void PlayWave(
EventInstance eventInstance,
double? prevVolume = null,
short? prevPitch = null
) {
PlayWaveEventInstance playWaveEventInstance =
(PlayWaveEventInstance) eventInstance;
PlayWaveEvent evt = (PlayWaveEvent) eventInstance.Event;
double finalVolume;
short finalPitch;
SoundEffectInstance sfi = evt.GenerateInstance(
INTERNAL_activeSound.Sound.Volume,
INTERNAL_activeSound.Sound.Pitch,
playWaveEventInstance.LoopCount,
prevVolume,
prevPitch,
out finalVolume,
out finalPitch
);
if (sfi != null)
{
if (INTERNAL_isPositional)
{
sfi.Apply3D(INTERNAL_listener, INTERNAL_emitter);
}
foreach (uint curDSP in INTERNAL_activeSound.Sound.DSPCodes)
{
// FIXME: This only applies the last DSP!
sfi.INTERNAL_applyReverb(
INTERNAL_baseEngine.INTERNAL_getDSP(curDSP)
);
}
INTERNAL_instancePool.Add(sfi);
INTERNAL_instanceVolumes.Add(finalVolume);
INTERNAL_instancePitches.Add(finalPitch);
INTERNAL_playWaveEventBySound.Add(sfi, playWaveEventInstance);
INTERNAL_rpcTrackVolumes.Add(0.0f);
INTERNAL_rpcTrackPitches.Add(0.0f);
sfi.Play();
}
}
private float INTERNAL_calculateDoppler(Vector3 emitterToListener, float distance)
{
/* Adapted from algorithm published as a part of the webaudio specification:
* https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html#Spatialization-doppler-shift
* -Chad
*/
float dopplerShift = 1.0f;
float dopplerFactor = INTERNAL_emitter.DopplerScale;
if (dopplerFactor > 0.0f)
{
float speedOfSound = INTERNAL_baseEngine.GetGlobalVariable("SpeedOfSound");
float scaledSpeedOfSound = speedOfSound / dopplerFactor;
// Project the velocities along the emitter to listener vector.
float projectedListenerVelocity = Vector3.Dot(
emitterToListener,
INTERNAL_listener.Velocity
) / distance;
float projectedEmitterVelocity = Vector3.Dot(
emitterToListener,
INTERNAL_emitter.Velocity
) / distance;
// Clamp to the speed of the medium.
projectedListenerVelocity = Math.Min(
projectedListenerVelocity,
scaledSpeedOfSound
);
projectedEmitterVelocity = Math.Min(
projectedEmitterVelocity,
scaledSpeedOfSound
);
// Apply doppler effect.
dopplerShift = (
speedOfSound - dopplerFactor * projectedListenerVelocity
) / (
speedOfSound - dopplerFactor * projectedEmitterVelocity
);
if (float.IsNaN(dopplerShift))
{
dopplerShift = 1.0f;
}
// Limit the pitch shifting to 2 octaves up and 1 octaves down per XACT behavior.
dopplerShift = MathHelper.Clamp(dopplerShift, 0.5f, 4.0f);
}
return dopplerShift;
}
#endregion
}
internal class XACTSoundInstance
{
public readonly XACTSound Sound;
public readonly List<XACTClipInstance> Clips = new List<XACTClipInstance>();
public XACTSoundInstance(XACTSound sound)
{
Sound = sound;
}
public void Dispose(AudioEngine audioEngine, List<string> waveBankNames)
{
Clips.Clear();
Sound.DisposeInstance(this, audioEngine, waveBankNames);
}
internal void InitializeClips()
{
// Create clip instances for each clip (track).
foreach (XACTClip curClip in Sound.INTERNAL_clips)
{
XACTClipInstance clipInstance = new XACTClipInstance(curClip);
Clips.Add(clipInstance);
}
}
}
internal class XACTClipInstance
{
public readonly XACTClip Clip;
public readonly List<EventInstance> Events = new List<EventInstance>();
public XACTClipInstance(XACTClip clip)
{
Clip = clip;
// Create event instances for each event.
foreach (XACTEvent evt in Clip.Events)
{
// TODO: How best to eliminate this switch? Factory template method? Table of delegates?
EventInstance eventInstance = null;
if (evt is PlayWaveEvent)
{
eventInstance = new PlayWaveEventInstance((PlayWaveEvent) evt);
}
else if (evt is StopEvent)
{
eventInstance = new StopEventInstance((StopEvent) evt);
}
else if (evt is SetValueEvent)
{
eventInstance = new SetValueEventInstance((SetValueEvent) evt);
}
else if (evt is SetRandomValueEvent)
{
eventInstance = new SetRandomValueEventInstance((SetRandomValueEvent) evt);
}
else if (evt is SetRampValueEvent)
{
eventInstance = new SetRampValueEventInstance((SetRampValueEvent) evt);
}
else if (evt is MarkerEvent)
{
eventInstance = new MarkerEventInstance((MarkerEvent) evt);
}
Debug.Assert(eventInstance != null);
Events.Add(eventInstance);
}
}
}
internal abstract class EventInstance
{
public readonly XACTEvent Event;
public float Timestamp;
public int LoopCount;
public bool Played;
public EventInstance(XACTEvent evt)
{
Event = evt;
Timestamp = (
Event.Timestamp +
XACTEvent.Random.Next(0, Event.RandomOffset)
);
LoopCount = Event.LoopCount;
Played = false;
}
public abstract void Apply(Cue cue, XACTClip track, float elapsedTime);
protected void HandleRepeating()
{
if (LoopCount > 0)
{
// If not set to infinite looping.
if (Event.LoopCount != 65535)
{
LoopCount = LoopCount - 1;
}
// FIXME: Use Frequency Units (Seconds / Beats per Minute) instead of constant of seconds.
Timestamp = Timestamp + Event.Frequency * 1000.0f;
}
else
{
Played = true;
}
}
}
internal class PlayWaveEventInstance : EventInstance
{
public PlayWaveEventInstance(PlayWaveEvent evt)
: base(evt)
{
}
public override void Apply(Cue cue, XACTClip track, float elapsedTime)
{
// Only actually play if we are not in the process of stopping.
if (!cue.IsStopping)
{
cue.PlayWave(this);
}
Played = true;
}
}
internal class StopEventInstance : EventInstance
{
public StopEventInstance(StopEvent evt)
: base(evt)
{
}
public override void Apply(Cue cue, XACTClip track, float elapsedTime)
{
StopEvent evt = (StopEvent) Event;
AudioStopOptions stopOptions = evt.StopOptions;
switch (evt.Scope)
{
case XACTClip.StopEventScope.Cue:
cue.Stop(stopOptions);
break;
case XACTClip.StopEventScope.Track:
/* FIXME: Need to stop this and ONLY this track
* track.Stop(stopOptions);
*/
break;
}
Played = true;
}
}
internal class SetValueEventInstance : EventInstance
{
public SetValueEventInstance(SetValueEvent evt)
: base(evt)
{
}
public override void Apply(Cue cue, XACTClip track, float elapsedTime)
{
SetValueEvent evt = (SetValueEvent) Event;
switch (evt.Property)
{
case CueProperty.Volume:
cue.eventVolume = evt.GetVolume(cue.eventVolume);
break;
case CueProperty.Pitch:
cue.eventPitch = evt.GetPitch(cue.eventPitch);
break;
}
HandleRepeating();
}
}
internal class SetRandomValueEventInstance : EventInstance
{
public SetRandomValueEventInstance(SetRandomValueEvent evt)
: base(evt)
{
}
public override void Apply(Cue cue, XACTClip track, float elapsedTime)
{
SetRandomValueEvent evt = (SetRandomValueEvent) Event;
switch (evt.Property)
{
case CueProperty.Volume:
cue.eventVolume = evt.GetVolume(cue.eventVolume);
break;
case CueProperty.Pitch:
cue.eventPitch = evt.GetPitch(cue.eventPitch);
break;
}
HandleRepeating();
}
}
internal class SetRampValueEventInstance : EventInstance
{
public SetRampValueEventInstance(SetRampValueEvent evt)
: base(evt)
{
}
public override void Apply(Cue cue, XACTClip track, float elapsedTime)
{
SetRampValueEvent evt = (SetRampValueEvent) Event;
if (elapsedTime <= Timestamp / 1000.0f + evt.Duration)
{
switch (evt.Property)
{
case CueProperty.Volume:
cue.eventVolume = GetValue(evt, elapsedTime);
break;
case CueProperty.Pitch:
cue.eventPitch = GetValue(evt, elapsedTime);
break;
}
}
else
{
HandleRepeating();
}
}
private float GetValue(SetRampValueEvent x, float elapsedTime)
{
// Number of slices to break up the duration.
const float slices = 10;
float endValue = x.InitialSlope * x.Duration * slices + x.InitialValue;
// FIXME: Incorporate 2nd derivative into the interpolated pitch.
float amount = MathHelper.Clamp(
(elapsedTime - Timestamp / 1000.0f) / x.Duration,
0.0f,
1.0f
);
return MathHelper.Lerp(x.InitialValue, endValue, amount);
}
}
internal class MarkerEventInstance : EventInstance
{
public MarkerEventInstance(MarkerEvent evt)
: base(evt)
{
}
public override void Apply(Cue cue, XACTClip track, float elapsedTime)
{
// FIXME: Implement action for a marker event. Some kind of callback?
HandleRepeating();
}
}
}