/// <summary> /// Play one sound from a container based on container behavior. /// </summary> /// <param name="currentContainer"></param> /// <param name="activeEvent"></param> /// <returns>The estimated ActiveTime for the clip, or InfiniteLoop if the container and/or clip are set to loop.</returns> private float PlaySingleClip(AudioContainer currentContainer, ActiveEvent activeEvent) { float tempDelay = 0; if (currentContainer.ContainerType == AudioContainerType.Random) { currentContainer.CurrentClip = Random.Range(0, currentContainer.Sounds.Length); } UAudioClip currentClip = currentContainer.Sounds[currentContainer.CurrentClip]; // Trigger sound and save the delay (in seconds) to add to the total amount of time the event will be considered active. tempDelay = PlayClipAndGetTime(currentClip, activeEvent.PrimarySource, activeEvent); // Ready the next clip in the series if sequence container. if (currentContainer.ContainerType == AudioContainerType.Sequence) { currentContainer.CurrentClip++; if (currentContainer.CurrentClip >= currentContainer.Sounds.Length) { currentContainer.CurrentClip = 0; } } // Return active time based on Looping or clip time. return(GetActiveTimeEstimate(currentClip, activeEvent, tempDelay)); }
/// <summary> /// Keep an event in the "ActiveEvents" list for the amount of time we think it will be playing, plus the instance buffer. /// This is mostly done for instance limiting purposes. /// </summary> /// <param name="activeEvent">The persistent reference to the event as long as it is playing.</param> /// <returns>The coroutine.</returns> private IEnumerator RecordEventInstanceCoroutine(ActiveEvent activeEvent) { // Unity has no callback for an AudioClip ending, so we have to estimate it ahead of time. // Changing the pitch during playback will alter actual playback time. ActiveEvents.Add(activeEvent); // Only return active time if sound is not Looping/continuous. if (activeEvent.ActiveTime > 0) { yield return(new WaitForSeconds(activeEvent.ActiveTime)); // Mark this event so it no longer counts against the instance limit. activeEvent.IsActiveTimeComplete = true; // 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) // wait here until it is finished, so that we don't cut off the end. if (activeEvent.IsPlaying) { yield return(null); } } // Otherwise, continue at next frame. else { yield return(null); } if (activeEvent.ActiveTime != InfiniteLoop) { RemoveEventInstance(activeEvent); } }
/// <summary> /// Play a single clip on an AudioSource; if Looping forever, return InfiniteLoop for the event time. /// </summary> /// <param name="audioClip">The audio clip to play.</param> /// <param name="emitter">The emitter to use.</param> /// <param name="activeEvent">The persistent reference to the event as long as it is playing.</param> /// <returns>The amount of delay, if any, we are waiting before playing the clip. A Looping clip will always return InfiniteLoop.</returns> private float PlayClipAndGetTime(UAudioClip audioClip, AudioSource emitter, ActiveEvent activeEvent) { if (audioClip.DelayCenter == 0) { emitter.PlayClip(audioClip.Sound, audioClip.Looping); if (audioClip.Looping) { return(InfiniteLoop); } return(0); } else { float rndDelay = Random.Range(audioClip.DelayCenter - audioClip.DelayRandomization, audioClip.DelayCenter + audioClip.DelayRandomization); StartCoroutine(PlayClipDelayedCoroutine(audioClip, emitter, rndDelay, activeEvent)); if (audioClip.Looping) { return(InfiniteLoop); } return(rndDelay); } }
// Populate an array of the active events, and add it to the timeline list of all captured audio frames. private void CollectProfilerEvents(ProfilerEvent[] currentEvents) { List <ActiveEvent> activeEvents = UAudioManager.Instance.ProfilerEvents; currentEvents = new ProfilerEvent[activeEvents.Count]; for (int i = 0; i < currentEvents.Length; i++) { ActiveEvent currentEvent = activeEvents[i]; ProfilerEvent tempEvent = new ProfilerEvent(); tempEvent.EventName = currentEvent.AudioEvent.Name; tempEvent.EmitterName = currentEvent.AudioEmitter.name; // The bus might be null, Unity defaults to Editor-hidden master bus. if (currentEvent.AudioEvent.AudioBus == null) { tempEvent.BusName = "-MasterBus-"; } else { tempEvent.BusName = currentEvent.AudioEvent.AudioBus.name; } currentEvents[i] = tempEvent; } this.eventTimeline.Add(currentEvents); // Trim the first event if we have exceeded the maximum stored frames. if (this.eventTimeline.Count > MaxFrames) { this.eventTimeline.RemoveAt(0); } this.currentFrame = this.eventTimeline.Count - 1; }
/// <summary> /// Play all clips in container simultaneously /// </summary> private float PlaySimultaneousClips(AudioContainer currentContainer, ActiveEvent activeEvent) { float tempDelay = 0; float finalActiveTime = 0f; if (currentContainer.Looping) { finalActiveTime = InfiniteLoop; } for (int i = 0; i < currentContainer.Sounds.Length; i++) { tempDelay = PlayClipAndGetTime(currentContainer.Sounds[i], activeEvent.PrimarySource, activeEvent); if (finalActiveTime != InfiniteLoop) { float estimatedActiveTimeNeeded = GetActiveTimeEstimate(currentContainer.Sounds[i], activeEvent, tempDelay); if (estimatedActiveTimeNeeded == InfiniteLoop || estimatedActiveTimeNeeded > finalActiveTime) { finalActiveTime = estimatedActiveTimeNeeded; } } } return(finalActiveTime); }
/// <summary> /// Play a non-continuous container. /// </summary> private float PlayOneOffContainer(ActiveEvent activeEvent) { AudioContainer currentContainer = activeEvent.AudioEvent.Container; // Fading or Looping overrides immediate volume settings. if (activeEvent.AudioEvent.FadeInTime == 0 && !activeEvent.AudioEvent.Container.Looping) { activeEvent.VolDest = activeEvent.PrimarySource.volume; } // Simultaneous sounds. float clipTime = 0; if (currentContainer.ContainerType == AudioContainerType.Simultaneous) { clipTime = PlaySimultaneousClips(currentContainer, activeEvent); } // Sequential and Random sounds. else { clipTime = PlaySingleClip(currentContainer, activeEvent); } activeEvent.ActiveTime = clipTime; return(clipTime); }
/// <summary> /// Coroutine for fading out an AudioSource, and stopping the event once fade is complete. /// </summary> /// <param name="activeEvent">The persistent reference to the event as long as it is playing.</param> /// <param name="fadeTime">The amount of time, in seconds, to completely fade out the sound.</param> /// <returns>The coroutine.</returns> protected IEnumerator StopEventWithFadeCoroutine(ActiveEvent activeEvent, float fadeTime) { if (activeEvent.IsStoppable) { activeEvent.IsStoppable = false; activeEvent.VolDest = 0f; activeEvent.AltVolDest = 0f; activeEvent.CurrentFade = fadeTime; yield return(new WaitForSeconds(fadeTime)); if (activeEvent.PrimarySource != null) { activeEvent.PrimarySource.Stop(); } if (activeEvent.SecondarySource != null) { activeEvent.SecondarySource.Stop(); } activeEvent.CancelEvent = true; RemoveEventInstance(activeEvent); } }
/// <summary> /// Plays an AudioEvent. /// </summary> /// <param name="audioEvent">The AudioEvent to play.</param> /// <param name="emitter">The GameObject on which the AudioEvent is to be played.</param> /// <param name="primarySource">The AudioSource component to use as the primary source for the event.</param> /// <param name="secondarySource">The AudioSource component to use as the secondary source for the event.</param> /// <param name="messageOnAudioEnd">The Message to Send to the GameObject when the sound has finished playing.</param> private void PlayEvent(AudioEvent audioEvent, GameObject emitter, AudioSource primarySource, AudioSource secondarySource, string messageOnAudioEnd = null) { ActiveEvent tempEvent = new ActiveEvent(audioEvent, emitter, primarySource, secondarySource, messageOnAudioEnd); // The base class owns this event once we pass it to PlayContainer, and may dispose it if it cannot be played. PlayContainer(tempEvent); }
/// <summary> /// Remove event from the currently active events. /// </summary> /// <param name="activeEvent">The persistent reference to the event as long as it is playing.</param> private void RemoveEventInstance(ActiveEvent activeEvent) { ActiveEvents.Remove(activeEvent); // Send message notifying user that sound is complete if (!string.IsNullOrEmpty(activeEvent.MessageOnAudioEnd)) { activeEvent.AudioEmitter.SendMessage(activeEvent.MessageOnAudioEnd); } activeEvent.Dispose(); }
/// <summary> /// Stop event by gameObject. /// </summary> /// <param name="eventName"></param> /// <param name="gameObjectToStop"></param> /// <param name="fadeOutTime"></param> public void StopEventsOnGameObject(string eventName, GameObject gameObjectToStop, float fadeOutTime = 0f) { for (int i = ActiveEvents.Count - 1; i >= 0; i--) { ActiveEvent activeEvent = ActiveEvents[i]; if (activeEvent.AudioEmitter == gameObjectToStop) { StopEvent(activeEvent.AudioEvent.Name, gameObjectToStop, fadeOutTime); } } }
/// <summary> /// Stops the first (oldest) instance of an event with the matching name /// </summary> /// <param name="eventName">The name associated with the AudioEvent to stop.</param> private void KillOldestInstance(string eventName) { for (int i = 0; i < ActiveEvents.Count; i++) { ActiveEvent tempEvent = ActiveEvents[i]; if (tempEvent.AudioEvent.Name == eventName) { StopEvent(tempEvent); return; } } }
/// <summary> /// Calculates the estimated active time for an ActiveEvent playing the given clip. /// </summary> /// <param name="audioClip">The clip being played.</param> /// <param name="activeEvent">The event being played.</param> /// <param name="additionalDelay">The delay before playing in seconds.</param> /// <returns>The estimated active time of the event based on Looping or clip time. If Looping, this will return InfiniteLoop.</returns> private static float GetActiveTimeEstimate(UAudioClip audioClip, ActiveEvent activeEvent, float additionalDelay) { if (audioClip.Looping || activeEvent.AudioEvent.Container.Looping || additionalDelay == InfiniteLoop) { return(InfiniteLoop); } else { float pitchAdjustedClipLength = activeEvent.PrimarySource.pitch != 0 ? (audioClip.Sound.length / activeEvent.PrimarySource.pitch) : 0; // Restrict non-Looping ActiveTime values to be non-negative. return(Mathf.Max(0.0f, pitchAdjustedClipLength + activeEvent.AudioEvent.InstanceTimeBuffer + additionalDelay)); } }
/// <summary> /// Begin playing a non-continuous container, loop if applicable. /// </summary> private void StartOneOffEvent(ActiveEvent activeEvent) { if (activeEvent.AudioEvent.Container.Looping) { StartCoroutine(PlayLoopingOneOffContainerCoroutine(activeEvent)); activeEvent.ActiveTime = InfiniteLoop; } else { PlayOneOffContainer(activeEvent); } StartCoroutine(RecordEventInstanceCoroutine(activeEvent)); }
/// <summary> /// Stop audio sources in an event, and clean up instance references. /// </summary> /// <param name="activeEvent">The persistent reference to the event as long as it is playing.</param> protected void StopEvent(ActiveEvent activeEvent) { if (activeEvent.PrimarySource != null) { activeEvent.PrimarySource.Stop(); } if (activeEvent.SecondarySource != null) { activeEvent.SecondarySource.Stop(); } activeEvent.CancelEvent = true; RemoveEventInstance(activeEvent); }
/// <summary> /// Repeatedly trigger the one-off container based on the loop time. /// </summary> private IEnumerator PlayLoopingOneOffContainerCoroutine(ActiveEvent activeEvent) { while (!activeEvent.CancelEvent) { float tempLoopTime = PlayOneOffContainer(activeEvent); float eventLoopTime = activeEvent.AudioEvent.Container.LoopTime; // Protect against containers Looping every frame by defaulting to the length of the audio clip. if (eventLoopTime != 0) { tempLoopTime = eventLoopTime; } yield return(new WaitForSeconds(tempLoopTime)); } }
/// <summary> /// Sets the pitch value on active AudioEvents. /// </summary> /// <param name="eventName">The name associated with the AudioEvents.</param> /// <param name="newPitch">The value to set the pitch, between 0 (exclusive) and 3 (inclusive).</param> public void SetPitch(string eventName, float newPitch) { if (newPitch <= 0 || newPitch > 3) { Debug.LogErrorFormat(this, "Invalid pitch {0} set for event \"{1}\"", newPitch, eventName); return; } for (int i = ActiveEvents.Count - 1; i >= 0; i--) { ActiveEvent activeEvent = ActiveEvents[i]; if (activeEvent.AudioEvent.Name == eventName) { activeEvent.SetPitch(newPitch); } } }
/// <summary> /// Stops all events by name. /// </summary> /// <param name="eventName">The name associated with the AudioEvent.</param> /// <param name="fadeOutTime">The amount of time in seconds to completely fade out the sound.</param> public void StopAllEvents(string eventName, GameObject emitter = null, float fadeOutTime = 0f) { for (int i = ActiveEvents.Count - 1; i >= 0; i--) { ActiveEvent activeEvent = ActiveEvents[i]; if (activeEvent.AudioEvent.Name == eventName) { if (fadeOutTime > 0) { StartCoroutine(StopEventWithFadeCoroutine(activeEvent, fadeOutTime)); } else { StartCoroutine(StopEventWithFadeCoroutine(activeEvent, activeEvent.AudioEvent.FadeOutTime)); } } } }
/// <summary> /// Determine which rules to follow for container playback, and begin the appropriate function. /// </summary> /// <param name="activeEvent">The event to play.</param> protected void PlayContainer(ActiveEvent activeEvent) { if (activeEvent.AudioEvent.Container.Sounds.Length == 0) { Debug.LogErrorFormat(this, "Trying to play container \"{0}\" with no clips.", activeEvent.AudioEvent.Container); // Clean up the ActiveEvent before we discard it, so it will release its AudioSource(s). activeEvent.Dispose(); return; } switch (activeEvent.AudioEvent.Container.ContainerType) { case AudioContainerType.Random: StartOneOffEvent(activeEvent); break; case AudioContainerType.Simultaneous: StartOneOffEvent(activeEvent); break; case AudioContainerType.Sequence: StartOneOffEvent(activeEvent); break; case AudioContainerType.ContinuousSequence: PlayContinuousSequenceContainer(activeEvent.AudioEvent.Container, activeEvent.PrimarySource, activeEvent); break; case AudioContainerType.ContinuousRandom: PlayContinuousRandomContainer(activeEvent.AudioEvent.Container, activeEvent.PrimarySource, activeEvent); break; default: Debug.LogErrorFormat(this, "Trying to play container \"{0}\" with an unknown AudioContainerType \"{1}\".", activeEvent.AudioEvent.Container, activeEvent.AudioEvent.Container.ContainerType); // Clean up the ActiveEvent before we discard it, so it will release its AudioSource(s). activeEvent.Dispose(); break; } }
/// <summary> /// Sets the volume for active AudioEvents. /// </summary> /// <param name="eventName">The name associated with the AudioEvents.</param> /// <param name="emitter">The GameObject associated, as the audio emitter, for the AudioEvents.</param> /// <param name="volume">The new volume.</param> public void ModulateVolume(string eventName, GameObject emitter, float volume) { emitter = ApplyAudioEmitterTransform(emitter); if (emitter == null) { return; } for (int i = 0; i < ActiveEvents.Count; i++) { ActiveEvent activeEvent = ActiveEvents[i]; if (ActiveEvents[i].AudioEvent.Name == eventName && ActiveEvents[i].AudioEmitter == emitter) { activeEvent.VolDest = volume; activeEvent.AltVolDest = volume; activeEvent.CurrentFade = 0; } } }
/// <summary> /// Linearly interpolates the volume property on all of the AudioSource components in the ActiveEvents. /// </summary> private void UpdateEmitterVolumes() { // Move through each active event and change the settings for the AudioSource components to smoothly fade volumes. for (int i = 0; i < ActiveEvents.Count; i++) { ActiveEvent currentEvent = this.ActiveEvents[i]; // If we have a secondary source (for crossfades) adjust the volume based on the current fade time for each active event. if (currentEvent.SecondarySource != null && currentEvent.SecondarySource.volume != currentEvent.AltVolDest) { if (Mathf.Abs(currentEvent.AltVolDest - currentEvent.SecondarySource.volume) < Time.deltaTime / currentEvent.CurrentFade) { currentEvent.SecondarySource.volume = currentEvent.AltVolDest; } else { currentEvent.SecondarySource.volume += (currentEvent.AltVolDest - currentEvent.SecondarySource.volume) * Time.deltaTime / currentEvent.CurrentFade; } } // Adjust the volume of the main source based on the current fade time for each active event. if (currentEvent.PrimarySource != null && currentEvent.PrimarySource.volume != currentEvent.VolDest) { if (Mathf.Abs(currentEvent.VolDest - currentEvent.PrimarySource.volume) < Time.deltaTime / currentEvent.CurrentFade) { currentEvent.PrimarySource.volume = currentEvent.VolDest; } else { currentEvent.PrimarySource.volume += (currentEvent.VolDest - currentEvent.PrimarySource.volume) * Time.deltaTime / currentEvent.CurrentFade; } } // If there is no time left in the fade, make sure we are set to the destination volume. if (currentEvent.CurrentFade > 0) { currentEvent.CurrentFade -= Time.deltaTime; } } }
/// <summary> /// Coroutine for "continuous" random containers that alternates between two sources to crossfade clips for continuous playlist Looping. /// </summary> /// <param name="audioContainer">The audio container.</param> /// <param name="activeEvent">The persistent reference to the event as long as it is playing.</param> /// <param name="waitTime">The time in seconds to wait before switching AudioSources for crossfading.</param> /// <returns>The coroutine.</returns> private IEnumerator ContinueRandomContainerCoroutine(AudioContainer audioContainer, ActiveEvent activeEvent, float waitTime) { while (!activeEvent.CancelEvent) { yield return(new WaitForSeconds(waitTime)); audioContainer.CurrentClip = Random.Range(0, audioContainer.Sounds.Length); UAudioClip tempClip = audioContainer.Sounds[audioContainer.CurrentClip]; // Play on primary source. if (activeEvent.PlayingAlt) { activeEvent.PrimarySource.volume = 0f; activeEvent.VolDest = activeEvent.AudioEvent.VolumeCenter; activeEvent.AltVolDest = 0f; activeEvent.CurrentFade = audioContainer.CrossfadeTime; waitTime = (tempClip.Sound.length / activeEvent.PrimarySource.pitch) - audioContainer.CrossfadeTime; PlayClipAndGetTime(tempClip, activeEvent.PrimarySource, activeEvent); } // Play on secondary source. else { activeEvent.SecondarySource.volume = 0f; activeEvent.AltVolDest = activeEvent.AudioEvent.VolumeCenter; activeEvent.VolDest = 0f; activeEvent.CurrentFade = audioContainer.CrossfadeTime; waitTime = (tempClip.Sound.length / activeEvent.SecondarySource.pitch) - audioContainer.CrossfadeTime; PlayClipAndGetTime(tempClip, activeEvent.SecondarySource, activeEvent); } activeEvent.PlayingAlt = !activeEvent.PlayingAlt; } }
/// <summary> /// Play the current clip in a container, and call the coroutine to constantly choose new audio clips when the current clip ends. /// </summary> /// <param name="audioContainer">The audio container.</param> /// <param name="emitter">The emitter to use.</param> /// <param name="activeEvent">The persistent reference to the event as long as it is playing.</param> private void PlayContinuousSequenceContainer(AudioContainer audioContainer, AudioSource emitter, ActiveEvent activeEvent) { UAudioClip tempClip = audioContainer.Sounds[audioContainer.CurrentClip]; activeEvent.PrimarySource.volume = 0f; activeEvent.VolDest = activeEvent.AudioEvent.VolumeCenter; activeEvent.AltVolDest = 0f; activeEvent.CurrentFade = audioContainer.CrossfadeTime; float waitTime = (tempClip.Sound.length / emitter.pitch) - activeEvent.AudioEvent.Container.CrossfadeTime; // Ignore clip delay since the container is continuous. PlayClipAndGetTime(tempClip, emitter, activeEvent); activeEvent.ActiveTime = InfiniteLoop; StartCoroutine(RecordEventInstanceCoroutine(activeEvent)); audioContainer.CurrentClip++; if (audioContainer.CurrentClip >= audioContainer.Sounds.Length) { audioContainer.CurrentClip = 0; } StartCoroutine(ContinueSequenceContainerCoroutine(audioContainer, activeEvent, waitTime)); }
/// <summary> /// Coroutine for "continuous" sequence containers that alternates between two sources to crossfade clips for continuous playlist Looping. /// </summary> /// <param name="audioContainer">The audio container.</param> /// <param name="activeEvent">The persistent reference to the event as long as it is playing.</param> /// <param name="waitTime">The time in seconds to wait before switching AudioSources to crossfading.</param> /// <returns>The coroutine.</returns> private IEnumerator ContinueSequenceContainerCoroutine(AudioContainer audioContainer, ActiveEvent activeEvent, float waitTime) { while (!activeEvent.CancelEvent) { yield return(new WaitForSeconds(waitTime)); UAudioClip tempClip = audioContainer.Sounds[audioContainer.CurrentClip]; if (tempClip.Sound == null) { Debug.LogErrorFormat(this, "Sound clip in event \"{0}\" is null!", activeEvent.AudioEvent.Name); waitTime = 0; } else { // Play on primary source. if (activeEvent.PlayingAlt) { activeEvent.PrimarySource.volume = 0f; activeEvent.VolDest = activeEvent.AudioEvent.VolumeCenter; activeEvent.AltVolDest = 0f; activeEvent.CurrentFade = audioContainer.CrossfadeTime; waitTime = (tempClip.Sound.length / activeEvent.PrimarySource.pitch) - audioContainer.CrossfadeTime; PlayClipAndGetTime(tempClip, activeEvent.PrimarySource, activeEvent); } // Play on secondary source. else { activeEvent.SecondarySource.volume = 0f; activeEvent.AltVolDest = activeEvent.AudioEvent.VolumeCenter; activeEvent.VolDest = 0f; activeEvent.CurrentFade = audioContainer.CrossfadeTime; waitTime = (tempClip.Sound.length / activeEvent.SecondarySource.pitch) - audioContainer.CrossfadeTime; PlayClipAndGetTime(tempClip, activeEvent.SecondarySource, activeEvent); } } audioContainer.CurrentClip++; if (audioContainer.CurrentClip >= audioContainer.Sounds.Length) { audioContainer.CurrentClip = 0; } activeEvent.PlayingAlt = !activeEvent.PlayingAlt; } }
/// <summary> /// Coroutine for playing a clip after a delay (in seconds). /// </summary> /// <param name="audioClip">The clip to play.</param> /// <param name="emitter">The emitter to use.</param> /// <param name="delay">The amount of time in seconds to wait before playing audio clip.</param> /// <param name="activeEvent">The persistent reference to the event as long as it is playing.</param> /// <returns>The coroutine.</returns> private IEnumerator PlayClipDelayedCoroutine(UAudioClip audioClip, AudioSource emitter, float delay, ActiveEvent activeEvent) { yield return(new WaitForSeconds(delay)); if (this.ActiveEvents.Contains(activeEvent)) { emitter.PlayClip(audioClip.Sound, audioClip.Looping); } }