AR Design
UBC EML collab with UBC SALA - visualizing IoT data in AR
UAudioManagerBase.cs
Go to the documentation of this file.
1 // Copyright (c) Microsoft Corporation. All rights reserved.
2 // Licensed under the MIT License. See LICENSE in the project root for license information.
3 
4 using System;
5 using System.Collections;
6 using System.Collections.Generic;
7 using UnityEngine;
8 using Random = UnityEngine.Random;
9 
10 namespace HoloToolkit.Unity
11 {
18  public partial class UAudioManagerBase<TEvent, TBank> : MonoBehaviour where TEvent : AudioEvent, new() where TBank : AudioBank<TEvent>, new()
19  {
20  public TBank[] DefaultBanks = null;
21 
22  [SerializeField]
23  [Obsolete]
24  protected TEvent[] Events = null;
25 
26  protected const float InfiniteLoop = -1;
27  protected List<ActiveEvent> ActiveEvents;
28 
29 #if UNITY_EDITOR
30  // Temp disable this obsolete warning until we remove Events field.
31  // This public editor only field is here for enabling users to export audio events
32  // into banks for the new paradigm.
33 #pragma warning disable 612
34  public TEvent[] EditorEvents { get { return Events; } set { Events = value; } }
35 #pragma warning restore 612
36  public List<ActiveEvent> ProfilerEvents { get { return ActiveEvents; } }
37 #endif
38 
39  protected List<TBank> LoadedBanks;
40 
41  protected void Awake()
42  {
43  ActiveEvents = new List<ActiveEvent>();
44  LoadedBanks = new List<TBank>(DefaultBanks.Length + 5);
45  for (int i = 0; i < DefaultBanks.Length; i++)
46  {
47  LoadBank(DefaultBanks[i]);
48  }
49  }
50 
51  private void Update()
52  {
53  UpdateEmitterVolumes();
54  }
55 
56  protected void OnDestroy()
57  {
58  StopAllEvents();
59  }
60 
61  protected virtual void BanksChanged()
62  {
63  }
64 
65  public bool IsLoaded(TBank bank)
66  {
67  return LoadedBanks.Contains(bank);
68  }
69 
70  public void LoadBank(TBank bank)
71  {
72  if (IsLoaded(bank))
73  {
74  Debug.LogWarningFormat("Attempting to Load {0} bank twice", bank.name);
75  }
76  else
77  {
78  LoadedBanks.Add(bank);
79  BanksChanged();
80  }
81  }
82 
83  public void UnloadBank(TBank bank)
84  {
85  LoadedBanks.Remove(bank);
86  BanksChanged();
87  }
88 
92  public void StopAllEvents()
93  {
94  for (int i = ActiveEvents.Count - 1; i >= 0; i--)
95  {
96  StopEvent(ActiveEvents[i]);
97  }
98  }
99 
104  public void StopAllEvents(float fadeTime)
105  {
106  for (int i = ActiveEvents.Count - 1; i >= 0; i--)
107  {
108  StartCoroutine(StopEventWithFadeCoroutine(ActiveEvents[i], fadeTime));
109  }
110  }
111 
115  public void StopAllEvents(GameObject emitter)
116  {
117  for (int i = ActiveEvents.Count - 1; i >= 0; i--)
118  {
119  if (ActiveEvents[i].AudioEmitter == emitter)
120  {
121  StopEvent(ActiveEvents[i]);
122  }
123  }
124  }
125 
129  public void StopAllEvents(AudioSource emitter)
130  {
131  for (int i = ActiveEvents.Count - 1; i >= 0; i--)
132  {
133  if (ActiveEvents[i].PrimarySource == emitter)
134  {
135  StopEvent(ActiveEvents[i]);
136  }
137  }
138  }
139 
143  private void UpdateEmitterVolumes()
144  {
145  // Move through each active event and change the settings for the AudioSource components to smoothly fade volumes.
146  for (int i = 0; i < ActiveEvents.Count; i++)
147  {
148  ActiveEvent currentEvent = this.ActiveEvents[i];
149 
150  // If we have a secondary source (for crossfades) adjust the volume based on the current fade time for each active event.
151  if (currentEvent.SecondarySource != null && currentEvent.SecondarySource.volume != currentEvent.AltVolDest)
152  {
153  if (Mathf.Abs(currentEvent.AltVolDest - currentEvent.SecondarySource.volume) < Time.deltaTime / currentEvent.CurrentFade)
154  {
155  currentEvent.SecondarySource.volume = currentEvent.AltVolDest;
156  }
157  else
158  {
159  currentEvent.SecondarySource.volume += (currentEvent.AltVolDest - currentEvent.SecondarySource.volume) * Time.deltaTime / currentEvent.CurrentFade;
160  }
161  }
162 
163  // Adjust the volume of the main source based on the current fade time for each active event.
164  if (currentEvent.PrimarySource != null && currentEvent.PrimarySource.volume != currentEvent.VolDest)
165  {
166  if (Mathf.Abs(currentEvent.VolDest - currentEvent.PrimarySource.volume) < Time.deltaTime / currentEvent.CurrentFade)
167  {
168  currentEvent.PrimarySource.volume = currentEvent.VolDest;
169  }
170  else
171  {
172  currentEvent.PrimarySource.volume += (currentEvent.VolDest - currentEvent.PrimarySource.volume) * Time.deltaTime / currentEvent.CurrentFade;
173  }
174  }
175 
176  // If there is no time left in the fade, make sure we are set to the destination volume.
177  if (currentEvent.CurrentFade > 0)
178  {
179  currentEvent.CurrentFade -= Time.deltaTime;
180  }
181  }
182  }
183 
188  protected void PlayContainer(ActiveEvent activeEvent)
189  {
190  if (activeEvent.AudioEvent.Container.Sounds.Length == 0)
191  {
192  Debug.LogErrorFormat(this, "Trying to play container \"{0}\" with no clips.", activeEvent.AudioEvent.Container);
193 
194  // Clean up the ActiveEvent before we discard it, so it will release its AudioSource(s).
195  activeEvent.Dispose();
196  return;
197  }
198 
199  switch (activeEvent.AudioEvent.Container.ContainerType)
200  {
201  case AudioContainerType.Random:
202  StartOneOffEvent(activeEvent);
203  break;
204 
205  case AudioContainerType.Simultaneous:
206  StartOneOffEvent(activeEvent);
207  break;
208 
209  case AudioContainerType.Sequence:
210  StartOneOffEvent(activeEvent);
211  break;
212 
213  case AudioContainerType.ContinuousSequence:
214  PlayContinuousSequenceContainer(activeEvent.AudioEvent.Container, activeEvent.PrimarySource, activeEvent);
215  break;
216 
217  case AudioContainerType.ContinuousRandom:
218  PlayContinuousRandomContainer(activeEvent.AudioEvent.Container, activeEvent.PrimarySource, activeEvent);
219  break;
220 
221  default:
222  Debug.LogErrorFormat(this, "Trying to play container \"{0}\" with an unknown AudioContainerType \"{1}\".", activeEvent.AudioEvent.Container, activeEvent.AudioEvent.Container.ContainerType);
223 
224  // Clean up the ActiveEvent before we discard it, so it will release its AudioSource(s).
225  activeEvent.Dispose();
226  break;
227  }
228  }
229 
233  private void StartOneOffEvent(ActiveEvent activeEvent)
234  {
235  if (activeEvent.AudioEvent.Container.Looping)
236  {
237  StartCoroutine(PlayLoopingOneOffContainerCoroutine(activeEvent));
238  activeEvent.ActiveTime = InfiniteLoop;
239  }
240  else
241  {
242  PlayOneOffContainer(activeEvent);
243  }
244 
245  StartCoroutine(RecordEventInstanceCoroutine(activeEvent));
246  }
247 
251  private float PlayOneOffContainer(ActiveEvent activeEvent)
252  {
253  AudioContainer currentContainer = activeEvent.AudioEvent.Container;
254 
255  // Fading or Looping overrides immediate volume settings.
256  if (activeEvent.AudioEvent.FadeInTime == 0 && !activeEvent.AudioEvent.Container.Looping)
257  {
258  activeEvent.VolDest = activeEvent.PrimarySource.volume;
259  }
260 
261  // Simultaneous sounds.
262  float clipTime = 0;
263 
264  if (currentContainer.ContainerType == AudioContainerType.Simultaneous)
265  {
266  clipTime = PlaySimultaneousClips(currentContainer, activeEvent);
267  }
268  // Sequential and Random sounds.
269  else
270  {
271  clipTime = PlaySingleClip(currentContainer, activeEvent);
272  }
273 
274  activeEvent.ActiveTime = clipTime;
275  return clipTime;
276  }
277 
281  private float PlaySimultaneousClips(AudioContainer currentContainer, ActiveEvent activeEvent)
282  {
283  float tempDelay = 0;
284  float finalActiveTime = 0f;
285 
286  if (currentContainer.Looping)
287  {
288  finalActiveTime = InfiniteLoop;
289  }
290 
291  for (int i = 0; i < currentContainer.Sounds.Length; i++)
292  {
293  tempDelay = PlayClipAndGetTime(currentContainer.Sounds[i], activeEvent.PrimarySource, activeEvent);
294 
295  if (finalActiveTime != InfiniteLoop)
296  {
297  float estimatedActiveTimeNeeded = GetActiveTimeEstimate(currentContainer.Sounds[i], activeEvent, tempDelay);
298 
299  if (estimatedActiveTimeNeeded == InfiniteLoop || estimatedActiveTimeNeeded > finalActiveTime)
300  {
301  finalActiveTime = estimatedActiveTimeNeeded;
302  }
303  }
304  }
305 
306  return finalActiveTime;
307  }
308 
315  private float PlaySingleClip(AudioContainer currentContainer, ActiveEvent activeEvent)
316  {
317  float tempDelay = 0;
318  if (currentContainer.ContainerType == AudioContainerType.Random)
319  {
320  currentContainer.CurrentClip = Random.Range(0, currentContainer.Sounds.Length);
321  }
322  UAudioClip currentClip = currentContainer.Sounds[currentContainer.CurrentClip];
323 
324  // Trigger sound and save the delay (in seconds) to add to the total amount of time the event will be considered active.
325  tempDelay = PlayClipAndGetTime(currentClip, activeEvent.PrimarySource, activeEvent);
326 
327  // Ready the next clip in the series if sequence container.
328  if (currentContainer.ContainerType == AudioContainerType.Sequence)
329  {
330  currentContainer.CurrentClip++;
331  if (currentContainer.CurrentClip >= currentContainer.Sounds.Length)
332  {
333  currentContainer.CurrentClip = 0;
334  }
335  }
336 
337  // Return active time based on Looping or clip time.
338  return GetActiveTimeEstimate(currentClip, activeEvent, tempDelay);
339  }
340 
344  private IEnumerator PlayLoopingOneOffContainerCoroutine(ActiveEvent activeEvent)
345  {
346  while (!activeEvent.CancelEvent)
347  {
348  float tempLoopTime = PlayOneOffContainer(activeEvent);
349  float eventLoopTime = activeEvent.AudioEvent.Container.LoopTime;
350 
351  // Protect against containers Looping every frame by defaulting to the length of the audio clip.
352  if (eventLoopTime != 0)
353  {
354  tempLoopTime = eventLoopTime;
355  }
356 
357  yield return new WaitForSeconds(tempLoopTime);
358  }
359  }
360 
367  private void PlayContinuousRandomContainer(AudioContainer audioContainer, AudioSource emitter, ActiveEvent activeEvent)
368  {
369  audioContainer.CurrentClip = Random.Range(0, audioContainer.Sounds.Length);
370  UAudioClip tempClip = audioContainer.Sounds[audioContainer.CurrentClip];
371 
372  activeEvent.PrimarySource.volume = 0f;
373  activeEvent.VolDest = activeEvent.AudioEvent.VolumeCenter;
374  activeEvent.AltVolDest = 0f;
375  activeEvent.CurrentFade = audioContainer.CrossfadeTime;
376 
377  float waitTime = (tempClip.Sound.length / emitter.pitch) - activeEvent.AudioEvent.Container.CrossfadeTime;
378 
379  // Ignore clip delay since container is continuous.
380  PlayClipAndGetTime(tempClip, emitter, activeEvent);
381  activeEvent.ActiveTime = InfiniteLoop;
382  StartCoroutine(RecordEventInstanceCoroutine(activeEvent));
383  audioContainer.CurrentClip++;
384  if (audioContainer.CurrentClip >= audioContainer.Sounds.Length)
385  {
386  audioContainer.CurrentClip = 0;
387  }
388  StartCoroutine(ContinueRandomContainerCoroutine(audioContainer, activeEvent, waitTime));
389  }
390 
398  private IEnumerator ContinueRandomContainerCoroutine(AudioContainer audioContainer, ActiveEvent activeEvent, float waitTime)
399  {
400  while (!activeEvent.CancelEvent)
401  {
402  yield return new WaitForSeconds(waitTime);
403 
404  audioContainer.CurrentClip = Random.Range(0, audioContainer.Sounds.Length);
405  UAudioClip tempClip = audioContainer.Sounds[audioContainer.CurrentClip];
406 
407  // Play on primary source.
408  if (activeEvent.PlayingAlt)
409  {
410  activeEvent.PrimarySource.volume = 0f;
411  activeEvent.VolDest = activeEvent.AudioEvent.VolumeCenter;
412  activeEvent.AltVolDest = 0f;
413  activeEvent.CurrentFade = audioContainer.CrossfadeTime;
414  waitTime = (tempClip.Sound.length / activeEvent.PrimarySource.pitch) - audioContainer.CrossfadeTime;
415  PlayClipAndGetTime(tempClip, activeEvent.PrimarySource, activeEvent);
416  }
417  // Play on secondary source.
418  else
419  {
420  activeEvent.SecondarySource.volume = 0f;
421  activeEvent.AltVolDest = activeEvent.AudioEvent.VolumeCenter;
422  activeEvent.VolDest = 0f;
423  activeEvent.CurrentFade = audioContainer.CrossfadeTime;
424  waitTime = (tempClip.Sound.length / activeEvent.SecondarySource.pitch) - audioContainer.CrossfadeTime;
425  PlayClipAndGetTime(tempClip, activeEvent.SecondarySource, activeEvent);
426  }
427 
428  activeEvent.PlayingAlt = !activeEvent.PlayingAlt;
429  }
430  }
431 
438  private void PlayContinuousSequenceContainer(AudioContainer audioContainer, AudioSource emitter, ActiveEvent activeEvent)
439  {
440  UAudioClip tempClip = audioContainer.Sounds[audioContainer.CurrentClip];
441 
442  activeEvent.PrimarySource.volume = 0f;
443  activeEvent.VolDest = activeEvent.AudioEvent.VolumeCenter;
444  activeEvent.AltVolDest = 0f;
445  activeEvent.CurrentFade = audioContainer.CrossfadeTime;
446 
447  float waitTime = (tempClip.Sound.length / emitter.pitch) - activeEvent.AudioEvent.Container.CrossfadeTime;
448 
449  // Ignore clip delay since the container is continuous.
450  PlayClipAndGetTime(tempClip, emitter, activeEvent);
451  activeEvent.ActiveTime = InfiniteLoop;
452  StartCoroutine(RecordEventInstanceCoroutine(activeEvent));
453  audioContainer.CurrentClip++;
454 
455  if (audioContainer.CurrentClip >= audioContainer.Sounds.Length)
456  {
457  audioContainer.CurrentClip = 0;
458  }
459 
460  StartCoroutine(ContinueSequenceContainerCoroutine(audioContainer, activeEvent, waitTime));
461  }
462 
470  private IEnumerator ContinueSequenceContainerCoroutine(AudioContainer audioContainer, ActiveEvent activeEvent, float waitTime)
471  {
472  while (!activeEvent.CancelEvent)
473  {
474  yield return new WaitForSeconds(waitTime);
475  UAudioClip tempClip = audioContainer.Sounds[audioContainer.CurrentClip];
476  if (tempClip.Sound == null)
477  {
478  Debug.LogErrorFormat(this, "Sound clip in event \"{0}\" is null!", activeEvent.AudioEvent.Name);
479  waitTime = 0;
480  }
481  else
482  {
483  // Play on primary source.
484  if (activeEvent.PlayingAlt)
485  {
486  activeEvent.PrimarySource.volume = 0f;
487  activeEvent.VolDest = activeEvent.AudioEvent.VolumeCenter;
488  activeEvent.AltVolDest = 0f;
489  activeEvent.CurrentFade = audioContainer.CrossfadeTime;
490  waitTime = (tempClip.Sound.length / activeEvent.PrimarySource.pitch) - audioContainer.CrossfadeTime;
491  PlayClipAndGetTime(tempClip, activeEvent.PrimarySource, activeEvent);
492  }
493  // Play on secondary source.
494  else
495  {
496  activeEvent.SecondarySource.volume = 0f;
497  activeEvent.AltVolDest = activeEvent.AudioEvent.VolumeCenter;
498  activeEvent.VolDest = 0f;
499  activeEvent.CurrentFade = audioContainer.CrossfadeTime;
500  waitTime = (tempClip.Sound.length / activeEvent.SecondarySource.pitch) - audioContainer.CrossfadeTime;
501  PlayClipAndGetTime(tempClip, activeEvent.SecondarySource, activeEvent);
502  }
503  }
504 
505  audioContainer.CurrentClip++;
506 
507  if (audioContainer.CurrentClip >= audioContainer.Sounds.Length)
508  {
509  audioContainer.CurrentClip = 0;
510  }
511 
512  activeEvent.PlayingAlt = !activeEvent.PlayingAlt;
513  }
514  }
515 
523  private float PlayClipAndGetTime(UAudioClip audioClip, AudioSource emitter, ActiveEvent activeEvent)
524  {
525  if (audioClip.DelayCenter == 0)
526  {
527  emitter.PlayClip(audioClip.Sound, audioClip.Looping);
528 
529  if (audioClip.Looping)
530  {
531  return InfiniteLoop;
532  }
533 
534  return 0;
535  }
536  else
537  {
538  float rndDelay = Random.Range(audioClip.DelayCenter - audioClip.DelayRandomization, audioClip.DelayCenter + audioClip.DelayRandomization);
539 
540  StartCoroutine(PlayClipDelayedCoroutine(audioClip, emitter, rndDelay, activeEvent));
541 
542  if (audioClip.Looping)
543  {
544  return InfiniteLoop;
545  }
546 
547  return rndDelay;
548  }
549  }
550 
559  private IEnumerator PlayClipDelayedCoroutine(UAudioClip audioClip, AudioSource emitter, float delay, ActiveEvent activeEvent)
560  {
561  yield return new WaitForSeconds(delay);
562 
563  if (this.ActiveEvents.Contains(activeEvent))
564  {
565  emitter.PlayClip(audioClip.Sound, audioClip.Looping);
566  }
567  }
568 
573  protected void StopEvent(ActiveEvent activeEvent)
574  {
575  if (activeEvent.PrimarySource != null)
576  {
577  activeEvent.PrimarySource.Stop();
578  }
579 
580  if (activeEvent.SecondarySource != null)
581  {
582  activeEvent.SecondarySource.Stop();
583  }
584 
585  activeEvent.CancelEvent = true;
586  RemoveEventInstance(activeEvent);
587  }
588 
595  protected IEnumerator StopEventWithFadeCoroutine(ActiveEvent activeEvent, float fadeTime)
596  {
597  if (activeEvent.IsStoppable)
598  {
599  activeEvent.IsStoppable = false;
600  activeEvent.VolDest = 0f;
601  activeEvent.AltVolDest = 0f;
602  activeEvent.CurrentFade = fadeTime;
603 
604  yield return new WaitForSeconds(fadeTime);
605 
606  if (activeEvent.PrimarySource != null)
607  {
608  activeEvent.PrimarySource.Stop();
609  }
610 
611  if (activeEvent.SecondarySource != null)
612  {
613  activeEvent.SecondarySource.Stop();
614  }
615 
616  activeEvent.CancelEvent = true;
617  RemoveEventInstance(activeEvent);
618  }
619  }
620 
627  private IEnumerator RecordEventInstanceCoroutine(ActiveEvent activeEvent)
628  {
629  // Unity has no callback for an AudioClip ending, so we have to estimate it ahead of time.
630  // Changing the pitch during playback will alter actual playback time.
631  ActiveEvents.Add(activeEvent);
632 
633  // Only return active time if sound is not Looping/continuous.
634  if (activeEvent.ActiveTime > 0)
635  {
636  yield return new WaitForSeconds(activeEvent.ActiveTime);
637 
638  // Mark this event so it no longer counts against the instance limit.
639  activeEvent.IsActiveTimeComplete = true;
640 
641  // Since the ActiveTime estimate may not be enough time to complete the clip (due to pitch changes during playback, or a negative instanceBuffer value, for example)
642  // wait here until it is finished, so that we don't cut off the end.
643  if (activeEvent.IsPlaying)
644  {
645  yield return null;
646  }
647  }
648  // Otherwise, continue at next frame.
649  else
650  {
651  yield return null;
652  }
653 
654  if (activeEvent.ActiveTime != InfiniteLoop)
655  {
656  RemoveEventInstance(activeEvent);
657  }
658  }
659 
664  private void RemoveEventInstance(ActiveEvent activeEvent)
665  {
666  ActiveEvents.Remove(activeEvent);
667 
668  // Send message notifying user that sound is complete
669  if (!string.IsNullOrEmpty(activeEvent.MessageOnAudioEnd))
670  {
671  activeEvent.AudioEmitter.SendMessage(activeEvent.MessageOnAudioEnd);
672  }
673 
674  activeEvent.Dispose();
675  }
676 
682  protected int GetInstances(string eventName)
683  {
684  int tempInstances = 0;
685 
686  for (int i = 0; i < ActiveEvents.Count; i++)
687  {
688  var eventInstance = ActiveEvents[i];
689 
690  if (!eventInstance.IsActiveTimeComplete && eventInstance.AudioEvent.Name == eventName)
691  {
692  tempInstances++;
693  }
694  }
695 
696  return tempInstances;
697  }
698 
706  private static float GetActiveTimeEstimate(UAudioClip audioClip, ActiveEvent activeEvent, float additionalDelay)
707  {
708  if (audioClip.Looping || activeEvent.AudioEvent.Container.Looping || additionalDelay == InfiniteLoop)
709  {
710  return InfiniteLoop;
711  }
712  else
713  {
714  float pitchAdjustedClipLength = activeEvent.PrimarySource.pitch != 0 ? (audioClip.Sound.length / activeEvent.PrimarySource.pitch) : 0;
715 
716  // Restrict non-Looping ActiveTime values to be non-negative.
717  return Mathf.Max(0.0f, pitchAdjustedClipLength + activeEvent.AudioEvent.InstanceTimeBuffer + additionalDelay);
718  }
719  }
720  }
721 }
AudioContainer Container
Contains the sounds associated with this AudioEvent.
Definition: AudioEvent.cs:138
int GetInstances(string eventName)
Return the number of instances matching the name eventName for instance limiting check.
void StopAllEvents(AudioSource emitter)
Stops all events on one AudioSource.
AudioContainerType
The different rules for how audio should be played back.
Definition: AudioEvent.cs:13
void StopAllEvents(GameObject emitter)
Stops all events on a single emitter.
Class which supports IAudioInfluencers being used with audio sources.
Definition: AudioEmitter.cs:19
UAudioManagerBase provides the base functionality for UAudioManager classes.
Encapsulate a single Unity AudioClip with playback settings.
Definition: AudioClip.cs:12
IEnumerator StopEventWithFadeCoroutine(ActiveEvent activeEvent, float fadeTime)
Coroutine for fading out an AudioSource, and stopping the event once fade is complete.
The AudioEvent class is the main component of UAudioManager and contains settings and a container for...
Definition: AudioEvent.cs:54
void StopAllEvents()
Stops all ActiveEvents
void PlayContainer(ActiveEvent activeEvent)
Determine which rules to follow for container playback, and begin the appropriate function...
The audio bank contains a list of audio events that can be loaded into a UAudioManager for playback...
Definition: AudioBank.cs:11
void StopAllEvents(float fadeTime)
Fades out all of the events over fadeTime and stops once completely faded out.
Currently active AudioEvents along with their AudioSource components for instance limiting events ...
Definition: ActiveEvent.cs:12
The AudioContainer class is sound container for an AudioEvent. It also specifies the rules of how to ...
void StopEvent(ActiveEvent activeEvent)
Stop audio sources in an event, and clean up instance references.
UnityEngine.AudioClip Sound
Definition: AudioClip.cs:14
AudioContainerType ContainerType