Improve audio export on versions below Unity 5

* Resolves #353
This commit is contained in:
ds5678 2024-02-17 15:05:52 -05:00
parent 3c33256957
commit be74e2cee9
4 changed files with 74 additions and 177 deletions

View File

@ -1,5 +1,4 @@
using AssetRipper.Import.Logging;
using AssetRipper.SourceGenerated.Classes.ClassID_83;
using AssetRipper.SourceGenerated.Classes.ClassID_83;
using AssetRipper.SourceGenerated.Extensions;
using Fmod5Sharp;
using Fmod5Sharp.FmodTypes;
@ -9,174 +8,71 @@ namespace AssetRipper.Export.UnityProjects.Audio
{
public static class AudioClipDecoder
{
public static bool CanDecode(IAudioClip audioClip)
public static bool TryDecode(
IAudioClip audioClip,
[NotNullWhen(true)] out byte[]? decodedData,
[NotNullWhen(true)] out string? fileExtension,
[NotNullWhen(false)] out string? message)
{
byte[] rawData = audioClip.GetAudioData();
if (rawData.Length == 0)
{
Logger.Info(LogCategory.Export, $"Can't decode audio clip '{audioClip.Name}' with default decoder because its audio data could not be found.");
return false;
}
if (TryGetAudioType(rawData, out FmodAudioType audioType))
{
if (FmodAudioTypeExtensions.IsSupported(audioType))
{
return true;
}
else
{
Logger.Info(LogCategory.Export, $"Can't decode audio clip '{audioClip.Name}' with default decoder because it's '{audioType}' encoded.");
return false;
}
}
else
{
Logger.Info(LogCategory.Export, $"Can't decode audio clip '{audioClip.Name}' with default decoder because its {nameof(audioType)} could not be determined.");
return false;
}
}
public static bool TryGetDecodedAudioClipData(IAudioClip audioClip, [NotNullWhen(true)] out byte[]? decodedData, [NotNullWhen(true)] out string? fileExtension)
{
return TryGetDecodedAudioClipData(audioClip.GetAudioData(), out decodedData, out fileExtension);
}
public static bool TryGetDecodedAudioClipData(byte[] rawData, [NotNullWhen(true)] out byte[]? decodedData, [NotNullWhen(true)] out string? fileExtension)
{
decodedData = null;
fileExtension = null;
if (rawData.Length == 0)
{
decodedData = null;
fileExtension = null;
message = $"Can't decode audio clip '{audioClip.Name}' with default decoder because its audio data could not be found.";
return false;
}
if (FsbLoader.TryLoadFsbFromByteArray(rawData, out FmodSoundBank? fsbData))
if (audioClip.Has_Type())
{
fileExtension = audioClip.GetSoundType().ToRawExtension();
decodedData = rawData;
message = null;
return true;
}
else if (CheckMagic(rawData, "FSB5"u8) && FsbLoader.TryLoadFsbFromByteArray(rawData, out FmodSoundBank? fsbData))
{
FmodAudioType audioType = fsbData!.Header.AudioType;
try
{
if (audioType.IsSupported() && fsbData.Samples.Single().RebuildAsStandardFileFormat(out decodedData, out fileExtension))
{
message = null;
return true;
}
else
{
decodedData = null;
fileExtension = null;
message = $"Can't decode audio clip '{audioClip.Name}' with default decoder because it's '{audioType}' encoded.";
return false;
}
}
catch (Exception ex)
{
Logger.Error(LogCategory.Export, $"Failed to convert audio ({Enum.GetName(audioType)})", ex);
decodedData = null;
fileExtension = null;
message = $"Failed to convert audio ({Enum.GetName(audioType)})\n{ex}";
return false;
}
}
else
{
Logger.Error(LogCategory.Export, $"Failed to convert audio");
return false;
}
}
/// <summary>
/// Decodes WAV data from an AudioClip
/// </summary>
/// <param name="audioClip">The audio clip to extract the data from</param>
/// <param name="decodedData">The decoded data in the wav audio format</param>
/// <returns>True if the audio could be exported in the wav format</returns>
public static bool TryGetDecodedWavData(IAudioClip audioClip, [NotNullWhen(true)] out byte[]? decodedData)
{
return TryGetDecodedWavData(audioClip.GetAudioData(), out decodedData);
}
/// <summary>
/// Decodes WAV data from FSB data
/// </summary>
/// <param name="fsbData">The data from an FSB file</param>
/// <param name="decodedData">The decoded data in the wav audio format</param>
/// <returns>True if the audio could be exported in the wav format</returns>
public static bool TryGetDecodedWavData(byte[] fsbData, [NotNullWhen(true)] out byte[]? decodedData)
{
if (TryGetDecodedAudioClipData(fsbData, out decodedData, out string? fileExtension))
{
if (fileExtension == "ogg")
{
decodedData = AudioConverter.OggToWav(decodedData);
return true;
}
else
{
return fileExtension == "wav";
}
}
else
{
decodedData = null;
fileExtension = null;
message = "Failed to convert audio";
return false;
}
}
public static bool TryGetAudioType(byte[] rawData, out FmodAudioType type)
private static bool CheckMagic(byte[] data, ReadOnlySpan<byte> magic)
{
if (rawData.Length == 0)
if (data.Length < magic.Length)
{
type = default;
return false;
}
using MemoryStream input = new MemoryStream(rawData);
using BinaryReader reader = new BinaryReader(input);
try
{
if (CheckMagic(reader))
{
FmodAudioHeader header = new FmodAudioHeader(reader);
type = header.AudioType;
return true;
}
else
{
type = default;
Logger.Info(LogCategory.Export, "Audio clip data is not an FSB5 binary.");
return false;
}
}
catch (Exception ex)
{
Logger.Warning($"An exception was thrown while attempting to determine the audio type:{Environment.NewLine}{ex.Message}");
type = default;
return false;
}
}
public static string GetFileExtension(IAudioClip audioClip) => GetFileExtension(audioClip.GetAudioData());
public static string GetFileExtension(byte[] rawData)
{
if (TryGetAudioType(rawData, out FmodAudioType audioType))
{
return audioType.FileExtension() ?? throw new Exception($"No extension for {audioType}");
}
else if (rawData.Length == 0)
{
throw new Exception($"{nameof(rawData)} was empty.");
}
else
{
throw new Exception($"{nameof(FmodAudioType)} could not be determined.");
}
}
private static bool CheckMagic(BinaryReader reader)
{
const byte magic0 = (byte)'F';
const byte magic1 = (byte)'S';
const byte magic2 = (byte)'B';
const byte magic3 = (byte)'5';
long initialPosition = reader.BaseStream.Position;
bool isValid = reader.ReadByte() == magic0 && reader.ReadByte() == magic1 && reader.ReadByte() == magic2 && reader.ReadByte() == magic3;
reader.BaseStream.Position = initialPosition;
return isValid;
return data.AsSpan(0, magic.Length).SequenceEqual(magic);
}
}
}

View File

@ -1,32 +1,36 @@
using AssetRipper.Assets;
using AssetRipper.Export.UnityProjects.Configuration;
using AssetRipper.Assets.Export;
using AssetRipper.SourceGenerated.Classes.ClassID_83;
namespace AssetRipper.Export.UnityProjects.Audio
{
public sealed class AudioClipExportCollection : AudioExportCollection
{
public AudioClipExportCollection(AudioClipExporter assetExporter, IAudioClip asset) : base(assetExporter, asset)
private byte[]? data;
private readonly string fileExtension;
public AudioClipExportCollection(AudioClipExporter assetExporter, IAudioClip asset, byte[] data, string fileExtension) : base(assetExporter, asset)
{
this.data = data;
this.fileExtension = fileExtension;
}
protected override bool ExportInner(IExportContainer container, string filePath, string dirPath)
{
if (data is null or { Length: 0 })
{
return false;
}
else
{
System.IO.File.WriteAllBytes(filePath, data);
data = null;
return true;
}
}
protected override string GetExportExtension(IUnityObjectBase asset)
{
string defaultExtension = AudioClipDecoder.GetFileExtension((IAudioClip)asset);
if (IsWavExtension((AudioClipExporter)AssetExporter, defaultExtension))
{
return "wav";
}
else
{
return defaultExtension;
}
}
private static bool IsWavExtension(AudioClipExporter assetExporter, string defaultExtension)
{
return assetExporter.AudioFormat == AudioExportFormat.PreferWav
&& defaultExtension == "ogg";
return fileExtension;
}
}
}

View File

@ -1,8 +1,8 @@
using AssetRipper.Assets;
using AssetRipper.Assets.Export;
using AssetRipper.Export.UnityProjects.Configuration;
using AssetRipper.Import.Logging;
using AssetRipper.SourceGenerated.Classes.ClassID_83;
using AssetRipper.SourceGenerated.Extensions;
namespace AssetRipper.Export.UnityProjects.Audio
{
@ -19,37 +19,34 @@ namespace AssetRipper.Export.UnityProjects.Audio
public override bool TryCreateCollection(IUnityObjectBase asset, [NotNullWhen(true)] out IExportCollection? exportCollection)
{
if (asset is IAudioClip audio && AudioClipDecoder.CanDecode(audio))
if (asset is IAudioClip audio)
{
exportCollection = new AudioClipExportCollection(this, audio);
return true;
}
else
{
exportCollection = null;
return false;
if (AudioClipDecoder.TryDecode(audio, out byte[]? decodedData, out string? fileExtension, out string? message))
{
if (AudioFormat == AudioExportFormat.PreferWav && fileExtension == "ogg")
{
exportCollection = new AudioClipExportCollection(this, audio, AudioConverter.OggToWav(decodedData), "wav");
}
else
{
exportCollection = new AudioClipExportCollection(this, audio, decodedData, fileExtension);
}
return true;
}
else
{
Logger.Error(LogCategory.Export, message);
}
}
exportCollection = null;
return false;
}
public override bool Export(IExportContainer container, IUnityObjectBase asset, string path)
{
if (!AudioClipDecoder.TryGetDecodedAudioClipData((IAudioClip)asset, out byte[]? decodedData, out string? fileExtension))
{
return false;
}
if (AudioFormat == AudioExportFormat.PreferWav && fileExtension == "ogg")
{
decodedData = AudioConverter.OggToWav(decodedData);
}
if (decodedData.IsNullOrEmpty())
{
return false;
}
File.WriteAllBytes(path, decodedData);
return true;
throw new NotSupportedException();
}
}
}

View File

@ -38,7 +38,7 @@ internal sealed class AudioTab : HtmlTab
private static string TryDecode(IUnityObjectBase asset)
{
if (asset is IAudioClip clip && AudioClipDecoder.TryGetDecodedAudioClipData(clip, out byte[]? decodedAudioData, out string? extension))
if (asset is IAudioClip clip && AudioClipDecoder.TryDecode(clip, out byte[]? decodedAudioData, out string? extension, out _))
{
return $"data:audio/{extension};base64,{Convert.ToBase64String(decodedAudioData, Base64FormattingOptions.None)}";
}