using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using NLayer; using NVorbis; using SideLoader; using SideLoader.SLPacks.Categories; using UnityEngine; using UnityEngine.Networking; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("AudioFormats")] [assembly: AssemblyDescription("Adds .ogg and .mp3 support to SideLoader's audio loading.")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("AudioFormats")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("c4688ed5-9644-4f6d-a9b6-1cce1f1d9015")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8.1", FrameworkDisplayName = ".NET Framework 4.8.1")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] namespace Tchernobill.AudioFormats { public static class AudioFormatsConfig { public static ConfigEntry PreferManagedDecoders; public static ConfigEntry VerboseLogging; public static void Bind(ConfigFile config) { PreferManagedDecoders = config.Bind("Decoding", "PreferManagedDecoders", false, "If active, .ogg and .mp3 files are always decoded by the bundled NVorbis / NLayer decoders instead of Unity's built-in one. Slower and uses more memory, but gives identical results on every machine. Leave this off unless a file that plays fine elsewhere is refused by the game."); VerboseLogging = config.Bind("Debug", "VerboseLogging", false, "If active, log the sample rate, channel count and decoder used for each loaded clip."); } } [BepInPlugin("Tchernobill.AudioFormats", "Audio Formats", "1.0.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class AudioFormatsPlugin : BaseUnityPlugin { public const string GUID = "Tchernobill.AudioFormats"; public const string NAME = "Audio Formats"; public const string VERSION = "1.0.0"; public static AudioFormatsPlugin Instance { get; private set; } private void Awake() { //IL_0037: Unknown result type (might be due to invalid IL or missing references) Instance = this; Log.Init(((BaseUnityPlugin)this).Logger); AudioFormatsConfig.Bind(((BaseUnityPlugin)this).Config); AppDomain.CurrentDomain.AssemblyResolve += ResolveBundledDecoder; new Harmony("Tchernobill.AudioFormats").PatchAll(); } private static Assembly ResolveBundledDecoder(object sender, ResolveEventArgs args) { string name = new AssemblyName(args.Name).Name; if (name != "NVorbis" && name != "NLayer") { return null; } try { string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); string text = Path.Combine(directoryName, name + ".dll"); if (!File.Exists(text)) { Log.Warning("'" + name + ".dll' is missing from '" + directoryName + "'. Reinstall AudioFormats to get the fallback decoders back."); return null; } return Assembly.LoadFrom(text); } catch (Exception ex) { Log.Warning("Could not load '" + name + ".dll': " + ex.Message); return null; } } } public static class AudioClipLoader { public static void LoadFromFile(string filePath, SLPack pack = null, Action onClipLoaded = null) { if (!File.Exists(filePath)) { Log.Warning("No such audio file: '" + filePath + "'."); return; } AudioFileFormat format = AudioFileFormats.FromExtension(filePath); if (!AudioFileFormats.IsAdded(format)) { Log.Warning("'" + Path.GetFileName(filePath) + "' is not an .ogg or .mp3 file."); return; } string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(filePath); if (AudioFormatsConfig.PreferManagedDecoders.Value) { Complete(ManagedAudioDecoder.DecodeFile(filePath, format, fileNameWithoutExtension), fileNameWithoutExtension, filePath, pack, onClipLoaded); return; } AudioFormatsPlugin instance = AudioFormatsPlugin.Instance; if (!Object.op_Implicit((Object)(object)instance)) { Complete(ManagedAudioDecoder.DecodeFile(filePath, format, fileNameWithoutExtension), fileNameWithoutExtension, filePath, pack, onClipLoaded); } else { ((MonoBehaviour)instance).StartCoroutine(LoadWithFallbackCoroutine(filePath, format, fileNameWithoutExtension, pack, onClipLoaded)); } } public static AudioClip LoadFromBytes(byte[] data, string clipName, SLPack pack = null) { AudioFileFormat format = AudioFileFormats.FromHeader(data); if (!AudioFileFormats.IsAdded(format)) { Log.Warning("'" + clipName + "' is not ogg or mp3 data."); return null; } AudioClip val = ManagedAudioDecoder.Decode(data, format, clipName); if (!Object.op_Implicit((Object)(object)val)) { return null; } return SideLoaderBridge.FinalizeAudioClip(val, clipName, pack); } private static IEnumerator LoadWithFallbackCoroutine(string filePath, AudioFileFormat format, string clipName, SLPack pack, Action onClipLoaded) { AudioClip clip = null; string failure = null; yield return UnityAudioDecoder.LoadCoroutine(filePath, format, clipName, delegate(AudioClip result) { clip = result; }, delegate(string error) { failure = error; }); if (!Object.op_Implicit((Object)(object)clip)) { Log.Message("Unity could not decode '" + Path.GetFileName(filePath) + "' (" + failure + "), retrying with the managed decoder."); clip = ManagedAudioDecoder.DecodeFile(filePath, format, clipName); } Complete(clip, clipName, filePath, pack, onClipLoaded); } private static void Complete(AudioClip clip, string clipName, string filePath, SLPack pack, Action onClipLoaded) { if (!Object.op_Implicit((Object)(object)clip)) { Log.Warning("Could not load audio clip '" + Path.GetFileName(filePath) + "'."); return; } SideLoaderBridge.FinalizeAudioClip(clip, clipName, pack); Log.Message("Loaded audio clip: " + Path.GetFileName(filePath)); onClipLoaded?.Invoke(clip); } } public enum AudioFileFormat { Unknown, Wav, Ogg, Mp3 } public static class AudioFileFormats { public static readonly string[] AddedExtensions = new string[2] { ".ogg", ".mp3" }; public static bool IsAdded(AudioFileFormat format) { if (format != AudioFileFormat.Ogg) { return format == AudioFileFormat.Mp3; } return true; } public static AudioFileFormat FromExtension(string filePath) { if (string.IsNullOrEmpty(filePath)) { return AudioFileFormat.Unknown; } string extension = Path.GetExtension(filePath); if (string.Equals(extension, ".ogg", StringComparison.OrdinalIgnoreCase)) { return AudioFileFormat.Ogg; } if (string.Equals(extension, ".mp3", StringComparison.OrdinalIgnoreCase)) { return AudioFileFormat.Mp3; } if (string.Equals(extension, ".wav", StringComparison.OrdinalIgnoreCase)) { return AudioFileFormat.Wav; } return AudioFileFormat.Unknown; } public static AudioFileFormat FromHeader(byte[] data) { if (data == null || data.Length < 4) { return AudioFileFormat.Unknown; } if (data[0] == 79 && data[1] == 103 && data[2] == 103 && data[3] == 83) { return AudioFileFormat.Ogg; } if (data[0] == 82 && data[1] == 73 && data[2] == 70 && data[3] == 70) { return AudioFileFormat.Wav; } if (data[0] == 73 && data[1] == 68 && data[2] == 51) { return AudioFileFormat.Mp3; } if (data[0] == byte.MaxValue && (data[1] & 0xE0) == 224) { return AudioFileFormat.Mp3; } return AudioFileFormat.Unknown; } public static AudioType ToUnityAudioType(AudioFileFormat format) { return (AudioType)(format switch { AudioFileFormat.Ogg => 14, AudioFileFormat.Mp3 => 13, AudioFileFormat.Wav => 20, _ => 0, }); } } public static class ManagedAudioDecoder { private const int ReadChunkFloats = 16384; private static bool? s_oggAvailable; private static bool? s_mp3Available; public static bool OggAvailable => (s_oggAvailable ?? (s_oggAvailable = Type.GetType("NVorbis.VorbisReader, NVorbis") != null)).Value; public static bool Mp3Available => (s_mp3Available ?? (s_mp3Available = Type.GetType("NLayer.MpegFile, NLayer") != null)).Value; public static bool IsAvailable(AudioFileFormat format) { return format switch { AudioFileFormat.Ogg => OggAvailable, AudioFileFormat.Mp3 => Mp3Available, _ => false, }; } public static AudioClip DecodeFile(string filePath, AudioFileFormat format, string clipName) { try { using FileStream stream = File.OpenRead(filePath); return Decode(stream, format, clipName); } catch (Exception ex) { Log.Warning("Managed decoding of '" + filePath + "' failed: " + ex.Message); return null; } } public static AudioClip Decode(byte[] data, AudioFileFormat format, string clipName) { try { using MemoryStream stream = new MemoryStream(data, writable: false); return Decode(stream, format, clipName); } catch (Exception ex) { Log.Warning("Managed decoding of '" + clipName + "' failed: " + ex.Message); return null; } } public static AudioClip Decode(Stream stream, AudioFileFormat format, string clipName) { if (!IsAvailable(format)) { Log.Warning("Cannot decode '" + clipName + "': the " + ((format == AudioFileFormat.Ogg) ? "NVorbis" : "NLayer") + ".dll shipped with AudioFormats was not found next to the plugin."); return null; } try { switch (format) { case AudioFileFormat.Ogg: return DecodeOgg(stream, clipName); case AudioFileFormat.Mp3: return DecodeMp3(stream, clipName); default: Log.Warning($"Cannot decode '{clipName}': unsupported format '{format}'."); return null; } } catch (Exception ex) { Log.Warning("Managed decoding of '" + clipName + "' failed: " + ex.Message); return null; } } [MethodImpl(MethodImplOptions.NoInlining)] private static AudioClip DecodeOgg(Stream stream, string clipName) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Expected O, but got Unknown VorbisReader val = new VorbisReader(stream, false); try { long num = val.TotalSamples * val.Channels; bool flag = num > 0 && num < int.MaxValue; PcmBuffer pcmBuffer = new PcmBuffer((int)(flag ? num : 16384)); float[] array = new float[16384]; int num2; while ((num2 = val.ReadSamples(array, 0, array.Length)) > 0) { if (flag && pcmBuffer.Count + num2 > num) { num2 = (int)(num - pcmBuffer.Count); } if (num2 <= 0) { break; } pcmBuffer.Append(array, num2); } return BuildClip(clipName, pcmBuffer, val.Channels, val.SampleRate); } finally { ((IDisposable)val)?.Dispose(); } } [MethodImpl(MethodImplOptions.NoInlining)] private static AudioClip DecodeMp3(Stream stream, string clipName) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Expected O, but got Unknown MpegFile val = new MpegFile(stream); try { PcmBuffer pcmBuffer = new PcmBuffer(16384); float[] array = new float[16384]; int count; while ((count = val.ReadSamples(array, 0, array.Length)) > 0) { pcmBuffer.Append(array, count); } return BuildClip(clipName, pcmBuffer, val.Channels, val.SampleRate); } finally { ((IDisposable)val)?.Dispose(); } } private static AudioClip BuildClip(string clipName, PcmBuffer pcm, int channels, int sampleRate) { if (channels <= 0 || sampleRate <= 0) { Log.Warning($"Cannot build '{clipName}': invalid channels ({channels}) or sample rate ({sampleRate})."); return null; } float[] array = pcm.ToInterleavedArray(channels); int num = array.Length / channels; if (num <= 0) { Log.Warning("Cannot build '" + clipName + "': the file decoded to 0 sample."); return null; } AudioClip val = AudioClip.Create(clipName, num, channels, sampleRate, false); val.SetData(array, 0); Log.Debug($"Decoded '{clipName}' with the managed decoder: {num} samples, {channels} ch, {sampleRate} Hz."); return val; } } internal class PcmBuffer { private float[] m_data; private int m_count; public int Count => m_count; public PcmBuffer(int initialCapacity) { m_data = new float[Math.Max(initialCapacity, 4096)]; } public void Append(float[] source, int count) { EnsureCapacity(m_count + count); Buffer.BlockCopy(source, 0, m_data, m_count * 4, count * 4); m_count += count; } public float[] ToInterleavedArray(int channels) { int num = m_count - m_count % channels; if (num == m_data.Length) { return m_data; } float[] array = new float[num]; Buffer.BlockCopy(m_data, 0, array, 0, num * 4); return array; } private void EnsureCapacity(int required) { if (required > m_data.Length) { long num; for (num = m_data.Length; num < required; num *= 2) { } Array.Resize(ref m_data, (int)Math.Min(num, 2147483647L)); } } } internal static class SideLoaderBridge { private static readonly MethodInfo s_finalizeAudioClip = AccessTools.Method(typeof(CustomAudio), "FinalizeAudioClip", new Type[3] { typeof(AudioClip), typeof(string), typeof(SLPack) }, (Type[])null); private static readonly MethodInfo s_packLoadAudioClip = AccessTools.Method(typeof(SLPack), "LoadAudioClip", new Type[3] { typeof(string), typeof(string), typeof(Action) }, (Type[])null); public static AudioClip FinalizeAudioClip(AudioClip clip, string name, SLPack pack) { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Expected O, but got Unknown if (s_finalizeAudioClip == null) { Log.Warning("CustomAudio.FinalizeAudioClip was not found - is SideLoader up to date?"); return clip; } try { return (AudioClip)s_finalizeAudioClip.Invoke(null, new object[3] { clip, name, pack }); } catch (Exception ex) { Log.Warning("Could not finalize clip '" + name + "': " + (ex.InnerException?.Message ?? ex.Message)); return clip; } } public static void PackLoadAudioClip(SLPack pack, string relativeDirectory, string file, Action onClipLoaded) { if (s_packLoadAudioClip == null) { Log.Warning("SLPack.LoadAudioClip was not found - is SideLoader up to date?"); return; } try { s_packLoadAudioClip.Invoke(pack, new object[3] { relativeDirectory, file, onClipLoaded }); } catch (Exception ex) { Log.Warning("Could not load '" + file + "' from pack '" + ((pack != null) ? pack.Name : null) + "': " + (ex.InnerException?.Message ?? ex.Message)); } } } internal static class UnityAudioDecoder { public static IEnumerator LoadCoroutine(string filePath, AudioFileFormat format, string clipName, Action onSuccess, Action onFailure) { string absoluteUri; try { absoluteUri = new Uri(Path.GetFullPath(filePath)).AbsoluteUri; } catch (Exception ex) { onFailure("could not build a file:// url (" + ex.Message + ")"); yield break; } UnityWebRequest request = UnityWebRequestMultimedia.GetAudioClip(absoluteUri, AudioFileFormats.ToUnityAudioType(format)); yield return request.SendWebRequest(); while (!request.isDone) { yield return null; } if (!string.IsNullOrEmpty(request.error)) { onFailure(request.error); yield break; } AudioClip content; try { content = DownloadHandlerAudioClip.GetContent(request); } catch (Exception ex2) { onFailure(ex2.Message); yield break; } if (!Object.op_Implicit((Object)(object)content) || (int)content.loadState == 3 || content.samples <= 0) { onFailure("Unity could not decode the file"); yield break; } ((Object)content).name = clipName; Log.Debug($"Decoded '{clipName}' with Unity's decoder: {content.samples} samples, {content.channels} ch, {content.frequency} Hz."); onSuccess(content); } } internal static class Log { private static ManualLogSource s_source; public static void Init(ManualLogSource source) { s_source = source; } public static void Message(string message) { ManualLogSource obj = s_source; if (obj != null) { obj.LogMessage((object)message); } } public static void Warning(string message) { ManualLogSource obj = s_source; if (obj != null) { obj.LogWarning((object)message); } } public static void Debug(string message) { if (AudioFormatsConfig.VerboseLogging != null && AudioFormatsConfig.VerboseLogging.Value) { ManualLogSource obj = s_source; if (obj != null) { obj.LogInfo((object)message); } } } } } namespace Tchernobill.AudioFormats.Patches { [HarmonyPatch(typeof(AudioClipCategory), "LoadContent")] internal static class Patch_AudioClipCategory_LoadContent { [HarmonyPostfix] public static void Postfix(SLPack pack, Dictionary __result) { if (pack == null || __result == null) { return; } string pathForCategory = pack.GetPathForCategory(); if (!pack.DirectoryExists(pathForCategory)) { return; } string[] addedExtensions = AudioFileFormats.AddedExtensions; foreach (string text in addedExtensions) { string[] files = pack.GetFiles(pathForCategory, text); foreach (string text2 in files) { string key = text2; SideLoaderBridge.PackLoadAudioClip(pack, pathForCategory, Path.GetFileName(text2), delegate(AudioClip clip) { if (Object.op_Implicit((Object)(object)clip)) { __result[key] = clip; } }); } } } } [HarmonyPatch(typeof(CustomAudio), "ConvertByteArrayToAudioClip")] internal static class Patch_CustomAudio_ConvertByteArrayToAudioClip { [HarmonyPrefix] public static bool Prefix(byte[] sourceData, string name, ref AudioClip __result) { AudioFileFormat format = AudioFileFormats.FromHeader(sourceData); if (!AudioFileFormats.IsAdded(format)) { return true; } __result = ManagedAudioDecoder.Decode(sourceData, format, name); return false; } } [HarmonyPatch(typeof(CustomAudio), "LoadAudioClip", new Type[] { typeof(byte[]), typeof(string), typeof(SLPack) })] internal static class Patch_CustomAudio_LoadAudioClip_Bytes { [HarmonyPrefix] public static bool Prefix(byte[] data, string name, SLPack pack, ref AudioClip __result) { if (!AudioFileFormats.IsAdded(AudioFileFormats.FromHeader(data))) { return true; } __result = AudioClipLoader.LoadFromBytes(data, name, pack); return false; } } [HarmonyPatch(typeof(CustomAudio), "LoadAudioClip", new Type[] { typeof(string), typeof(SLPack), typeof(Action) })] internal static class Patch_CustomAudio_LoadAudioClip_File { [HarmonyPrefix] public static bool Prefix(string filePath, SLPack pack, Action onClipLoaded) { if (!AudioFileFormats.IsAdded(AudioFileFormats.FromExtension(filePath))) { return true; } AudioClipLoader.LoadFromFile(filePath, pack, onClipLoaded); return false; } } }