// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.MixedReality.Toolkit.Utilities; using System; using System.Collections.Generic; using UnityEngine; using UnityPhysics = UnityEngine.Physics; namespace Microsoft.MixedReality.Toolkit.Audio { /// /// Class which supports components implementing being used with audio sources. /// /// /// AudioInfluencerController requires an AudioSource component. If one is not attached, it will be added automatically. /// Each sound playing game object needs to have an AudioInfluencerController attached in order to have its audio influenced. /// [RequireComponent(typeof(AudioSource))] [DisallowMultipleComponent] [AddComponentMenu("Scripts/MRTK/SDK/AudioInfluencerController")] public class AudioInfluencerController : MonoBehaviour { /// /// Frequency below the nominal range of human hearing. /// /// /// This frequency can be used to set a high pass filter to allow all /// audible frequencies through the filter. /// public static readonly float NeutralLowFrequency = 10.0f; /// /// Frequency above the nominal range of human hearing. /// /// /// This frequency can be used to set a low pass filter to allow all /// audible frequencies through the filter. /// public static readonly float NeutralHighFrequency = 22000.0f; /// /// Time, in seconds, between audio influence updates. /// /// /// The UpdateInterval range is between 0.0 and 1.0, inclusive. /// The default value is 0.25. /// A value of 0.0f indicates that updates occur every frame. /// [Tooltip("Time, in seconds, between audio influence updates. 0 indicates to update every frame.")] [Range(0.0f, 1.0f)] [SerializeField] private float updateInterval = 0.25f; public float UpdateInterval { get { return updateInterval; } set { updateInterval = Mathf.Clamp(value, 0.0f, 1.0f); } } /// /// Maximum distance, in meters, to look when attempting to find the user and any influencers. /// /// /// The MaxDistance range is 1.0 to 50.0, inclusive. /// The default value is 20.0. /// [Tooltip("Maximum distance, in meters, to look when attempting to find the user and any influencers.")] [Range(1.0f, 50.0f)] [SerializeField] private float maxDistance = 20.0f; public float MaxDistance { get { return maxDistance; } set { maxDistance = Mathf.Clamp(value, 1.0f, 50.0f); } } /// /// Maximum number of objects that will be considered when looking for influencers. /// Setting this value too high may have a negative impact on the performance of your experience. /// /// /// MaxObjects can only be set in the Unity Inspector. /// The MaxObjects range is 1 to 25, inclusive. /// The default value is 10. /// [Tooltip("Maximum number of objects that will be considered when looking for influencers.")] [Range(1, 25)] [SerializeField] private int maxObjects = 10; /// /// Time of last audio processing update. /// private DateTime lastUpdate = DateTime.MinValue; /// /// The source of the audio. /// [SerializeField] private AudioSource audioSource; /// /// The initial volume level of the audio source. /// private float initialAudioSourceVolume; /// /// The hits returned by Physics.RaycastAll /// private RaycastHit[] hits; /// /// The collection of previously applied audio influencers. /// private List previousInfluencers = new List(); /// /// Potential effects manipulated by an audio influencer. /// private AudioLowPassFilter lowPassFilter; private AudioHighPassFilter highPassFilter; private float nativeLowPassCutoffFrequency; /// /// Gets or sets the native low pass cutoff frequency for the /// sound emitter. /// public float NativeLowPassCutoffFrequency { get { return nativeLowPassCutoffFrequency; } set { value = nativeLowPassCutoffFrequency; } } private float nativeHighPassCutoffFrequency; /// /// Gets or sets the native high pass cutoff frequency for the /// sound emitter. /// public float NativeHighPassCutoffFrequency { get { return nativeHighPassCutoffFrequency; } set { value = nativeHighPassCutoffFrequency; } } private void Awake() { if (audioSource == null) { audioSource = GetComponent(); } initialAudioSourceVolume = audioSource.volume; // Get optional filters (and initial values) that the sound designer / developer // may have applied to this game object lowPassFilter = gameObject.GetComponent(); nativeLowPassCutoffFrequency = (lowPassFilter != null) ? lowPassFilter.cutoffFrequency : NeutralHighFrequency; highPassFilter = gameObject.GetComponent(); nativeHighPassCutoffFrequency = (highPassFilter != null) ? highPassFilter.cutoffFrequency : NeutralLowFrequency; // Preallocate the array that will be used to collect RaycastHit structures. hits = new RaycastHit[maxObjects]; } private void Update() { DateTime now = DateTime.UtcNow; // Audio influences are not updated every frame. if ((UpdateInterval * 1000.0f) <= (now - lastUpdate).TotalMilliseconds) { audioSource.volume = initialAudioSourceVolume; // Get the audio influencers that should apply to the audio source. List influencers = GetInfluencers(); for (int i = 0; i < influencers.Count; i++) { // Apply the influencer's effect. influencers[i].ApplyEffect(gameObject); } // Find and remove the audio influencers that are to be removed from the audio source. List influencersToRemove = new List(); for (int i = 0; i < previousInfluencers.Count; i++) { var audioInfluencer = previousInfluencers[i]; // Remove influencers that are // no longer in line of sight, // have been destroyed, // or have been disabled if (!influencers.Contains(audioInfluencer) || !audioInfluencer.TryGetMonoBehaviour(out MonoBehaviour mbPrev) || !mbPrev.isActiveAndEnabled) { influencersToRemove.Add(audioInfluencer); } } RemoveInfluencers(influencersToRemove); previousInfluencers = influencers; lastUpdate = now; } } /// /// Removes the effects applied by specified audio influencers. /// /// Collection of IAudioInfluencer objects on which to remove the effect. private void RemoveInfluencers(List influencers) { for (int i = 0; i < influencers.Count; i++) { influencers[i].RemoveEffect(gameObject); } } /// /// Finds the IAudioInfluencer objects that are to be applied to the audio source. /// /// Collection of IAudioInfluencers between the user and the game object. private List GetInfluencers() { List influencers = new List(); Transform cameraTransform = CameraCache.Main.transform; // Influencers take effect only when between the emitter and the user. // Perform a raycast from the user toward the object. Vector3 direction = (gameObject.transform.position - cameraTransform.position).normalized; float distance = Vector3.Distance(cameraTransform.position, gameObject.transform.position); int count = UnityPhysics.RaycastNonAlloc(cameraTransform.position, direction, hits, distance, UnityPhysics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore); for (int i = 0; i < count; i++) { IAudioInfluencer influencer = hits[i].collider.gameObject.GetComponentInParent(); if (influencer != null) { influencers.Add(influencer); } } return influencers; } } }