Files
dungeons/Assets/UltimateXR/Runtime/Scripts/Extensions/Unity/Audio/AudioClipExt.StreamedAudioClip.cs
2024-08-06 21:58:35 +02:00

138 lines
5.1 KiB
C#

// --------------------------------------------------------------------------------------------------------------------
// <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
}
}