2 // Radegast Metaverse Client
3 // Copyright (c) 2009-2013, Radegast Development Team
4 // All rights reserved.
6 // Redistribution and use in source and binary forms, with or without
7 // modification, are permitted provided that the following conditions are met:
9 // * Redistributions of source code must retain the above copyright notice,
10 // this list of conditions and the following disclaimer.
11 // * Redistributions in binary form must reproduce the above copyright
12 // notice, this list of conditions and the following disclaimer in the
13 // documentation and/or other materials provided with the distribution.
14 // * Neither the name of the application "Radegast", nor the names of its
15 // contributors may be used to endorse or promote products derived from
16 // this software without specific prior written permission.
18 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 using System.Collections.Generic;
35 using System.Threading;
37 using OpenMetaverse.Assets;
38 #if (COGBOT_LIBOMV || USE_STHREADS)
40 using Thread = ThreadPoolUtil.Thread;
41 using ThreadPool = ThreadPoolUtil.ThreadPool;
42 using Monitor = ThreadPoolUtil.Monitor;
45 namespace Radegast.Media
47 public class MediaManager : MediaObject
50 /// Indicated wheather spund sytem is ready for use
52 public bool SoundSystemAvailable { get { return soundSystemAvailable; } }
53 private bool soundSystemAvailable = false;
54 private Thread soundThread;
55 private Thread listenerThread;
56 public RadegastInstance Instance;
58 private List<MediaObject> sounds = new List<MediaObject>();
59 ManualResetEvent initDone = new ManualResetEvent(false);
61 public MediaManager(RadegastInstance instance)
64 this.Instance = instance;
67 if (MainProgram.CommandLine.DisableSound)
69 soundSystemAvailable = false;
73 endCallback = new FMOD.CHANNEL_CALLBACK(DispatchEndCallback);
74 allBuffers = new Dictionary<UUID, BufferSound>();
76 // Start the background thread that does all the FMOD calls.
77 soundThread = new Thread(new ThreadStart(CommandLoop));
78 soundThread.IsBackground = true;
79 soundThread.Name = "SoundThread";
82 // Start the background thread that updates listerner position.
83 listenerThread = new Thread(new ThreadStart(ListenerUpdate));
84 listenerThread.IsBackground = true;
85 listenerThread.Name = "ListenerThread";
86 listenerThread.Start();
88 Instance.ClientChanged += new EventHandler<ClientChangedEventArgs>(Instance_ClientChanged);
90 // Wait for init to complete
95 void Instance_ClientChanged(object sender, ClientChangedEventArgs e)
97 UnregisterClientEvents(e.OldClient);
99 RegisterClientEvents(e.Client);
102 void RegisterClientEvents(GridClient client)
104 client.Sound.SoundTrigger += new EventHandler<SoundTriggerEventArgs>(Sound_SoundTrigger);
105 client.Sound.AttachedSound += new EventHandler<AttachedSoundEventArgs>(Sound_AttachedSound);
106 client.Sound.PreloadSound += new EventHandler<PreloadSoundEventArgs>(Sound_PreloadSound);
107 client.Objects.ObjectUpdate += new EventHandler<PrimEventArgs>(Objects_ObjectUpdate);
108 client.Objects.KillObject += new EventHandler<KillObjectEventArgs>(Objects_KillObject);
109 client.Network.SimChanged += new EventHandler<SimChangedEventArgs>(Network_SimChanged);
110 client.Self.ChatFromSimulator += new EventHandler<ChatEventArgs>(Self_ChatFromSimulator);
113 void UnregisterClientEvents(GridClient client)
115 client.Sound.SoundTrigger -= new EventHandler<SoundTriggerEventArgs>(Sound_SoundTrigger);
116 client.Sound.AttachedSound -= new EventHandler<AttachedSoundEventArgs>(Sound_AttachedSound);
117 client.Sound.PreloadSound -= new EventHandler<PreloadSoundEventArgs>(Sound_PreloadSound);
118 client.Objects.ObjectUpdate -= new EventHandler<PrimEventArgs>(Objects_ObjectUpdate);
119 client.Objects.KillObject -= new EventHandler<KillObjectEventArgs>(Objects_KillObject);
120 client.Network.SimChanged -= new EventHandler<SimChangedEventArgs>(Network_SimChanged);
121 client.Self.ChatFromSimulator -= new EventHandler<ChatEventArgs>(Self_ChatFromSimulator);
125 /// Thread that processes FMOD calls.
127 private void CommandLoop()
129 SoundDelegate action = null;
131 // Initialze a bunch of static values
139 allSounds = new Dictionary<IntPtr, MediaObject>();
140 allChannels = new Dictionary<IntPtr, MediaObject>();
142 // Initialize the command queue.
143 queue = new Queue<SoundDelegate>();
145 // Initialize the FMOD sound package
148 if (!this.soundSystemAvailable) return;
152 // Wait for something to show up in the queue.
155 while (queue.Count == 0)
159 action = queue.Dequeue();
162 // We have an action, so call it.
170 Logger.Log("Error in sound action:\n " + e.Message + "\n" + e.StackTrace,
171 Helpers.LogLevel.Error);
177 /// Initialize the FMOD sound system.
179 private void InitFMOD()
183 FMODExec(FMOD.Factory.System_Create(ref system));
185 FMODExec(system.getVersion(ref version));
187 if (version < FMOD.VERSION.number)
188 throw new MediaException("You are using an old version of FMOD " +
189 version.ToString("X") +
190 ". This program requires " +
191 FMOD.VERSION.number.ToString("X") + ".");
193 // Assume no special hardware capabilities except 5.1 surround sound.
194 FMOD.CAPS caps = FMOD.CAPS.NONE;
195 FMOD.SPEAKERMODE speakermode = FMOD.SPEAKERMODE._5POINT1;
197 // Try to detect soud system used
198 if (System.Environment.OSVersion.Platform == PlatformID.Unix || System.Environment.OSVersion.Platform == PlatformID.MacOSX)
200 bool audioOK = false;
201 var res = system.setOutput(FMOD.OUTPUTTYPE.COREAUDIO);
202 if (res == RESULT.OK)
209 res = system.setOutput(FMOD.OUTPUTTYPE.PULSEAUDIO);
210 if (res == RESULT.OK)
218 res = system.setOutput(FMOD.OUTPUTTYPE.ALSA);
219 if (res == RESULT.OK)
227 res = system.setOutput(FMOD.OUTPUTTYPE.OSS);
228 if (res == RESULT.OK)
236 res = system.setOutput(FMOD.OUTPUTTYPE.AUTODETECT);
237 if (res == RESULT.OK)
245 FMOD.OUTPUTTYPE outputType = OUTPUTTYPE.UNKNOWN;
246 FMODExec(system.getOutput(ref outputType));
248 // Fancy param checking on Linux can cause init to fail
251 // Get the capabilities of the driver.
253 FMODExec(system.getDriverCaps(0, ref caps,
256 // Set FMOD speaker mode to what the driver supports.
257 FMODExec(system.setSpeakerMode(speakermode));
261 // The user has the 'Acceleration' slider set to off, which
262 // is really bad for latency. At 48khz, the latency between
263 // issuing an fmod command and hearing it will now be about 213ms.
264 if ((caps & FMOD.CAPS.HARDWARE_EMULATED) == FMOD.CAPS.HARDWARE_EMULATED)
266 FMODExec(system.setDSPBufferSize(1024, 10));
271 StringBuilder name = new StringBuilder(128);
272 // Get driver information so we can check for a wierd one.
273 FMOD.GUID guid = new FMOD.GUID();
274 FMODExec(system.getDriverInfo(0, name, 128, ref guid));
276 // Sigmatel sound devices crackle for some reason if the format is pcm 16bit.
277 // pcm floating point output seems to solve it.
278 if (name.ToString().IndexOf("SigmaTel") != -1)
280 FMODExec(system.setSoftwareFormat(
282 FMOD.SOUND_FORMAT.PCMFLOAT,
284 FMOD.DSP_RESAMPLER.LINEAR)
290 // Try to initialize with all those settings, and Max 32 channels.
291 FMOD.RESULT result = system.init(32, FMOD.INITFLAGS.NORMAL, (IntPtr)null);
292 if (result == FMOD.RESULT.ERR_OUTPUT_CREATEBUFFER)
294 // Can not handle surround sound - back to Stereo.
295 FMODExec(system.setSpeakerMode(FMOD.SPEAKERMODE.STEREO));
298 FMODExec(system.init(
300 FMOD.INITFLAGS.NORMAL,
304 else if (result != FMOD.RESULT.OK)
306 throw(new Exception(result.ToString()));
309 // Set real-world effect scales.
310 FMODExec(system.set3DSettings(
311 1.0f, // Doppler scale
312 1.0f, // Distance scale is meters
313 1.0f) // Rolloff factor
316 soundSystemAvailable = true;
317 Logger.Log("Initialized FMOD Ex: " + outputType.ToString(), Helpers.LogLevel.Debug);
321 Logger.Log("Failed to initialize the sound system: " + ex.ToString(), Helpers.LogLevel.Warning);
325 public override void Dispose()
327 if (Instance.Client != null)
328 UnregisterClientEvents(Instance.Client);
332 for (int i = 0; i < sounds.Count; i++)
334 if (!sounds[i].Disposed)
344 Logger.Log("FMOD interface stopping", Helpers.LogLevel.Info);
349 if (listenerThread != null)
351 if (listenerThread.IsAlive)
352 listenerThread.Abort();
353 listenerThread = null;
356 if (soundThread != null)
358 if (soundThread.IsAlive)
367 /// Thread to update listener position and generally keep
370 private void ListenerUpdate()
372 // Notice changes in position or direction.
373 Vector3 lastpos = new Vector3(0.0f, 0.0f, 0.0f);
374 float lastface = 0.0f;
378 // Two updates per second.
381 if (system == null) continue;
383 AgentManager my = Instance.Client.Self;
384 Vector3 newPosition = new Vector3(my.SimPosition);
385 float newFace = my.SimRotation.W;
387 // If we are standing still, nothing to update now, but
388 // FMOD needs a 'tick' anyway for callbacks, etc. In looping
389 // 'game' programs, the loop is the 'tick'. Since Radegast
390 // uses events and has no loop, we use this position update
391 // thread to drive the FMOD tick. Have to move more than
392 // 500mm or turn more than 10 desgrees to bother with.
394 if (newPosition.ApproxEquals(lastpos, 0.5f) &&
395 Math.Abs(newFace - lastface) < 0.2)
397 invoke(new SoundDelegate(delegate
399 FMODExec(system.update());
404 // We have moved or turned. Remember new position.
405 lastpos = newPosition;
408 // Convert coordinate spaces.
409 FMOD.VECTOR listenerpos = FromOMVSpace(newPosition);
411 // Get azimuth from the facing Quaternion. Note we assume the
412 // avatar is standing upright. Avatars in unusual positions
413 // hear things from unpredictable directions.
414 // By definition, facing.W = Cos( angle/2 )
415 // With angle=0 meaning East.
416 double angle = 2.0 * Math.Acos(newFace);
418 // Construct facing unit vector in FMOD coordinates.
419 // Z is East, X is South, Y is up.
420 FMOD.VECTOR forward = new FMOD.VECTOR();
421 forward.x = (float)Math.Sin(angle); // South
423 forward.z = (float)Math.Cos(angle); // East
427 // "Standing at <{0:0.0},{1:0.0},{2:0.0}> facing {3:d}",
431 // (int)(angle * 180.0 / 3.141592)),
432 // Helpers.LogLevel.Debug);
434 // Tell FMOD the new orientation.
435 invoke(new SoundDelegate(delegate
437 FMODExec(system.set3DListenerAttributes(
439 ref listenerpos, // Position
440 ref ZeroVector, // Velocity
441 ref forward, // Facing direction
442 ref UpVector)); // Top of head
444 FMODExec(system.update());
450 /// Handle request to play a sound, which might (or mioght not) have been preloaded.
452 /// <param name="sender"></param>
453 /// <param name="e"></param>
454 private void Sound_SoundTrigger(object sender, SoundTriggerEventArgs e)
456 if (e.SoundID == UUID.Zero) return;
458 Logger.Log("Trigger sound " + e.SoundID.ToString() +
459 " in object " + e.ObjectID.ToString(),
460 Helpers.LogLevel.Debug);
468 e.Gain * ObjectVolume);
472 /// Handle sound attached to an object
474 /// <param name="sender"></param>
475 /// <param name="e"></param>
476 private void Sound_AttachedSound(object sender, AttachedSoundEventArgs e)
478 // This event tells us the Object ID, but not the Prim info directly.
479 // So we look it up in our internal Object memory.
480 Simulator sim = e.Simulator;
481 Primitive p = sim.ObjectsPrimitives.Find((Primitive p2) => { return p2.ID == e.ObjectID; });
482 if (p == null) return;
484 // Only one attached sound per prim, so we kill any previous
485 BufferSound.Kill(p.ID);
487 // If this is stop sound, we're done since we've already killed sound for this object
488 if ((e.Flags & SoundFlags.Stop) == SoundFlags.Stop)
493 // We seem to get a lot of these zero sounds.
494 if (e.SoundID == UUID.Zero) return;
496 // If this is a child prim, its position is relative to the root.
497 Vector3 fullPosition = p.Position;
499 while (p != null && p.ParentID != 0)
502 if (sim.ObjectsAvatars.TryGetValue(p.ParentID, out av))
505 fullPosition += p.Position;
509 if (sim.ObjectsPrimitives.TryGetValue(p.ParentID, out p))
511 fullPosition += p.Position;
516 // Didn't find root prim
517 if (p == null) return;
522 (e.Flags & SoundFlags.Loop) == SoundFlags.Loop,
525 e.Gain * ObjectVolume);
530 /// Handle request to preload a sound for playing later.
532 /// <param name="sender"></param>
533 /// <param name="e"></param>
534 private void Sound_PreloadSound(object sender, PreloadSoundEventArgs e)
536 if (e.SoundID == UUID.Zero) return;
538 if (!Instance.Client.Assets.Cache.HasAsset(e.SoundID))
539 new BufferSound(e.SoundID);
543 /// Handle object updates, looking for sound events
545 /// <param name="sender"></param>
546 /// <param name="e"></param>
547 private void Objects_ObjectUpdate(object sender, PrimEventArgs e)
549 HandleObjectSound(e.Prim, e.Simulator);
553 /// Handle deletion of a noise-making object
555 /// <param name="sender"></param>
556 /// <param name="e"></param>
557 void Objects_KillObject(object sender, KillObjectEventArgs e)
560 if (!e.Simulator.ObjectsPrimitives.TryGetValue(e.ObjectLocalID, out p)) return;
562 // Objects without sounds are not interesting.
563 if (p.Sound == UUID.Zero) return;
565 BufferSound.Kill(p.ID);
569 /// Common object sound processing for various Update events
571 /// <param name="p"></param>
572 /// <param name="s"></param>
573 private void HandleObjectSound(Primitive p, Simulator s)
575 // Objects without sounds are not interesting.
576 if (p.Sound == UUID.Zero) return;
578 if ((p.SoundFlags & SoundFlags.Stop) == SoundFlags.Stop)
580 BufferSound.Kill(p.ID);
584 // If this is a child prim, its position is relative to the root prim.
585 Vector3 fullPosition = p.Position;
589 if (!s.ObjectsPrimitives.TryGetValue(p.ParentID, out parentP)) return;
590 fullPosition += parentP.Position;
593 // See if this is an update to something we already know about.
594 if (allBuffers.ContainsKey(p.ID))
596 // Exists already, so modify existing sound.
597 BufferSound snd = allBuffers[p.ID];
598 snd.Volume = p.SoundGain * ObjectVolume;
599 snd.Position = fullPosition;
603 // Does not exist, so create a new one.
607 (p.SoundFlags & SoundFlags.Loop) == SoundFlags.Loop,
609 fullPosition, //Instance.State.GlobalPosition(e.Simulator, fullPosition),
610 p.SoundGain * ObjectVolume);
615 /// Control the volume of all inworld sounds
617 public float ObjectVolume
621 AllObjectVolume = value;
622 BufferSound.AdjustVolumes();
624 get { return AllObjectVolume; }
630 public float UIVolume = 0.5f;
632 private bool m_objectEnabled = true;
634 /// Enable and Disable inworld sounds
636 public bool ObjectEnable
642 // Subscribe to events about inworld sounds
643 RegisterClientEvents(Instance.Client);
644 Logger.Log("Inworld sound enabled", Helpers.LogLevel.Info);
648 // Subscribe to events about inworld sounds
649 UnregisterClientEvents(Instance.Client);
650 // Stop all running sounds
651 BufferSound.KillAll();
652 Logger.Log("Inworld sound disabled", Helpers.LogLevel.Info);
655 m_objectEnabled = value;
657 get { return m_objectEnabled; }
660 void Self_ChatFromSimulator(object sender, ChatEventArgs e)
662 if (e.Type == ChatType.StartTyping)
675 /// Watch for Teleports to cancel all the old sounds
677 /// <param name="sender"></param>
678 /// <param name="e"></param>
679 void Network_SimChanged(object sender, SimChangedEventArgs e)
681 BufferSound.KillAll();
687 /// <param name="sound">UUID of the sound to play</param>
688 public void PlayUISound(UUID sound)
690 if (!soundSystemAvailable) return;
697 Instance.Client.Self.SimPosition,
704 public class MediaException : Exception
706 public MediaException(string msg)