Files
2025-11-13 17:40:28 +08:00

586 lines
19 KiB
C#

// Copyright © 2015-2022 Pico Technology Co., Ltd. All Rights Reserved.
using System;
using System.Collections;
using PXR_Audio.Spatializer;
using UnityEditor;
using UnityEngine;
[RequireComponent(typeof(AudioSource))]
public class PXR_Audio_Spatializer_AudioSource : MonoBehaviour
{
[SerializeField] [Range(0.0f, 24.0f)]
private float sourceGainDB = 0.0f;
private float sourceGainAmplitude = 1.0f;
[SerializeField] [Range(-120.0f, 48.0f)]
private float reflectionGainDB = 0.0f;
private float reflectionGainAmplitude = 1.0f;
[SerializeField] [Range(0.0f, 100000.0f)]
private float sourceSize = 0.0f;
[Tooltip(
"Whether Pico Doppler Simulation is enabled for this sound source, which affects both direct and reflection path of it.\n" +
" - If you disabled this option before run or build, you cannot turn it on during runtime, since doppler effect unit is not initialized to save memory.")]
[SerializeField]
private bool enableDoppler = true;
[Tooltip(
"Mode of distance attenuation of this sound source.\n" +
" - None && Fixed: Source volume doesn't decrease when source-listener distance increases.\n" +
" - Inversed Squared: Source volume decrease when source-listener distance increases, just like the real world.\n" +
" - Customized: Don't use it!!!!")]
[SerializeField]
public SourceAttenuationMode sourceAttenuationMode = SourceAttenuationMode.InverseSquare;
[Tooltip(
"Source volume will not further increase when source-listener distance is less than this.\n" +
" - Only effective when source attenuation mode == Inversed Squared")]
[SerializeField]
public float minAttenuationDistance = 1.0f;
[Tooltip(
"Source volume will not further decrease when source-listener distance is more than this.\n" +
" - Only effective when source attenuation mode == Inversed Squared")]
[SerializeField]
public float maxAttenuationDistance = 100.0f;
[Tooltip("Determine shape of the radiation polar pattern of this sound source.\n" +
" - Alpha = 0 gives you omnidirectional polar pattern\n" +
" - Alpha = 0.5 gives you cardioid polar pattern\n" +
" - Alpha = 1 gives you figure-8 polar pattern")]
[SerializeField] [Range(0.0f, 1.0f)] private float directivityAlpha = 0.0f;
[Tooltip("Determine width of the radiation polar pattern of this sound source.\n" +
" - Larger order gives you narrower radiation pattern.")]
[SerializeField] [Range(0.0f, 1000.0f)]
private float directivityOrder = 1.0f;
#if UNITY_EDITOR
private Mesh directivityDisplayMesh;
#endif
private SourceConfig sourceConfig;
private uint sourcePropertyMask = 0;
private bool isActive;
private bool isAudioDSPInProgress = false;
public bool IsAudioDSPInProgress
{
get { return isAudioDSPInProgress; }
}
private PXR_Audio_Spatializer_Context context;
private PXR_Audio_Spatializer_Context Context
{
get
{
if (context == null)
context = PXR_Audio_Spatializer_Context.Instance;
return context;
}
}
private AudioSource nativeSource;
private int sourceId = -1;
private int currentContextUuid = -2;
private float[] positionArray = new float[3] { 0.0f, 0.0f, 0.0f };
private float playheadPosition = 0.0f;
private bool wasPlaying = false;
private void OnEnable()
{
if (Context != null && Context.Initialized)
{
if (Context.UUID == currentContextUuid)
isActive = true;
else
RegisterInternal();
}
else
{
sourceId = -1;
currentContextUuid = -2;
}
}
/// <summary>
/// Register this audio source in spatializer
/// </summary>
internal void RegisterInternal()
{
nativeSource = GetComponent<AudioSource>();
positionArray[0] = transform.position.x;
positionArray[1] = transform.position.y;
positionArray[2] = -transform.position.z;
sourceConfig = new SourceConfig(PXR_Audio.Spatializer.SourceMode.Spatialize);
sourcePropertyMask = 0;
sourceConfig.position.x = positionArray[0];
sourceConfig.position.y = positionArray[1];
sourceConfig.position.z = positionArray[2];
sourceConfig.front.x = transform.forward.x;
sourceConfig.front.y = transform.forward.y;
sourceConfig.front.z = -transform.forward.z;
sourceConfig.up.x = transform.up.x;
sourceConfig.up.y = transform.up.y;
sourceConfig.up.z = -transform.up.z;
sourceConfig.enableDoppler = enableDoppler;
sourceGainAmplitude = DB2Mag(sourceGainDB);
sourceConfig.sourceGain = sourceGainAmplitude;
reflectionGainAmplitude = DB2Mag(reflectionGainDB);
sourceConfig.reflectionGain = reflectionGainAmplitude;
sourceConfig.radius = sourceSize;
sourceConfig.attenuationMode = sourceAttenuationMode;
sourceConfig.minAttenuationDistance = minAttenuationDistance;
sourceConfig.maxAttenuationDistance = maxAttenuationDistance;
sourceConfig.directivityAlpha = directivityAlpha;
sourceConfig.directivityOrder = directivityOrder;
PXR_Audio.Spatializer.Result ret = Context.AddSourceWithConfig(
ref sourceConfig,
ref sourceId,
false);
if (ret != PXR_Audio.Spatializer.Result.Success)
{
Debug.LogError("Failed to add source.");
return;
}
isActive = true;
currentContextUuid = Context.UUID;
Debug.Log("Source #" + sourceId + " is added.");
}
/// <summary>
/// Resume playing status of this source
/// </summary>
public void Resume()
{
nativeSource.time = playheadPosition;
if (wasPlaying)
{
nativeSource.Play();
}
}
/// <summary>
/// Setup source gain in dB
/// </summary>
/// <param name="gainDB">Gain in dB</param>
public void SetGainDB(float gainDB)
{
// if (Mathf.Abs(gainDB - sourceGainDB) < 1e-7) return;
sourceGainDB = gainDB;
sourceConfig.sourceGain = sourceGainAmplitude = DB2Mag(gainDB);
sourcePropertyMask |= (uint)SourceProperty.SourceGain;
}
/// <summary>
/// Get source gain in dB
/// </summary>
public float GetGainDB()
{
return sourceGainDB;
}
/// <summary>
/// Setup source gain in Amplitude
/// </summary>
/// <param name="gainAmplitude">Gain in Amplitude</param>
public void SetGainAmplitude(float gainAmplitude)
{
sourceConfig.sourceGain = sourceGainAmplitude = gainAmplitude;
sourceGainDB = Mag2DB(gainAmplitude);
sourcePropertyMask |= (uint)SourceProperty.SourceGain;
}
/// <summary>
/// Setup source reflection gain in dB
/// </summary>
/// <param name="gainDB">Gain in dB</param>
public void SetReflectionGainDB(float gainDB)
{
reflectionGainDB = gainDB;
sourceConfig.reflectionGain = reflectionGainAmplitude = DB2Mag(gainDB);
sourcePropertyMask |= (uint)SourceProperty.ReflectionGain;
}
/// <summary>
/// Get source reflection gain in dB
/// </summary>
public float GetReflectionGainDB()
{
return reflectionGainDB;
}
/// <summary>
/// Setup source radius in meters
/// </summary>
/// <param name="radius">source radius in meter</param>
public void SetSize(float radius)
{
sourceConfig.radius = sourceSize = radius;
sourcePropertyMask |= (uint)SourceProperty.VolumetricRadius;
}
/// <summary>
/// Get source radius in meters
/// </summary>
public float GetSize()
{
return sourceSize;
}
/// <summary>
/// Turn on/off in-engine doppler effect
/// </summary>
/// <param name="on">Turn doppler effect on/off </param>
public void SetDopplerStatus(bool on)
{
sourceConfig.enableDoppler = enableDoppler = on;
sourcePropertyMask |= (uint)SourceProperty.DopplerOnOff;
}
/// <summary>
/// Get in-engine doppler effect status
/// </summary>
public bool GetDopplerStatus()
{
return sourceConfig.enableDoppler;
}
/// <summary>
/// Get source attenuation mode
/// </summary>
public SourceAttenuationMode GetAttenuationMode()
{
return sourceConfig.attenuationMode;
}
/// <summary>
/// Setup min attenuation range
/// </summary>
/// <param name="min"> Minimum attenuation range. Source loudness would stop increasing when source-listener
/// distance is shorter than this </param>
public void SetMinAttenuationRange(float min)
{
sourceConfig.minAttenuationDistance = minAttenuationDistance = min;
sourcePropertyMask |= (uint)SourceProperty.RangeMin;
}
/// <summary>
/// Get min attenuation range
/// </summary>
public float GetMinAttenuationRange()
{
return sourceConfig.minAttenuationDistance;
}
/// <summary>
/// Setup max attenuation range
/// </summary>
/// <param name="max"> Maximum attenuation range. Source loudness would stop decreasing when source-listener
/// distance is further than this </param>
public void SetMaxAttenuationRange(float max)
{
sourceConfig.maxAttenuationDistance = maxAttenuationDistance = max;
sourcePropertyMask |= (uint)SourceProperty.RangeMax;
}
/// <summary>
/// Get max attenuation range
/// </summary>
public float GetMaxAttenuationRange()
{
return sourceConfig.maxAttenuationDistance;
}
/// <summary>
/// Setup the radiation polar pattern of source, which describes the gain of initial sound wave radiated towards
/// different directions. The relation between sound emission direction, alpha, and order can be described as
/// follows: Let theta equals the angle between radiation direction and source front direction, the directivity
/// gain g is:
/// g = (|1 - alpha| + alpha * cos(theta)) ^ order;
/// </summary>
/// <param name="alpha"> Define the shape of the directivity pattern.
/// <param name="order"> Indicates how sharp the source polar pattern is.
public void SetDirectivity(float alpha, float order)
{
sourceConfig.directivityAlpha = directivityAlpha = alpha;
sourceConfig.directivityOrder = directivityOrder = order;
sourcePropertyMask |= (uint)SourceProperty.Directivity;
}
public float GetDirectivityAlpha()
{
return sourceConfig.directivityAlpha;
}
public float GetDirectivityOrder()
{
return sourceConfig.directivityOrder;
}
void Update()
{
if (isActive && sourceId >= 0 && context != null && context.Initialized)
{
if (transform.hasChanged)
{
sourceConfig.position.x = transform.position.x;
sourceConfig.position.y = transform.position.y;
sourceConfig.position.z = -transform.position.z;
sourceConfig.front.x = transform.forward.x;
sourceConfig.front.y = transform.forward.y;
sourceConfig.front.z = -transform.forward.z;
sourceConfig.up.x = transform.up.x;
sourceConfig.up.y = transform.up.y;
sourceConfig.up.z = -transform.up.z;
sourcePropertyMask |= (uint)SourceProperty.Position | (uint)SourceProperty.Orientation;
transform.hasChanged = false;
}
if (sourcePropertyMask != 0)
{
var ret = Context.SetSourceConfig(sourceId, ref sourceConfig, sourcePropertyMask);
if (ret == Result.Success)
sourcePropertyMask = 0;
}
if (nativeSource.isPlaying)
playheadPosition = nativeSource.time;
wasPlaying = nativeSource.isPlaying;
}
}
private void OnDisable()
{
isActive = false;
isAudioDSPInProgress = false;
}
private void OnDestroy()
{
DestroyInternal();
}
#if UNITY_EDITOR
void OnValidate()
{
if (EditorApplication.isPlaying)
{
SetGainDB(sourceGainDB);
SetReflectionGainDB(reflectionGainDB);
SetSize(sourceSize);
SetDopplerStatus(enableDoppler);
SetDirectivity(directivityAlpha, directivityOrder);
}
}
#endif
private void DestroyInternal()
{
isActive = false;
if (context != null && context.Initialized)
{
var ret = context.RemoveSource(sourceId);
if (ret != PXR_Audio.Spatializer.Result.Success)
{
Debug.LogError("Failed to delete source #" + sourceId + ", error code is: " + ret);
}
else
{
Debug.Log("Source #" + sourceId + " is deleted.");
}
}
isAudioDSPInProgress = false;
sourceId = -1;
}
private void OnAudioFilterRead(float[] data, int channels)
{
if (!isActive || sourceId < 0 || context == null || !context.Initialized)
{
// Mute Original signal
for (int i = 0; i < data.Length; ++i)
data[i] = 0.0f;
return;
}
isAudioDSPInProgress = true;
int numFrames = data.Length / channels;
float oneOverChannelsF = 1.0f / ((float)channels);
// force to mono
if (channels > 1)
{
for (int frame = 0; frame < numFrames; ++frame)
{
float sample = 0.0f;
for (int channel = 0; channel < channels; ++channel)
{
sample += data[frame * channels + channel];
}
data[frame] = sample * oneOverChannelsF;
}
}
Context.SubmitSourceBuffer(sourceId, data, (uint)numFrames);
// Mute Original signal
for (int i = 0; i < data.Length; ++i)
data[i] = 0.0f;
isAudioDSPInProgress = false;
}
private float DB2Mag(float db)
{
return Mathf.Pow(10.0f, db / 20.0f);
}
private float Mag2DB(float mag)
{
return 20 * Mathf.Log10(mag);
}
void OnDrawGizmos()
{
Color c;
const float colorSolidAlpha = 0.1f;
// VolumetricRadius (purple)
c.r = 1.0f;
c.g = 0.0f;
c.b = 1.0f;
c.a = 1.0f;
Gizmos.color = c;
Gizmos.DrawWireSphere(transform.position, sourceSize);
c.a = colorSolidAlpha;
Gizmos.color = c;
Gizmos.DrawSphere(transform.position, sourceSize);
// Attenuation distance (min && max)
if (sourceAttenuationMode == SourceAttenuationMode.InverseSquare)
{
// min
c.r = 1.0f;
c.g = 0.35f;
c.b = 0.0f;
c.a = 1.0f;
Gizmos.color = c;
Gizmos.DrawWireSphere(transform.position, minAttenuationDistance);
c.a = colorSolidAlpha;
Gizmos.color = c;
Gizmos.DrawSphere(transform.position, minAttenuationDistance);
// max
c.r = 0.0f;
c.g = 1.0f;
c.b = 1.0f;
c.a = 1.0f;
Gizmos.color = c;
Gizmos.DrawWireSphere(transform.position, maxAttenuationDistance);
c.a = colorSolidAlpha;
Gizmos.color = c;
Gizmos.DrawSphere(transform.position, maxAttenuationDistance);
}
}
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
// Draw directivity mesh
GeneratePolarPatternMesh(directivityDisplayMesh, directivityAlpha, directivityOrder);
}
private void GeneratePolarPatternMesh(Mesh mesh, float alpha, float order)
{
if (mesh == null)
mesh = new Mesh();
Vector2[] cardioidVertices2D = GeneratePolarPatternVertices2D(alpha, order, 90);
int numVertices = cardioidVertices2D.Length * 2;
Vector3[] vertices = new Vector3[numVertices];
for (int i = 0; i < cardioidVertices2D.Length; ++i)
{
var vertex2D = cardioidVertices2D[i];
vertices[i] = new Vector3(vertex2D.x, 0.0f, vertex2D.y);
vertices[cardioidVertices2D.Length + i] = Quaternion.AngleAxis(45, Vector3.forward) *
new Vector3(vertex2D.x, 0.0f, vertex2D.y);
}
int[] indices = new int[cardioidVertices2D.Length * 2 * 3];
int idx = 0;
for (idx = 0; idx < cardioidVertices2D.Length - 1; ++idx)
{
indices[idx * 6 + 0] = idx;
indices[idx * 6 + 1] = idx + 1;
indices[idx * 6 + 2] = idx + cardioidVertices2D.Length;
indices[idx * 6 + 3] = idx + 1;
indices[idx * 6 + 4] = idx + cardioidVertices2D.Length + 1;
indices[idx * 6 + 5] = idx + cardioidVertices2D.Length;
}
// Construct a new mesh for the gizmo.
mesh.vertices = vertices;
mesh.triangles = indices;
mesh.RecalculateNormals();
// Draw the mesh.
Vector3 scale = 2.0f * Mathf.Max(transform.lossyScale.x, transform.lossyScale.z) * Vector3.one;
Color c;
c.r = 0.2f;
c.g = 0.5f;
c.b = 0.7f;
c.a = 0.5f;
Gizmos.color = c;
Gizmos.DrawMesh(mesh, transform.position, transform.rotation, scale);
Gizmos.DrawMesh(mesh, transform.position, transform.rotation * Quaternion.AngleAxis(45, Vector3.forward),
scale);
Gizmos.DrawMesh(mesh, transform.position, transform.rotation * Quaternion.AngleAxis(90, Vector3.forward),
scale);
Gizmos.DrawMesh(mesh, transform.position, transform.rotation * Quaternion.AngleAxis(135, Vector3.forward),
scale);
Gizmos.DrawMesh(mesh, transform.position, transform.rotation * Quaternion.AngleAxis(180, Vector3.forward),
scale);
Gizmos.DrawMesh(mesh, transform.position, transform.rotation * Quaternion.AngleAxis(225, Vector3.forward),
scale);
Gizmos.DrawMesh(mesh, transform.position, transform.rotation * Quaternion.AngleAxis(270, Vector3.forward),
scale);
Gizmos.DrawMesh(mesh, transform.position, transform.rotation * Quaternion.AngleAxis(315, Vector3.forward),
scale);
}
private Vector2[] GeneratePolarPatternVertices2D(float alpha, float order, int numVertices)
{
Vector2[] points = new Vector2[numVertices];
float interval = Mathf.PI / (numVertices - 1);
for (int i = 0; i < numVertices; ++i)
{
float theta = 0.0f;
if (i != numVertices - 1)
theta = i * interval;
else
theta = Mathf.PI;
// Magnitude |r| for |theta| in radians.
float r = Mathf.Pow(Mathf.Abs((1 - alpha) + alpha * Mathf.Cos(theta)), order);
points[i] = new Vector2(r * Mathf.Sin(theta), r * Mathf.Cos(theta));
}
return points;
}
#endif
}