Add ultimate xr

This commit is contained in:
2024-08-06 21:58:35 +02:00
parent 864033bf10
commit 7165bacd9d
3952 changed files with 2162037 additions and 35 deletions

View File

@@ -0,0 +1,113 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="AudioClipExt.PcmData.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System;
using UltimateXR.Extensions.System;
namespace UltimateXR.Extensions.Unity.Audio
{
public static partial class AudioClipExt
{
#region Private Types & Data
/// <summary>
/// Container of PCM audio data.
/// </summary>
private readonly struct PcmData
{
#region Public Types & Data
/// <summary>
/// Gets the sample data.
/// </summary>
public float[] Value { get; }
/// <summary>
/// Gets the sample count.
/// </summary>
public int Length { get; }
/// <summary>
/// Gets the number of audio channels.
/// </summary>
public int Channels { get; }
/// <summary>
/// Gets the sample rate in Hz.
/// </summary>
public int SampleRate { get; }
#endregion
#region Constructors & Finalizer
/// <summary>
/// Constructor.
/// </summary>
/// <param name="value">Sample data</param>
/// <param name="channels">Audio channel count</param>
/// <param name="sampleRate">Sample rate in Hz</param>
private PcmData(float[] value, int channels, int sampleRate)
{
Value = value;
Length = value.Length;
Channels = channels;
SampleRate = sampleRate;
}
#endregion
#region Public Methods
/// <summary>
/// Creates a <see cref="PcmData" /> object from a byte data array.
/// </summary>
/// <param name="bytes">Byte data array with the PCM header and sample data</param>
/// <returns><see cref="PcmData" /> object with the audio data</returns>
/// <exception cref="ArgumentOutOfRangeException">The PCM header contains invalid data</exception>
public static PcmData FromBytes(byte[] bytes)
{
bytes.ThrowIfNull(nameof(bytes));
PcmHeader pcmHeader = PcmHeader.FromBytes(bytes);
if (pcmHeader.BitDepth != 16 && pcmHeader.BitDepth != 32 && pcmHeader.BitDepth != 8)
{
throw new ArgumentOutOfRangeException(nameof(pcmHeader.BitDepth), pcmHeader.BitDepth, "Supported values are: 8, 16, 32");
}
float[] samples = new float[pcmHeader.AudioSampleCount];
for (int i = 0; i < samples.Length; ++i)
{
int byteIndex = pcmHeader.AudioStartIndex + i * pcmHeader.AudioSampleSize;
float rawSample;
switch (pcmHeader.BitDepth)
{
case 8:
rawSample = bytes[byteIndex];
break;
case 16:
rawSample = BitConverter.ToInt16(bytes, byteIndex);
break;
case 32:
rawSample = BitConverter.ToInt32(bytes, byteIndex);
break;
default: throw new ArgumentOutOfRangeException(nameof(pcmHeader.BitDepth), pcmHeader.BitDepth, "Supported values are: 8, 16, 32");
}
samples[i] = pcmHeader.NormalizeSample(rawSample); // normalize sample between [-1f, 1f]
}
return new PcmData(samples, pcmHeader.Channels, pcmHeader.SampleRate);
}
#endregion
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bff70ee0e42241268ac7cd1479f8f5cc
timeCreated: 1630064761

View File

@@ -0,0 +1,195 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="AudioClipExt.PcmHeader.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.IO;
using UnityEngine;
namespace UltimateXR.Extensions.Unity.Audio
{
public static partial class AudioClipExt
{
#region Private Types & Data
/// <summary>
/// Describes a PCM audio data header.
/// </summary>
private readonly struct PcmHeader
{
#region Public Types & Data
/// <summary>
/// Gets the bits per audio sample.
/// </summary>
public int BitDepth { get; }
/// <summary>
/// Gets the audio total sample size in bytes.
/// </summary>
public int AudioSampleSize { get; }
/// <summary>
/// Gets the number of audio samples.
/// </summary>
public int AudioSampleCount { get; }
/// <summary>
/// Gets the number of audio channels.
/// </summary>
public ushort Channels { get; }
/// <summary>
/// Gets the sample rate in audio samples per second.
/// </summary>
public int SampleRate { get; }
/// <summary>
/// Gets the data index where the audio data starts.
/// </summary>
public int AudioStartIndex { get; }
/// <summary>
/// Gets the audio data bytes per second.
/// </summary>
public int ByteRate { get; }
/// <summary>
/// Gets the data block alignment.
/// </summary>
public ushort BlockAlign { get; }
#endregion
#region Constructors & Finalizer
/// <summary>
/// Constructor.
/// </summary>
/// <param name="bitDepth">Sample bit size</param>
/// <param name="audioSize">Total audio data size in bytes</param>
/// <param name="audioStartIndex">Index where the audio sample data starts</param>
/// <param name="channels">The number of audio channels</param>
/// <param name="sampleRate">The number of samples per second</param>
/// <param name="byteRate">The number of bytes per second</param>
/// <param name="blockAlign">The block alignment</param>
private PcmHeader(int bitDepth,
int audioSize,
int audioStartIndex,
ushort channels,
int sampleRate,
int byteRate,
ushort blockAlign)
{
BitDepth = bitDepth;
_negativeDepth = Mathf.Pow(2f, BitDepth - 1f);
_positiveDepth = _negativeDepth - 1f;
AudioSampleSize = bitDepth / 8;
AudioSampleCount = Mathf.FloorToInt(audioSize / (float)AudioSampleSize);
AudioStartIndex = audioStartIndex;
Channels = channels;
SampleRate = sampleRate;
ByteRate = byteRate;
BlockAlign = blockAlign;
}
#endregion
#region Public Methods
/// <summary>
/// Creates a <see cref="PcmHeader" /> object reading from a byte array.
/// </summary>
/// <param name="pcmBytes">Source byte array</param>
/// <returns><see cref="PcmHeader" /> object</returns>
public static PcmHeader FromBytes(byte[] pcmBytes)
{
using var memoryStream = new MemoryStream(pcmBytes);
return FromStream(memoryStream);
}
/// <summary>
/// Creates a <see cref="PcmHeader" /> object reading from a data stream.
/// </summary>
/// <param name="pcmStream">Source data</param>
/// <returns><see cref="PcmHeader" /> object</returns>
public static PcmHeader FromStream(Stream pcmStream)
{
pcmStream.Position = SizeIndex;
using BinaryReader reader = new BinaryReader(pcmStream);
int headerSize = reader.ReadInt32(); // 16
ushort audioFormatCode = reader.ReadUInt16(); // 20
string audioFormat = GetAudioFormatFromCode(audioFormatCode);
if (audioFormatCode != 1 && audioFormatCode == 65534)
{
// Only uncompressed PCM wav files are supported.
throw new ArgumentOutOfRangeException(nameof(pcmStream),
$"Detected format code '{audioFormatCode}' {audioFormat}, but only PCM and WaveFormatExtensible uncompressed formats are currently supported.");
}
ushort channelCount = reader.ReadUInt16(); // 22
int sampleRate = reader.ReadInt32(); // 24
int byteRate = reader.ReadInt32(); // 28
ushort blockAlign = reader.ReadUInt16(); // 32
ushort bitDepth = reader.ReadUInt16(); //34
pcmStream.Position = SizeIndex + headerSize + 2 * sizeof(int); // Header end index
int audioSize = reader.ReadInt32(); // Audio size index
return new PcmHeader(bitDepth, audioSize, (int)pcmStream.Position, channelCount, sampleRate, byteRate, blockAlign); // audio start index
}
/// <summary>
/// Normalizes a raw audio sample.
/// </summary>
/// <param name="rawSample">Audio sample to normalize</param>
/// <returns>Normalized audio sample</returns>
public float NormalizeSample(float rawSample)
{
float sampleDepth = rawSample < 0 ? _negativeDepth : _positiveDepth;
return rawSample / sampleDepth;
}
#endregion
#region Private Methods
/// <summary>
/// Gets the audio format string from the numerical code.
/// </summary>
/// <param name="code">Numerical audio format code</param>
/// <returns>Audio format string</returns>
/// <exception cref="ArgumentOutOfRangeException">The code is not valid</exception>
private static string GetAudioFormatFromCode(ushort code)
{
switch (code)
{
case 1: return "PCM";
case 2: return "ADPCM";
case 3: return "IEEE";
case 7: return "?-law";
case 65534: return "WaveFormatExtensible";
default: throw new ArgumentOutOfRangeException(nameof(code), code, "Unknown wav code format.");
}
}
#endregion
#region Private Types & Data
private const int SizeIndex = 16;
private readonly float _positiveDepth;
private readonly float _negativeDepth;
#endregion
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2bfbb7d822e34d8f92097fa24960e1a6
timeCreated: 1630065461

View File

@@ -0,0 +1,138 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="AudioClipExt.StreamedAudioClip.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.IO;
using UltimateXR.Extensions.System;
using UnityEngine;
namespace UltimateXR.Extensions.Unity.Audio
{
public static partial class AudioClipExt
{
#region Public Types & Data
/// <summary>
/// Describes a PCM audio clip.
/// </summary>
public sealed class StreamedPcmClip : IDisposable
{
#region Public Types & Data
/// <summary>
/// Gets the <see cref="AudioClip" /> described by the object.
/// </summary>
public AudioClip InnerClip { get; }
#endregion
#region Constructors & Finalizer
/// <summary>
/// Constructor.
/// </summary>
/// <param name="pcmStream">PCM data</param>
/// <param name="clipName">Name assigned to the audio clip</param>
/// <param name="header">PCM data header</param>
private StreamedPcmClip(Stream pcmStream, string clipName, in PcmHeader header)
{
_pcmHeader = header;
_pcmStream = pcmStream;
_pcmReader = new BinaryReader(pcmStream);
InnerClip = AudioClip.Create(clipName, header.AudioSampleCount, header.Channels, header.SampleRate, true, OnPcmRead, OnPcmSetPosition);
}
#endregion
#region Implicit IDisposable
/// <inheritdoc />
public void Dispose()
{
_pcmReader.Dispose();
}
#endregion
#region Public Methods
/// <summary>
/// Creates a <see cref="StreamedPcmClip" /> object from a data stream.
/// </summary>
/// <param name="pcmStream">Source data stream</param>
/// <param name="clipName">Name that will be assigned to the clip</param>
/// <returns><see cref="StreamedPcmClip" /> describing the PCM audio clip</returns>
/// <exception cref="ArgumentOutOfRangeException">The bit depth is not supported</exception>
public static StreamedPcmClip Create(Stream pcmStream, string clipName = "pcm")
{
pcmStream.ThrowIfNull(nameof(pcmStream));
clipName.ThrowIfNullOrWhitespace(nameof(clipName));
var pcmHeader = PcmHeader.FromStream(pcmStream);
if (pcmHeader.BitDepth != 16 && pcmHeader.BitDepth != 32 && pcmHeader.BitDepth != 8)
{
throw new ArgumentOutOfRangeException(nameof(pcmHeader.BitDepth), pcmHeader.BitDepth, "Supported values are: 8, 16, 32");
}
return new StreamedPcmClip(pcmStream, clipName, in pcmHeader);
}
#endregion
#region Event Trigger Methods
/// <summary>
/// PCM reader callback.
/// </summary>
/// <param name="data">Source data</param>
/// <exception cref="ArgumentOutOfRangeException">Unsupported audio bit depth</exception>
private void OnPcmRead(float[] data)
{
for (int i = 0; i < data.Length && _pcmStream.Position < _pcmStream.Length; ++i)
{
float rawSample;
switch (_pcmHeader.AudioSampleSize)
{
case 1:
rawSample = _pcmReader.ReadByte();
break;
case 2:
rawSample = _pcmReader.ReadInt16();
break;
case 3:
rawSample = _pcmReader.ReadInt32();
break;
default: throw new ArgumentOutOfRangeException(nameof(_pcmHeader.BitDepth), _pcmHeader.BitDepth, "Supported values are: 8, 16, 32");
}
data[i] = _pcmHeader.NormalizeSample(rawSample); // needs to be scaled to be within the range of - 1.0f to 1.0f.
}
}
/// <summary>
/// PCM reader positioning callback.
/// </summary>
/// <param name="newPosition">New index where to position the read cursor</param>
private void OnPcmSetPosition(int newPosition)
{
_pcmStream.Position = _pcmHeader.AudioStartIndex + newPosition * _pcmHeader.AudioSampleSize;
}
#endregion
#region Private Types & Data
private readonly Stream _pcmStream;
private readonly BinaryReader _pcmReader;
private readonly PcmHeader _pcmHeader;
#endregion
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2f3e1a097a2b4481b486c9cc54c4a03c
timeCreated: 1629966369

View File

@@ -0,0 +1,269 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="AudioClipExt.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using UltimateXR.Exceptions;
using UltimateXR.Extensions.System;
using UltimateXR.Extensions.System.IO;
using UltimateXR.Extensions.Unity.IO;
using UnityEngine;
namespace UltimateXR.Extensions.Unity.Audio
{
/// <summary>
/// Audio extensions.
/// </summary>
public static partial class AudioClipExt
{
#region Public Methods
/// <summary>
/// Ubiquitously plays an <see cref="AudioClip" />.
/// </summary>
/// <remarks>
/// This function creates an <see cref="AudioSource" /> but automatically disposes of it once the clip has finished
/// playing.
/// </remarks>
/// <param name="self">Reference to the sound clip file that will be played.</param>
/// <param name="volume">How loud the sound is at a distance of one world unit (one meter) [0.0, 1.0].</param>
/// <param name="delay">Delay time specified in seconds.</param>
/// <param name="pitch">
/// Amount of change in pitch due to slowdown/speed up of the Audio Clip. Value 1 is normal playback speed.
/// </param>
/// <param name="offsetSeconds">Start offset in seconds</param>
/// <returns>The just created temporal <see cref="AudioSource" />.</returns>
/// <seealso cref="AudioSourceExt.PlayClip" />
public static AudioSource PlayClip(AudioClip self,
float volume = 1.0f,
float delay = 0.0f,
float pitch = 1.0f,
float offsetSeconds = 0.0f)
{
return AudioSourceExt.PlayClip(self, volume, delay, pitch, offsetSeconds);
}
/// <summary>
/// Plays an AudioClip at a given position in world space.
/// </summary>
/// <remarks>
/// This function creates an <see cref="AudioSource" /> but automatically disposes of it once the clip has finished
/// playing.
/// </remarks>
/// <param name="self">Reference to the sound clip file that will be played.</param>
/// <param name="point">Position in world space from which sound originates.</param>
/// <param name="volume">How loud the sound is at a distance of one world unit (one meter) [0.0, 1.0].</param>
/// <param name="delay">Delay time specified in seconds.</param>
/// <param name="pitch">
/// Amount of change in pitch due to slowdown/speed up of the Audio Clip. Value 1 is normal playback
/// speed.
/// </param>
/// <param name="spatialBlend">Sets how much the 3D engine has an effect on the audio source [0.0, 1.0].</param>
/// <param name="offsetSeconds">Start offset in seconds</param>
/// <returns>The just created temporal <see cref="AudioSource" />.</returns>
/// <seealso cref="AudioSourceExt.PlayClipAtPoint" />
public static AudioSource PlayClipAtPoint(AudioClip self,
Vector3 point,
float volume = 1.0f,
float delay = 0.0f,
float pitch = 1.0f,
float spatialBlend = AudioSourceExt.SpatialBlend3D,
float offsetSeconds = 0.0f)
{
return AudioSourceExt.PlayClipAtPoint(self, point, volume, delay, pitch, spatialBlend, offsetSeconds);
}
/// <summary>
/// Asynchronous and ubiquitously plays the <see cref="AudioClip" />.
/// </summary>
/// <remarks>
/// This function creates an <see cref="AudioSource" /> but automatically disposes of it once the clip has finished
/// playing.
/// </remarks>
/// <param name="self">Reference to the sound clip file that will be played.</param>
/// <param name="volume">How loud the sound is at a distance of one world unit (one meter) [0.0, 1.0].</param>
/// <param name="delay">Delay time specified in seconds.</param>
/// <param name="pitch">
/// Amount of change in pitch due to slowdown/speed up of the Audio Clip. Value 1 is normal playback
/// speed.
/// </param>
/// <param name="offsetSeconds">Start offset in seconds</param>
/// <param name="ct"><see cref="CancellationToken" /> to stop playing.</param>
/// <returns>An awaitable <see cref="Task" />.</returns>
/// <seealso cref="AudioSourceExt.PlayClipAsync" />
public static Task PlayAsync(this AudioClip self,
float volume = 1.0f,
float delay = 0.0f,
float pitch = 1.0f,
float offsetSeconds = 0.0f,
CancellationToken ct = default)
{
return AudioSourceExt.PlayClipAsync(self, volume, delay, pitch, offsetSeconds, ct);
}
/// <summary>
/// Asynchronously plays the <see cref="AudioClip" /> at a given position in world space.
/// </summary>
/// <remarks>
/// This function creates an <see cref="AudioSource" /> but automatically disposes of it once the clip has finished
/// playing.
/// </remarks>
/// <param name="self">Reference to the sound clip file that will be played.</param>
/// <param name="point">Position in world space from which sound originates.</param>
/// <param name="volume">How loud the sound is at a distance of one world unit (one meter) [0.0, 1.0].</param>
/// <param name="delay">Delay time specified in seconds.</param>
/// <param name="pitch">
/// Amount of change in pitch due to slowdown/speed up of the Audio Clip. Value 1 is normal playback
/// speed.
/// </param>
/// <param name="spatialBlend">Sets how much the 3D engine has an effect on the audio source [0.0, 1.0].</param>
/// <param name="offsetSeconds">Start offset in seconds</param>
/// <param name="ct"><see cref="CancellationToken" /> to stop playing.</param>
/// <returns>An awaitable <see cref="Task" />.</returns>
/// <seealso cref="AudioSourceExt.PlayClipAtPointAsync" />
public static Task PlayAtPointAsync(this AudioClip self,
Vector3 point,
float volume = 1.0f,
float delay = 0.0f,
float pitch = 1.0f,
float spatialBlend = AudioSourceExt.SpatialBlend3D,
float offsetSeconds = 0.0f,
CancellationToken ct = default)
{
return AudioSourceExt.PlayClipAtPointAsync(self, point, volume, delay, pitch, spatialBlend, offsetSeconds, ct);
}
/// <summary>
/// Creates an <see cref="AudioClip" /> from a PCM stream.
/// </summary>
/// <param name="sourceStream">The source stream</param>
/// <param name="clipName">The name assigned to the clip</param>
/// <returns>The <see cref="AudioClip" /> object</returns>
public static AudioClip FromPcmStream(Stream sourceStream, string clipName = "pcm")
{
clipName.ThrowIfNullOrWhitespace(nameof(clipName));
byte[] bytes = new byte[sourceStream.Length];
sourceStream.Read(bytes, 0, bytes.Length);
return FromPcmBytes(bytes, clipName);
}
/// <summary>
/// Creates an <see cref="AudioClip" /> from a PCM stream asynchronously.
/// </summary>
/// <param name="sourceStream">The source stream</param>
/// <param name="clipName">The name assigned to the clip</param>
/// <param name="ct">The optional cancellation token, to cancel the task</param>
/// <returns>An awaitable task that returns the <see cref="AudioClip" /> object</returns>
public static async Task<AudioClip> FromPcmStreamAsync(Stream sourceStream, string clipName = "pcm", CancellationToken ct = default)
{
clipName.ThrowIfNullOrWhitespace(nameof(clipName));
byte[] bytes = new byte[sourceStream.Length];
await sourceStream.ReadAsync(bytes, 0, bytes.Length, ct);
return await FromPcmBytesAsync(bytes, clipName, ct);
}
/// <summary>
/// Creates an <see cref="AudioClip" /> from a PCM byte array.
/// </summary>
/// <param name="bytes">The source data</param>
/// <param name="clipName">The name assigned to the clip</param>
/// <returns>The <see cref="AudioClip" /> object</returns>
public static AudioClip FromPcmBytes(byte[] bytes, string clipName = "pcm")
{
clipName.ThrowIfNullOrWhitespace(nameof(clipName));
var pcmData = PcmData.FromBytes(bytes);
var audioClip = AudioClip.Create(clipName, pcmData.Length, pcmData.Channels, pcmData.SampleRate, false);
audioClip.SetData(pcmData.Value, 0);
return audioClip;
}
/// <summary>
/// Creates an <see cref="AudioClip" /> from a PCM byte array asynchronously.
/// </summary>
/// <param name="bytes">The source data</param>
/// <param name="clipName">The name assigned to the clip</param>
/// <param name="ct">The optional cancellation token, to cancel the task</param>
/// <returns>An awaitable task that returns the <see cref="AudioClip" /> object</returns>
public static async Task<AudioClip> FromPcmBytesAsync(byte[] bytes, string clipName = "pcm", CancellationToken ct = default)
{
clipName.ThrowIfNullOrWhitespace(nameof(clipName));
var pcmData = await Task.Run(() => PcmData.FromBytes(bytes), ct);
var audioClip = AudioClip.Create(clipName, pcmData.Length, pcmData.Channels, pcmData.SampleRate, false);
audioClip.SetData(pcmData.Value, 0);
return audioClip;
}
/// <summary>
/// Asynchronously reads and loads an <see cref="AudioClip" /> into memory from a given <paramref name="uri" />
/// </summary>
/// <param name="uri">Full path for <see cref="AudioClip" /> file</param>
/// <param name="ct">The optional cancellation token, to cancel the task</param>
/// <returns>Loaded <see cref="AudioClip" /></returns>
/// <exception cref="HttpUwrException">
/// HttpError flag is on
/// </exception>
/// <exception cref="NetUwrException">
/// NetworkError flag is on
/// </exception>
/// <exception cref="OperationCanceledException">
/// The task was canceled using <paramref name="ct" />
/// </exception>
public static Task<AudioClip> FromFile(string uri, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
uri.ThrowIfNullOrWhitespace(nameof(uri));
try
{
return UnityWebRequestExt.LoadAudioClipAsync(uri, ct);
}
catch (UwrException e)
{
throw new FileNotFoundException(e.Message, uri, e);
}
}
/// <summary>
/// Asynchronously reads and loads an <see cref="AudioClip" /> into memory from a given <paramref name="uri" />
/// pointing to a file with PCM bytes.
/// </summary>
/// <param name="uri">Full path with the PCM bytes</param>
/// <param name="ct">Optional cancellation token to cancel the task</param>
/// <returns>Loaded <see cref="AudioClip" /></returns>
/// <exception cref="HttpUwrException">
/// HttpError flag is on
/// </exception>
/// <exception cref="NetUwrException">
/// NetworkError flag is on
/// </exception>
/// <exception cref="OperationCanceledException">
/// The task was canceled using <paramref name="ct" />
/// </exception>
public static async Task<AudioClip> FromPcmFile(string uri, CancellationToken ct = default)
{
string fileName = Path.GetFileNameWithoutExtension(uri);
byte[] bytes = await FileExt.Read(uri, ct);
return await FromPcmBytesAsync(bytes, fileName, ct);
}
/// <summary>
/// Creates a <see cref="StreamedPcmClip" /> object from a stream containing PCM data.
/// </summary>
/// <param name="pcmStream">PCM data</param>
/// <param name="clipName">The name that will be assigned to the clip</param>
/// <returns><see cref="StreamedPcmClip" /> object</returns>
public static StreamedPcmClip CreatePcmStreamed(Stream pcmStream, string clipName = "pcm")
{
return StreamedPcmClip.Create(pcmStream, clipName);
}
#endregion
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 543e6a1c3d51e984f8ef22cd13008d57
timeCreated: 1620807376

View File

@@ -0,0 +1,277 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="AudioSourceExt.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Threading;
using System.Threading.Tasks;
using UltimateXR.Extensions.System;
using UltimateXR.Extensions.System.Threading;
using UnityEngine;
using Object = UnityEngine.Object;
namespace UltimateXR.Extensions.Unity.Audio
{
/// <summary>
/// <see cref="AudioSource" /> extensions.
/// </summary>
public static class AudioSourceExt
{
#region Public Types & Data
/// <summary>
/// Default spatial blend for 3D positioned audio.
/// </summary>
public const float SpatialBlend3D = 0.9f;
#endregion
#region Public Methods
/// <summary>
/// Ubiquitously plays an <see cref="AudioClip" />.
/// </summary>
/// <remarks>
/// This function creates an <see cref="AudioSource" /> but automatically disposes of it once the clip has finished
/// playing.
/// </remarks>
/// <param name="clip">Reference to the sound clip file that will be played.</param>
/// <param name="volume">How loud the sound is at a distance of one world unit (one meter) [0.0, 1.0].</param>
/// <param name="delay">Delay time specified in seconds.</param>
/// <param name="pitch">
/// Amount of change in pitch due to slowdown/speed up of the Audio Clip. Value 1 is normal playback
/// speed.
/// </param>
/// <param name="offsetSeconds">Start offset in seconds</param>
/// <returns>The just created temporal <see cref="AudioSource" />.</returns>
public static AudioSource PlayClip(AudioClip clip,
float volume = 1.0f,
float delay = 0.0f,
float pitch = 1.0f,
float offsetSeconds = 0.0f)
{
if (!Application.isPlaying)
{
throw new InvalidOperationException("Playback is only allowed while playing.");
}
clip.ThrowIfNull(nameof(clip));
volume = Mathf.Clamp01(volume);
pitch = Mathf.Clamp01(pitch);
var gameObject = new GameObject($"{nameof(AudioSourceExt)}_{nameof(PlayClip)}_{clip.name}");
var audioSource = gameObject.AddComponent<AudioSource>();
audioSource.clip = clip;
audioSource.volume = volume;
audioSource.pitch = pitch;
audioSource.spatialBlend = SpatialBlendUbiquitous;
if (offsetSeconds - delay >= clip.length)
{
audioSource.Stop();
Object.Destroy(gameObject, 1.0f);
return audioSource;
}
if (delay > offsetSeconds)
{
audioSource.PlayDelayed(delay - offsetSeconds);
}
else
{
audioSource.Play();
audioSource.time = offsetSeconds - delay;
}
float duration = (delay + clip.length - offsetSeconds) * (Time.timeScale < 0.00999999977648258 ? 0.01f : Time.timeScale);
Object.Destroy(gameObject, duration);
return audioSource;
}
/// <summary>
/// Plays an AudioClip at a given position in world space.
/// </summary>
/// <remarks>
/// This function creates an <see cref="AudioSource" /> but automatically disposes of it once the clip has finished
/// playing.
/// </remarks>
/// <param name="clip">Reference to the sound clip file that will be played.</param>
/// <param name="point">Position in world space from which sound originates.</param>
/// <param name="volume">How loud the sound is at a distance of one world unit (one meter) [0.0, 1.0].</param>
/// <param name="delay">Delay time specified in seconds.</param>
/// <param name="pitch">
/// Amount of change in pitch due to slowdown/speed up of the Audio Clip. Value 1 is normal playback
/// speed.
/// </param>
/// <param name="spatialBlend">Sets how much the 3D engine has an effect on the audio source [0.0, 1.0].</param>
/// <param name="offsetSeconds">Start offset in seconds</param>
/// <returns>The just created temporal <see cref="AudioSource" />.</returns>
/// <seealso cref="AudioSource.PlayClipAtPoint(AudioClip, Vector3, float)" />
public static AudioSource PlayClipAtPoint(AudioClip clip,
Vector3 point,
float volume = 1.0f,
float delay = 0.0f,
float pitch = 1.0f,
float spatialBlend = SpatialBlend3D,
float offsetSeconds = 0.0f)
{
if (!Application.isPlaying)
{
throw new InvalidOperationException("Playback is only allowed while playing.");
}
clip.ThrowIfNull(nameof(clip));
volume = Mathf.Clamp01(volume);
spatialBlend = Mathf.Clamp01(spatialBlend);
var gameObject = new GameObject($"{nameof(AudioSourceExt)}_{nameof(PlayClipAtPoint)}_{clip.name}") { transform = { position = point } };
var audioSource = gameObject.AddComponent<AudioSource>();
audioSource.clip = clip;
audioSource.volume = volume;
audioSource.pitch = pitch;
audioSource.spatialBlend = spatialBlend;
if (offsetSeconds - delay >= clip.length)
{
audioSource.Stop();
Object.Destroy(gameObject, 1.0f);
return audioSource;
}
audioSource.Play();
offsetSeconds = Mathf.Max(offsetSeconds, 0.0f);
if (delay > offsetSeconds)
{
audioSource.PlayDelayed(delay - offsetSeconds);
}
else
{
audioSource.Play();
audioSource.time = offsetSeconds - delay;
}
float duration = (delay + clip.length - offsetSeconds) * (Time.timeScale < 0.00999999977648258 ? 0.01f : Time.timeScale);
Object.Destroy(gameObject, duration);
return audioSource;
}
/// <summary>
/// Asynchronous and ubiquitously plays an <see cref="AudioClip" />.
/// </summary>
/// <remarks>
/// This function creates an <see cref="AudioSource" /> but automatically disposes of it once the clip has finished
/// playing.
/// </remarks>
/// <param name="clip">Reference to the sound clip file that will be played.</param>
/// <param name="volume">How loud the sound is at a distance of one world unit (one meter) [0.0, 1.0].</param>
/// <param name="delay">Delay time specified in seconds.</param>
/// <param name="pitch">
/// Amount of change in pitch due to slowdown/speed up of the Audio Clip. Value 1 is normal playback
/// speed.
/// </param>
/// <param name="offsetSeconds">Start offset in seconds</param>
/// <param name="ct"><see cref="CancellationToken" /> to stop playing.</param>
/// <returns>An awaitable <see cref="Task" />.</returns>
public static async Task PlayClipAsync(AudioClip clip,
float volume = 1.0f,
float delay = 0.0f,
float pitch = 1.0f,
float offsetSeconds = 0.0f,
CancellationToken ct = default)
{
if (ct.IsCancellationRequested)
{
return;
}
if (!Application.isPlaying)
{
throw new InvalidOperationException("Playback is only allowed while playing.");
}
if (offsetSeconds >= clip.length)
{
return;
}
offsetSeconds = Mathf.Max(offsetSeconds, 0.0f);
float duration = (delay + clip.length - offsetSeconds) * (Time.timeScale < 0.00999999977648258 ? 0.01f : Time.timeScale);
AudioSource audioSource = PlayClip(clip, volume, delay, pitch, offsetSeconds);
await TaskExt.Delay(duration, ct);
if (ct.IsCancellationRequested && audioSource != null)
{
audioSource.Stop();
Object.Destroy(audioSource.gameObject);
}
}
/// <summary>
/// Asynchronously plays an <see cref="AudioClip" /> at a given position in world space.
/// </summary>
/// <remarks>
/// This function creates an <see cref="AudioSource" /> but automatically disposes of it once the clip has finished
/// playing.
/// </remarks>
/// <param name="clip">Reference to the sound clip file that will be played.</param>
/// <param name="point">Position in world space from which sound originates.</param>
/// <param name="volume">How loud the sound is at a distance of one world unit (one meter) [0.0, 1.0].</param>
/// <param name="delay">Delay time specified in seconds.</param>
/// <param name="pitch">
/// Amount of change in pitch due to slowdown/speed up of the Audio Clip. Value 1 is normal playback
/// speed.
/// </param>
/// <param name="spatialBlend">Sets how much the 3D engine has an effect on the audio source [0.0, 1.0].</param>
/// <param name="offsetSeconds">Start offset in seconds</param>
/// <param name="ct"><see cref="CancellationToken" /> to stop playing.</param>
/// <returns>An awaitable <see cref="Task" />.</returns>
public static async Task PlayClipAtPointAsync(AudioClip clip,
Vector3 point,
float volume = 1.0f,
float delay = 0.0f,
float pitch = 1.0f,
float spatialBlend = SpatialBlend3D,
float offsetSeconds = 0.0f,
CancellationToken ct = default)
{
if (ct.IsCancellationRequested)
{
return;
}
if (!Application.isPlaying)
{
throw new InvalidOperationException("Playback is only allowed while playing.");
}
if (offsetSeconds >= clip.length)
{
return;
}
offsetSeconds = Mathf.Max(offsetSeconds, 0.0f);
float duration = (delay + clip.length - offsetSeconds) * (Time.timeScale < 0.00999999977648258 ? 0.01f : Time.timeScale);
AudioSource audioSource = PlayClipAtPoint(clip, point, volume, delay, pitch, spatialBlend, offsetSeconds);
await TaskExt.Delay(duration, ct);
if (ct.IsCancellationRequested && audioSource != null)
{
audioSource.Stop();
Object.Destroy(audioSource.gameObject);
}
}
#endregion
#region Private Types & Data
/// <summary>
/// Spatial blend for ubiquitous playback.
/// </summary>
private const float SpatialBlendUbiquitous = 0f;
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5c9177b2f45a03b43867e37e8ea7e869
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: