diff --git a/MiraAPI/Utilities/Assets/AudioTools.cs b/MiraAPI/Utilities/Assets/AudioTools.cs new file mode 100644 index 0000000..abb6bc1 --- /dev/null +++ b/MiraAPI/Utilities/Assets/AudioTools.cs @@ -0,0 +1,131 @@ +using System; +using System.IO; +using System.Reflection; +using System.Resources; +using System.Text; +using UnityEngine; + +namespace MiraAPI.Utilities.Assets; + +/// +/// A utility class for various audio-related operations. +/// +public static class AudioTools +{ + /// + /// Loads and returns an audio clip from a resource path using the specified . + /// + /// The path to the resource within the . + /// The from which to load the resource. + /// A object loaded from the specified resource path. + /// Attempted to load an Audio file in non WAV format. + /// Stream failed to load. Check if the name of your asset was correct. + public static AudioClip LoadAudioFromResources(string resourcePath, Assembly assembly) + { + using var assetStream = assembly.GetManifestResourceStream(resourcePath) + ?? throw new MissingManifestResourceException($"Stream failed to load. Check if the asset name is correct: {resourcePath}"); + + using var reader = new BinaryReader(assetStream); + + // Read RIFF header + if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "RIFF") + { + throw new NotSupportedException("Invalid WAV file (missing RIFF header)."); + } + + reader.ReadInt32(); // File size (ignore) + + if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "WAVE") + { + throw new NotSupportedException("Invalid WAV file (missing WAVE header)."); + } + + // Read chunks + int channels = 0, sampleRate = 0, bitsPerSample = 0; + byte[]? audioData = null; + + while (reader.BaseStream.Position < reader.BaseStream.Length) + { + var chunkId = Encoding.ASCII.GetString(reader.ReadBytes(4)); + var chunkSize = reader.ReadInt32(); + + // Format chunk + if (chunkId == "fmt ") + { + int audioFormat = reader.ReadInt16(); + channels = reader.ReadInt16(); + sampleRate = reader.ReadInt32(); + reader.ReadInt32(); // Byte rate (ignore) + reader.ReadInt16(); // Block align (ignore) + bitsPerSample = reader.ReadInt16(); + + if (audioFormat != 1) // Only PCM supported + throw new NotSupportedException("Only PCM WAV files are supported."); + } + + // Data chunk + else if (chunkId == "data") + { + audioData = reader.ReadBytes(chunkSize); + break; // No need to read further + } + else + { + reader.BaseStream.Seek(chunkSize, SeekOrigin.Current); // Skip unknown chunks + } + } + + if (audioData == null) + { + throw new InvalidOperationException("WAV file does not contain audio data."); + } + + if (channels == 0 || sampleRate == 0 || bitsPerSample == 0) + { + throw new InvalidOperationException("WAV file does not contain valid format information."); + } + + // Convert PCM data to float array + var samples = ConvertPcmToFloat(audioData, bitsPerSample); + + // Create the AudioClip + var audioClip = AudioClip.Create(resourcePath, samples.Length / channels, channels, sampleRate, false); + audioClip.SetData(samples, 0); + + return audioClip; + } + + // This is somewhat magic to me, I just copied it from elsewhere. + private static float[] ConvertPcmToFloat(byte[] pcmData, int bitsPerSample) + { + var sampleCount = pcmData.Length / (bitsPerSample / 8); + var floatData = new float[sampleCount]; + + for (var i = 0; i < sampleCount; i++) + { + var byteIndex = i * (bitsPerSample / 8); + + switch (bitsPerSample) + { + case 8: + floatData[i] = (pcmData[byteIndex] - 128) / 128f; // 8-bit PCM is unsigned + break; + case 16: + floatData[i] = BitConverter.ToInt16(pcmData, byteIndex) / 32768f; + break; + case 24: + var sample24 = pcmData[byteIndex] | (pcmData[byteIndex + 1] << 8) | (pcmData[byteIndex + 2] << 16); + if (sample24 > 0x7FFFFF) sample24 -= 0x1000000; // Convert from unsigned to signed + floatData[i] = sample24 / 8388608f; + break; + case 32: + floatData[i] = BitConverter.ToInt32(pcmData, byteIndex) / 2147483648f; + break; + default: + throw new NotSupportedException($"Unsupported bit depth: {bitsPerSample}"); + } + } + + return floatData; + } +} diff --git a/MiraAPI/Utilities/Assets/LoadableAudioResourceAsset.cs b/MiraAPI/Utilities/Assets/LoadableAudioResourceAsset.cs index 1bb75ae..8dbc521 100644 --- a/MiraAPI/Utilities/Assets/LoadableAudioResourceAsset.cs +++ b/MiraAPI/Utilities/Assets/LoadableAudioResourceAsset.cs @@ -1,8 +1,4 @@ -using System; -using System.IO; using System.Reflection; -using System.Resources; -using System.Text; using UnityEngine; namespace MiraAPI.Utilities.Assets; @@ -19,114 +15,14 @@ public class LoadableAudioResourceAsset(string path) : LoadableAsset /// Loads the asset from embedded resources. /// /// The asset to load. - /// Attempted to load an Audio file in non WAV format. - /// Stream failed to load. Check if the name of your asset was correct. + /// public override AudioClip LoadAsset() { - using var assetStream = _assembly.GetManifestResourceStream(path) - ?? throw new MissingManifestResourceException($"Stream failed to load. Check if the asset name is correct: {path}"); - - using var reader = new BinaryReader(assetStream); - - // Read RIFF header - if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "RIFF") + if (!LoadedAsset) { - throw new NotSupportedException("Invalid WAV file (missing RIFF header)."); + LoadedAsset = AudioTools.LoadAudioFromResources(path, _assembly); } - reader.ReadInt32(); // File size (ignore) - - if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "WAVE") - { - throw new NotSupportedException("Invalid WAV file (missing WAVE header)."); - } - - // Read chunks - int channels = 0, sampleRate = 0, bitsPerSample = 0; - byte[]? audioData = null; - - while (reader.BaseStream.Position < reader.BaseStream.Length) - { - var chunkId = Encoding.ASCII.GetString(reader.ReadBytes(4)); - var chunkSize = reader.ReadInt32(); - - // Format chunk - if (chunkId == "fmt ") - { - int audioFormat = reader.ReadInt16(); - channels = reader.ReadInt16(); - sampleRate = reader.ReadInt32(); - reader.ReadInt32(); // Byte rate (ignore) - reader.ReadInt16(); // Block align (ignore) - bitsPerSample = reader.ReadInt16(); - - if (audioFormat != 1) // Only PCM supported - throw new NotSupportedException("Only PCM WAV files are supported."); - } - - // Data chunk - else if (chunkId == "data") - { - audioData = reader.ReadBytes(chunkSize); - break; // No need to read further - } - else - { - reader.BaseStream.Seek(chunkSize, SeekOrigin.Current); // Skip unknown chunks - } - } - - if (audioData == null) - { - throw new InvalidOperationException("WAV file does not contain audio data."); - } - - if (channels == 0 || sampleRate == 0 || bitsPerSample == 0) - { - throw new InvalidOperationException("WAV file does not contain valid format information."); - } - - // Convert PCM data to float array - var samples = ConvertPcmToFloat(audioData, bitsPerSample); - - // Create the AudioClip - var audioClip = AudioClip.Create(path, samples.Length / channels, channels, sampleRate, false); - audioClip.SetData(samples, 0); - - return audioClip; - } - - // This is somewhat magic to me, I just copied it from elsewhere. - private static float[] ConvertPcmToFloat(byte[] pcmData, int bitsPerSample) - { - var sampleCount = pcmData.Length / (bitsPerSample / 8); - var floatData = new float[sampleCount]; - - for (var i = 0; i < sampleCount; i++) - { - var byteIndex = i * (bitsPerSample / 8); - - switch (bitsPerSample) - { - case 8: - floatData[i] = (pcmData[byteIndex] - 128) / 128f; // 8-bit PCM is unsigned - break; - case 16: - floatData[i] = BitConverter.ToInt16(pcmData, byteIndex) / 32768f; - break; - case 24: - var sample24 = pcmData[byteIndex] | (pcmData[byteIndex + 1] << 8) | (pcmData[byteIndex + 2] << 16); - if (sample24 > 0x7FFFFF) sample24 -= 0x1000000; // Convert from unsigned to signed - floatData[i] = sample24 / 8388608f; - break; - case 32: - floatData[i] = BitConverter.ToInt32(pcmData, byteIndex) / 2147483648f; - break; - default: - throw new NotSupportedException($"Unsupported bit depth: {bitsPerSample}"); - } - } - - return floatData; + return LoadedAsset!; } } diff --git a/MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs b/MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs index 85c76ac..80de3b4 100644 --- a/MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs +++ b/MiraAPI/Utilities/Assets/LoadableBundleSubAssetHolder.cs @@ -9,36 +9,26 @@ namespace MiraAPI.Utilities.Assets; /// /// A utility class for loading multiple assets from an . /// -public class LoadableBundleSubAssetHolder +/// The name of the assets to pull from. +/// The that contains the assets. +public class LoadableBundleSubAssetHolder(string[] names, AssetBundle bundle) { - private readonly string[] spriteNames; - private readonly AssetBundle bundle; + private readonly string[] spriteNames = names; + private readonly AssetBundle bundle = bundle; /// /// Gets the sprites contained within the asset. /// public Sprite[] SubSprites { get; private set; } = []; - /// - /// Initializes a new instance of the class. - /// - /// The name of the assets to pull from. - /// The that contains the assets. - public LoadableBundleSubAssetHolder(string[] names, AssetBundle bundle) - { - this.bundle = bundle; - spriteNames = names; - } - /// /// Initializes a new instance of the class. /// /// The name of the asset. /// The that contains the assets. public LoadableBundleSubAssetHolder(string name, AssetBundle bundle) + : this([name], bundle) { - this.bundle = bundle; - spriteNames = [name]; } ///