259 lines
10 KiB
C#
259 lines
10 KiB
C#
// 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
|
|
{
|
|
/// <summary>
|
|
/// Class which supports components implementing <see cref="Microsoft.MixedReality.Toolkit.Audio.IAudioInfluencer"/> being used with audio sources.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>AudioInfluencerController requires an <see href="https://docs.unity3d.com/ScriptReference/AudioSource.html">AudioSource</see> 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.</para>
|
|
/// </remarks>
|
|
[RequireComponent(typeof(AudioSource))]
|
|
[DisallowMultipleComponent]
|
|
[AddComponentMenu("Scripts/MRTK/SDK/AudioInfluencerController")]
|
|
public class AudioInfluencerController : MonoBehaviour
|
|
{
|
|
/// <summary>
|
|
/// Frequency below the nominal range of human hearing.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>This frequency can be used to set a high pass filter to allow all
|
|
/// audible frequencies through the filter.</para>
|
|
/// </remarks>
|
|
public static readonly float NeutralLowFrequency = 10.0f;
|
|
|
|
/// <summary>
|
|
/// Frequency above the nominal range of human hearing.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>This frequency can be used to set a low pass filter to allow all
|
|
/// audible frequencies through the filter.</para>
|
|
/// </remarks>
|
|
public static readonly float NeutralHighFrequency = 22000.0f;
|
|
|
|
/// <summary>
|
|
/// Time, in seconds, between audio influence updates.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>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.</para>
|
|
/// </remarks>
|
|
[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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maximum distance, in meters, to look when attempting to find the user and any influencers.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>The MaxDistance range is 1.0 to 50.0, inclusive.
|
|
/// The default value is 20.0.</para>
|
|
/// </remarks>
|
|
[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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>MaxObjects can only be set in the Unity Inspector.
|
|
/// The MaxObjects range is 1 to 25, inclusive.
|
|
/// The default value is 10.</para>
|
|
/// </remarks>
|
|
[Tooltip("Maximum number of objects that will be considered when looking for influencers.")]
|
|
[Range(1, 25)]
|
|
[SerializeField]
|
|
private int maxObjects = 10;
|
|
|
|
/// <summary>
|
|
/// Time of last audio processing update.
|
|
/// </summary>
|
|
private DateTime lastUpdate = DateTime.MinValue;
|
|
|
|
/// <summary>
|
|
/// The source of the audio.
|
|
/// </summary>
|
|
[SerializeField]
|
|
private AudioSource audioSource;
|
|
|
|
/// <summary>
|
|
/// The initial volume level of the audio source.
|
|
/// </summary>
|
|
private float initialAudioSourceVolume;
|
|
|
|
/// <summary>
|
|
/// The hits returned by Physics.RaycastAll
|
|
/// </summary>
|
|
private RaycastHit[] hits;
|
|
|
|
/// <summary>
|
|
/// The collection of previously applied audio influencers.
|
|
/// </summary>
|
|
private List<IAudioInfluencer> previousInfluencers = new List<IAudioInfluencer>();
|
|
|
|
/// <summary>
|
|
/// Potential effects manipulated by an audio influencer.
|
|
/// </summary>
|
|
private AudioLowPassFilter lowPassFilter;
|
|
private AudioHighPassFilter highPassFilter;
|
|
|
|
private float nativeLowPassCutoffFrequency;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the native low pass cutoff frequency for the
|
|
/// sound emitter.
|
|
/// </summary>
|
|
public float NativeLowPassCutoffFrequency
|
|
{
|
|
get { return nativeLowPassCutoffFrequency; }
|
|
set { value = nativeLowPassCutoffFrequency; }
|
|
}
|
|
|
|
private float nativeHighPassCutoffFrequency;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the native high pass cutoff frequency for the
|
|
/// sound emitter.
|
|
/// </summary>
|
|
public float NativeHighPassCutoffFrequency
|
|
{
|
|
get { return nativeHighPassCutoffFrequency; }
|
|
set { value = nativeHighPassCutoffFrequency; }
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
if (audioSource == null)
|
|
{
|
|
audioSource = GetComponent<AudioSource>();
|
|
}
|
|
|
|
initialAudioSourceVolume = audioSource.volume;
|
|
|
|
// Get optional filters (and initial values) that the sound designer / developer
|
|
// may have applied to this game object
|
|
lowPassFilter = gameObject.GetComponent<AudioLowPassFilter>();
|
|
nativeLowPassCutoffFrequency = (lowPassFilter != null) ? lowPassFilter.cutoffFrequency : NeutralHighFrequency;
|
|
highPassFilter = gameObject.GetComponent<AudioHighPassFilter>();
|
|
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<IAudioInfluencer> 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<IAudioInfluencer> influencersToRemove = new List<IAudioInfluencer>();
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the effects applied by specified audio influencers.
|
|
/// </summary>
|
|
/// <param name="influencers">Collection of IAudioInfluencer objects on which to remove the effect.</param>
|
|
private void RemoveInfluencers(List<IAudioInfluencer> influencers)
|
|
{
|
|
for (int i = 0; i < influencers.Count; i++)
|
|
{
|
|
influencers[i].RemoveEffect(gameObject);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds the IAudioInfluencer objects that are to be applied to the audio source.
|
|
/// </summary>
|
|
/// <returns>Collection of IAudioInfluencers between the user and the game object.</returns>
|
|
private List<IAudioInfluencer> GetInfluencers()
|
|
{
|
|
List<IAudioInfluencer> influencers = new List<IAudioInfluencer>();
|
|
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<IAudioInfluencer>();
|
|
if (influencer != null)
|
|
{
|
|
influencers.Add(influencer);
|
|
}
|
|
}
|
|
|
|
return influencers;
|
|
}
|
|
}
|
|
} |