Add ultimate xr
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bff70ee0e42241268ac7cd1479f8f5cc
|
||||
timeCreated: 1630064761
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2bfbb7d822e34d8f92097fa24960e1a6
|
||||
timeCreated: 1630065461
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f3e1a097a2b4481b486c9cc54c4a03c
|
||||
timeCreated: 1629966369
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 543e6a1c3d51e984f8ef22cd13008d57
|
||||
timeCreated: 1620807376
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5c9177b2f45a03b43867e37e8ea7e869
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user