OSDN Git Service

Attached sound finds correct root prim. It's complicated
[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
77         /// <summary>
78         /// Thread that processes FMOD calls.
79         /// </summary>
80         private void CommandLoop()
81         {
82             SoundDelegate action = null;
83
84             // Initialze a bunch of static values
85             UpVector.x = 0.0f;
86             UpVector.y = 1.0f;
87             UpVector.z = 0.0f;
88             ZeroVector.x = 0.0f;
89             ZeroVector.y = 0.0f;
90             ZeroVector.z = 0.0f;
91
92             allSounds = new Dictionary<IntPtr,MediaObject>();
93             allChannels = new Dictionary<IntPtr, MediaObject>();
94
95             // Initialize the FMOD sound package
96             InitFMOD();
97             if (!this.soundSystemAvailable) return;
98
99             // Initialize the command queue.
100             queue = new Queue<SoundDelegate>();
101
102             while (true)
103             {
104                 // Wait for something to show up in the queue.
105                 lock (queue)
106                 {
107                     while (queue.Count == 0)
108                     {
109                         Monitor.Wait(queue);
110                     }
111                     action = queue.Dequeue();
112                 }
113
114                 // We have an action, so call it.
115                 try
116                 {
117                     action();
118                     action = null;
119                 }
120                 catch (Exception e)
121                 {
122                     Logger.Log("Error in sound action:\n    " + e.Message + "\n" + e.StackTrace,
123                         Helpers.LogLevel.Error);
124                 }
125             }
126         }
127
128         /// <summary>
129         /// Initialize the FMOD sound system.
130         /// </summary>
131         private void InitFMOD()
132         {
133             try
134             {
135                 FMODExec(FMOD.Factory.System_Create(ref system));
136                 uint version = 0;
137                 FMODExec(system.getVersion(ref version));
138
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") + ".");
144
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;
148
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,
153                     ref minfrequency,
154                     ref maxfrequency,
155                     ref speakermode)
156                 );
157
158                 // Set FMOD speaker mode to what the driver supports.
159                 FMODExec(system.setSpeakerMode(speakermode));
160
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));
164
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)
169                 {
170                     FMODExec(system.setDSPBufferSize(1024, 10));
171                 }
172
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));
176
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)
180                 {
181                     FMODExec(system.setSoftwareFormat(
182                         48000,
183                         FMOD.SOUND_FORMAT.PCMFLOAT,
184                         0, 0,
185                         FMOD.DSP_RESAMPLER.LINEAR)
186                     );
187                 }
188
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)
192                 {
193                     // Can not handle surround sound - back to Stereo.
194                     FMODExec(system.setSpeakerMode(FMOD.SPEAKERMODE.STEREO));
195
196                     // And init again.
197                     FMODExec(system.init(
198                         32,
199                         FMOD.INITFLAG.NORMAL,
200                         (IntPtr)null)
201                     );
202                 }
203
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
209                 );
210
211                 soundSystemAvailable = true;
212                 Logger.Log("Initialized FMOD Ex", Helpers.LogLevel.Debug);
213             }
214             catch (Exception ex)
215             {
216                 Logger.Log("Failed to initialize the sound system: ", Helpers.LogLevel.Warning, ex);
217             }
218         }
219
220         public override void Dispose()
221         {
222             lock (sounds)
223             {
224                 for (int i = 0; i < sounds.Count; i++)
225                 {
226                     if (!sounds[i].Disposed)
227                         sounds[i].Dispose();
228                 }
229                 sounds.Clear();
230             }
231
232             sounds = null;
233
234             if (system != null)
235             {
236                 Logger.Log("FMOD interface stopping", Helpers.LogLevel.Info);
237                 system.release();
238                 system = null;
239             }
240
241             base.Dispose();
242         }
243
244         /// <summary>
245         /// Thread to update listener position and generally keep
246         /// FMOD up to date.
247         /// </summary>
248         private void ListenerUpdate()
249         {
250             // Notice changes in position or direction.
251             Vector3 lastpos = new Vector3(0.0f, 0.0f, 0.0f );
252             float lastface = 0.0f;
253
254             while (true)
255             {
256                 // Two updates per second.
257                 Thread.Sleep(500);
258
259                 if (system == null) continue;
260
261                 AgentManager my = Instance.Client.Self;
262                 Vector3 newPosition = new Vector3(my.SimPosition);
263                 float newFace = my.SimRotation.W;
264
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.
271                 //
272                 if (newPosition.ApproxEquals(lastpos, 0.5f) &&
273                     Math.Abs(newFace - lastface) < 0.2)
274                 {
275                     invoke(new SoundDelegate(delegate
276                     {
277                         FMODExec(system.update());
278                     }));
279                     continue;
280                 }
281
282                 // We have moved or turned.  Remember new position.
283                 lastpos = newPosition;
284                 lastface = newFace;
285
286                 // Convert coordinate spaces.
287                 FMOD.VECTOR listenerpos = FromOMVSpace(newPosition);
288
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);
295
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
300                 forward.y = 0.0f;
301                 forward.z = (float)Math.Cos(angle); // East
302
303                 Logger.Log(
304                     String.Format(
305                         "Standing at <{0:0.0},{1:0.0},{2:0.0}> facing {3:d}",
306                             listenerpos.x,
307                             listenerpos.y,
308                             listenerpos.z,
309                             (int)(angle * 180.0 / 3.141592)),
310                     Helpers.LogLevel.Debug);
311
312                 // Tell FMOD the new orientation.
313                 invoke( new SoundDelegate( delegate
314                 {
315                     FMODExec( system.set3DListenerAttributes(
316                         0,
317                         ref listenerpos,        // Position
318                         ref ZeroVector,         // Velocity
319                         ref forward,            // Facing direction
320                         ref UpVector ));        // Top of head
321
322                     FMODExec(system.update());
323                 }));
324             }
325         }
326
327         /// <summary>
328         /// Handle request to play a sound, which might (or mioght not) have been preloaded.
329         /// </summary>
330         /// <param name="sender"></param>
331         /// <param name="e"></param>
332         private void Sound_SoundTrigger(object sender, SoundTriggerEventArgs e)
333         {
334             if (e.SoundID == UUID.Zero) return;
335
336             Logger.Log("Trigger sound " + e.SoundID.ToString() +
337                 " in object " + e.ObjectID.ToString(),
338                 Helpers.LogLevel.Debug);
339
340             new BufferSound(
341                 e.ObjectID,
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             // 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;
361
362             // Only one attached sound per prim, so we kill any previous
363             BufferSound.Kill(p.ID);
364
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)
367             {
368                 return;
369             }
370
371             // We seem to get a lot of these zero sounds.
372             if (e.SoundID == UUID.Zero) return;
373
374             // If this is a child prim, its position is relative to the root.
375             Vector3 fullPosition = p.Position;
376
377             while (p != null && p.ParentID != 0)
378             {
379                 Avatar av;
380                 if (sim.ObjectsAvatars.TryGetValue(p.ParentID, out av))
381                 {
382                     p = av;
383                     fullPosition += p.Position;
384                 }
385                 else
386                 {
387                     if (sim.ObjectsPrimitives.TryGetValue(p.ParentID, out p))
388                     {
389                         fullPosition += p.Position;
390                     }
391                 }
392             }
393
394             // Didn't find root prim
395             if (p == null) return;
396
397             new BufferSound(
398                 e.ObjectID,
399                 e.SoundID,
400                 (e.Flags & SoundFlags.Loop) == SoundFlags.Loop,
401                 true,
402                 fullPosition,
403                 e.Gain * ObjectVolume);
404         }
405
406         
407         /// <summary>
408         /// Handle request to preload a sound for playing later.
409         /// </summary>
410         /// <param name="sender"></param>
411         /// <param name="e"></param>
412         private void Sound_PreloadSound(object sender, PreloadSoundEventArgs e)
413         {
414             if (e.SoundID == UUID.Zero) return;
415
416             new BufferSound( e.SoundID );
417         }
418
419         private void Objects_ObjectPropertiesUpdated(object sender, ObjectPropertiesUpdatedEventArgs e)
420         {
421             HandleObjectSound(e.Prim, e.Simulator );
422         }
423      
424      /// <summary>
425         /// Handle object updates, looking for sound events
426         /// </summary>
427         /// <param name="sender"></param>
428         /// <param name="e"></param>
429         private void Objects_ObjectUpdate(object sender, PrimEventArgs e)
430         {
431             HandleObjectSound(e.Prim, e.Simulator );
432         }
433
434         /// <summary>
435         /// Handle deletion of a noise-making object
436         /// </summary>
437         /// <param name="sender"></param>
438         /// <param name="e"></param>
439         void Objects_KillObject(object sender, KillObjectEventArgs e)
440         {
441             Primitive p = null;
442             if (!e.Simulator.ObjectsPrimitives.TryGetValue(e.ObjectLocalID, out  p)) return;
443
444             // Objects without sounds are not interesting.
445             if (p.Sound == null) return;
446             if (p.Sound == UUID.Zero) return;
447
448             BufferSound.Kill(p.ID);
449         }
450
451         /// <summary>
452         /// Common object sound processing for various Update events
453         /// </summary>
454         /// <param name="p"></param>
455         /// <param name="s"></param>
456         private void HandleObjectSound(Primitive p, Simulator s)
457         {
458             // Objects without sounds are not interesting.
459             if (p.Sound == null) return;
460             if (p.Sound == UUID.Zero) return;
461            
462             if ((p.SoundFlags & SoundFlags.Stop) == SoundFlags.Stop)
463             {
464                 BufferSound.Kill(p.ID);
465                 return;
466             }
467
468             // If this is a child prim, its position is relative to the root prim.
469             Vector3 fullPosition = p.Position;
470             if (p.ParentID != 0)
471             {
472                 Primitive parentP;
473                 if (!s.ObjectsPrimitives.TryGetValue(p.ParentID, out parentP)) return;
474                 fullPosition += parentP.Position;
475             }
476   
477             // See if this is an update to  something we already know about.
478             if (allBuffers.ContainsKey(p.ID))
479             {
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;
485                 */
486             }
487             else
488             {
489                 // Does not exist, so create a new one.
490                 new BufferSound(
491                     p.ID,
492                     p.Sound,
493                     (p.SoundFlags & SoundFlags.Loop) == SoundFlags.Loop,
494                     true,
495                     fullPosition, //Instance.State.GlobalPosition(e.Simulator, fullPosition),
496                     p.SoundGain * ObjectVolume);
497             }
498         }
499
500         /// <summary>
501         /// Control the volume of all inworld sounds
502         /// </summary>
503         public float ObjectVolume
504         {
505             set
506             {
507                 AllObjectVolume = value;
508                 BufferSound.AdjustVolumes();
509             }
510             get { return AllObjectVolume; }
511         }
512
513         private bool m_objectEnabled = true;
514         /// <summary>
515         /// Enable and Disable inworld sounds
516         /// </summary>
517         public bool ObjectEnable
518         {
519             set
520             {
521                 try
522                 {
523                     if (value)
524                     {
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);
534                     }
535                     else
536                     {
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);
545
546                         // Stop all running sounds
547                         BufferSound.KillAll();
548
549                         Logger.Log("Inworld sound disabled", Helpers.LogLevel.Info);
550                     }
551                 }
552                 catch (Exception e)
553                 {
554                     System.Console.WriteLine("Error on enable/disable: "+e.Message);
555                 }
556
557                 m_objectEnabled = value;
558             }
559             get { return m_objectEnabled; }
560         }
561
562         /// <summary>
563         /// Watch for Teleports to cancel all the old sounds
564         /// </summary>
565         /// <param name="sender"></param>
566         /// <param name="e"></param>
567         void Network_SimChanged(object sender, SimChangedEventArgs e)
568         {
569             BufferSound.KillAll();
570         }
571
572
573     }
574
575     public class MediaException : Exception
576     {
577         public MediaException(string msg)
578             : base(msg)
579         {
580         }
581     }
582 }