432 lines
16 KiB
C#
432 lines
16 KiB
C#
|
// Copyright 2016 Google Inc. All rights reserved.
|
||
|
//
|
||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
// you may not use this file except in compliance with the License.
|
||
|
// You may obtain a copy of the License at
|
||
|
//
|
||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||
|
//
|
||
|
// Unless required by applicable law or agreed to in writing, software
|
||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
// See the License for the specific language governing permissions and
|
||
|
// limitations under the License.
|
||
|
|
||
|
using UnityEngine;
|
||
|
using System;
|
||
|
using System.Collections;
|
||
|
using System.Collections.Generic;
|
||
|
using System.IO;
|
||
|
using System.Runtime.InteropServices;
|
||
|
|
||
|
/// This is the main GVR audio class that communicates with the native code implementation of
|
||
|
/// the audio system. Native functions of the system can only be called through this class to
|
||
|
/// preserve the internal system functionality. Public function calls are *not* thread-safe.
|
||
|
#if UNITY_2017_1_OR_NEWER
|
||
|
[System.Obsolete("GvrAudio is deprecated. Please upgrade to Resonance Audio (https://developers.google.com/resonance-audio/migrate).")]
|
||
|
#endif // UNITY_2017_1_OR_NEWER
|
||
|
public static class GvrAudio {
|
||
|
/// Audio system rendering quality.
|
||
|
public enum Quality {
|
||
|
Stereo = 0, ///< Stereo-only rendering
|
||
|
Low = 1, ///< Low quality binaural rendering (first-order HRTF)
|
||
|
High = 2 ///< High quality binaural rendering (third-order HRTF)
|
||
|
}
|
||
|
|
||
|
/// Native audio spatializer effect data.
|
||
|
public enum SpatializerData {
|
||
|
Id = 0, /// ID.
|
||
|
Type = 1, /// Spatializer type.
|
||
|
NumChannels = 2, /// Number of input channels.
|
||
|
ChannelSet = 3, /// Soundfield channel set.
|
||
|
Gain = 4, /// Gain.
|
||
|
DistanceAttenuation = 5, /// Computed distance attenuation.
|
||
|
MinDistance = 6, /// Minimum distance for distance-based attenuation.
|
||
|
ZeroOutput = 7, /// Should zero out the output buffer?
|
||
|
}
|
||
|
|
||
|
/// Native audio spatializer type.
|
||
|
public enum SpatializerType {
|
||
|
Source = 0, /// 3D sound object.
|
||
|
Soundfield = 1 /// First-order ambisonic soundfield.
|
||
|
}
|
||
|
|
||
|
/// System sampling rate.
|
||
|
public static int SampleRate {
|
||
|
get { return sampleRate; }
|
||
|
}
|
||
|
private static int sampleRate = -1;
|
||
|
|
||
|
/// System number of output channels.
|
||
|
public static int NumChannels {
|
||
|
get { return numChannels; }
|
||
|
}
|
||
|
private static int numChannels = -1;
|
||
|
|
||
|
/// System number of frames per buffer.
|
||
|
public static int FramesPerBuffer {
|
||
|
get { return framesPerBuffer; }
|
||
|
}
|
||
|
private static int framesPerBuffer = -1;
|
||
|
|
||
|
/// Initializes the audio system with the current audio configuration.
|
||
|
/// @note This should only be called from the main Unity thread.
|
||
|
public static void Initialize (GvrAudioListener listener, Quality quality) {
|
||
|
if (!initialized) {
|
||
|
// Initialize the audio system.
|
||
|
AudioConfiguration config = AudioSettings.GetConfiguration();
|
||
|
sampleRate = config.sampleRate;
|
||
|
numChannels = (int)config.speakerMode;
|
||
|
framesPerBuffer = config.dspBufferSize;
|
||
|
if (numChannels != (int)AudioSpeakerMode.Stereo) {
|
||
|
Debug.LogError("Only 'Stereo' speaker mode is supported by GVR Audio.");
|
||
|
return;
|
||
|
}
|
||
|
Initialize((int) quality, sampleRate, numChannels, framesPerBuffer);
|
||
|
listenerTransform = listener.transform;
|
||
|
|
||
|
initialized = true;
|
||
|
} else if (listener.transform != listenerTransform) {
|
||
|
Debug.LogError("Only one GvrAudioListener component is allowed in the scene.");
|
||
|
GvrAudioListener.Destroy(listener);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Shuts down the audio system.
|
||
|
/// @note This should only be called from the main Unity thread.
|
||
|
public static void Shutdown (GvrAudioListener listener) {
|
||
|
if (initialized && listener.transform == listenerTransform) {
|
||
|
initialized = false;
|
||
|
|
||
|
Shutdown();
|
||
|
sampleRate = -1;
|
||
|
numChannels = -1;
|
||
|
framesPerBuffer = -1;
|
||
|
listenerTransform = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Updates the audio listener.
|
||
|
/// @note This should only be called from the main Unity thread.
|
||
|
public static void UpdateAudioListener (float globalGainDb, LayerMask occlusionMask) {
|
||
|
if (initialized) {
|
||
|
occlusionMaskValue = occlusionMask.value;
|
||
|
SetListenerGain(ConvertAmplitudeFromDb(globalGainDb));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Creates a new first-order ambisonic soundfield with a unique id.
|
||
|
/// @note This should only be called from the main Unity thread.
|
||
|
public static int CreateAudioSoundfield () {
|
||
|
int soundfieldId = -1;
|
||
|
if (initialized) {
|
||
|
soundfieldId = CreateSoundfield(numFoaChannels);
|
||
|
}
|
||
|
return soundfieldId;
|
||
|
}
|
||
|
|
||
|
/// Updates the |soundfield| with given |id| and its properties.
|
||
|
/// @note This should only be called from the main Unity thread.
|
||
|
public static void UpdateAudioSoundfield (int id, GvrAudioSoundfield soundfield) {
|
||
|
if (initialized) {
|
||
|
SetSourceBypassRoomEffects(id, soundfield.bypassRoomEffects);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Creates a new audio source with a unique id.
|
||
|
/// @note This should only be called from the main Unity thread.
|
||
|
public static int CreateAudioSource (bool hrtfEnabled) {
|
||
|
int sourceId = -1;
|
||
|
if (initialized) {
|
||
|
sourceId = CreateSoundObject(hrtfEnabled);
|
||
|
}
|
||
|
return sourceId;
|
||
|
}
|
||
|
|
||
|
/// Destroys the audio source with given |id|.
|
||
|
/// @note This should only be called from the main Unity thread.
|
||
|
public static void DestroyAudioSource (int id) {
|
||
|
if (initialized) {
|
||
|
DestroySource(id);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Updates the audio |source| with given |id| and its properties.
|
||
|
/// @note This should only be called from the main Unity thread.
|
||
|
public static void UpdateAudioSource (int id, GvrAudioSource source, float currentOcclusion) {
|
||
|
if (initialized) {
|
||
|
SetSourceBypassRoomEffects(id, source.bypassRoomEffects);
|
||
|
SetSourceDirectivity(id, source.directivityAlpha, source.directivitySharpness);
|
||
|
SetSourceListenerDirectivity(id, source.listenerDirectivityAlpha,
|
||
|
source.listenerDirectivitySharpness);
|
||
|
SetSourceOcclusionIntensity(id, currentOcclusion);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Updates the room effects of the environment with given |room| properties.
|
||
|
/// @note This should only be called from the main Unity thread.
|
||
|
public static void UpdateAudioRoom(GvrAudioRoom room, bool roomEnabled) {
|
||
|
// Update the enabled rooms list.
|
||
|
if (roomEnabled) {
|
||
|
if (!enabledRooms.Contains(room)) {
|
||
|
enabledRooms.Add(room);
|
||
|
}
|
||
|
} else {
|
||
|
enabledRooms.Remove(room);
|
||
|
}
|
||
|
// Update the current room effects to be applied.
|
||
|
if(initialized) {
|
||
|
if (enabledRooms.Count > 0) {
|
||
|
GvrAudioRoom currentRoom = enabledRooms[enabledRooms.Count - 1];
|
||
|
RoomProperties roomProperties = GetRoomProperties(currentRoom);
|
||
|
// Pass the room properties into a pointer.
|
||
|
IntPtr roomPropertiesPtr = Marshal.AllocHGlobal(Marshal.SizeOf(roomProperties));
|
||
|
Marshal.StructureToPtr(roomProperties, roomPropertiesPtr, false);
|
||
|
SetRoomProperties(roomPropertiesPtr);
|
||
|
Marshal.FreeHGlobal(roomPropertiesPtr);
|
||
|
} else {
|
||
|
// Set the room properties to null, which will effectively disable the room effects.
|
||
|
SetRoomProperties(IntPtr.Zero);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Computes the occlusion intensity of a given |source| using point source detection.
|
||
|
/// @note This should only be called from the main Unity thread.
|
||
|
public static float ComputeOcclusion (Transform sourceTransform) {
|
||
|
float occlusion = 0.0f;
|
||
|
if (initialized) {
|
||
|
Vector3 listenerPosition = listenerTransform.position;
|
||
|
Vector3 sourceFromListener = sourceTransform.position - listenerPosition;
|
||
|
int numHits = Physics.RaycastNonAlloc(listenerPosition, sourceFromListener, occlusionHits,
|
||
|
sourceFromListener.magnitude, occlusionMaskValue);
|
||
|
for (int i = 0; i < numHits; ++i) {
|
||
|
if (occlusionHits[i].transform != listenerTransform &&
|
||
|
occlusionHits[i].transform != sourceTransform) {
|
||
|
occlusion += 1.0f;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return occlusion;
|
||
|
}
|
||
|
|
||
|
/// Converts given |db| value to its amplitude equivalent where 'dB = 20 * log10(amplitude)'.
|
||
|
public static float ConvertAmplitudeFromDb (float db) {
|
||
|
return Mathf.Pow(10.0f, 0.05f * db);
|
||
|
}
|
||
|
|
||
|
/// Generates a set of points to draw a 2D polar pattern.
|
||
|
public static Vector2[] Generate2dPolarPattern (float alpha, float order, int resolution) {
|
||
|
Vector2[] points = new Vector2[resolution];
|
||
|
float interval = 2.0f * Mathf.PI / resolution;
|
||
|
for (int i = 0; i < resolution; ++i) {
|
||
|
float theta = i * interval;
|
||
|
// 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;
|
||
|
}
|
||
|
|
||
|
/// Returns whether the listener is currently inside the given |room| boundaries.
|
||
|
public static bool IsListenerInsideRoom(GvrAudioRoom room) {
|
||
|
bool isInside = false;
|
||
|
if(initialized) {
|
||
|
Vector3 relativePosition = listenerTransform.position - room.transform.position;
|
||
|
Quaternion rotationInverse = Quaternion.Inverse(room.transform.rotation);
|
||
|
|
||
|
bounds.size = Vector3.Scale(room.transform.lossyScale, room.size);
|
||
|
isInside = bounds.Contains(rotationInverse * relativePosition);
|
||
|
}
|
||
|
return isInside;
|
||
|
}
|
||
|
|
||
|
/// Listener directivity GUI color.
|
||
|
public static readonly Color listenerDirectivityColor = 0.65f * Color.magenta;
|
||
|
|
||
|
/// Source directivity GUI color.
|
||
|
public static readonly Color sourceDirectivityColor = 0.65f * Color.blue;
|
||
|
|
||
|
/// Minimum distance threshold between |minDistance| and |maxDistance|.
|
||
|
public const float distanceEpsilon = 0.01f;
|
||
|
|
||
|
/// Max distance limit that can be set for volume rolloff.
|
||
|
public const float maxDistanceLimit = 1000000.0f;
|
||
|
|
||
|
/// Min distance limit that can be set for volume rolloff.
|
||
|
public const float minDistanceLimit = 990099.0f;
|
||
|
|
||
|
/// Maximum allowed gain value in decibels.
|
||
|
public const float maxGainDb = 24.0f;
|
||
|
|
||
|
/// Minimum allowed gain value in decibels.
|
||
|
public const float minGainDb = -24.0f;
|
||
|
|
||
|
/// Maximum allowed reverb brightness modifier value.
|
||
|
public const float maxReverbBrightness = 1.0f;
|
||
|
|
||
|
/// Minimum allowed reverb brightness modifier value.
|
||
|
public const float minReverbBrightness = -1.0f;
|
||
|
|
||
|
/// Maximum allowed reverb time modifier value.
|
||
|
public const float maxReverbTime = 3.0f;
|
||
|
|
||
|
/// Maximum allowed reflectivity multiplier of a room surface material.
|
||
|
public const float maxReflectivity = 2.0f;
|
||
|
|
||
|
/// Maximum allowed number of raycast hits for occlusion computation per source.
|
||
|
public const int maxNumOcclusionHits = 12;
|
||
|
|
||
|
/// Source occlusion detection rate in seconds.
|
||
|
public const float occlusionDetectionInterval = 0.2f;
|
||
|
|
||
|
/// Number of first-order ambisonic input channels.
|
||
|
public const int numFoaChannels = 4;
|
||
|
|
||
|
[StructLayout(LayoutKind.Sequential)]
|
||
|
private struct RoomProperties {
|
||
|
// Center position of the room in world space.
|
||
|
public float positionX;
|
||
|
public float positionY;
|
||
|
public float positionZ;
|
||
|
|
||
|
// Rotation (quaternion) of the room in world space.
|
||
|
public float rotationX;
|
||
|
public float rotationY;
|
||
|
public float rotationZ;
|
||
|
public float rotationW;
|
||
|
|
||
|
// Size of the shoebox room in world space.
|
||
|
public float dimensionsX;
|
||
|
public float dimensionsY;
|
||
|
public float dimensionsZ;
|
||
|
|
||
|
// Material name of each surface of the shoebox room.
|
||
|
public GvrAudioRoom.SurfaceMaterial materialLeft;
|
||
|
public GvrAudioRoom.SurfaceMaterial materialRight;
|
||
|
public GvrAudioRoom.SurfaceMaterial materialBottom;
|
||
|
public GvrAudioRoom.SurfaceMaterial materialTop;
|
||
|
public GvrAudioRoom.SurfaceMaterial materialFront;
|
||
|
public GvrAudioRoom.SurfaceMaterial materialBack;
|
||
|
|
||
|
// User defined uniform scaling factor for reflectivity. This parameter has no effect when set
|
||
|
// to 1.0f.
|
||
|
public float reflectionScalar;
|
||
|
|
||
|
// User defined reverb tail gain multiplier. This parameter has no effect when set to 0.0f.
|
||
|
public float reverbGain;
|
||
|
|
||
|
// Adjusts the reverberation time across all frequency bands. RT60 values are multiplied by this
|
||
|
// factor. Has no effect when set to 1.0f.
|
||
|
public float reverbTime;
|
||
|
|
||
|
// Controls the slope of a line from the lowest to the highest RT60 values (increases high
|
||
|
// frequency RT60s when positive, decreases when negative). Has no effect when set to 0.0f.
|
||
|
public float reverbBrightness;
|
||
|
};
|
||
|
|
||
|
// Converts given |position| and |rotation| from Unity space to audio space.
|
||
|
private static void ConvertAudioTransformFromUnity (ref Vector3 position,
|
||
|
ref Quaternion rotation) {
|
||
|
transformMatrix = Pose3D.FlipHandedness(Matrix4x4.TRS(position, rotation, Vector3.one));
|
||
|
position = transformMatrix.GetColumn(3);
|
||
|
rotation = Quaternion.LookRotation(transformMatrix.GetColumn(2), transformMatrix.GetColumn(1));
|
||
|
}
|
||
|
|
||
|
// Returns room properties of the given |room|.
|
||
|
private static RoomProperties GetRoomProperties(GvrAudioRoom room) {
|
||
|
RoomProperties roomProperties;
|
||
|
Vector3 position = room.transform.position;
|
||
|
Quaternion rotation = room.transform.rotation;
|
||
|
Vector3 scale = Vector3.Scale(room.transform.lossyScale, room.size);
|
||
|
ConvertAudioTransformFromUnity(ref position, ref rotation);
|
||
|
roomProperties.positionX = position.x;
|
||
|
roomProperties.positionY = position.y;
|
||
|
roomProperties.positionZ = position.z;
|
||
|
roomProperties.rotationX = rotation.x;
|
||
|
roomProperties.rotationY = rotation.y;
|
||
|
roomProperties.rotationZ = rotation.z;
|
||
|
roomProperties.rotationW = rotation.w;
|
||
|
roomProperties.dimensionsX = scale.x;
|
||
|
roomProperties.dimensionsY = scale.y;
|
||
|
roomProperties.dimensionsZ = scale.z;
|
||
|
roomProperties.materialLeft = room.leftWall;
|
||
|
roomProperties.materialRight = room.rightWall;
|
||
|
roomProperties.materialBottom = room.floor;
|
||
|
roomProperties.materialTop = room.ceiling;
|
||
|
roomProperties.materialFront = room.frontWall;
|
||
|
roomProperties.materialBack = room.backWall;
|
||
|
roomProperties.reverbGain = ConvertAmplitudeFromDb(room.reverbGainDb);
|
||
|
roomProperties.reverbTime = room.reverbTime;
|
||
|
roomProperties.reverbBrightness = room.reverbBrightness;
|
||
|
roomProperties.reflectionScalar = room.reflectivity;
|
||
|
return roomProperties;
|
||
|
}
|
||
|
|
||
|
// Boundaries instance to be used in room detection logic.
|
||
|
private static Bounds bounds = new Bounds(Vector3.zero, Vector3.zero);
|
||
|
|
||
|
// Container to store the currently active rooms in the scene.
|
||
|
private static List<GvrAudioRoom> enabledRooms = new List<GvrAudioRoom>();
|
||
|
|
||
|
// Denotes whether the system is initialized properly.
|
||
|
private static bool initialized = false;
|
||
|
|
||
|
// Listener transform.
|
||
|
private static Transform listenerTransform = null;
|
||
|
|
||
|
// Pre-allocated raycast hit list for occlusion computation.
|
||
|
private static RaycastHit[] occlusionHits = new RaycastHit[maxNumOcclusionHits];
|
||
|
|
||
|
// Occlusion layer mask.
|
||
|
private static int occlusionMaskValue = -1;
|
||
|
|
||
|
// 4x4 transformation matrix to be used in transform space conversion.
|
||
|
private static Matrix4x4 transformMatrix = Matrix4x4.identity;
|
||
|
|
||
|
#if !UNITY_EDITOR && UNITY_IOS
|
||
|
private const string pluginName = "__Internal";
|
||
|
#else
|
||
|
private const string pluginName = "audioplugingvrunity";
|
||
|
#endif // !UNITY_EDITOR && UNITY_IOS
|
||
|
|
||
|
// Listener handlers.
|
||
|
[DllImport(pluginName)]
|
||
|
private static extern void SetListenerGain (float gain);
|
||
|
|
||
|
// Soundfield handlers.
|
||
|
[DllImport(pluginName)]
|
||
|
private static extern int CreateSoundfield (int numChannels);
|
||
|
|
||
|
// Source handlers.
|
||
|
[DllImport(pluginName)]
|
||
|
private static extern int CreateSoundObject (bool enableHrtf);
|
||
|
|
||
|
[DllImport(pluginName)]
|
||
|
private static extern void DestroySource (int sourceId);
|
||
|
|
||
|
[DllImport(pluginName)]
|
||
|
private static extern void SetSourceBypassRoomEffects (int sourceId, bool bypassRoomEffects);
|
||
|
|
||
|
[DllImport(pluginName)]
|
||
|
private static extern void SetSourceDirectivity (int sourceId, float alpha, float order);
|
||
|
|
||
|
[DllImport(pluginName)]
|
||
|
private static extern void SetSourceListenerDirectivity (int sourceId, float alpha, float order);
|
||
|
|
||
|
[DllImport(pluginName)]
|
||
|
private static extern void SetSourceOcclusionIntensity (int sourceId, float intensity);
|
||
|
|
||
|
// Room handlers.
|
||
|
[DllImport(pluginName)]
|
||
|
private static extern void SetRoomProperties (IntPtr roomProperties);
|
||
|
|
||
|
// System handlers.
|
||
|
[DllImport(pluginName)]
|
||
|
private static extern void Initialize (int quality, int sampleRate, int numChannels,
|
||
|
int framesPerBuffer);
|
||
|
|
||
|
[DllImport(pluginName)]
|
||
|
private static extern void Shutdown ();
|
||
|
}
|