public RadegastInstance Instance;
private List<MediaObject> sounds = new List<MediaObject>();
+ ManualResetEvent initDone = new ManualResetEvent(false);
public MediaManager(RadegastInstance instance)
: base()
this.Instance = instance;
manager = this;
+
loadCallback = new FMOD.SOUND_NONBLOCKCALLBACK(DispatchNonBlockCallback);
endCallback = new FMOD.CHANNEL_CALLBACK(DispatchEndCallback);
- allBuffers = new LinkedList<BufferSound>();
+ allBuffers = new Dictionary<UUID, BufferSound>();
// Start the background thread that does all the FMOD calls.
soundThread = new Thread(new ThreadStart(CommandLoop));
listenerThread.Name = "ListenerThread";
listenerThread.Start();
- // Subscribe to events about inworld sounds
- instance.Client.Sound.SoundTrigger += new EventHandler<SoundTriggerEventArgs>(Sound_SoundTrigger);
- instance.Client.Sound.AttachedSound += new EventHandler<AttachedSoundEventArgs>(Sound_AttachedSound);
- instance.Client.Sound.PreloadSound += new EventHandler<PreloadSoundEventArgs>(Sound_PreloadSound);
+ // Wait for init to complete
+ initDone.WaitOne();
+ initDone = null;
}
+ /// <summary>
+ /// Thread that processes FMOD calls.
+ /// </summary>
private void CommandLoop()
{
SoundDelegate action = null;
+ // Initialze a bunch of static values
UpVector.x = 0.0f;
UpVector.y = 1.0f;
UpVector.z = 0.0f;
ZeroVector.y = 0.0f;
ZeroVector.z = 0.0f;
- allSounds = new Dictionary<IntPtr,MediaObject>();
+ allSounds = new Dictionary<IntPtr, MediaObject>();
allChannels = new Dictionary<IntPtr, MediaObject>();
- InitFMOD();
- if (!this.soundSystemAvailable) return;
-
// Initialize the command queue.
queue = new Queue<SoundDelegate>();
+ // Initialize the FMOD sound package
+ InitFMOD();
+ initDone.Set();
+ if (!this.soundSystemAvailable) return;
+
while (true)
{
// Wait for something to show up in the queue.
}
catch (Exception e)
{
- Logger.Log("Error in sound action: " + e.Message,
+ Logger.Log("Error in sound action:\n " + e.Message + "\n" + e.StackTrace,
Helpers.LogLevel.Error);
}
}
system = null;
}
+ if (listenerThread != null)
+ {
+ if (listenerThread.IsAlive)
+ listenerThread.Abort();
+ listenerThread = null;
+ }
+
+ if (soundThread != null)
+ {
+ if (soundThread.IsAlive)
+ soundThread.Abort();
+ soundThread = null;
+ }
+
base.Dispose();
}
/// </summary>
private void ListenerUpdate()
{
- Vector3 lastpos = new Vector3(0.0f, 0.0f, 0.0f );
+ // Notice changes in position or direction.
+ Vector3 lastpos = new Vector3(0.0f, 0.0f, 0.0f);
float lastface = 0.0f;
while (true)
{
+ // Two updates per second.
Thread.Sleep(500);
if (system == null) continue;
AgentManager my = Instance.Client.Self;
+ Vector3 newPosition = new Vector3(my.SimPosition);
+ float newFace = my.SimRotation.W;
// If we are standing still, nothing to update now, but
// FMOD needs a 'tick' anyway for callbacks, etc. In looping
// 'game' programs, the loop is the 'tick'. Since Radegast
// uses events and has no loop, we use this position update
- // thread to drive the FMOD tick.
- if (my.SimPosition.Equals(lastpos) &&
- my.Movement.BodyRotation.W == lastface)
+ // thread to drive the FMOD tick. Have to move more than
+ // 500mm or turn more than 10 desgrees to bother with.
+ //
+ if (newPosition.ApproxEquals(lastpos, 0.5f) &&
+ Math.Abs(newFace - lastface) < 0.2)
{
invoke(new SoundDelegate(delegate
{
continue;
}
- lastpos = my.SimPosition;
- lastface = my.Movement.BodyRotation.W;
+ // We have moved or turned. Remember new position.
+ lastpos = newPosition;
+ lastface = newFace;
// Convert coordinate spaces.
- FMOD.VECTOR listenerpos = FromOMVSpace(my.SimPosition);
+ FMOD.VECTOR listenerpos = FromOMVSpace(newPosition);
// Get azimuth from the facing Quaternion. Note we assume the
// avatar is standing upright. Avatars in unusual positions
// hear things from unpredictable directions.
// By definition, facing.W = Cos( angle/2 )
- double angle = 2.0 * Math.Acos(my.Movement.BodyRotation.W);
+ // With angle=0 meaning East.
+ double angle = 2.0 * Math.Acos(newFace);
+
+ // Construct facing unit vector in FMOD coordinates.
+ // Z is East, X is South, Y is up.
FMOD.VECTOR forward = new FMOD.VECTOR();
- forward.x = (float)Math.Asin(angle);
+ forward.x = (float)Math.Sin(angle); // South
forward.y = 0.0f;
- forward.z = (float)Math.Acos(angle);
+ forward.z = (float)Math.Cos(angle); // East
+
+ Logger.Log(
+ String.Format(
+ "Standing at <{0:0.0},{1:0.0},{2:0.0}> facing {3:d}",
+ listenerpos.x,
+ listenerpos.y,
+ listenerpos.z,
+ (int)(angle * 180.0 / 3.141592)),
+ Helpers.LogLevel.Debug);
// Tell FMOD the new orientation.
- invoke( new SoundDelegate( delegate
+ invoke(new SoundDelegate(delegate
{
- FMODExec( system.set3DListenerAttributes(
+ FMODExec(system.set3DListenerAttributes(
0,
ref listenerpos, // Position
ref ZeroVector, // Velocity
ref forward, // Facing direction
- ref UpVector )); // Top of head
+ ref UpVector)); // Top of head
FMODExec(system.update());
}));
}
}
- /**
- * Handle triggering a sound to play
- */
+ /// <summary>
+ /// Handle request to play a sound, which might (or mioght not) have been preloaded.
+ /// </summary>
+ /// <param name="sender"></param>
+ /// <param name="e"></param>
private void Sound_SoundTrigger(object sender, SoundTriggerEventArgs e)
{
+ if (e.SoundID == UUID.Zero) return;
+
+ Logger.Log("Trigger sound " + e.SoundID.ToString() +
+ " in object " + e.ObjectID.ToString(),
+ Helpers.LogLevel.Debug);
+
new BufferSound(
+ e.ObjectID,
e.SoundID,
false,
true,
e.Position,
- e.Gain);
+ e.Gain * ObjectVolume);
}
+ /// <summary>
+ /// Handle sound attached to an object
+ /// </summary>
+ /// <param name="sender"></param>
+ /// <param name="e"></param>
private void Sound_AttachedSound(object sender, AttachedSoundEventArgs e)
{
-
+ // This event tells us the Object ID, but not the Prim info directly.
+ // So we look it up in our internal Object memory.
+ Simulator sim = e.Simulator;
+ Primitive p = sim.ObjectsPrimitives.Find((Primitive p2) => { return p2.ID == e.ObjectID; });
+ if (p == null) return;
+
+ // Only one attached sound per prim, so we kill any previous
+ BufferSound.Kill(p.ID);
+
+ // If this is stop sound, we're done since we've already killed sound for this object
+ if ((e.Flags & SoundFlags.Stop) == SoundFlags.Stop)
+ {
+ return;
+ }
+
+ // We seem to get a lot of these zero sounds.
+ if (e.SoundID == UUID.Zero) return;
+
+ // If this is a child prim, its position is relative to the root.
+ Vector3 fullPosition = p.Position;
+
+ while (p != null && p.ParentID != 0)
+ {
+ Avatar av;
+ if (sim.ObjectsAvatars.TryGetValue(p.ParentID, out av))
+ {
+ p = av;
+ fullPosition += p.Position;
+ }
+ else
+ {
+ if (sim.ObjectsPrimitives.TryGetValue(p.ParentID, out p))
+ {
+ fullPosition += p.Position;
+ }
+ }
+ }
+
+ // Didn't find root prim
+ if (p == null) return;
+
+ new BufferSound(
+ e.ObjectID,
+ e.SoundID,
+ (e.Flags & SoundFlags.Loop) == SoundFlags.Loop,
+ true,
+ fullPosition,
+ e.Gain * ObjectVolume);
}
- /**
- * Handle request to preload a sound resource.
- */
+
+ /// <summary>
+ /// Handle request to preload a sound for playing later.
+ /// </summary>
+ /// <param name="sender"></param>
+ /// <param name="e"></param>
private void Sound_PreloadSound(object sender, PreloadSoundEventArgs e)
{
- new BufferSound( e.SoundID );
+ if (e.SoundID == UUID.Zero) return;
+
+ if (!Instance.Client.Assets.Cache.HasAsset(e.SoundID))
+ new BufferSound(e.SoundID);
}
+ /// <summary>
+ /// Handle object updates, looking for sound events
+ /// </summary>
+ /// <param name="sender"></param>
+ /// <param name="e"></param>
+ private void Objects_ObjectUpdate(object sender, PrimEventArgs e)
+ {
+ HandleObjectSound(e.Prim, e.Simulator);
+ }
- }
+ /// <summary>
+ /// Handle deletion of a noise-making object
+ /// </summary>
+ /// <param name="sender"></param>
+ /// <param name="e"></param>
+ void Objects_KillObject(object sender, KillObjectEventArgs e)
+ {
+ Primitive p = null;
+ if (!e.Simulator.ObjectsPrimitives.TryGetValue(e.ObjectLocalID, out p)) return;
- public class PlayingSound
- {
- private UUID Id;
- private Vector3 position;
- private float volume;
+ // Objects without sounds are not interesting.
+ if (p.Sound == null) return;
+ if (p.Sound == UUID.Zero) return;
+
+ BufferSound.Kill(p.ID);
+ }
+
+ /// <summary>
+ /// Common object sound processing for various Update events
+ /// </summary>
+ /// <param name="p"></param>
+ /// <param name="s"></param>
+ private void HandleObjectSound(Primitive p, Simulator s)
+ {
+ // Objects without sounds are not interesting.
+ if (p.Sound == UUID.Zero) return;
+
+ if ((p.SoundFlags & SoundFlags.Stop) == SoundFlags.Stop)
+ {
+ BufferSound.Kill(p.ID);
+ return;
+ }
+
+ // If this is a child prim, its position is relative to the root prim.
+ Vector3 fullPosition = p.Position;
+ if (p.ParentID != 0)
+ {
+ Primitive parentP;
+ if (!s.ObjectsPrimitives.TryGetValue(p.ParentID, out parentP)) return;
+ fullPosition += parentP.Position;
+ }
+
+ // See if this is an update to something we already know about.
+ if (allBuffers.ContainsKey(p.ID))
+ {
+ // Exists already, so modify existing sound.
+ BufferSound snd = allBuffers[p.ID];
+ snd.Volume = p.SoundGain * ObjectVolume;
+ snd.Position = fullPosition;
+ }
+ else
+ {
+ // Does not exist, so create a new one.
+ new BufferSound(
+ p.ID,
+ p.Sound,
+ (p.SoundFlags & SoundFlags.Loop) == SoundFlags.Loop,
+ true,
+ fullPosition, //Instance.State.GlobalPosition(e.Simulator, fullPosition),
+ p.SoundGain * ObjectVolume);
+ }
+ }
+
+ /// <summary>
+ /// Control the volume of all inworld sounds
+ /// </summary>
+ public float ObjectVolume
+ {
+ set
+ {
+ AllObjectVolume = value;
+ BufferSound.AdjustVolumes();
+ }
+ get { return AllObjectVolume; }
+ }
- public PlayingSound( UUID soundId )
+ private bool m_objectEnabled = true;
+ /// <summary>
+ /// Enable and Disable inworld sounds
+ /// </summary>
+ public bool ObjectEnable
{
- Id = soundId;
+ set
+ {
+ try
+ {
+ if (value)
+ {
+ // Subscribe to events about inworld sounds
+ Instance.Client.Sound.SoundTrigger += new EventHandler<SoundTriggerEventArgs>(Sound_SoundTrigger);
+ Instance.Client.Sound.AttachedSound += new EventHandler<AttachedSoundEventArgs>(Sound_AttachedSound);
+ Instance.Client.Sound.PreloadSound += new EventHandler<PreloadSoundEventArgs>(Sound_PreloadSound);
+ Instance.Client.Objects.ObjectUpdate += new EventHandler<PrimEventArgs>(Objects_ObjectUpdate);
+ Instance.Client.Objects.KillObject += new EventHandler<KillObjectEventArgs>(Objects_KillObject);
+ Instance.Client.Network.SimChanged += new EventHandler<SimChangedEventArgs>(Network_SimChanged);
+ Instance.Client.Self.ChatFromSimulator += new EventHandler<ChatEventArgs>(Self_ChatFromSimulator);
+ Logger.Log("Inworld sound enabled", Helpers.LogLevel.Info);
+ }
+ else
+ {
+ // Subscribe to events about inworld sounds
+ Instance.Client.Sound.SoundTrigger -= new EventHandler<SoundTriggerEventArgs>(Sound_SoundTrigger);
+ Instance.Client.Sound.AttachedSound -= new EventHandler<AttachedSoundEventArgs>(Sound_AttachedSound);
+ Instance.Client.Sound.PreloadSound -= new EventHandler<PreloadSoundEventArgs>(Sound_PreloadSound);
+ Instance.Client.Objects.ObjectUpdate -= new EventHandler<PrimEventArgs>(Objects_ObjectUpdate);
+ Instance.Client.Objects.KillObject -= new EventHandler<KillObjectEventArgs>(Objects_KillObject);
+ Instance.Client.Network.SimChanged -= new EventHandler<SimChangedEventArgs>(Network_SimChanged);
+ Instance.Client.Self.ChatFromSimulator -= new EventHandler<ChatEventArgs>(Self_ChatFromSimulator);
+ // Stop all running sounds
+ BufferSound.KillAll();
+
+ Logger.Log("Inworld sound disabled", Helpers.LogLevel.Info);
+ }
+ }
+ catch (Exception e)
+ {
+ System.Console.WriteLine("Error on enable/disable: " + e.Message);
+ }
+
+ m_objectEnabled = value;
+ }
+ get { return m_objectEnabled; }
}
+
+ void Self_ChatFromSimulator(object sender, ChatEventArgs e)
+ {
+ if (e.Type == ChatType.StartTyping)
+ {
+ new BufferSound(
+ UUID.Random(),
+ new UUID("5e191c7b-8996-9ced-a177-b2ac32bfea06"),
+ false,
+ true,
+ e.Position,
+ ObjectVolume / 2f);
+ }
+ }
+
+ /// <summary>
+ /// Watch for Teleports to cancel all the old sounds
+ /// </summary>
+ /// <param name="sender"></param>
+ /// <param name="e"></param>
+ void Network_SimChanged(object sender, SimChangedEventArgs e)
+ {
+ BufferSound.KillAll();
+ }
+
+
}
public class MediaException : Exception