OSDN Git Service

Disable volume/position updates until we find why it gets Invalid Handle.
[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:\n    " + e.Message + "\n" + e.StackTrace,
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                 float newFace = my.SimRotation.W;
267
268                 // If we are standing still, nothing to update now, but
269                 // FMOD needs a 'tick' anyway for callbacks, etc.  In looping
270                 // 'game' programs, the loop is the 'tick'.   Since Radegast
271                 // uses events and has no loop, we use this position update
272                 // thread to drive the FMOD tick.  Have to move more than
273                 // 500mm or turn more than 10 desgrees to bother with.
274                 //
275                 if (newPosition.ApproxEquals(lastpos, 0.5f) &&
276                     Math.Abs(newFace - lastface) < 0.2)
277                 {
278                     invoke(new SoundDelegate(delegate
279                     {
280                         FMODExec(system.update());
281                     }));
282                     continue;
283                 }
284
285                 // We have moved or turned.  Remember new position.
286                 lastpos = newPosition;
287                 lastface = newFace;
288
289                 // Convert coordinate spaces.
290                 FMOD.VECTOR listenerpos = FromOMVSpace(newPosition);
291
292                 // Get azimuth from the facing Quaternion.  Note we assume the
293                 // avatar is standing upright.  Avatars in unusual positions
294                 // hear things from unpredictable directions.
295                 // By definition, facing.W = Cos( angle/2 )
296                 // With angle=0 meaning East.
297                 double angle = 2.0 * Math.Acos(newFace);
298
299                 // Construct facing unit vector in FMOD coordinates.
300                 // Z is East, X is South, Y is up.
301                 FMOD.VECTOR forward = new FMOD.VECTOR();
302                 forward.x = (float)Math.Sin(angle); // South
303                 forward.y = 0.0f;
304                 forward.z = (float)Math.Cos(angle); // East
305
306                 Logger.Log(
307                     String.Format(
308                         "Standing at <{0:0.0},{1:0.0},{2:0.0}> facing {3:d}",
309                             listenerpos.x,
310                             listenerpos.y,
311                             listenerpos.z,
312                             (int)(angle * 180.0 / 3.141592)),
313                     Helpers.LogLevel.Debug);
314
315                 // Tell FMOD the new orientation.
316                 invoke( new SoundDelegate( delegate
317                 {
318                     FMODExec( system.set3DListenerAttributes(
319                         0,
320                         ref listenerpos,        // Position
321                         ref ZeroVector,         // Velocity
322                         ref forward,            // Facing direction
323                         ref UpVector ));        // Top of head
324
325                     FMODExec(system.update());
326                 }));
327             }
328         }
329
330         /// <summary>
331         /// Handle request to play a sound, which might (or mioght not) have been preloaded.
332         /// </summary>
333         /// <param name="sender"></param>
334         /// <param name="e"></param>
335         private void Sound_SoundTrigger(object sender, SoundTriggerEventArgs e)
336         {
337             if (e.SoundID == UUID.Zero) return;
338
339             Logger.Log("Trigger sound " + e.SoundID.ToString() +
340                 " in object " + e.ObjectID.ToString(),
341                 Helpers.LogLevel.Debug);
342
343             new BufferSound(
344                 e.ObjectID,
345                 e.SoundID,
346                 false,
347                 true,
348                 e.Position,
349                 e.Gain * ObjectVolume);
350         }
351
352         /// <summary>
353         /// Handle sound attached to an object
354         /// </summary>
355         /// <param name="sender"></param>
356         /// <param name="e"></param>
357         private void Sound_AttachedSound(object sender, AttachedSoundEventArgs e)
358         {
359             // We seem to get a lot of these zero sounds.
360             if (e.SoundID == UUID.Zero) return;
361
362             if ((e.Flags & SoundFlags.Stop) == SoundFlags.Stop)
363             {
364                 BufferSound.Kill(e.SoundID);
365                 return;
366             }
367
368             // This event tells us the Object ID, but not the Prim info directly.
369             // So we look it up in our internal Object memory.
370             Simulator sim = Instance.Client.Network.CurrentSim;
371             Primitive p = sim.ObjectsPrimitives.Find((Primitive p2) => { return p2.ID == e.ObjectID; });
372             if (p==null) return;
373
374             // If this is a child prim, its position is relative to the root.
375             Vector3 fullPosition = p.Position;
376             if (p.ParentID != 0)
377             {
378                 Primitive parentP;
379                 sim.ObjectsPrimitives.TryGetValue(p.ParentID, out parentP);
380                 if (parentP == null) return;
381                 fullPosition += parentP.Position;
382             }
383
384             new BufferSound(
385                 e.ObjectID,
386                 e.SoundID,
387                 (e.Flags & SoundFlags.Loop) == SoundFlags.Loop,
388                 true,
389                 fullPosition,
390                 e.Gain * ObjectVolume);
391         }
392
393         
394         /// <summary>
395         /// Handle request to preload a sound for playing later.
396         /// </summary>
397         /// <param name="sender"></param>
398         /// <param name="e"></param>
399         private void Sound_PreloadSound(object sender, PreloadSoundEventArgs e)
400         {
401             if (e.SoundID == UUID.Zero) return;
402
403             new BufferSound( e.SoundID );
404         }
405
406         private void Objects_ObjectPropertiesUpdated(object sender, ObjectPropertiesUpdatedEventArgs e)
407         {
408             HandleObjectSound(e.Prim, e.Simulator );
409         }
410      
411      /// <summary>
412         /// Handle object updates, looking for sound events
413         /// </summary>
414         /// <param name="sender"></param>
415         /// <param name="e"></param>
416         private void Objects_ObjectUpdate(object sender, PrimEventArgs e)
417         {
418             HandleObjectSound(e.Prim, e.Simulator );
419         }
420
421         /// <summary>
422         /// Handle deletion of a noise-making object
423         /// </summary>
424         /// <param name="sender"></param>
425         /// <param name="e"></param>
426         void Objects_KillObject(object sender, KillObjectEventArgs e)
427         {
428             Primitive p = null;
429             if (!e.Simulator.ObjectsPrimitives.TryGetValue(e.ObjectLocalID, out  p)) return;
430
431             // Objects without sounds are not interesting.
432             if (p.Sound == null) return;
433             if (p.Sound == UUID.Zero) return;
434
435             BufferSound.Kill(p.Sound);
436         }
437
438         /// <summary>
439         /// Common object sound processing for various Update events
440         /// </summary>
441         /// <param name="p"></param>
442         /// <param name="s"></param>
443         private void HandleObjectSound(Primitive p, Simulator s)
444         {
445             // Objects without sounds are not interesting.
446             if (p.Sound == null) return;
447             if (p.Sound == UUID.Zero) return;
448            
449             if ((p.SoundFlags & SoundFlags.Stop) == SoundFlags.Stop)
450             {
451                 BufferSound.Kill(p.ID);
452                 return;
453             }
454
455             // If this is a child prim, its position is relative to the root prim.
456             Vector3 fullPosition = p.Position;
457             if (p.ParentID != 0)
458             {
459                 Primitive parentP;
460                 if (!s.ObjectsPrimitives.TryGetValue(p.ParentID, out parentP)) return;
461                 fullPosition += parentP.Position;
462             }
463   
464             // See if this is an update to  something we already know about.
465             if (allBuffers.ContainsKey(p.ID))
466             {
467                 // Exists already, so modify existing sound.
468                 //TODO posible to change sound on the same object.  Key by Object?
469                 BufferSound snd = allBuffers[p.ID];
470                 snd.Volume = p.SoundGain * ObjectVolume;
471                 snd.Position = fullPosition;
472             }
473             else
474             {
475                 // Does not exist, so create a new one.
476                 new BufferSound(
477                     p.ID,
478                     p.Sound,
479                     (p.SoundFlags & SoundFlags.Loop) == SoundFlags.Loop,
480                     true,
481                     fullPosition, //Instance.State.GlobalPosition(e.Simulator, fullPosition),
482                     p.SoundGain * ObjectVolume);
483             }
484         }
485
486         /// <summary>
487         /// Control the volume of all inworld sounds
488         /// </summary>
489         public float ObjectVolume
490         {
491             set
492             {
493                 AllObjectVolume = value;
494                 BufferSound.AdjustVolumes();
495             }
496             get { return AllObjectVolume; }
497         }
498
499         private bool m_objectEnabled = true;
500         /// <summary>
501         /// Enable and Disable inworld sounds
502         /// </summary>
503         public bool ObjectEnable
504         {
505             set
506             {
507                 try
508                 {
509                     if (value)
510                     {
511                         // Subscribe to events about inworld sounds
512                         Instance.Client.Sound.SoundTrigger += new EventHandler<SoundTriggerEventArgs>(Sound_SoundTrigger);
513                         Instance.Client.Sound.AttachedSound += new EventHandler<AttachedSoundEventArgs>(Sound_AttachedSound);
514                         Instance.Client.Sound.PreloadSound += new EventHandler<PreloadSoundEventArgs>(Sound_PreloadSound);
515                         Instance.Client.Objects.ObjectUpdate += new EventHandler<PrimEventArgs>(Objects_ObjectUpdate);
516                         Instance.Client.Objects.ObjectPropertiesUpdated += new EventHandler<ObjectPropertiesUpdatedEventArgs>(Objects_ObjectPropertiesUpdated);
517                         Instance.Client.Objects.KillObject += new EventHandler<KillObjectEventArgs>(Objects_KillObject);
518                         Instance.Client.Network.SimChanged += new EventHandler<SimChangedEventArgs>(Network_SimChanged);
519                         Logger.Log("Inworld sound enabled", Helpers.LogLevel.Info);
520                     }
521                     else
522                     {
523                         // Subscribe to events about inworld sounds
524                         Instance.Client.Sound.SoundTrigger -= new EventHandler<SoundTriggerEventArgs>(Sound_SoundTrigger);
525                         Instance.Client.Sound.AttachedSound -= new EventHandler<AttachedSoundEventArgs>(Sound_AttachedSound);
526                         Instance.Client.Sound.PreloadSound -= new EventHandler<PreloadSoundEventArgs>(Sound_PreloadSound);
527                         Instance.Client.Objects.ObjectUpdate -= new EventHandler<PrimEventArgs>(Objects_ObjectUpdate);
528                         Instance.Client.Objects.ObjectPropertiesUpdated -= new EventHandler<ObjectPropertiesUpdatedEventArgs>(Objects_ObjectPropertiesUpdated);
529                         Instance.Client.Objects.KillObject -= new EventHandler<KillObjectEventArgs>(Objects_KillObject);
530                         Instance.Client.Network.SimChanged -= new EventHandler<SimChangedEventArgs>(Network_SimChanged);
531
532                         // Stop all running sounds
533                         BufferSound.KillAll();
534
535                         Logger.Log("Inworld sound disabled", Helpers.LogLevel.Info);
536                     }
537                 }
538                 catch (Exception e)
539                 {
540                     System.Console.WriteLine("Error on enable/disable: "+e.Message);
541                 }
542
543                 m_objectEnabled = value;
544             }
545             get { return m_objectEnabled; }
546         }
547
548         /// <summary>
549         /// Watch for Teleports to cancel all the old sounds
550         /// </summary>
551         /// <param name="sender"></param>
552         /// <param name="e"></param>
553         void Network_SimChanged(object sender, SimChangedEventArgs e)
554         {
555             BufferSound.KillAll();
556         }
557
558
559     }
560
561     public class MediaException : Exception
562     {
563         public MediaException(string msg)
564             : base(msg)
565         {
566         }
567     }
568 }