// 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 enabledRooms = new List(); // 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 (); }