Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Lyriclib v1.0.0
BepInEx\plugins\Lyriclib.dll
Decompiled 3 weeks agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Threading; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using Lyriclib.Flac; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("Lyriclib")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("Lyriclib")] [assembly: AssemblyTitle("Lyriclib")] [assembly: AssemblyVersion("1.0.0.0")] namespace Lyriclib { public static class LyriclibApi { public const string Version = "1.0.0"; public static bool IsFlacSupportEnabled => FlacMusicSupport.IsEnabled; public static bool IsBoomboxAvailable => FlacMusicSupport.IsCatalogReady; public static bool IsInCustomBoomboxMusicFolder(string path) { if (!string.IsNullOrEmpty(path)) { return FlacMusicSupport.IsUnderMusicFolder(path); } return false; } public static bool IsFlac(byte[] bytes) { if (bytes != null && bytes.Length >= 4 && bytes[0] == 102 && bytes[1] == 76 && bytes[2] == 97) { return bytes[3] == 67; } return false; } public static FlacAudio DecodeFile(string path, out string error) { error = null; if (string.IsNullOrEmpty(path)) { error = "no path given"; return null; } byte[] bytes; try { bytes = File.ReadAllBytes(path); } catch (Exception ex) { error = "could not read the file: " + ex.Message; return null; } return Decode(bytes, out error); } public static FlacAudio Decode(byte[] bytes, out string error) { error = null; if (bytes == null || bytes.Length == 0) { error = "no data given"; return null; } if (!IsFlac(bytes)) { error = "not a native FLAC stream (the fLaC marker is missing)"; return null; } try { FlacAudio flacAudio = FlacDecoder.Decode(bytes); if (flacAudio == null || flacAudio.Samples == null || flacAudio.Samples.Length == 0) { error = "no audio frames decoded"; return null; } return flacAudio; } catch (Exception ex) { error = ex.Message; return null; } } public static uint ComputeTrackCrc(byte[] bytes) { return Crc32.Calculate(bytes); } public static AudioClip CreateClip(FlacAudio audio, string clipName) { if (audio == null) { throw new ArgumentNullException("audio"); } if (audio.Channels <= 0 || audio.SampleRate <= 0 || audio.Samples == null || audio.Samples.Length == 0) { throw new ArgumentException("the decoded audio is incomplete", "audio"); } int num = audio.Samples.Length / audio.Channels; AudioClip val = AudioClip.Create(string.IsNullOrEmpty(clipName) ? "flac" : clipName, num, audio.Channels, audio.SampleRate, false); if ((Object)(object)val == (Object)null) { throw new InvalidOperationException("Unity refused to create the AudioClip"); } if (!val.SetData(audio.ToFloatArray(), 0)) { Object.Destroy((Object)(object)val); throw new InvalidOperationException("uploading the PCM data to the clip failed"); } return val; } public static bool TryRegisterTrack(string path, AudioClip clip, uint crc, out string error) { return FlacMusicSupport.TryRegisterClip(path, crc, clip, out error); } public static bool IsTrackRegistered(uint crc) { return FlacMusicSupport.IsTrackRegistered(crc); } public static AudioClip LoadTrack(string path, out string error) { error = null; if (string.IsNullOrEmpty(path)) { error = "no path given"; return null; } byte[] bytes; try { bytes = File.ReadAllBytes(path); } catch (Exception ex) { error = "could not read the file: " + ex.Message; return null; } FlacAudio flacAudio = Decode(bytes, out error); if (flacAudio == null) { return null; } AudioClip val; try { val = CreateClip(flacAudio, Path.GetFileNameWithoutExtension(path)); } catch (Exception ex2) { error = ex2.Message; return null; } if (!TryRegisterTrack(path, val, ComputeTrackCrc(bytes), out error)) { Object.Destroy((Object)(object)val); return null; } return val; } } public static class LyriclibInfo { public const string Guid = "Lyriclib"; public const string Name = "Lyriclib"; public const string Version = "1.0.0"; } [BepInPlugin("Lyriclib", "Lyriclib", "1.0.0")] public sealed class LyriclibPlugin : BaseUnityPlugin { internal static LyriclibPlugin Instance; internal static ManualLogSource Log; private ConfigEntry<bool> _enabled; private void Awake() { Instance = this; Log = ((BaseUnityPlugin)this).Logger; _enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Decode the .flac tracks in your CustomBoomboxMusic folders with Lyriclib's own FLAC decoder and register them with CustomBoomboxMusic (every other format is still loaded by CustomBoomboxMusic itself). Turn this off and flac files are skipped, which is CustomBoomboxMusic's stock behaviour. Temporary shim: it will be removed as soon as CustomBoomboxMusic supports flac playback on its own."); FlacMusicSupport.Initialize(_enabled, ((BaseUnityPlugin)this).Logger); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Lyriclib 1.0.0 loaded: flac playback for CustomBoomboxMusic, plus the shared Lyriclib API."); } private void Update() { FlacMusicSupport.Instance?.Tick(); } } } namespace Lyriclib.Flac { public static class Crc32 { private static readonly uint[] Table = BuildTable(); private static uint[] BuildTable() { uint[] array = new uint[256]; for (uint num = 0u; num < 256; num++) { uint num2 = num; for (int i = 0; i < 8; i++) { num2 = (((num2 & 1) != 0) ? ((num2 >> 1) ^ 0xEDB88320u) : (num2 >> 1)); } array[num] = num2; } return array; } public static uint Calculate(byte[] data) { if (data == null) { throw new ArgumentNullException("data"); } uint num = uint.MaxValue; for (int i = 0; i < data.Length; i++) { num = (num >> 8) ^ Table[(num ^ data[i]) & 0xFF]; } return ~num; } } public sealed class FlacException : Exception { public FlacException(string message) : base(message) { } } public sealed class FlacAudio { public int SampleRate; public int Channels; public int BitsPerSample; public long TotalSamples; public int FrameCount; public int[] Samples; public float[] ToFloatArray() { float num = 1f / (float)(1 << BitsPerSample - 1); float[] array = new float[Samples.Length]; for (int i = 0; i < Samples.Length; i++) { array[i] = (float)Samples[i] * num; } return array; } } internal sealed class BitReader { private readonly byte[] _data; private int _bitPos; public int BytePosition => _bitPos + 7 >> 3; public int BitPosition => _bitPos; public BitReader(byte[] data, int byteOffset) { _data = data; _bitPos = byteOffset * 8; } public uint ReadBit() { int num = _bitPos >> 3; if (num >= _data.Length) { throw new FlacException("unexpected end of stream"); } int result = (_data[num] >> 7 - (_bitPos & 7)) & 1; _bitPos++; return (uint)result; } public uint ReadBits(int count) { if (count == 0) { return 0u; } uint num = 0u; for (int i = 0; i < count; i++) { num = (num << 1) | ReadBit(); } return num; } public ulong ReadBitsLong(int count) { ulong num = 0uL; for (int i = 0; i < count; i++) { num = (num << 1) | ReadBit(); } return num; } public int ReadSigned(int count) { if (count <= 0) { return 0; } uint num = ReadBits(count); if (count >= 32) { return (int)num; } uint num2 = (uint)(1 << count - 1); if ((num & num2) != 0) { return (int)num | (-1 << count); } return (int)num; } public int ReadUnary() { int num = 0; while (ReadBit() == 0) { num++; } return num; } public void SkipBits(int count) { if (count > 0) { if (_bitPos + count + 7 >> 3 > _data.Length) { throw new FlacException("unexpected end of stream"); } _bitPos += count; } } public void AlignToByte() { int num = _bitPos & 7; if (num != 0) { SkipBits(8 - num); } } } internal sealed class FrameBuffer { private int[][] _channels = new int[8][]; public int[][] Channels => _channels; public void Ensure(int channelCount, int blockSize) { if (_channels.Length < channelCount) { Array.Resize(ref _channels, channelCount); } for (int i = 0; i < channelCount; i++) { int[] array = _channels[i]; if (array == null || array.Length < blockSize) { _channels[i] = new int[blockSize]; } } } } public static class FlacDecoder { private const uint FrameSync = 16382u; public static FlacAudio Decode(byte[] data) { ParseMetadata(data, out var firstFramePos, out var streamRate, out var streamChannels, out var streamBits, out var streamTotal); List<int> list = new List<int>(65536); FrameBuffer frameBuffer = new FrameBuffer(); int num = 0; int num2 = streamRate; int frameChannels = streamChannels; int frameBits = streamBits; bool flag = false; string text = null; while (SeekFrameSync(data, ref firstFramePos)) { try { num2 = ReadFrame(data, firstFramePos, streamRate, streamChannels, streamBits, frameBuffer, out var blockSize, out frameChannels, out frameBits, out var frameEnd); int[][] channels = frameBuffer.Channels; for (int i = 0; i < blockSize; i++) { for (int j = 0; j < frameChannels; j++) { list.Add(channels[j][i]); } } num += blockSize; firstFramePos = frameEnd; } catch (FlacException ex) { flag = true; text = ex.Message; break; } if (streamTotal > 0 && num >= streamTotal) { break; } } if (flag && num == 0) { throw new FlacException("frame decode failed: " + text); } if (num == 0) { throw new FlacException("no audio frames found"); } return new FlacAudio { SampleRate = ((num2 > 0) ? num2 : streamRate), Channels = frameChannels, BitsPerSample = frameBits, TotalSamples = streamTotal, FrameCount = num, Samples = list.ToArray() }; } public static FlacStreamReader Open(byte[] data) { return new FlacStreamReader(data); } internal static void ParseMetadata(byte[] data, out int firstFramePos, out int streamRate, out int streamChannels, out int streamBits, out long streamTotal) { if (data == null || data.Length < 8) { throw new FlacException("file too small"); } if (data[0] != 102 || data[1] != 76 || data[2] != 97 || data[3] != 67) { throw new FlacException("not a FLAC stream (missing fLaC marker)"); } int num = 4; bool flag = false; bool flag2 = false; streamRate = 0; streamChannels = 0; streamBits = 0; streamTotal = 0L; while (!flag) { if (num + 4 > data.Length) { throw new FlacException("truncated metadata header"); } byte num2 = data[num]; flag = (num2 & 0x80) != 0; int num3 = num2 & 0x7F; int num4 = (data[num + 1] << 16) | (data[num + 2] << 8) | data[num + 3]; int num5 = num + 4; if (num5 + num4 > data.Length) { throw new FlacException("truncated metadata block"); } if (num3 == 0) { if (num4 < 34) { throw new FlacException("bad STREAMINFO size"); } BitReader bitReader = new BitReader(data, num5); bitReader.ReadBits(16); bitReader.ReadBits(16); bitReader.ReadBits(24); bitReader.ReadBits(24); streamRate = (int)bitReader.ReadBits(20); streamChannels = (int)(bitReader.ReadBits(3) + 1); streamBits = (int)(bitReader.ReadBits(5) + 1); streamTotal = (long)bitReader.ReadBitsLong(36); flag2 = true; } num = num5 + num4; } if (!flag2) { throw new FlacException("missing STREAMINFO block"); } if (streamRate <= 0 || streamChannels <= 0 || streamBits <= 0) { throw new FlacException("STREAMINFO has invalid rate/channels/bits"); } if (streamBits > 24) { throw new FlacException("unsupported bit depth: " + streamBits); } firstFramePos = num; } internal static bool SeekFrameSync(byte[] data, ref int pos) { for (int i = pos; i + 1 < data.Length; i++) { if (data[i] == byte.MaxValue && (data[i + 1] & 0xFC) == 248) { pos = i; return true; } } pos = data.Length; return false; } internal static int ReadFrame(byte[] data, int frameStart, int streamRate, int streamChannels, int streamBits, FrameBuffer frame, out int blockSize, out int frameChannels, out int frameBits, out int frameEnd) { BitReader bitReader = new BitReader(data, frameStart); if (bitReader.ReadBits(14) != 16382) { throw new FlacException("bad frame sync"); } bitReader.ReadBit(); bitReader.ReadBit(); int num = (int)bitReader.ReadBits(4); int num2 = (int)bitReader.ReadBits(4); int num3 = (int)bitReader.ReadBits(4); int num4 = (int)bitReader.ReadBits(3); bitReader.ReadBit(); ReadUtf8Number(bitReader); if (num == 0) { throw new FlacException("reserved block size code"); } if (num == 1) { blockSize = 192; } else if (num <= 5) { blockSize = 576 << num - 2; } else { switch (num) { case 6: blockSize = (int)(bitReader.ReadBits(8) + 1); break; case 7: blockSize = (int)(bitReader.ReadBits(16) + 1); break; default: blockSize = 256 << num - 8; break; } } int result = num2 switch { 0 => streamRate, 1 => 88200, 2 => 176400, 3 => 192000, 4 => 8000, 5 => 16000, 6 => 22050, 7 => 24000, 8 => 32000, 9 => 44100, 10 => 48000, 11 => 96000, 12 => (int)(bitReader.ReadBits(8) * 1000), 13 => (int)bitReader.ReadBits(16), 14 => (int)(bitReader.ReadBits(16) * 10), _ => throw new FlacException("invalid sample rate code"), }; switch (num4) { case 0: frameBits = streamBits; break; case 1: frameBits = 8; break; case 2: frameBits = 12; break; case 4: frameBits = 16; break; case 5: frameBits = 20; break; case 6: frameBits = 24; break; default: throw new FlacException("invalid sample size code"); } if (num3 <= 7) { frameChannels = num3 + 1; } else { if (num3 > 10) { throw new FlacException("invalid channel assignment"); } frameChannels = 2; } if (frameChannels != streamChannels) { throw new FlacException("channel count changed mid-stream"); } bitReader.SkipBits(8); frame.Ensure(frameChannels, blockSize); int[][] channels = frame.Channels; for (int i = 0; i < frameChannels; i++) { int num5 = frameBits; if ((num3 == 8 && i == 1) || (num3 == 9 && i == 0) || (num3 == 10 && i == 1)) { num5++; } ReadSubframe(bitReader, num5, blockSize, channels[i]); } switch (num3) { case 8: { int[] array3 = channels[0]; int[] array4 = channels[1]; for (int k = 0; k < blockSize; k++) { array4[k] = array3[k] - array4[k]; } break; } case 9: { int[] array5 = channels[0]; int[] array6 = channels[1]; for (int l = 0; l < blockSize; l++) { array5[l] = array6[l] + array5[l]; } break; } case 10: { int[] array = channels[0]; int[] array2 = channels[1]; for (int j = 0; j < blockSize; j++) { int num6 = array[j]; int num7 = array2[j]; int num8 = (num6 << 1) | (num7 & 1); array[j] = num8 + num7 >> 1; array2[j] = num8 - num7 >> 1; } break; } } bitReader.AlignToByte(); bitReader.SkipBits(16); frameEnd = bitReader.BytePosition; return result; } private static void ReadUtf8Number(BitReader br) { uint num = br.ReadBits(8); if ((num & 0x80) == 0) { return; } int num2; if ((num & 0xE0) == 192) { num2 = 1; } else if ((num & 0xF0) == 224) { num2 = 2; } else if ((num & 0xF8) == 240) { num2 = 3; } else if ((num & 0xFC) == 248) { num2 = 4; } else if ((num & 0xFE) == 252) { num2 = 5; } else { if (num != 254) { throw new FlacException("invalid UTF-8 frame number"); } num2 = 6; } for (int i = 0; i < num2; i++) { br.ReadBits(8); } } private static void ReadSubframe(BitReader br, int bits, int blockSize, int[] output) { if (br.ReadBit() != 0) { throw new FlacException("subframe padding bit set"); } uint num = br.ReadBits(6); bool num2 = br.ReadBit() != 0; int num3 = 0; if (num2) { num3 = br.ReadUnary() + 1; bits -= num3; if (bits <= 0) { throw new FlacException("invalid wasted-bits count"); } } switch (num) { case 0u: { int num4 = br.ReadSigned(bits) << num3; for (int j = 0; j < blockSize; j++) { output[j] = num4; } return; } case 1u: { for (int i = 0; i < blockSize; i++) { output[i] = br.ReadSigned(bits) << num3; } return; } case 8u: case 9u: case 10u: case 11u: case 12u: { int num5 = (int)(num - 8); if (num5 > blockSize) { throw new FlacException("fixed order larger than block"); } int[] array = new int[blockSize - num5]; for (int k = 0; k < num5; k++) { output[k] = br.ReadSigned(bits); } ReadResidual(br, blockSize, num5, array); for (int l = num5; l < blockSize; l++) { int num6 = num5 switch { 0 => 0, 1 => output[l - 1], 2 => 2 * output[l - 1] - output[l - 2], 3 => 3 * output[l - 1] - 3 * output[l - 2] + output[l - 3], _ => 4 * output[l - 1] - 6 * output[l - 2] + 4 * output[l - 3] - output[l - 4], }; output[l] = array[l - num5] + num6; } if (num3 != 0) { for (int m = 0; m < blockSize; m++) { output[m] <<= num3; } } return; } } if (num >= 32) { int num7 = (int)(num - 31); if (num7 > blockSize) { throw new FlacException("LPC order larger than block"); } int[] array2 = new int[blockSize - num7]; for (int n = 0; n < num7; n++) { output[n] = br.ReadSigned(bits); } int num8 = (int)(br.ReadBits(4) + 1); if (num8 == 16) { throw new FlacException("invalid LPC precision"); } int num9 = br.ReadSigned(5); int[] array3 = new int[num7]; for (int num10 = 0; num10 < num7; num10++) { array3[num10] = br.ReadSigned(num8); } ReadResidual(br, blockSize, num7, array2); for (int num11 = num7; num11 < blockSize; num11++) { long num12 = 0L; for (int num13 = 0; num13 < num7; num13++) { num12 += (long)array3[num13] * (long)output[num11 - 1 - num13]; } int num14 = (int)((num9 >= 0) ? (num12 >> num9) : (num12 << -num9)); output[num11] = array2[num11 - num7] + num14; } if (num3 != 0) { for (int num15 = 0; num15 < blockSize; num15++) { output[num15] <<= num3; } } return; } throw new FlacException("reserved subframe type " + num); } private static void ReadResidual(BitReader br, int blockSize, int order, int[] residual) { uint num = br.ReadBits(2); if ((int)num > 1) { throw new FlacException("invalid residual coding method"); } int count = ((num == 0) ? 4 : 5); int num2 = ((num == 0) ? 15 : 31); int num3 = (int)br.ReadBits(4); int num4 = 1 << num3; int num5 = blockSize >> num3; if (num5 == 0) { throw new FlacException("invalid residual partition order"); } int num6 = 0; for (int i = 0; i < num4; i++) { int num7 = num5 - ((i == 0) ? order : 0); if (num7 < 0) { throw new FlacException("residual partition smaller than predictor order"); } int num8 = (int)br.ReadBits(count); if (num8 == num2) { int num9 = (int)br.ReadBits(5); for (int j = 0; j < num7; j++) { residual[num6++] = ((num9 != 0) ? br.ReadSigned(num9) : 0); } continue; } for (int k = 0; k < num7; k++) { int num10 = br.ReadUnary(); uint num11 = ((num8 != 0) ? br.ReadBits(num8) : 0u); uint num12 = (uint)(num10 << num8) | num11; residual[num6++] = (int)((num12 >> 1) ^ (0 - (num12 & 1))); } } if (num6 != blockSize - order) { throw new FlacException("residual element count mismatch"); } } } public sealed class FlacStreamReader { private readonly byte[] _data; private readonly FrameBuffer _frame = new FrameBuffer(); private int _pos; private readonly int _streamRate; private readonly int _streamChannels; private readonly int _streamBits; private bool _finished; public int SampleRate { get; private set; } public int Channels { get; private set; } public int BitsPerSample { get; private set; } public long TotalSamples { get; private set; } public int DecodedFrames { get; private set; } public bool Finished => _finished; public FlacStreamReader(byte[] data) { if (data == null) { throw new ArgumentNullException("data"); } _data = data; FlacDecoder.ParseMetadata(data, out var firstFramePos, out var streamRate, out var streamChannels, out var streamBits, out var streamTotal); _pos = firstFramePos; _streamRate = streamRate; _streamChannels = streamChannels; _streamBits = streamBits; SampleRate = streamRate; Channels = streamChannels; BitsPerSample = streamBits; TotalSamples = streamTotal; } public bool TryReadFrame(out int[][] channels, out int frameCount) { channels = null; frameCount = 0; if (_finished) { return false; } if (!FlacDecoder.SeekFrameSync(_data, ref _pos)) { _finished = true; return false; } try { FlacDecoder.ReadFrame(_data, _pos, _streamRate, _streamChannels, _streamBits, _frame, out var blockSize, out var _, out var _, out var frameEnd); _pos = frameEnd; DecodedFrames += blockSize; channels = _frame.Channels; frameCount = blockSize; return true; } catch (FlacException) { _finished = true; return false; } } } internal sealed class FlacMusicSupport { private sealed class InjectedTrack { public string Path; public uint Crc; public AudioClip Clip; public object AudioFile; } private sealed class LoadJob { public readonly string Path; public volatile bool Cancelled; public volatile bool Done; public string Error; public uint Crc; public float[] Samples; public int SampleCount; public int Channels; public int SampleRate; public Thread Worker; public LoadJob(string path) { Path = path; } } internal const string ConfigSection = "General"; internal const string ConfigKey = "Enabled"; internal const string ConfigDescription = "Decode the .flac tracks in your CustomBoomboxMusic folders with Lyriclib's own FLAC decoder and register them with CustomBoomboxMusic (every other format is still loaded by CustomBoomboxMusic itself). Turn this off and flac files are skipped, which is CustomBoomboxMusic's stock behaviour. Temporary shim: it will be removed as soon as CustomBoomboxMusic supports flac playback on its own."; private const string MusicFolderName = "CustomBoomboxMusic"; private const float PruneIntervalSeconds = 0.5f; private const float CatalogStableSeconds = 0.5f; private const float HookRetrySeconds = 2f; private const float SweepIntervalSeconds = 30f; private const int MaxHookAttempts = 15; private const int MaxDurationSeconds = 1800; private const long MaxInterleavedSamples = 200000000L; private static FlacMusicSupport _instance; private readonly ConfigEntry<bool> _config; private readonly ManualLogSource _log; private readonly string _bepInExRoot; private readonly Dictionary<string, InjectedTrack> _injected = new Dictionary<string, InjectedTrack>(StringComparer.OrdinalIgnoreCase); private readonly List<string> _pending = new List<string>(); private FieldInfo _audioClipsField; private ConstructorInfo _audioFileCtor; private LoadJob _job; private float _nextPrune; private float _nextHookAttempt; private float _nextSweep; private int _lastCatalogCount = -1; private float _catalogStableSince; private int _hookAttempts; private bool _cbmReady; private readonly HashSet<string> _ignored = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private FieldInfo _crcField; private bool _crcFieldProbed; private FieldInfo _pathField; private bool _pathFieldProbed; internal static FlacMusicSupport Instance => _instance; internal static bool IsEnabled { get { if (_instance != null) { return _instance._config.Value; } return false; } } internal static bool IsCatalogReady { get { if (_instance != null) { return _instance.EnsureHooked(); } return false; } } internal static void Initialize(ConfigEntry<bool> config, ManualLogSource log) { if (_instance == null) { _instance = new FlacMusicSupport(config, log); config.SettingChanged += _instance.OnSettingChanged; } } private FlacMusicSupport(ConfigEntry<bool> config, ManualLogSource log) { _config = config; _log = log; _bepInExRoot = Paths.BepInExRootPath; _nextSweep = 0f; } internal void Tick() { if (!_config.Value) { UnloadAll(); } else { if (!EnsureHooked()) { return; } float unscaledTime = Time.unscaledTime; if (unscaledTime >= _nextPrune) { _nextPrune = unscaledTime + 0.5f; PruneInjected(); } if (_job != null) { CompleteJobIfReady(); return; } SweepIfDue(); if (_pending.Count > 0 && CatalogSettled()) { StartNextJob(); } } } private void OnSettingChanged(object sender, EventArgs e) { if (_config.Value) { _nextSweep = 0f; _log.LogInfo((object)"[FLAC] flac support enabled."); } else { _log.LogInfo((object)"[FLAC] flac support disabled."); UnloadAll(); } } private bool EnsureHooked() { if (_cbmReady) { return true; } if (_hookAttempts >= 15) { return false; } float unscaledTime = Time.unscaledTime; if (unscaledTime < _nextHookAttempt) { return false; } _nextHookAttempt = unscaledTime + 2f; _hookAttempts++; try { Assembly assembly = FindCbmAssembly(); if (assembly == null) { if (_hookAttempts >= 15) { _log.LogWarning((object)"[FLAC] CustomBoomboxMusic was not found; flac support stays idle."); } return false; } Type type = assembly.GetType("CustomBoomboxMusic.AudioManager", throwOnError: false); Type type2 = assembly.GetType("CustomBoomboxMusic.AudioFile", throwOnError: false); if (type == null || type2 == null) { _hookAttempts = 15; _log.LogWarning((object)"[FLAC] CustomBoomboxMusic assemblies found but AudioManager/AudioFile are missing; flac support disabled."); return false; } FieldInfo field = type.GetField("audioClips", BindingFlags.Static | BindingFlags.NonPublic); ConstructorInfo constructor = type2.GetConstructor(new Type[3] { typeof(uint), typeof(AudioClip), typeof(string) }); if (field == null || constructor == null) { _hookAttempts = 15; _log.LogWarning((object)"[FLAC] CustomBoomboxMusic layout changed (audioClips field / AudioFile(uint, AudioClip, string) ctor not found); flac support disabled."); return false; } _audioClipsField = field; _audioFileCtor = constructor; _cbmReady = true; _log.LogInfo((object)"[FLAC] flac support active (CustomBoomboxMusic catalog hooked)."); return true; } catch (Exception ex) { _log.LogWarning((object)("[FLAC] hooking CustomBoomboxMusic failed: " + ex.Message)); return false; } } private static Assembly FindCbmAssembly() { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < assemblies.Length; i++) { AssemblyName name = assemblies[i].GetName(); if (name != null && name.Name == "baer1.CustomBoomboxMusic") { return assemblies[i]; } } return null; } private IList GetCatalog() { if (!_cbmReady || _audioClipsField == null) { return null; } try { return _audioClipsField.GetValue(null) as IList; } catch { return null; } } private bool CatalogSettled() { IList catalog = GetCatalog(); if (catalog == null) { return false; } float unscaledTime = Time.unscaledTime; int count = catalog.Count; if (count != _lastCatalogCount) { _lastCatalogCount = count; _catalogStableSince = unscaledTime; return false; } return unscaledTime - _catalogStableSince >= 0.5f; } private void PruneInjected() { if (_injected.Count == 0) { return; } IList catalog = GetCatalog(); if (catalog == null || !CatalogSettled()) { return; } List<string> list = null; foreach (KeyValuePair<string, InjectedTrack> item in _injected) { if (!ContainsReference(catalog, item.Value.AudioFile)) { if (list == null) { list = new List<string>(); } list.Add(item.Key); } } if (list == null) { return; } for (int i = 0; i < list.Count; i++) { string text = list[i]; InjectedTrack injectedTrack = _injected[text]; _injected.Remove(text); DestroyClip(injectedTrack.Clip); if (!_pending.Contains(text)) { _pending.Add(text); } } _ignored.Clear(); _log.LogInfo((object)("[FLAC] CustomBoomboxMusic rebuilt its catalog; re-adding " + list.Count + " flac track(s).")); _nextSweep = 0f; } private void SweepIfDue() { float unscaledTime = Time.unscaledTime; if (unscaledTime < _nextSweep) { return; } _nextSweep = unscaledTime + 30f; string[] files; try { files = Directory.GetFiles(_bepInExRoot, "*.flac", SearchOption.AllDirectories); } catch (Exception ex) { _log.LogWarning((object)("[FLAC] could not scan for flac files: " + ex.Message)); return; } int num = 0; foreach (string text in files) { if (IsUnderMusicFolder(text) && !_injected.ContainsKey(text) && !_pending.Contains(text) && !_ignored.Contains(text)) { _pending.Add(text); num++; } } if (num > 0) { _log.LogInfo((object)("[FLAC] queued " + num + " flac track(s) for loading.")); } } internal static bool IsUnderMusicFolder(string path) { string text = Path.GetDirectoryName(path); while (!string.IsNullOrEmpty(text)) { if (string.Equals(Path.GetFileName(text), "CustomBoomboxMusic", StringComparison.OrdinalIgnoreCase)) { return true; } DirectoryInfo parent = Directory.GetParent(text); if (parent == null) { break; } text = parent.FullName; } return false; } private void StartNextJob() { string path = _pending[0]; _pending.RemoveAt(0); LoadJob job = new LoadJob(path); _job = job; Thread thread = new Thread((ThreadStart)delegate { RunJob(job); }); thread.IsBackground = true; thread.Name = "Lyriclib flac loader"; job.Worker = thread; thread.Start(); } private void RunJob(LoadJob job) { try { byte[] data = File.ReadAllBytes(job.Path); job.Crc = Crc32.Calculate(data); FlacStreamReader flacStreamReader = FlacDecoder.Open(data); int channels = flacStreamReader.Channels; long totalSamples = flacStreamReader.TotalSamples; long num = ((totalSamples > 0) ? (totalSamples * channels) : 0); if (totalSamples > 0 && totalSamples > (long)flacStreamReader.SampleRate * 1800L) { throw new FlacException("longer than " + 30 + " minutes"); } if (num > 200000000) { throw new FlacException("PCM buffer would be too large (" + num + " samples)"); } float num2 = 1f / (float)(1 << flacStreamReader.BitsPerSample - 1); float[] array = ((num > 0 && num <= int.MaxValue) ? new float[(int)num] : null); List<float> list = ((array == null) ? new List<float>(65536) : null); int num3 = 0; int[][] channels2; int frameCount; while (!job.Cancelled && flacStreamReader.TryReadFrame(out channels2, out frameCount)) { int num4 = num3 + frameCount * channels; if (array != null) { if (num4 > array.Length) { Array.Resize(ref array, num4); } int num5 = num3; for (int i = 0; i < frameCount; i++) { for (int j = 0; j < channels; j++) { array[num5++] = (float)channels2[j][i] * num2; } } } else { for (int k = 0; k < frameCount; k++) { for (int l = 0; l < channels; l++) { list.Add((float)channels2[l][k] * num2); } } } num3 = num4; } if (!job.Cancelled) { if (num3 == 0) { throw new FlacException("no audio frames decoded"); } num3 -= num3 % channels; float[] array2 = array; if (array2 != null && array2.Length != num3) { Array.Resize(ref array2, num3); } job.Samples = array2 ?? list.ToArray(); job.SampleCount = num3; job.Channels = channels; job.SampleRate = flacStreamReader.SampleRate; } } catch (Exception ex) { job.Error = ex.Message; } finally { job.Done = true; } } private void CompleteJobIfReady() { LoadJob job = _job; if (job == null || !job.Done) { return; } _job = null; if (job.Cancelled) { return; } string fileName = Path.GetFileName(job.Path); if (job.Error != null) { _log.LogWarning((object)("[FLAC] skipped '" + fileName + "': " + job.Error)); return; } AudioClip val = null; try { int num = job.SampleCount / job.Channels; val = AudioClip.Create(Path.GetFileNameWithoutExtension(job.Path), num, job.Channels, job.SampleRate, false); if ((Object)(object)val == (Object)null) { _log.LogWarning((object)("[FLAC] Unity refused to create a clip for '" + fileName + "'.")); return; } if (!val.SetData(job.Samples, 0)) { DestroyClip(val); _log.LogWarning((object)("[FLAC] uploading PCM data failed for '" + fileName + "'.")); return; } } catch (Exception ex) { DestroyClip(val); _log.LogWarning((object)("[FLAC] clip creation failed for '" + fileName + "': " + ex.Message)); return; } Inject(job, val); job.Samples = null; } private void Inject(LoadJob job, AudioClip clip) { if (!RegisterClip(job.Path, job.Crc, clip, destroyOnFailure: true, out var error)) { if (error != null) { _log.LogWarning((object)("[FLAC] skipped '" + Path.GetFileName(job.Path) + "': " + error)); } return; } float num = (float)job.SampleCount / (float)job.Channels / (float)job.SampleRate; _log.LogInfo((object)("[FLAC] loaded '" + Path.GetFileName(job.Path) + "' (" + num.ToString("0.0") + "s, " + job.Channels + "ch @ " + job.SampleRate + "Hz, crc " + job.Crc + ").")); } private bool RegisterClip(string path, uint crc, AudioClip clip, bool destroyOnFailure, out string error) { error = null; IList catalog = GetCatalog(); if (catalog == null) { if (destroyOnFailure) { DestroyClip(clip); } error = "CustomBoomboxMusic's catalog is not available"; return false; } string text = FindPathByCrc(catalog, crc); if (text != null) { if (destroyOnFailure) { DestroyClip(clip); } error = "already in the catalog as '" + Path.GetFileName(text) + "'"; _ignored.Add(path); return false; } object obj; try { obj = _audioFileCtor.Invoke(new object[3] { crc, clip, path }); } catch (Exception ex) { if (destroyOnFailure) { DestroyClip(clip); } error = "could not create the CustomBoomboxMusic entry: " + ex.Message; return false; } try { catalog.Add(obj); } catch (Exception ex2) { if (destroyOnFailure) { DestroyClip(clip); } error = "could not add to the catalog: " + ex2.Message; return false; } _injected[path] = new InjectedTrack { Path = path, Crc = crc, Clip = clip, AudioFile = obj }; return true; } private string FindPathByCrc(IList catalog, uint crc) { for (int i = 0; i < catalog.Count; i++) { object obj = catalog[i]; if (obj != null) { uint? num = ReadCrc(obj); if (num.HasValue && num.Value == crc) { return ReadPath(obj) ?? "(unknown file)"; } } } return null; } private uint? ReadCrc(object entry) { if (!_crcFieldProbed) { _crcFieldProbed = true; _crcField = FindNamedField(entry.GetType(), typeof(uint), "crc"); } if (_crcField == null) { return null; } try { object value = _crcField.GetValue(entry); return (value is uint) ? ((uint?)value) : ((uint?)null); } catch { return null; } } private string ReadPath(object entry) { if (!_pathFieldProbed) { _pathFieldProbed = true; _pathField = FindNamedField(entry.GetType(), typeof(string), "path", "file"); } if (_pathField == null) { return null; } try { return _pathField.GetValue(entry) as string; } catch { return null; } } private static FieldInfo FindNamedField(Type type, Type fieldType, params string[] nameParts) { FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); for (int i = 0; i < fields.Length; i++) { if (fields[i].FieldType != fieldType) { continue; } for (int j = 0; j < nameParts.Length; j++) { if (fields[i].Name.IndexOf(nameParts[j], StringComparison.OrdinalIgnoreCase) >= 0) { return fields[i]; } } } return null; } private void UnloadAll() { if (_injected.Count == 0 && _job == null && _pending.Count == 0) { return; } _pending.Clear(); LoadJob job = _job; if (job != null) { job.Cancelled = true; } _job = null; IList catalog = GetCatalog(); foreach (InjectedTrack value in _injected.Values) { if (catalog != null) { RemoveReference(catalog, value.AudioFile); } DestroyClip(value.Clip); } int count = _injected.Count; _injected.Clear(); if (count > 0) { _log.LogInfo((object)("[FLAC] " + count + " injected flac track(s) removed from the catalog.")); } } private static bool ContainsReference(IList list, object item) { for (int i = 0; i < list.Count; i++) { if (list[i] == item) { return true; } } return false; } private static void RemoveReference(IList list, object item) { for (int num = list.Count - 1; num >= 0; num--) { if (list[num] == item) { list.RemoveAt(num); } } } private static void DestroyClip(AudioClip clip) { if ((Object)(object)clip == (Object)null) { return; } try { Object.Destroy((Object)(object)clip); } catch { } } internal static bool IsTrackRegistered(uint crc) { FlacMusicSupport instance = _instance; if (instance == null) { return false; } IList catalog = instance.GetCatalog(); if (catalog != null) { return instance.FindPathByCrc(catalog, crc) != null; } return false; } internal static bool TryRegisterClip(string path, uint crc, AudioClip clip, out string error) { error = null; FlacMusicSupport instance = _instance; if (instance == null) { error = "Lyriclib is not initialised"; return false; } if (!instance.EnsureHooked()) { error = "CustomBoomboxMusic is not installed, not loaded yet, or its layout changed"; return false; } return instance.RegisterClip(path, crc, clip, destroyOnFailure: false, out error); } } }