2 // Radegast Metaverse Client
3 // Copyright (c) 2009, 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;
39 namespace Radegast.Media
41 public class MediaManager : MediaObject
44 /// Indicated wheather spund sytem is ready for use
46 public bool SoundSystemAvailable { get { return soundSystemAvailable; } }
47 private bool soundSystemAvailable = false;
48 private Thread soundThread;
49 private Thread listenerThread;
50 public RadegastInstance Instance;
52 private List<MediaObject> sounds = new List<MediaObject>();
54 public MediaManager(RadegastInstance instance)
57 this.Instance = instance;
60 loadCallback = new FMOD.SOUND_NONBLOCKCALLBACK(DispatchNonBlockCallback);
61 endCallback = new FMOD.CHANNEL_CALLBACK(DispatchEndCallback);
62 allBuffers = new Dictionary<UUID,BufferSound>();
64 // Start the background thread that does all the FMOD calls.
65 soundThread = new Thread(new ThreadStart(CommandLoop));
66 soundThread.IsBackground = true;
67 soundThread.Name = "SoundThread";
70 // Start the background thread that updates listerner position.
71 listenerThread = new Thread(new ThreadStart(ListenerUpdate));
72 listenerThread.IsBackground = true;
73 listenerThread.Name = "ListenerThread";
74 listenerThread.Start();
78 /// Thread that processes FMOD calls.
80 private void CommandLoop()
82 SoundDelegate action = null;
84 // Initialze a bunch of static values
92 allSounds = new Dictionary<IntPtr,MediaObject>();
93 allChannels = new Dictionary<IntPtr, MediaObject>();
95 // Initialize the FMOD sound package
97 if (!this.soundSystemAvailable) return;
99 // Initialize the command queue.
100 queue = new Queue<SoundDelegate>();
104 // Wait for something to show up in the queue.
107 while (queue.Count == 0)
111 action = queue.Dequeue();
114 // We have an action, so call it.
122 Logger.Log("Error in sound action:\n " + e.Message + "\n" + e.StackTrace,
123 Helpers.LogLevel.Error);
129 /// Initialize the FMOD sound system.
131 private void InitFMOD()
135 FMODExec(FMOD.Factory.System_Create(ref system));
137 FMODExec(system.getVersion(ref version));
139 if (version < FMOD.VERSION.number)
140 throw new MediaException("You are using an old version of FMOD " +
141 version.ToString("X") +
142 ". This program requires " +
143 FMOD.VERSION.number.ToString("X") + ".");
145 // Assume no special hardware capabilities except 5.1 surround sound.
146 FMOD.CAPS caps = FMOD.CAPS.NONE;
147 FMOD.SPEAKERMODE speakermode = FMOD.SPEAKERMODE._5POINT1;
149 // Get the capabilities of the driver.
150 int minfrequency = 0, maxfrequency = 0;
151 StringBuilder name = new StringBuilder(128);
152 FMODExec(system.getDriverCaps(0, ref caps,
158 // Set FMOD speaker mode to what the driver supports.
159 FMODExec(system.setSpeakerMode(speakermode));
161 // Forcing the ALSA sound system on Linux seems to avoid a CPU loop
162 if (System.Environment.OSVersion.Platform == PlatformID.Unix)
163 FMODExec(system.setOutput(FMOD.OUTPUTTYPE.ALSA));
165 // The user has the 'Acceleration' slider set to off, which
166 // is really bad for latency. At 48khz, the latency between
167 // issuing an fmod command and hearing it will now be about 213ms.
168 if ((caps & FMOD.CAPS.HARDWARE_EMULATED) == FMOD.CAPS.HARDWARE_EMULATED)
170 FMODExec(system.setDSPBufferSize(1024, 10));
173 // Get driver information so we can check for a wierd one.
174 FMOD.GUID guid = new FMOD.GUID();
175 FMODExec(system.getDriverInfo(0, name, 128, ref guid));
177 // Sigmatel sound devices crackle for some reason if the format is pcm 16bit.
178 // pcm floating point output seems to solve it.
179 if (name.ToString().IndexOf("SigmaTel") != -1)
181 FMODExec(system.setSoftwareFormat(
183 FMOD.SOUND_FORMAT.PCMFLOAT,
185 FMOD.DSP_RESAMPLER.LINEAR)
189 // Try to initialize with all those settings, and Max 32 channels.
190 FMOD.RESULT result = system.init(32, FMOD.INITFLAG.NORMAL, (IntPtr)null);
191 if (result == FMOD.RESULT.ERR_OUTPUT_CREATEBUFFER)
193 // Can not handle surround sound - back to Stereo.
194 FMODExec(system.setSpeakerMode(FMOD.SPEAKERMODE.STEREO));
197 FMODExec(system.init(
199 FMOD.INITFLAG.NORMAL,
204 // Set real-world effect scales.
205 FMODExec(system.set3DSettings(
206 1.0f, // Doppler scale
207 1.0f, // Distance scale is meters
208 1.0f) // Rolloff factor
211 soundSystemAvailable = true;
212 Logger.Log("Initialized FMOD Ex", Helpers.LogLevel.Debug);
216 Logger.Log("Failed to initialize the sound system: ", Helpers.LogLevel.Warning, ex);
220 public override void Dispose()
224 for (int i = 0; i < sounds.Count; i++)
226 if (!sounds[i].Disposed)
236 Logger.Log("FMOD interface stopping", Helpers.LogLevel.Info);
245 /// Thread to update listener position and generally keep
248 private void ListenerUpdate()
250 // Notice changes in position or direction.
251 Vector3 lastpos = new Vector3(0.0f, 0.0f, 0.0f );
252 float lastface = 0.0f;
256 // Two updates per second.
259 if (system == null) continue;
261 AgentManager my = Instance.Client.Self;
262 Vector3 newPosition = new Vector3(my.SimPosition);
263 float newFace = my.SimRotation.W;
265 // If we are standing still, nothing to update now, but
266 // FMOD needs a 'tick' anyway for callbacks, etc. In looping
267 // 'game' programs, the loop is the 'tick'. Since Radegast
268 // uses events and has no loop, we use this position update
269 // thread to drive the FMOD tick. Have to move more than
270 // 500mm or turn more than 10 desgrees to bother with.
272 if (newPosition.ApproxEquals(lastpos, 0.5f) &&
273 Math.Abs(newFace - lastface) < 0.2)
275 invoke(new SoundDelegate(delegate
277 FMODExec(system.update());
282 // We have moved or turned. Remember new position.
283 lastpos = newPosition;
286 // Convert coordinate spaces.
287 FMOD.VECTOR listenerpos = FromOMVSpace(newPosition);
289 // Get azimuth from the facing Quaternion. Note we assume the
290 // avatar is standing upright. Avatars in unusual positions
291 // hear things from unpredictable directions.
292 // By definition, facing.W = Cos( angle/2 )
293 // With angle=0 meaning East.
294 double angle = 2.0 * Math.Acos(newFace);
296 // Construct facing unit vector in FMOD coordinates.
297 // Z is East, X is South, Y is up.
298 FMOD.VECTOR forward = new FMOD.VECTOR();
299 forward.x = (float)Math.Sin(angle); // South
301 forward.z = (float)Math.Cos(angle); // East
305 "Standing at <{0:0.0},{1:0.0},{2:0.0}> facing {3:d}",
309 (int)(angle * 180.0 / 3.141592)),
310 Helpers.LogLevel.Debug);
312 // Tell FMOD the new orientation.
313 invoke( new SoundDelegate( delegate
315 FMODExec( system.set3DListenerAttributes(
317 ref listenerpos, // Position
318 ref ZeroVector, // Velocity
319 ref forward, // Facing direction
320 ref UpVector )); // Top of head
322 FMODExec(system.update());
328 /// Handle request to play a sound, which might (or mioght not) have been preloaded.
330 /// <param name="sender"></param>
331 /// <param name="e"></param>
332 private void Sound_SoundTrigger(object sender, SoundTriggerEventArgs e)
334 if (e.SoundID == UUID.Zero) return;
336 Logger.Log("Trigger sound " + e.SoundID.ToString() +
337 " in object " + e.ObjectID.ToString(),
338 Helpers.LogLevel.Debug);
346 e.Gain * ObjectVolume);
350 /// Handle sound attached to an object
352 /// <param name="sender"></param>
353 /// <param name="e"></param>
354 private void Sound_AttachedSound(object sender, AttachedSoundEventArgs e)
356 // This event tells us the Object ID, but not the Prim info directly.
357 // So we look it up in our internal Object memory.
358 Simulator sim = e.Simulator;
359 Primitive p = sim.ObjectsPrimitives.Find((Primitive p2) => { return p2.ID == e.ObjectID; });
360 if (p == null) return;
362 // Only one attached sound per prim, so we kill any previous
363 BufferSound.Kill(p.ID);
365 // If this is stop sound, we're done since we've already killed sound for this object
366 if ((e.Flags & SoundFlags.Stop) == SoundFlags.Stop)
371 // We seem to get a lot of these zero sounds.
372 if (e.SoundID == UUID.Zero) return;
374 // If this is a child prim, its position is relative to the root.
375 Vector3 fullPosition = p.Position;
377 while (p != null && p.ParentID != 0)
380 if (sim.ObjectsAvatars.TryGetValue(p.ParentID, out av))
383 fullPosition += p.Position;
387 if (sim.ObjectsPrimitives.TryGetValue(p.ParentID, out p))
389 fullPosition += p.Position;
394 // Didn't find root prim
395 if (p == null) return;
400 (e.Flags & SoundFlags.Loop) == SoundFlags.Loop,
403 e.Gain * ObjectVolume);
408 /// Handle request to preload a sound for playing later.
410 /// <param name="sender"></param>
411 /// <param name="e"></param>
412 private void Sound_PreloadSound(object sender, PreloadSoundEventArgs e)
414 if (e.SoundID == UUID.Zero) return;
416 new BufferSound( e.SoundID );
419 private void Objects_ObjectPropertiesUpdated(object sender, ObjectPropertiesUpdatedEventArgs e)
421 HandleObjectSound(e.Prim, e.Simulator );
425 /// Handle object updates, looking for sound events
427 /// <param name="sender"></param>
428 /// <param name="e"></param>
429 private void Objects_ObjectUpdate(object sender, PrimEventArgs e)
431 HandleObjectSound(e.Prim, e.Simulator );
435 /// Handle deletion of a noise-making object
437 /// <param name="sender"></param>
438 /// <param name="e"></param>
439 void Objects_KillObject(object sender, KillObjectEventArgs e)
442 if (!e.Simulator.ObjectsPrimitives.TryGetValue(e.ObjectLocalID, out p)) return;
444 // Objects without sounds are not interesting.
445 if (p.Sound == null) return;
446 if (p.Sound == UUID.Zero) return;
448 BufferSound.Kill(p.ID);
452 /// Common object sound processing for various Update events
454 /// <param name="p"></param>
455 /// <param name="s"></param>
456 private void HandleObjectSound(Primitive p, Simulator s)
458 // Objects without sounds are not interesting.
459 if (p.Sound == null) return;
460 if (p.Sound == UUID.Zero) return;
462 if ((p.SoundFlags & SoundFlags.Stop) == SoundFlags.Stop)
464 BufferSound.Kill(p.ID);
468 // If this is a child prim, its position is relative to the root prim.
469 Vector3 fullPosition = p.Position;
473 if (!s.ObjectsPrimitives.TryGetValue(p.ParentID, out parentP)) return;
474 fullPosition += parentP.Position;
477 // See if this is an update to something we already know about.
478 if (allBuffers.ContainsKey(p.ID))
480 // Exists already, so modify existing sound.
481 BufferSound snd = allBuffers[p.ID];
482 /* TODO These cause INVALID_HANDLE errors. Removed until we find why
483 snd.Volume = p.SoundGain * ObjectVolume;
484 snd.Position = fullPosition;
489 // Does not exist, so create a new one.
493 (p.SoundFlags & SoundFlags.Loop) == SoundFlags.Loop,
495 fullPosition, //Instance.State.GlobalPosition(e.Simulator, fullPosition),
496 p.SoundGain * ObjectVolume);
501 /// Control the volume of all inworld sounds
503 public float ObjectVolume
507 AllObjectVolume = value;
508 BufferSound.AdjustVolumes();
510 get { return AllObjectVolume; }
513 private bool m_objectEnabled = true;
515 /// Enable and Disable inworld sounds
517 public bool ObjectEnable
525 // Subscribe to events about inworld sounds
526 Instance.Client.Sound.SoundTrigger += new EventHandler<SoundTriggerEventArgs>(Sound_SoundTrigger);
527 Instance.Client.Sound.AttachedSound += new EventHandler<AttachedSoundEventArgs>(Sound_AttachedSound);
528 Instance.Client.Sound.PreloadSound += new EventHandler<PreloadSoundEventArgs>(Sound_PreloadSound);
529 Instance.Client.Objects.ObjectUpdate += new EventHandler<PrimEventArgs>(Objects_ObjectUpdate);
530 Instance.Client.Objects.ObjectPropertiesUpdated += new EventHandler<ObjectPropertiesUpdatedEventArgs>(Objects_ObjectPropertiesUpdated);
531 Instance.Client.Objects.KillObject += new EventHandler<KillObjectEventArgs>(Objects_KillObject);
532 Instance.Client.Network.SimChanged += new EventHandler<SimChangedEventArgs>(Network_SimChanged);
533 Logger.Log("Inworld sound enabled", Helpers.LogLevel.Info);
537 // Subscribe to events about inworld sounds
538 Instance.Client.Sound.SoundTrigger -= new EventHandler<SoundTriggerEventArgs>(Sound_SoundTrigger);
539 Instance.Client.Sound.AttachedSound -= new EventHandler<AttachedSoundEventArgs>(Sound_AttachedSound);
540 Instance.Client.Sound.PreloadSound -= new EventHandler<PreloadSoundEventArgs>(Sound_PreloadSound);
541 Instance.Client.Objects.ObjectUpdate -= new EventHandler<PrimEventArgs>(Objects_ObjectUpdate);
542 Instance.Client.Objects.ObjectPropertiesUpdated -= new EventHandler<ObjectPropertiesUpdatedEventArgs>(Objects_ObjectPropertiesUpdated);
543 Instance.Client.Objects.KillObject -= new EventHandler<KillObjectEventArgs>(Objects_KillObject);
544 Instance.Client.Network.SimChanged -= new EventHandler<SimChangedEventArgs>(Network_SimChanged);
546 // Stop all running sounds
547 BufferSound.KillAll();
549 Logger.Log("Inworld sound disabled", Helpers.LogLevel.Info);
554 System.Console.WriteLine("Error on enable/disable: "+e.Message);
557 m_objectEnabled = value;
559 get { return m_objectEnabled; }
563 /// Watch for Teleports to cancel all the old sounds
565 /// <param name="sender"></param>
566 /// <param name="e"></param>
567 void Network_SimChanged(object sender, SimChangedEventArgs e)
569 BufferSound.KillAll();
575 public class MediaException : Exception
577 public MediaException(string msg)