OSDN Git Service

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