OSDN Git Service

Better logging of sound/listener locations
[radegast/radegast.git] / Radegast / Core / Media / MediaManager.cs
1 // 
2 // Radegast Metaverse Client
3 // Copyright (c) 2009, Radegast Development Team
4 // All rights reserved.
5 // 
6 // Redistribution and use in source and binary forms, with or without
7 // modification, are permitted provided that the following conditions are met:
8 // 
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.
17 // 
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.
28 //
29 // $Id$
30 //
31 using System;
32 using System.Collections.Generic;
33 using System.Text;
34 using FMOD;
35 using System.Threading;
36 using OpenMetaverse;
37 using OpenMetaverse.Assets;
38
39 namespace Radegast.Media
40 {
41     public class MediaManager : MediaObject
42     {
43         /// <summary>
44         /// Indicated wheather spund sytem is ready for use
45         /// </summary>
46         public bool SoundSystemAvailable { get { return soundSystemAvailable; } }
47         private bool soundSystemAvailable = false;
48         private Thread soundThread;
49         private Thread listenerThread;
50         public RadegastInstance Instance;
51
52         private List<MediaObject> sounds = new List<MediaObject>();
53
54         public MediaManager(RadegastInstance instance)
55             : base()
56         {
57             this.Instance = instance;
58             manager = this;
59
60             loadCallback = new FMOD.SOUND_NONBLOCKCALLBACK(DispatchNonBlockCallback);
61             endCallback = new FMOD.CHANNEL_CALLBACK(DispatchEndCallback);
62             allBuffers = new Dictionary<UUID,BufferSound>();
63
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";
68             soundThread.Start();
69
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();
75
76             // Initial inworld-sound setting comes from Config.
77             ObjectEnable = true;
78         }
79
80         /// <summary>
81         /// Thread that processes FMOD calls.
82         /// </summary>
83         private void CommandLoop()
84         {
85             SoundDelegate action = null;
86
87             // Initialze a bunch of static values
88             UpVector.x = 0.0f;
89             UpVector.y = 1.0f;
90             UpVector.z = 0.0f;
91             ZeroVector.x = 0.0f;
92             ZeroVector.y = 0.0f;
93             ZeroVector.z = 0.0f;
94
95             allSounds = new Dictionary<IntPtr,MediaObject>();
96             allChannels = new Dictionary<IntPtr, MediaObject>();
97
98             // Initialize the FMOD sound package
99             InitFMOD();
100             if (!this.soundSystemAvailable) return;
101
102             // Initialize the command queue.
103             queue = new Queue<SoundDelegate>();
104
105             while (true)
106             {
107                 // Wait for something to show up in the queue.
108                 lock (queue)
109                 {
110                     while (queue.Count == 0)
111                     {
112                         Monitor.Wait(queue);
113                     }
114                     action = queue.Dequeue();
115                 }
116
117                 // We have an action, so call it.
118                 try
119                 {
120                     action();
121                     action = null;
122                 }
123                 catch (Exception e)
124                 {
125                     Logger.Log("Error in sound action: " + e.Message,
126                         Helpers.LogLevel.Error);
127                 }
128             }
129         }
130
131         /// <summary>
132         /// Initialize the FMOD sound system.
133         /// </summary>
134         private void InitFMOD()
135         {
136             try
137             {
138                 FMODExec(FMOD.Factory.System_Create(ref system));
139                 uint version = 0;
140                 FMODExec(system.getVersion(ref version));
141
142                 if (version < FMOD.VERSION.number)
143                     throw new MediaException("You are using an old version of FMOD " +
144                         version.ToString("X") +
145                         ".  This program requires " +
146                         FMOD.VERSION.number.ToString("X") + ".");
147
148                 // Assume no special hardware capabilities except 5.1 surround sound.
149                 FMOD.CAPS caps = FMOD.CAPS.NONE;
150                 FMOD.SPEAKERMODE speakermode = FMOD.SPEAKERMODE._5POINT1;
151
152                 // Get the capabilities of the driver.
153                 int minfrequency = 0, maxfrequency = 0;
154                 StringBuilder name = new StringBuilder(128);
155                 FMODExec(system.getDriverCaps(0, ref caps,
156                     ref minfrequency,
157                     ref maxfrequency,
158                     ref speakermode)
159                 );
160
161                 // Set FMOD speaker mode to what the driver supports.
162                 FMODExec(system.setSpeakerMode(speakermode));
163
164                 // Forcing the ALSA sound system on Linux seems to avoid a CPU loop
165                 if (System.Environment.OSVersion.Platform == PlatformID.Unix)
166                     FMODExec(system.setOutput(FMOD.OUTPUTTYPE.ALSA));
167
168                 // The user has the 'Acceleration' slider set to off, which
169                 // is really bad for latency.  At 48khz, the latency between
170                 // issuing an fmod command and hearing it will now be about 213ms.
171                 if ((caps & FMOD.CAPS.HARDWARE_EMULATED) == FMOD.CAPS.HARDWARE_EMULATED)
172                 {
173                     FMODExec(system.setDSPBufferSize(1024, 10));
174                 }
175
176                 // Get driver information so we can check for a wierd one.
177                 FMOD.GUID guid = new FMOD.GUID();
178                 FMODExec(system.getDriverInfo(0, name, 128, ref guid));
179
180                 // Sigmatel sound devices crackle for some reason if the format is pcm 16bit.
181                 // pcm floating point output seems to solve it.
182                 if (name.ToString().IndexOf("SigmaTel") != -1)
183                 {
184                     FMODExec(system.setSoftwareFormat(
185                         48000,
186                         FMOD.SOUND_FORMAT.PCMFLOAT,
187                         0, 0,
188                         FMOD.DSP_RESAMPLER.LINEAR)
189                     );
190                 }
191
192                 // Try to initialize with all those settings, and Max 32 channels.
193                 FMOD.RESULT result = system.init(32, FMOD.INITFLAG.NORMAL, (IntPtr)null);
194                 if (result == FMOD.RESULT.ERR_OUTPUT_CREATEBUFFER)
195                 {
196                     // Can not handle surround sound - back to Stereo.
197                     FMODExec(system.setSpeakerMode(FMOD.SPEAKERMODE.STEREO));
198
199                     // And init again.
200                     FMODExec(system.init(
201                         32,
202                         FMOD.INITFLAG.NORMAL,
203                         (IntPtr)null)
204                     );
205                 }
206
207                 // Set real-world effect scales.
208                 FMODExec(system.set3DSettings(
209                     1.0f,   // Doppler scale
210                     1.0f,   // Distance scale is meters
211                     1.0f)   // Rolloff factor
212                 );
213
214                 soundSystemAvailable = true;
215                 Logger.Log("Initialized FMOD Ex", Helpers.LogLevel.Debug);
216             }
217             catch (Exception ex)
218             {
219                 Logger.Log("Failed to initialize the sound system: ", Helpers.LogLevel.Warning, ex);
220             }
221         }
222
223         public override void Dispose()
224         {
225             lock (sounds)
226             {
227                 for (int i = 0; i < sounds.Count; i++)
228                 {
229                     if (!sounds[i].Disposed)
230                         sounds[i].Dispose();
231                 }
232                 sounds.Clear();
233             }
234
235             sounds = null;
236
237             if (system != null)
238             {
239                 Logger.Log("FMOD interface stopping", Helpers.LogLevel.Info);
240                 system.release();
241                 system = null;
242             }
243
244             base.Dispose();
245         }
246
247         /// <summary>
248         /// Thread to update listener position and generally keep
249         /// FMOD up to date.
250         /// </summary>
251         private void ListenerUpdate()
252         {
253             // Notice changes in position or direction.
254             Vector3 lastpos = new Vector3(0.0f, 0.0f, 0.0f );
255             float lastface = 0.0f;
256
257             while (true)
258             {
259                 // Two updates per second.
260                 Thread.Sleep(500);
261
262                 if (system == null) continue;
263
264                 AgentManager my = Instance.Client.Self;
265                 Vector3 newPosition = new Vector3(my.SimPosition);
266
267                 // If we are standing still, nothing to update now, but
268                 // FMOD needs a 'tick' anyway for callbacks, etc.  In looping
269                 // 'game' programs, the loop is the 'tick'.   Since Radegast
270                 // uses events and has no loop, we use this position update
271                 // thread to drive the FMOD tick.  Have to move more than
272                 // 500mm or turn more than 10 desgrees to bother with.
273                 //
274                 if (newPosition.ApproxEquals(lastpos, 0.5f) &&
275                     Math.Abs(my.Movement.BodyRotation.W - lastface) < 0.2)
276                 {
277                     invoke(new SoundDelegate(delegate
278                     {
279                         FMODExec(system.update());
280                     }));
281                     continue;
282                 }
283
284                 // We have moved or turned.  Remember new position.
285                 lastpos = newPosition;
286                 lastface = my.Movement.BodyRotation.W;
287
288                 // Convert coordinate spaces.
289                 FMOD.VECTOR listenerpos = FromOMVSpace(newPosition);
290
291                 // Get azimuth from the facing Quaternion.  Note we assume the
292                 // avatar is standing upright.  Avatars in unusual positions
293                 // hear things from unpredictable directions.
294                 // By definition, facing.W = Cos( angle/2 )
295                 // With angle=0 meaning East.
296                 double angle = 2.0 * Math.Acos(my.Movement.BodyRotation.W);
297
298                 // Construct facing unit vector in FMOD coordinates.
299                 FMOD.VECTOR forward = new FMOD.VECTOR();
300                 forward.x = (float)Math.Sin(-angle); // South
301                 forward.y = 0.0f;
302                 forward.z = (float)Math.Cos(angle); // East
303
304                 Logger.Log(
305                     String.Format(
306                         "Standing at <{0:0.0},{1:0.0},{2:0.0}> facing {3:d}",
307                             listenerpos.x,
308                             listenerpos.y,
309                             listenerpos.z,
310                             (int)(angle * 180.0 / 3.141592)),
311                     Helpers.LogLevel.Debug);
312
313                 // Tell FMOD the new orientation.
314                 invoke( new SoundDelegate( delegate
315                 {
316                     FMODExec( system.set3DListenerAttributes(
317                         0,
318                         ref listenerpos,        // Position
319                         ref ZeroVector,         // Velocity
320                         ref forward,            // Facing direction
321                         ref UpVector ));        // Top of head
322
323                     FMODExec(system.update());
324                 }));
325             }
326         }
327
328         /// <summary>
329         /// Handle request to play a sound, which might (or mioght not) have been preloaded.
330         /// </summary>
331         /// <param name="sender"></param>
332         /// <param name="e"></param>
333         private void Sound_SoundTrigger(object sender, SoundTriggerEventArgs e)
334         {
335             if (e.SoundID == UUID.Zero) return;
336
337             Logger.Log("Trigger sound " + e.SoundID.ToString() +
338                 " in object " + e.ObjectID.ToString(),
339                 Helpers.LogLevel.Debug);
340
341             new BufferSound(
342                 e.SoundID,
343                 false,
344                 true,
345                 e.Position,
346                 e.Gain * ObjectVolume);
347         }
348
349         /// <summary>
350         /// Handle sound attached to an object
351         /// </summary>
352         /// <param name="sender"></param>
353         /// <param name="e"></param>
354         private void Sound_AttachedSound(object sender, AttachedSoundEventArgs e)
355         {
356             // We seem to get a lot of these zero sounds.
357             if (e.SoundID == UUID.Zero) return;
358
359             if ((e.Flags & SoundFlags.Stop) == SoundFlags.Stop)
360             {
361                 BufferSound.Kill(e.SoundID);
362                 return;
363             }
364
365             // This event tells us the Object ID, but not the Prim info directly.
366             // So we look it up in our internal Object memory.
367             Simulator sim = Instance.Client.Network.CurrentSim;
368             Primitive p = sim.ObjectsPrimitives.Find((Primitive p2) => { return p2.ID == e.ObjectID; });
369             if (p==null) return;
370
371             // If this is a child prim, its position is relative to the root.
372             Vector3 fullPosition = p.Position;
373             if (p.ParentID != 0)
374             {
375                 Primitive parentP;
376                 sim.ObjectsPrimitives.TryGetValue(p.ParentID, out parentP);
377                 if (parentP == null) return;
378                 fullPosition += parentP.Position;
379             }
380
381             new BufferSound(
382                 e.SoundID,
383                 (e.Flags & SoundFlags.Loop) == SoundFlags.Loop,
384                 true,
385                 fullPosition,
386                 e.Gain * ObjectVolume);
387         }
388
389         
390         /// <summary>
391         /// Handle request to preload a sound for playing later.
392         /// </summary>
393         /// <param name="sender"></param>
394         /// <param name="e"></param>
395         private void Sound_PreloadSound(object sender, PreloadSoundEventArgs e)
396         {
397             if (e.SoundID == UUID.Zero) return;
398
399             new BufferSound( e.SoundID );
400         }
401
402         private void Objects_ObjectPropertiesUpdated(object sender, ObjectPropertiesUpdatedEventArgs e)
403         {
404             HandleObjectSound(e.Prim, e.Simulator );
405         }
406      
407      /// <summary>
408         /// Handle object updates, looking for sound events
409         /// </summary>
410         /// <param name="sender"></param>
411         /// <param name="e"></param>
412         private void Objects_ObjectUpdate(object sender, PrimEventArgs e)
413         {
414             HandleObjectSound(e.Prim, e.Simulator );
415         }
416
417         /// <summary>
418         /// Handle deletion of a noise-making object
419         /// </summary>
420         /// <param name="sender"></param>
421         /// <param name="e"></param>
422         void Objects_KillObject(object sender, KillObjectEventArgs e)
423         {
424             Primitive p = null;
425             if (!e.Simulator.ObjectsPrimitives.TryGetValue(e.ObjectLocalID, out  p)) return;
426
427             // Objects without sounds are not interesting.
428             if (p.Sound == null) return;
429             if (p.Sound == UUID.Zero) return;
430
431             BufferSound.Kill(p.Sound);
432         }
433
434         /// <summary>
435         /// Common object sound processing for various Update events
436         /// </summary>
437         /// <param name="p"></param>
438         /// <param name="s"></param>
439         private void HandleObjectSound(Primitive p, Simulator s)
440         {
441             // Objects without sounds are not interesting.
442             if (p.Sound == null) return;
443             if (p.Sound == UUID.Zero) return;
444            
445             if ((p.SoundFlags & SoundFlags.Stop) == SoundFlags.Stop)
446             {
447                 BufferSound.Kill(p.Sound);
448                 return;
449             }
450
451             // If this is a child prim, its position is relative to the root prim.
452             Vector3 fullPosition = p.Position;
453             if (p.ParentID != 0)
454             {
455                 Primitive parentP;
456                 if (!s.ObjectsPrimitives.TryGetValue(p.ParentID, out parentP)) return;
457                 fullPosition += parentP.Position;
458             }
459   
460             // See if this is an update to  something we already know about.
461             if (allBuffers.ContainsKey(p.Sound))
462             {
463                 // Exists already, so modify existing sound.
464                 //TODO posible to change sound on the same object.  Key by Object?
465                 BufferSound snd = allBuffers[p.Sound];
466                 snd.Volume = p.SoundGain * ObjectVolume;
467                 snd.Position = fullPosition;
468             }
469             else
470             {
471                 // Does not exist, so create a new one.
472                 new BufferSound(
473                     p.Sound,
474                     (p.SoundFlags & SoundFlags.Loop) == SoundFlags.Loop,
475                     true,
476                     fullPosition, //Instance.State.GlobalPosition(e.Simulator, fullPosition),
477                     p.SoundGain * ObjectVolume);
478             }
479         }
480
481         /// <summary>
482         /// Control the volume of all inworld sounds
483         /// </summary>
484         public float ObjectVolume
485         {
486             set
487             {
488                 AllObjectVolume = value;
489                 BufferSound.AdjustVolumes();
490             }
491             get { return AllObjectVolume; }
492         }
493
494         private bool m_objectEnabled = true;
495         /// <summary>
496         /// Enable and Disable inworld sounds
497         /// </summary>
498         public bool ObjectEnable
499         {
500             set
501             {
502                 try
503                 {
504                     if (value)
505                     {
506                         // Subscribe to events about inworld sounds
507                         Instance.Client.Sound.SoundTrigger += new EventHandler<SoundTriggerEventArgs>(Sound_SoundTrigger);
508                         Instance.Client.Sound.AttachedSound += new EventHandler<AttachedSoundEventArgs>(Sound_AttachedSound);
509                         Instance.Client.Sound.PreloadSound += new EventHandler<PreloadSoundEventArgs>(Sound_PreloadSound);
510                         Instance.Client.Objects.ObjectUpdate += new EventHandler<PrimEventArgs>(Objects_ObjectUpdate);
511                         Instance.Client.Objects.ObjectPropertiesUpdated += new EventHandler<ObjectPropertiesUpdatedEventArgs>(Objects_ObjectPropertiesUpdated);
512                         Instance.Client.Objects.KillObject += new EventHandler<KillObjectEventArgs>(Objects_KillObject);
513                         Instance.Client.Network.SimChanged += new EventHandler<SimChangedEventArgs>(Network_SimChanged);
514                         Logger.Log("Inworld sound enabled", Helpers.LogLevel.Info);
515                     }
516                     else
517                     {
518                         // Subscribe to events about inworld sounds
519                         Instance.Client.Sound.SoundTrigger -= new EventHandler<SoundTriggerEventArgs>(Sound_SoundTrigger);
520                         Instance.Client.Sound.AttachedSound -= new EventHandler<AttachedSoundEventArgs>(Sound_AttachedSound);
521                         Instance.Client.Sound.PreloadSound -= new EventHandler<PreloadSoundEventArgs>(Sound_PreloadSound);
522                         Instance.Client.Objects.ObjectUpdate -= new EventHandler<PrimEventArgs>(Objects_ObjectUpdate);
523                         Instance.Client.Objects.ObjectPropertiesUpdated -= new EventHandler<ObjectPropertiesUpdatedEventArgs>(Objects_ObjectPropertiesUpdated);
524                         Instance.Client.Objects.KillObject -= new EventHandler<KillObjectEventArgs>(Objects_KillObject);
525                         Instance.Client.Network.SimChanged -= new EventHandler<SimChangedEventArgs>(Network_SimChanged);
526
527                         // Stop all running sounds
528                         BufferSound.KillAll();
529
530                         Logger.Log("Inworld sound disabled", Helpers.LogLevel.Info);
531                     }
532                 }
533                 catch (Exception e)
534                 {
535                     System.Console.WriteLine("Error on enable/disable: "+e.Message);
536                 }
537
538                 m_objectEnabled = value;
539             }
540             get { return m_objectEnabled; }
541         }
542
543         /// <summary>
544         /// Watch for Teleports to cancel all the old sounds
545         /// </summary>
546         /// <param name="sender"></param>
547         /// <param name="e"></param>
548         void Network_SimChanged(object sender, SimChangedEventArgs e)
549         {
550             BufferSound.KillAll();
551         }
552
553
554     }
555
556     public class MediaException : Exception
557     {
558         public MediaException(string msg)
559             : base(msg)
560         {
561         }
562     }
563 }