Decompiled source of LethalVoiceRouter v0.5.0

plugins/LethalVoiceRouter/LethalVoiceRouter.Audio.dll

Decompiled a day ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis;

[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("LethalVoiceRouter.Audio")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+258e5d578787605554f3f95238b41255d59850fc")]
[assembly: AssemblyProduct("LethalVoiceRouter.Audio")]
[assembly: AssemblyTitle("LethalVoiceRouter.Audio")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace LethalVoiceRouter.Audio
{
	public sealed class AudioDevice
	{
		public string Id { get; }

		public string Name { get; }

		public AudioDevice(string id, string name)
		{
			Id = id;
			Name = name;
		}
	}
	public static class AudioDevices
	{
		public const string DefaultMultimedia = "default";

		public const string DefaultCommunications = "communications";

		public static AudioDevice[] Enumerate()
		{
			using (new ComApartment())
			{
				IntPtr instance = CreateEnumerator();
				IntPtr collection = IntPtr.Zero;
				try
				{
					CoreAudioNative.Check(CoreAudioNative.Method<CoreAudioNative.EnumEndpointsDelegate>(instance, 3)(instance, 0, 1u, out collection));
					CoreAudioNative.Check(CoreAudioNative.Method<CoreAudioNative.CountDelegate>(collection, 3)(collection, out var count));
					List<AudioDevice> list = new List<AudioDevice>();
					for (uint num = 0u; num < count; num++)
					{
						CoreAudioNative.Check(CoreAudioNative.Method<CoreAudioNative.ItemDelegate>(collection, 4)(collection, num, out var device));
						try
						{
							list.Add(Describe(device));
						}
						finally
						{
							CoreAudioNative.Release(ref device);
						}
					}
					return list.OrderBy<AudioDevice, string>((AudioDevice audioDevice) => audioDevice.Name, StringComparer.CurrentCultureIgnoreCase).ToArray();
				}
				finally
				{
					CoreAudioNative.Release(ref collection);
					CoreAudioNative.Release(ref instance);
				}
			}
		}

		public static AudioDevice Resolve(string selection)
		{
			using (new ComApartment())
			{
				IntPtr instance = Open(selection);
				try
				{
					return Describe(instance);
				}
				finally
				{
					CoreAudioNative.Release(ref instance);
				}
			}
		}

		internal static IntPtr Open(string selection)
		{
			IntPtr instance = CreateEnumerator();
			try
			{
				IntPtr device;
				if (selection == "default" || selection == "communications")
				{
					int role = ((!(selection == "communications")) ? 1 : 2);
					CoreAudioNative.Check(CoreAudioNative.Method<CoreAudioNative.DefaultEndpointDelegate>(instance, 4)(instance, 0, role, out device));
				}
				else
				{
					CoreAudioNative.Check(CoreAudioNative.Method<CoreAudioNative.GetDeviceDelegate>(instance, 5)(instance, selection, out device));
				}
				return device;
			}
			finally
			{
				CoreAudioNative.Release(ref instance);
			}
		}

		private static IntPtr CreateEnumerator()
		{
			CoreAudioNative.Check(CoreAudioNative.CoCreateInstance(in CoreAudioNative.DeviceEnumeratorClass, IntPtr.Zero, 1u, in CoreAudioNative.DeviceEnumeratorInterface, out var instance));
			return instance;
		}

		internal static AudioDevice Describe(IntPtr device)
		{
			CoreAudioNative.Check(CoreAudioNative.Method<CoreAudioNative.GetIdDelegate>(device, 5)(device, out var id));
			string text;
			try
			{
				text = Marshal.PtrToStringUni(id) ?? throw new InvalidOperationException("장치 ID가 없습니다.");
			}
			finally
			{
				Marshal.FreeCoTaskMem(id);
			}
			CoreAudioNative.Check(CoreAudioNative.Method<CoreAudioNative.OpenPropertiesDelegate>(device, 4)(device, 0u, out var store));
			try
			{
				CoreAudioNative.Check(CoreAudioNative.Method<CoreAudioNative.PropertyDelegate>(store, 5)(store, in CoreAudioNative.FriendlyName, out var value));
				try
				{
					string name = ((value.Type == 31) ? (Marshal.PtrToStringUni(value.Pointer) ?? text) : text);
					return new AudioDevice(text, name);
				}
				finally
				{
					CoreAudioNative.PropVariantClear(ref value);
				}
			}
			finally
			{
				CoreAudioNative.Release(ref store);
			}
		}
	}
	public static class ClearVoicePolicy
	{
		public const int MinimumBitrateKbps = 6;

		public const int MaximumBitrateKbps = 300;

		public const int MaximumPayloadBytes = 900;

		public const int MaximumPacketBytes = 1024;

		public const int VoiceHeaderBytes = 16;

		public const int ChannelHeaderBytes = 4;

		public const int RelayHeaderBytes = 10;

		public const int RecipientHeaderBytes = 2;

		public static bool FitsPacket(int payloadBytes, int channels, int recipients)
		{
			if (payloadBytes < 1 || payloadBytes > 900 || channels < 1 || channels > 65535 || recipients < 1 || recipients > 255)
			{
				return false;
			}
			return payloadBytes + 16 + 4L * (long)channels + 10 + 2L * (long)recipients <= 1024;
		}
	}
	internal static class CoreAudioNative
	{
		internal struct PropertyKey
		{
			public Guid FormatId;

			public uint PropertyId;
		}

		[StructLayout(LayoutKind.Explicit, Size = 24)]
		internal struct PropVariant
		{
			[FieldOffset(0)]
			public ushort Type;

			[FieldOffset(8)]
			public IntPtr Pointer;
		}

		[StructLayout(LayoutKind.Sequential, Pack = 2)]
		internal struct WaveFormat
		{
			public ushort FormatTag;

			public ushort Channels;

			public uint SamplesPerSecond;

			public uint AverageBytesPerSecond;

			public ushort BlockAlign;

			public ushort BitsPerSample;

			public ushort ExtraSize;
		}

		[UnmanagedFunctionPointer(CallingConvention.StdCall)]
		internal delegate uint ReleaseDelegate(IntPtr self);

		[UnmanagedFunctionPointer(CallingConvention.StdCall)]
		internal delegate int EnumEndpointsDelegate(IntPtr self, int flow, uint stateMask, out IntPtr collection);

		[UnmanagedFunctionPointer(CallingConvention.StdCall)]
		internal delegate int DefaultEndpointDelegate(IntPtr self, int flow, int role, out IntPtr device);

		[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)]
		internal delegate int GetDeviceDelegate(IntPtr self, [MarshalAs(UnmanagedType.LPWStr)] string id, out IntPtr device);

		[UnmanagedFunctionPointer(CallingConvention.StdCall)]
		internal delegate int CountDelegate(IntPtr self, out uint count);

		[UnmanagedFunctionPointer(CallingConvention.StdCall)]
		internal delegate int ItemDelegate(IntPtr self, uint index, out IntPtr device);

		[UnmanagedFunctionPointer(CallingConvention.StdCall)]
		internal delegate int GetIdDelegate(IntPtr self, out IntPtr id);

		[UnmanagedFunctionPointer(CallingConvention.StdCall)]
		internal delegate int OpenPropertiesDelegate(IntPtr self, uint access, out IntPtr store);

		[UnmanagedFunctionPointer(CallingConvention.StdCall)]
		internal delegate int PropertyDelegate(IntPtr self, in PropertyKey key, out PropVariant value);

		[UnmanagedFunctionPointer(CallingConvention.StdCall)]
		internal delegate int ActivateDelegate(IntPtr self, in Guid iid, uint context, IntPtr parameters, out IntPtr client);

		[UnmanagedFunctionPointer(CallingConvention.StdCall)]
		internal delegate int InitializeDelegate(IntPtr self, int shareMode, uint flags, long duration, long periodicity, in WaveFormat format, IntPtr sessionGuid);

		[UnmanagedFunctionPointer(CallingConvention.StdCall)]
		internal delegate int CommandDelegate(IntPtr self);

		[UnmanagedFunctionPointer(CallingConvention.StdCall)]
		internal delegate int ServiceDelegate(IntPtr self, in Guid iid, out IntPtr service);

		[UnmanagedFunctionPointer(CallingConvention.StdCall)]
		internal delegate int BufferDelegate(IntPtr self, uint frames, out IntPtr data);

		[UnmanagedFunctionPointer(CallingConvention.StdCall)]
		internal delegate int ReleaseBufferDelegate(IntPtr self, uint frames, uint flags);

		internal static readonly Guid DeviceEnumeratorClass = new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E");

		internal static readonly Guid DeviceEnumeratorInterface = new Guid("A95664D2-9614-4F35-A746-DE8DB63617E6");

		internal static readonly Guid AudioClientInterface = new Guid("1CB9AD4C-DBFA-4C32-B178-C2F568A703B2");

		internal static readonly Guid RenderClientInterface = new Guid("F294ACFC-3146-4483-A7BF-ADDCA7C260E2");

		internal static readonly PropertyKey FriendlyName = new PropertyKey
		{
			FormatId = new Guid("A45C254E-DF1C-4EFD-8020-67D146A850E0"),
			PropertyId = 14u
		};

		[DllImport("ole32.dll", ExactSpelling = true)]
		internal static extern int CoInitializeEx(IntPtr reserved, uint flags);

		[DllImport("ole32.dll", ExactSpelling = true)]
		internal static extern void CoUninitialize();

		[DllImport("ole32.dll", ExactSpelling = true)]
		internal static extern int CoCreateInstance(in Guid clsid, IntPtr outer, uint context, in Guid iid, out IntPtr instance);

		[DllImport("ole32.dll", ExactSpelling = true)]
		internal static extern int PropVariantClear(ref PropVariant value);

		internal static T Method<T>(IntPtr instance, int slot) where T : Delegate
		{
			return Marshal.GetDelegateForFunctionPointer<T>(Marshal.ReadIntPtr(Marshal.ReadIntPtr(instance), slot * IntPtr.Size));
		}

		internal static void Check(int result)
		{
			if (result < 0)
			{
				throw new COMException($"Windows 오디오 오류 (0x{result:X8})", result);
			}
		}

		internal static void Release(ref IntPtr instance)
		{
			if (!(instance == IntPtr.Zero))
			{
				Method<ReleaseDelegate>(instance, 2)(instance);
				instance = IntPtr.Zero;
			}
		}
	}
	internal sealed class ComApartment : IDisposable
	{
		private readonly bool initialized;

		public ComApartment()
		{
			if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || IntPtr.Size != 8)
			{
				throw new PlatformNotSupportedException("64비트 Windows가 필요합니다.");
			}
			int num = CoreAudioNative.CoInitializeEx(IntPtr.Zero, 0u);
			if (num != -2147417850)
			{
				CoreAudioNative.Check(num);
			}
			initialized = num >= 0;
		}

		public void Dispose()
		{
			if (initialized)
			{
				CoreAudioNative.CoUninitialize();
			}
		}
	}
	public sealed class DeviceChoices
	{
		public const string MultimediaLabel = "Windows 기본 출력 장치";

		public const string GameOriginalId = "game";

		public const string GameOriginalLabel = "기존 게임 출력";

		private readonly Dictionary<string, string> ids = new Dictionary<string, string>(StringComparer.Ordinal);

		private readonly Dictionary<string, string> names = new Dictionary<string, string>(StringComparer.Ordinal);

		public IReadOnlyList<string> Labels => ids.Keys.ToArray();

		private DeviceChoices()
		{
		}

		public static DeviceChoices Create(IReadOnlyList<AudioDevice> devices, string currentId, bool includeGameOutput = false)
		{
			DeviceChoices deviceChoices = new DeviceChoices();
			if (includeGameOutput)
			{
				deviceChoices.Add("game", "기존 게임 출력");
			}
			deviceChoices.Add("default", "Windows 기본 출력 장치");
			foreach (AudioDevice device in devices)
			{
				bool flag = devices.Count((AudioDevice other) => other.Name == device.Name) > 1 || deviceChoices.ids.ContainsKey(device.Name);
				deviceChoices.Add(device.Id, flag ? (device.Name + " [" + device.Id + "]") : device.Name);
			}
			if (!string.IsNullOrWhiteSpace(currentId) && currentId != "communications" && !deviceChoices.names.ContainsKey(currentId))
			{
				deviceChoices.Add(currentId, "연결되지 않은 장치 [" + currentId + "]");
			}
			return deviceChoices;
		}

		private void Add(string id, string name)
		{
			if (!names.ContainsKey(id))
			{
				names.Add(id, name);
				ids.Add(name, id);
			}
		}

		public bool TryGetDeviceId(string label, out string id)
		{
			return ids.TryGetValue(label, out id);
		}

		public string GetLabel(string id)
		{
			if (!names.TryGetValue(id, out string value))
			{
				return "Windows 기본 출력 장치";
			}
			return value;
		}
	}
	public sealed class FileProcessAudioBackup : IProcessAudioBackup
	{
		private readonly string path;

		public FileProcessAudioBackup(string path)
		{
			this.path = path;
		}

		public string?[]? Load()
		{
			if (!File.Exists(path))
			{
				return null;
			}
			string[] array = File.ReadAllLines(path, Encoding.UTF8);
			if (array.Length != 3 || array[0] != "LethalVoiceRouter.GameOutput.1")
			{
				throw new InvalidDataException("게임 출력 복구 파일이 올바르지 않습니다.");
			}
			return (from line in array.Skip(1)
				select (line.Length != 0) ? Encoding.UTF8.GetString(Convert.FromBase64String(line)) : null).ToArray();
		}

		public void Save(string?[] endpoints)
		{
			if (endpoints.Length != 2)
			{
				throw new ArgumentException("두 출력 역할의 설정이 필요합니다.", "endpoints");
			}
			string s = "LethalVoiceRouter.GameOutput.1\n" + string.Join("\n", endpoints.Select((string endpoint) => (endpoint != null) ? Convert.ToBase64String(Encoding.UTF8.GetBytes(endpoint)) : "")) + "\n";
			string sourceFileName = path + "." + Guid.NewGuid().ToString("N") + ".tmp";
			try
			{
				using (FileStream fileStream = new FileStream(sourceFileName, FileMode.CreateNew, FileAccess.Write, FileShare.None))
				{
					byte[] bytes = Encoding.UTF8.GetBytes(s);
					fileStream.Write(bytes, 0, bytes.Length);
					fileStream.Flush(flushToDisk: true);
				}
				File.Move(sourceFileName, path);
			}
			finally
			{
				if (File.Exists(sourceFileName))
				{
					File.Delete(sourceFileName);
				}
			}
		}

		public void Clear()
		{
			File.Delete(path);
		}
	}
	public sealed class GameOutputController : IDisposable
	{
		private readonly uint processId;

		private readonly string backupPath;

		private readonly AutoResetEvent wake = new AutoResetEvent(initialState: false);

		private readonly Thread thread;

		private string selection = "game";

		private OutputStatus status = new OutputStatus(ready: false, "게임 출력 준비 중");

		private string? restoreError;

		private int shutdown;

		public OutputStatus Status => Volatile.Read(in status);

		public string? RestoreError => Volatile.Read(in restoreError);

		public GameOutputController(uint processId, string backupPath)
		{
			this.processId = processId;
			this.backupPath = backupPath;
			thread = new Thread(Run)
			{
				IsBackground = true,
				Name = "Lethal Voice Router game output"
			};
			thread.Start();
		}

		public void Configure(string deviceId)
		{
			if (Volatile.Read(in shutdown) == 0)
			{
				Volatile.Write(ref selection, string.IsNullOrWhiteSpace(deviceId) ? "game" : deviceId.Trim());
				Volatile.Write(ref status, new OutputStatus(ready: false, "게임 출력 전환 중"));
				wake.Set();
			}
		}

		private void Run()
		{
			WindowsProcessAudioPolicy windowsProcessAudioPolicy = null;
			ProcessOutputRoute processOutputRoute = null;
			try
			{
				while (Volatile.Read(in shutdown) == 0)
				{
					try
					{
						string text = Volatile.Read(in selection);
						if (processOutputRoute == null && (text != "game" || File.Exists(backupPath)))
						{
							if (windowsProcessAudioPolicy == null)
							{
								windowsProcessAudioPolicy = new WindowsProcessAudioPolicy(processId);
							}
							processOutputRoute = new ProcessOutputRoute(windowsProcessAudioPolicy, new FileProcessAudioBackup(backupPath));
							processOutputRoute.Restore();
						}
						if (text == "game")
						{
							processOutputRoute?.Restore();
							Volatile.Write(ref status, new OutputStatus(ready: true, "게임 · 기존 게임 출력 사용", "game"));
							goto IL_0125;
						}
						AudioDevice audioDevice = ((text == "default") ? null : AudioDevices.Resolve(text));
						if (Volatile.Read(in shutdown) != 0 || text != Volatile.Read(in selection))
						{
							continue;
						}
						processOutputRoute.Apply(audioDevice?.Id);
						Volatile.Write(ref status, new OutputStatus(ready: true, "게임 출력 설정 · " + (audioDevice?.Name ?? "Windows 기본 출력 장치"), audioDevice?.Id ?? "default"));
						goto IL_0125;
						IL_0125:
						Volatile.Write(ref restoreError, null);
						goto IL_0193;
					}
					catch (Exception ex)
					{
						string text2 = (Restore(processOutputRoute) ? "이전 게임 출력으로 복구" : "이전 출력 복구 대기");
						Volatile.Write(ref status, new OutputStatus(ready: false, "게임 · " + text2 + " · " + ex.Message + " · 자동 재시도 중", null, failed: true));
						goto IL_0193;
					}
					IL_0193:
					wake.WaitOne(1000);
				}
			}
			finally
			{
				Restore(processOutputRoute);
				windowsProcessAudioPolicy?.Dispose();
			}
		}

		private bool Restore(ProcessOutputRoute? route)
		{
			try
			{
				if (route == null && File.Exists(backupPath))
				{
					throw new InvalidOperationException("게임 출력 복구 파일을 아직 읽지 못했습니다.");
				}
				route?.Restore();
				Volatile.Write(ref restoreError, null);
				return true;
			}
			catch (Exception ex)
			{
				Volatile.Write(ref restoreError, ex.Message);
				return false;
			}
		}

		public void Dispose()
		{
			if (Interlocked.Exchange(ref shutdown, 1) == 0)
			{
				wake.Set();
				if (thread.Join(3000))
				{
					wake.Dispose();
				}
				else
				{
					Volatile.Write(ref restoreError, "게임 출력 복구 스레드가 아직 종료되지 않았습니다.");
				}
			}
		}
	}
	public sealed class OutputStatus
	{
		public bool Ready { get; }

		public string Message { get; }

		public string? DeviceId { get; }

		public bool Failed { get; }

		public OutputStatus(bool ready, string message, string? deviceId = null, bool failed = false)
		{
			Ready = ready;
			Message = message;
			DeviceId = deviceId;
			Failed = failed;
		}
	}
	public sealed class OutputController : IDisposable
	{
		private sealed class EndpointObservation
		{
			public readonly string Selection;

			public readonly string Id;

			public readonly long Timestamp;

			public EndpointObservation(string selection, string id)
			{
				Selection = selection;
				Id = id;
				Timestamp = Stopwatch.GetTimestamp();
			}
		}

		private sealed class Configuration
		{
			public readonly string Selection;

			public readonly int SampleRate;

			public readonly int Latency;

			public readonly int MaximumQueue;

			public Configuration(string selection, int sampleRate, int latency, int maximumQueue)
			{
				Selection = selection;
				SampleRate = sampleRate;
				Latency = latency;
				MaximumQueue = maximumQueue;
			}
		}

		private readonly VoiceRouter router;

		private readonly AutoResetEvent wake = new AutoResetEvent(initialState: false);

		private readonly AutoResetEvent discoveryWake = new AutoResetEvent(initialState: false);

		private readonly Thread thread;

		private readonly Thread discoveryThread;

		private Configuration configuration = new Configuration("default", 48000, 40, 100);

		private OutputStatus status = new OutputStatus(ready: false, "음성 출력 준비 중");

		private AudioDevice[] devices = Array.Empty<AudioDevice>();

		private AudioDevice? defaultOutputDevice;

		private int shutdown;

		private EndpointObservation? observedEndpoint;

		private string? discoveryError;

		private long droppedFrames;

		public OutputStatus Status => Volatile.Read(in status);

		public AudioDevice[] Devices => Volatile.Read(in devices);

		public AudioDevice? DefaultOutputDevice => Volatile.Read(in defaultOutputDevice);

		public string? DiscoveryError => Volatile.Read(in discoveryError);

		public long DroppedFrames => Interlocked.Read(in droppedFrames);

		public OutputController(VoiceRouter router)
		{
			this.router = router;
			thread = new Thread(Run)
			{
				IsBackground = true,
				Name = "Lethal Voice Router WASAPI"
			};
			discoveryThread = new Thread(DiscoverDevices)
			{
				IsBackground = true,
				Name = "Lethal Voice Router devices"
			};
			discoveryThread.Start();
			thread.Start();
		}

		public void Configure(string selection, int sampleRate, int latencyMilliseconds, int maximumQueueMilliseconds)
		{
			if (string.IsNullOrWhiteSpace(selection))
			{
				throw new ArgumentException("출력 장치를 지정해야 합니다.", "selection");
			}
			if (sampleRate < 8000 || sampleRate > 192000)
			{
				throw new ArgumentOutOfRangeException("sampleRate");
			}
			if (latencyMilliseconds < 20 || latencyMilliseconds > 200)
			{
				throw new ArgumentOutOfRangeException("latencyMilliseconds");
			}
			if (maximumQueueMilliseconds < 20 || maximumQueueMilliseconds > 500)
			{
				throw new ArgumentOutOfRangeException("maximumQueueMilliseconds");
			}
			if (Volatile.Read(in shutdown) == 0)
			{
				Configuration value = new Configuration(selection, sampleRate, latencyMilliseconds, maximumQueueMilliseconds);
				router.RetireCurrentSession();
				Volatile.Write(ref configuration, value);
				Volatile.Write(ref status, new OutputStatus(ready: false, "음성 출력 연결 중"));
				wake.Set();
				discoveryWake.Set();
			}
		}

		public void RefreshDevices()
		{
			if (Volatile.Read(in shutdown) == 0)
			{
				discoveryWake.Set();
			}
		}

		private bool ShouldRun(Configuration expected)
		{
			if (Volatile.Read(in shutdown) == 0 && router.Enabled)
			{
				return expected == Volatile.Read(in configuration);
			}
			return false;
		}

		private void Run()
		{
			while (Volatile.Read(in shutdown) == 0)
			{
				Configuration requested = Volatile.Read(in configuration);
				try
				{
					if (!router.Enabled)
					{
						Volatile.Write(ref status, new OutputStatus(ready: false, "분리 꺼짐 · 기존 게임 출력 사용"));
						wake.WaitOne(100);
					}
					else
					{
						Render(requested);
					}
				}
				catch (Exception ex)
				{
					string text = (router.FallbackToGameOutput ? "게임 출력으로 복구" : "음성 음소거");
					Volatile.Write(ref status, new OutputStatus(ready: false, text + " · " + ex.Message + " · 자동 재연결 중", null, failed: true));
					wake.WaitOne(1000);
				}
			}
			Volatile.Write(ref status, new OutputStatus(ready: false, "음성 출력 종료"));
		}

		private void Render(Configuration requested)
		{
			using WasapiOutput wasapiOutput = new WasapiOutput(requested.Selection, requested.SampleRate, requested.Latency);
			float[] destination = new float[checked(wasapiOutput.BufferFrames * 2)];
			wasapiOutput.Start();
			if (!ShouldRun(requested))
			{
				return;
			}
			RoutingSession routingSession = router.BeginSession(requested.SampleRate, requested.MaximumQueue);
			Action<float[], int> write = wasapiOutput.Write;
			long timestamp = Stopwatch.GetTimestamp();
			try
			{
				Volatile.Write(ref status, new OutputStatus(ready: true, "분리 출력 중 · " + wasapiOutput.Device.Name, wasapiOutput.Device.Id));
				long num = Stopwatch.GetTimestamp();
				while (ShouldRun(requested) && routingSession.IsActive)
				{
					int availableFrames = wasapiOutput.AvailableFrames;
					if (availableFrames > 0)
					{
						routingSession.Render(destination, availableFrames, write);
					}
					Interlocked.Exchange(ref droppedFrames, routingSession.DroppedFrames);
					long timestamp2 = Stopwatch.GetTimestamp();
					if (timestamp2 >= num && (requested.Selection == "default" || requested.Selection == "communications"))
					{
						num = timestamp2 + Stopwatch.Frequency;
						EndpointObservation endpointObservation = Volatile.Read(in observedEndpoint);
						if (endpointObservation != null && endpointObservation.Timestamp > timestamp && endpointObservation.Selection == requested.Selection && endpointObservation.Id != wasapiOutput.Device.Id)
						{
							break;
						}
					}
					wake.WaitOne(3);
				}
			}
			finally
			{
				router.EndSession(routingSession);
				Volatile.Write(ref status, new OutputStatus(ready: false, "음성 출력 다시 연결 중"));
			}
		}

		private void DiscoverDevices()
		{
			while (Volatile.Read(in shutdown) == 0)
			{
				try
				{
					Volatile.Write(ref devices, AudioDevices.Enumerate());
					AudioDevice audioDevice = AudioDevices.Resolve("default");
					Volatile.Write(ref defaultOutputDevice, audioDevice);
					Configuration configuration = Volatile.Read(in this.configuration);
					if (configuration.Selection == "default" || configuration.Selection == "communications")
					{
						AudioDevice audioDevice2 = ((configuration.Selection == "default") ? audioDevice : AudioDevices.Resolve(configuration.Selection));
						Volatile.Write(ref observedEndpoint, new EndpointObservation(configuration.Selection, audioDevice2.Id));
					}
					Volatile.Write(ref discoveryError, null);
				}
				catch (Exception ex)
				{
					Volatile.Write(ref defaultOutputDevice, null);
					Volatile.Write(ref discoveryError, ex.Message);
				}
				discoveryWake.WaitOne(1000);
			}
		}

		public void Dispose()
		{
			if (Interlocked.Exchange(ref shutdown, 1) == 0)
			{
				router.RetireCurrentSession();
				wake.Set();
				discoveryWake.Set();
				if (thread.Join(3000))
				{
					wake.Dispose();
				}
				if (discoveryThread.Join(3000))
				{
					discoveryWake.Dispose();
				}
			}
		}
	}
	public interface IProcessAudioPolicy
	{
		string? GetEndpoint(int role);

		void SetEndpoint(int role, string? endpointId);
	}
	public interface IProcessAudioBackup
	{
		string?[]? Load();

		void Save(string?[] endpoints);

		void Clear();
	}
	public sealed class ProcessOutputRoute
	{
		private readonly IProcessAudioPolicy policy;

		private readonly IProcessAudioBackup backup;

		private string?[]? original;

		public ProcessOutputRoute(IProcessAudioPolicy policy, IProcessAudioBackup backup)
		{
			this.policy = policy;
			this.backup = backup;
			original = backup.Load();
			if (original != null && original.Length != 2)
			{
				throw new InvalidDataException("게임 출력 복구 파일이 올바르지 않습니다.");
			}
		}

		public void Apply(string? endpointId)
		{
			string[] array = ReadEndpoints();
			if (!array.All((string endpoint) => endpoint == endpointId))
			{
				if (original == null)
				{
					backup.Save(array);
					original = array;
				}
				ChangeEndpoints(new string[2] { endpointId, endpointId }, array);
			}
		}

		public void Restore()
		{
			if (original != null)
			{
				ChangeEndpoints(original, ReadEndpoints());
				backup.Clear();
				original = null;
			}
		}

		private string?[] ReadEndpoints()
		{
			return new string[2]
			{
				policy.GetEndpoint(0),
				policy.GetEndpoint(1)
			};
		}

		private void ChangeEndpoints(string?[] desired, string?[] current)
		{
			try
			{
				for (int i = 0; i < 2; i++)
				{
					if (current[i] != desired[i])
					{
						policy.SetEndpoint(i, desired[i]);
					}
				}
			}
			catch (Exception item)
			{
				List<Exception> list = new List<Exception> { item };
				for (int j = 0; j < 2; j++)
				{
					try
					{
						policy.SetEndpoint(j, current[j]);
					}
					catch (Exception item2)
					{
						list.Add(item2);
					}
				}
				if (list.Count > 1)
				{
					throw new AggregateException("게임 출력 변경과 이전 설정 복구에 실패했습니다.", list);
				}
				throw;
			}
		}
	}
	public sealed class StereoFrameQueue
	{
		private readonly float[] samples;

		private readonly int capacity;

		private long readFrame;

		private long writeFrame;

		private long droppedFrames;

		public long DroppedFrames => Interlocked.Read(in droppedFrames);

		public StereoFrameQueue(int capacityFrames)
		{
			if (capacityFrames < 1)
			{
				throw new ArgumentOutOfRangeException("capacityFrames");
			}
			capacity = capacityFrames;
			samples = new float[checked(capacityFrames * 2)];
		}

		public bool TryWrite(float[] source, int channels, float gain)
		{
			if (channels < 1 || source.Length % channels != 0)
			{
				return false;
			}
			int num = source.Length / channels;
			long num2 = writeFrame;
			if (num > capacity - (num2 - Volatile.Read(in readFrame)))
			{
				Interlocked.Add(ref droppedFrames, num);
				return false;
			}
			if (float.IsNaN(gain) || float.IsInfinity(gain))
			{
				gain = 0f;
			}
			for (int i = 0; i < num; i++)
			{
				int num3 = i * channels;
				int num4 = (int)((num2 + i) % capacity) * 2;
				float num5 = source[num3];
				float num6 = ((channels == 1) ? num5 : source[num3 + 1]);
				if (channels == 4)
				{
					num5 += source[num3 + 2] * 0.70710677f;
					num6 += source[num3 + 3] * 0.70710677f;
				}
				else if (channels > 2)
				{
					float num7 = source[num3 + 2] * 0.70710677f;
					num5 += num7;
					num6 += num7;
					if (channels == 5)
					{
						num5 += source[num3 + 3] * 0.70710677f;
						num6 += source[num3 + 4] * 0.70710677f;
					}
					else if (channels > 5)
					{
						num5 += source[num3 + 4] * 0.70710677f;
						num6 += source[num3 + 5] * 0.70710677f;
					}
					if (channels > 6)
					{
						num5 += source[num3 + 6] * 0.70710677f;
					}
					if (channels > 7)
					{
						num6 += source[num3 + 7] * 0.70710677f;
					}
				}
				samples[num4] = Finite(num5 * gain);
				samples[num4 + 1] = Finite(num6 * gain);
			}
			Volatile.Write(ref writeFrame, num2 + num);
			return true;
		}

		public int MixInto(float[] destination, int frames, int maximumQueuedFrames)
		{
			if (frames < 0 || frames > destination.Length / 2)
			{
				throw new ArgumentOutOfRangeException("frames");
			}
			if (maximumQueuedFrames < 1)
			{
				throw new ArgumentOutOfRangeException("maximumQueuedFrames");
			}
			long num = readFrame;
			long num2 = Volatile.Read(in writeFrame) - num;
			if (num2 > maximumQueuedFrames)
			{
				long num3 = num2 - maximumQueuedFrames;
				num += num3;
				num2 -= num3;
				Interlocked.Add(ref droppedFrames, num3);
			}
			int num4 = (int)Math.Min(frames, num2);
			for (int i = 0; i < num4; i++)
			{
				int num5 = (int)((num + i) % capacity) * 2;
				destination[i * 2] += samples[num5];
				destination[i * 2 + 1] += samples[num5 + 1];
			}
			Volatile.Write(ref readFrame, num + num4);
			return num4;
		}

		private static float Finite(float sample)
		{
			if (!float.IsNaN(sample) && !float.IsInfinity(sample))
			{
				return sample;
			}
			return 0f;
		}
	}
	public sealed class VoiceRouter
	{
		private readonly object gate = new object();

		private readonly List<VoiceStream> streams = new List<VoiceStream>();

		private RoutingSession? currentSession;

		private int enabled = 1;

		private int fallbackToGameOutput = 1;

		private float volume = 1f;

		public bool Enabled
		{
			get
			{
				return Volatile.Read(in enabled) != 0;
			}
			set
			{
				Volatile.Write(ref enabled, value ? 1 : 0);
				if (!value)
				{
					RetireCurrentSession();
				}
			}
		}

		public bool FallbackToGameOutput
		{
			get
			{
				return Volatile.Read(in fallbackToGameOutput) != 0;
			}
			set
			{
				Volatile.Write(ref fallbackToGameOutput, value ? 1 : 0);
			}
		}

		public float Volume
		{
			get
			{
				return Volatile.Read(in volume);
			}
			set
			{
				Volatile.Write(ref volume, (float.IsNaN(value) || float.IsInfinity(value)) ? 0f : Math.Max(0f, Math.Min(2f, value)));
			}
		}

		public int SourceCount
		{
			get
			{
				lock (gate)
				{
					return streams.Count;
				}
			}
		}

		public VoiceStream RegisterSource()
		{
			lock (gate)
			{
				VoiceStream voiceStream = new VoiceStream(this);
				streams.Add(voiceStream);
				if (currentSession != null && !voiceStream.Muted)
				{
					voiceStream.Target = currentSession.AddSource();
				}
				return voiceStream;
			}
		}

		internal void UnregisterSource(VoiceStream stream)
		{
			lock (gate)
			{
				stream.Target?.Retire();
				stream.Target = null;
				streams.Remove(stream);
				currentSession?.RemoveRetiredSources();
			}
		}

		internal void RefreshSource(VoiceStream stream)
		{
			lock (gate)
			{
				if (streams.Contains(stream))
				{
					stream.Target?.Retire();
					currentSession?.RemoveRetiredSources();
					stream.Target = ((currentSession != null && !stream.Muted) ? currentSession.AddSource() : null);
				}
			}
		}

		public RoutingSession BeginSession(int sampleRate, int maximumQueuedMilliseconds = 100)
		{
			if (sampleRate < 8000 || sampleRate > 192000)
			{
				throw new ArgumentOutOfRangeException("sampleRate");
			}
			if (maximumQueuedMilliseconds < 20 || maximumQueuedMilliseconds > 500)
			{
				throw new ArgumentOutOfRangeException("maximumQueuedMilliseconds");
			}
			lock (gate)
			{
				RetireCurrentSession();
				RoutingSession routingSession = new RoutingSession(sampleRate, maximumQueuedMilliseconds);
				foreach (VoiceStream stream in streams)
				{
					stream.Target = (stream.Muted ? null : routingSession.AddSource());
				}
				currentSession = routingSession;
				return routingSession;
			}
		}

		public void RetireCurrentSession()
		{
			lock (gate)
			{
				currentSession?.Retire();
				currentSession = null;
				foreach (VoiceStream stream in streams)
				{
					stream.Target = null;
				}
			}
		}

		public void EndSession(RoutingSession session)
		{
			lock (gate)
			{
				session.Retire();
				if (currentSession == session)
				{
					RetireCurrentSession();
				}
			}
		}
	}
	public sealed class VoiceStream : IDisposable
	{
		private readonly VoiceRouter router;

		private CaptureTarget? target;

		private int disposed;

		private int muted;

		private float gain = 1f;

		internal CaptureTarget? Target
		{
			get
			{
				return Volatile.Read(in target);
			}
			set
			{
				Volatile.Write(ref target, value);
			}
		}

		public float Gain
		{
			get
			{
				return Volatile.Read(in gain);
			}
			set
			{
				Volatile.Write(ref gain, value);
			}
		}

		public bool Muted
		{
			get
			{
				return Volatile.Read(in muted) != 0;
			}
			set
			{
				if (Interlocked.Exchange(ref muted, value ? 1 : 0) == (value ? 1 : 0))
				{
					if (value)
					{
						return;
					}
					CaptureTarget? captureTarget = Target;
					if (captureTarget == null || !captureTarget.IsRetired)
					{
						return;
					}
				}
				router.RefreshSource(this);
			}
		}

		internal VoiceStream(VoiceRouter router)
		{
			this.router = router;
		}

		public void DiscardBufferedAudio()
		{
			Target?.Retire();
		}

		public void Process(float[] data, int channels)
		{
			if (router.Enabled && Volatile.Read(in disposed) == 0)
			{
				CaptureTarget captureTarget = Target;
				if ((!Muted && captureTarget != null && captureTarget.IsActive && captureTarget.Queue.TryWrite(data, channels, Gain * router.Volume)) || !router.FallbackToGameOutput)
				{
					Array.Clear(data, 0, data.Length);
				}
			}
		}

		public void Dispose()
		{
			if (Interlocked.Exchange(ref disposed, 1) == 0)
			{
				router.UnregisterSource(this);
			}
		}
	}
	internal sealed class CaptureTarget
	{
		private readonly RoutingSession session;

		private int active = 1;

		public StereoFrameQueue Queue { get; }

		public bool IsActive
		{
			get
			{
				if (Volatile.Read(in active) != 0)
				{
					return session.IsActive;
				}
				return false;
			}
		}

		public bool IsRetired => Volatile.Read(in active) == 0;

		public CaptureTarget(RoutingSession session, int capacityFrames)
		{
			this.session = session;
			Queue = new StereoFrameQueue(capacityFrames);
		}

		public void Retire()
		{
			lock (session.RenderGate)
			{
				Volatile.Write(ref active, 0);
			}
		}
	}
	public sealed class RoutingSession
	{
		internal readonly object RenderGate = new object();

		private CaptureTarget[] sources = Array.Empty<CaptureTarget>();

		private int active = 1;

		private readonly int maximumQueuedFrames;

		public int SampleRate { get; }

		public bool IsActive => Volatile.Read(in active) != 0;

		public long DroppedFrames => Volatile.Read(in sources).Sum((CaptureTarget source) => source.Queue.DroppedFrames);

		internal RoutingSession(int sampleRate, int maximumQueuedMilliseconds)
		{
			SampleRate = sampleRate;
			maximumQueuedFrames = sampleRate * maximumQueuedMilliseconds / 1000;
		}

		internal CaptureTarget AddSource()
		{
			CaptureTarget captureTarget = new CaptureTarget(this, Math.Max(SampleRate / 2, 4096));
			CaptureTarget[] array = Volatile.Read(in sources);
			CaptureTarget[] array2 = new CaptureTarget[array.Length + 1];
			Array.Copy(array, array2, array.Length);
			array2[array.Length] = captureTarget;
			Volatile.Write(ref sources, array2);
			return captureTarget;
		}

		internal void RemoveRetiredSources()
		{
			Volatile.Write(ref sources, (from source in Volatile.Read(in sources)
				where !source.IsRetired
				select source).ToArray());
		}

		public void Mix(float[] destination, int frames)
		{
			lock (RenderGate)
			{
				MixFrames(destination, frames);
			}
		}

		internal void Render(float[] destination, int frames, Action<float[], int> write)
		{
			lock (RenderGate)
			{
				MixFrames(destination, frames);
				if (IsActive)
				{
					write(destination, frames);
				}
			}
		}

		private void MixFrames(float[] destination, int frames)
		{
			if (frames < 0 || frames > destination.Length / 2)
			{
				throw new ArgumentOutOfRangeException("frames");
			}
			int num = frames * 2;
			Array.Clear(destination, 0, num);
			if (!IsActive)
			{
				return;
			}
			CaptureTarget[] array = Volatile.Read(in sources);
			foreach (CaptureTarget captureTarget in array)
			{
				if (captureTarget.IsActive)
				{
					captureTarget.Queue.MixInto(destination, frames, maximumQueuedFrames);
				}
			}
			for (int j = 0; j < num; j++)
			{
				destination[j] = Math.Max(-1f, Math.Min(1f, destination[j]));
			}
		}

		public void Retire()
		{
			lock (RenderGate)
			{
				Volatile.Write(ref active, 0);
			}
		}
	}
	public sealed class WasapiOutput : IDisposable
	{
		private readonly ComApartment apartment;

		private IntPtr device;

		private IntPtr client;

		private IntPtr renderer;

		private readonly CoreAudioNative.CountDelegate padding;

		private readonly CoreAudioNative.BufferDelegate getBuffer;

		private readonly CoreAudioNative.ReleaseBufferDelegate releaseBuffer;

		private readonly CoreAudioNative.CommandDelegate stop;

		private bool started;

		private bool disposed;

		public AudioDevice Device { get; }

		public int BufferFrames { get; }

		public int AvailableFrames
		{
			get
			{
				ThrowIfDisposed();
				CoreAudioNative.Check(padding(client, out var count));
				return BufferFrames - checked((int)count);
			}
		}

		public WasapiOutput(string selection, int sampleRate, int latencyMilliseconds)
		{
			if (sampleRate < 8000 || sampleRate > 192000)
			{
				throw new ArgumentOutOfRangeException("sampleRate");
			}
			if (latencyMilliseconds < 20 || latencyMilliseconds > 200)
			{
				throw new ArgumentOutOfRangeException("latencyMilliseconds");
			}
			apartment = new ComApartment();
			try
			{
				device = AudioDevices.Open(selection);
				Device = AudioDevices.Describe(device);
				CoreAudioNative.Check(CoreAudioNative.Method<CoreAudioNative.ActivateDelegate>(device, 3)(device, in CoreAudioNative.AudioClientInterface, 23u, IntPtr.Zero, out client));
				CoreAudioNative.WaveFormat format = new CoreAudioNative.WaveFormat
				{
					FormatTag = 3,
					Channels = 2,
					SamplesPerSecond = (uint)sampleRate,
					AverageBytesPerSecond = (uint)(sampleRate * 8),
					BlockAlign = 8,
					BitsPerSample = 32
				};
				CoreAudioNative.Check(CoreAudioNative.Method<CoreAudioNative.InitializeDelegate>(client, 3)(client, 0, 2281701376u, (long)latencyMilliseconds * 10000L, 0L, in format, IntPtr.Zero));
				CoreAudioNative.Check(CoreAudioNative.Method<CoreAudioNative.CountDelegate>(client, 4)(client, out var count));
				BufferFrames = checked((int)count);
				CoreAudioNative.Check(CoreAudioNative.Method<CoreAudioNative.ServiceDelegate>(client, 14)(client, in CoreAudioNative.RenderClientInterface, out renderer));
				padding = CoreAudioNative.Method<CoreAudioNative.CountDelegate>(client, 6);
				getBuffer = CoreAudioNative.Method<CoreAudioNative.BufferDelegate>(renderer, 3);
				releaseBuffer = CoreAudioNative.Method<CoreAudioNative.ReleaseBufferDelegate>(renderer, 4);
				stop = CoreAudioNative.Method<CoreAudioNative.CommandDelegate>(client, 11);
			}
			catch
			{
				CoreAudioNative.Release(ref renderer);
				CoreAudioNative.Release(ref client);
				CoreAudioNative.Release(ref device);
				apartment.Dispose();
				throw;
			}
		}

		public void Start()
		{
			ThrowIfDisposed();
			if (!started)
			{
				CoreAudioNative.Check(CoreAudioNative.Method<CoreAudioNative.CommandDelegate>(client, 10)(client));
				started = true;
			}
		}

		public void Write(float[] samples, int frames)
		{
			ThrowIfDisposed();
			if (frames < 0 || frames > samples.Length / 2 || frames > BufferFrames)
			{
				throw new ArgumentOutOfRangeException("frames");
			}
			if (frames != 0)
			{
				CoreAudioNative.Check(getBuffer(renderer, (uint)frames, out var data));
				try
				{
					Marshal.Copy(samples, 0, data, frames * 2);
				}
				catch
				{
					releaseBuffer(renderer, (uint)frames, 2u);
					throw;
				}
				CoreAudioNative.Check(releaseBuffer(renderer, (uint)frames, 0u));
			}
		}

		private void ThrowIfDisposed()
		{
			if (disposed)
			{
				throw new ObjectDisposedException("WasapiOutput");
			}
		}

		public void Dispose()
		{
			if (!disposed)
			{
				disposed = true;
				if (started)
				{
					stop(client);
				}
				CoreAudioNative.Release(ref renderer);
				CoreAudioNative.Release(ref client);
				CoreAudioNative.Release(ref device);
				apartment.Dispose();
			}
		}
	}
	public sealed class WindowsProcessAudioPolicy : IProcessAudioPolicy, IDisposable
	{
		[UnmanagedFunctionPointer(CallingConvention.StdCall)]
		private delegate int GetEndpointDelegate(IntPtr self, uint processId, int flow, int role, out IntPtr endpoint);

		[UnmanagedFunctionPointer(CallingConvention.StdCall)]
		private delegate int SetEndpointDelegate(IntPtr self, uint processId, int flow, int role, IntPtr endpoint);

		private sealed class WindowsString : IDisposable
		{
			public IntPtr Handle { get; }

			public WindowsString(string? value)
			{
				if (value != null)
				{
					CoreAudioNative.Check(WindowsCreateString(value, checked((uint)value.Length), out var result));
					Handle = result;
				}
			}

			public void Dispose()
			{
				if (Handle != IntPtr.Zero)
				{
					WindowsDeleteString(Handle);
				}
			}
		}

		private const string DevicePrefix = "\\\\?\\SWD#MMDEVAPI#";

		private const string RenderSuffix = "#{e6327cad-dcec-4949-ae8a-991e976a79d2}";

		private readonly ComApartment apartment;

		private readonly uint processId;

		private IntPtr factory;

		private bool disposed;

		public WindowsProcessAudioPolicy(uint processId)
		{
			this.processId = processId;
			apartment = new ComApartment();
			try
			{
				using WindowsString windowsString = new WindowsString("Windows.Media.Internal.AudioPolicyConfig");
				int num = RoGetActivationFactory(windowsString.Handle, new Guid("ab3d4648-e242-459f-b02f-541c70306324"), out factory);
				if (num == -2147467262)
				{
					CoreAudioNative.Release(ref factory);
					num = RoGetActivationFactory(windowsString.Handle, new Guid("2a59116d-6c4f-45e0-a74f-707e3fef9258"), out factory);
				}
				CoreAudioNative.Check(num);
			}
			catch
			{
				Dispose();
				throw;
			}
		}

		public string? GetEndpoint(int role)
		{
			ValidateRole(role);
			IntPtr endpoint = IntPtr.Zero;
			try
			{
				int num = CoreAudioNative.Method<GetEndpointDelegate>(factory, 26)(factory, processId, 0, role, out endpoint);
				switch (num)
				{
				case -2147023728:
					return null;
				case -2147024809:
					throw new InvalidOperationException("앱 오디오 세션이 아직 준비되지 않았습니다.");
				default:
				{
					CoreAudioNative.Check(num);
					uint length;
					IntPtr ptr = WindowsGetStringRawBuffer(endpoint, out length);
					string text = ((length == 0) ? null : Marshal.PtrToStringUni(ptr, checked((int)length)));
					if (text != null && text.StartsWith("\\\\?\\SWD#MMDEVAPI#", StringComparison.OrdinalIgnoreCase))
					{
						text = text.Substring("\\\\?\\SWD#MMDEVAPI#".Length);
					}
					if (text != null && text.EndsWith("#{e6327cad-dcec-4949-ae8a-991e976a79d2}", StringComparison.OrdinalIgnoreCase))
					{
						text = text.Substring(0, text.Length - "#{e6327cad-dcec-4949-ae8a-991e976a79d2}".Length);
					}
					return string.IsNullOrWhiteSpace(text) ? null : text;
				}
				}
			}
			finally
			{
				if (endpoint != IntPtr.Zero)
				{
					WindowsDeleteString(endpoint);
				}
			}
		}

		public void SetEndpoint(int role, string? endpointId)
		{
			ValidateRole(role);
			using WindowsString windowsString = new WindowsString(string.IsNullOrWhiteSpace(endpointId) ? null : ("\\\\?\\SWD#MMDEVAPI#" + endpointId + "#{e6327cad-dcec-4949-ae8a-991e976a79d2}"));
			CoreAudioNative.Check(CoreAudioNative.Method<SetEndpointDelegate>(factory, 25)(factory, processId, 0, role, windowsString.Handle));
		}

		private void ValidateRole(int role)
		{
			if (factory == IntPtr.Zero)
			{
				throw new ObjectDisposedException("WindowsProcessAudioPolicy");
			}
			if (role < 0 || role > 1)
			{
				throw new ArgumentOutOfRangeException("role");
			}
		}

		public void Dispose()
		{
			if (!disposed)
			{
				disposed = true;
				CoreAudioNative.Release(ref factory);
				apartment.Dispose();
			}
		}

		[DllImport("combase.dll", ExactSpelling = true)]
		private static extern int RoGetActivationFactory(IntPtr name, in Guid iid, out IntPtr instance);

		[DllImport("combase.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
		private static extern int WindowsCreateString([MarshalAs(UnmanagedType.LPWStr)] string value, uint length, out IntPtr result);

		[DllImport("combase.dll", ExactSpelling = true)]
		private static extern int WindowsDeleteString(IntPtr value);

		[DllImport("combase.dll", ExactSpelling = true)]
		private static extern IntPtr WindowsGetStringRawBuffer(IntPtr value, out uint length);
	}
}

plugins/LethalVoiceRouter/LethalVoiceRouter.dll

Decompiled a day ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using Dissonance;
using Dissonance.Audio.Capture;
using Dissonance.Audio.Playback;
using Dissonance.Config;
using GameNetcodeStuff;
using HarmonyLib;
using LethalConfig;
using LethalConfig.ConfigItems;
using LethalConfig.ConfigItems.Options;
using LethalVoiceRouter.Audio;
using Microsoft.CodeAnalysis;
using NAudio.Wave;
using TMPro;
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.EventSystems;
using UnityEngine.Events;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.UI;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: InternalsVisibleTo("LethalVoiceRouter.GameTests")]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("LethalVoiceRouter")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.5.0.0")]
[assembly: AssemblyInformationalVersion("0.5.0+258e5d578787605554f3f95238b41255d59850fc")]
[assembly: AssemblyProduct("LethalVoiceRouter")]
[assembly: AssemblyTitle("LethalVoiceRouter")]
[assembly: AssemblyVersion("0.5.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace LethalVoiceRouter
{
	internal static class ClearVoiceOpus
	{
		internal const int SetBitrate = 4002;

		internal const int GetBitrate = 4003;

		internal const int SetComplexity = 4010;

		internal const int GetComplexity = 4011;

		internal const int SetFec = 4012;

		internal const int GetFec = 4013;

		[DllImport("opus", CallingConvention = CallingConvention.Cdecl)]
		private static extern IntPtr opus_encoder_create(int sampleRate, int channels, int application, out int error);

		[DllImport("opus", CallingConvention = CallingConvention.Cdecl)]
		private static extern void opus_encoder_destroy(IntPtr encoder);

		[DllImport("opus", CallingConvention = CallingConvention.Cdecl)]
		private static extern IntPtr opus_get_version_string();

		[DllImport("opus", CallingConvention = CallingConvention.Cdecl)]
		private static extern int dissonance_opus_encoder_ctl_in(IntPtr encoder, int request, int value);

		[DllImport("opus", CallingConvention = CallingConvention.Cdecl)]
		private static extern int dissonance_opus_encoder_ctl_out(IntPtr encoder, int request, out int value);

		internal static void Set(IntPtr encoder, int request, int value)
		{
			int num = dissonance_opus_encoder_ctl_in(encoder, request, value);
			if (num < 0)
			{
				throw new InvalidOperationException($"Opus 설정 실패: 요청 {request}, 오류 {num}");
			}
		}

		internal static int Get(IntPtr encoder, int request)
		{
			int value;
			int num = dissonance_opus_encoder_ctl_out(encoder, request, out value);
			if (num < 0)
			{
				throw new InvalidOperationException($"Opus 설정 조회 실패: 요청 {request}, 오류 {num}");
			}
			return value;
		}

		internal static string Verify()
		{
			int error;
			IntPtr intPtr = opus_encoder_create(48000, 1, 2048, out error);
			if (intPtr == IntPtr.Zero || error != 0)
			{
				throw new InvalidOperationException($"Opus 초기화 검사 실패: {error}");
			}
			try
			{
				Set(intPtr, 4002, 300000);
				Set(intPtr, 4010, 10);
				int[] array = new int[2] { 0, 1 };
				foreach (int num in array)
				{
					Set(intPtr, 4012, num);
					if (Get(intPtr, 4013) != num)
					{
						throw new InvalidOperationException("Opus FEC 설정을 확인할 수 없습니다.");
					}
				}
				if (Get(intPtr, 4003) != 300000 || Get(intPtr, 4011) != 10)
				{
					throw new InvalidOperationException("Opus 300kbps·복잡도 10 설정을 확인할 수 없습니다.");
				}
				return Marshal.PtrToStringAnsi(opus_get_version_string()) ?? "Opus";
			}
			finally
			{
				opus_encoder_destroy(intPtr);
			}
		}
	}
	internal static class ClearVoicePatches
	{
		private interface IRestorable
		{
			void Restore();
		}

		private sealed class EncoderState : IRestorable
		{
			private readonly object holder;

			private readonly object gate;

			private readonly int originalBitrate;

			private readonly int originalComplexity;

			private readonly int originalFec;

			private int appliedBitrate = -1;

			private int appliedFec = -1;

			internal EncoderState(object encoder)
			{
				holder = encoderHolder(encoder);
				gate = holderGate(holder);
				lock (gate)
				{
					IntPtr encoder2 = holderValue(holder);
					originalBitrate = ClearVoiceOpus.Get(encoder2, 4003);
					originalComplexity = ClearVoiceOpus.Get(encoder2, 4011);
					originalFec = ClearVoiceOpus.Get(encoder2, 4013);
				}
				Track(this);
			}

			internal void Apply()
			{
				lock (gate)
				{
					IntPtr intPtr = holderValue(holder);
					if (intPtr == IntPtr.Zero)
					{
						return;
					}
					if (!ClearVoiceRuntime.Active)
					{
						Restore();
						return;
					}
					int targetBitrate = ClearVoiceRuntime.TargetBitrate;
					int num = (ClearVoiceRuntime.UseFec ? 1 : 0);
					if (appliedBitrate != targetBitrate || appliedFec != num)
					{
						ClearVoiceOpus.Set(intPtr, 4002, targetBitrate);
						ClearVoiceOpus.Set(intPtr, 4012, num);
						ClearVoiceOpus.Set(intPtr, 4010, 10);
						int num2 = ClearVoiceOpus.Get(intPtr, 4003);
						int num3 = ClearVoiceOpus.Get(intPtr, 4013);
						if (num2 != targetBitrate || num3 != num)
						{
							throw new InvalidOperationException("Opus 비트레이트 또는 FEC 적용값이 요청과 다릅니다.");
						}
						ClearVoiceRuntime.Instance?.ObserveEncoder(num2, num3 != 0);
						appliedBitrate = targetBitrate;
						appliedFec = num;
					}
				}
			}

			public void Restore()
			{
				lock (gate)
				{
					IntPtr intPtr = holderValue(holder);
					if (!(intPtr == IntPtr.Zero))
					{
						ClearVoiceOpus.Set(intPtr, 4002, originalBitrate);
						ClearVoiceOpus.Set(intPtr, 4012, originalFec);
						ClearVoiceOpus.Set(intPtr, 4010, originalComplexity);
						appliedBitrate = (appliedFec = -1);
					}
				}
			}
		}

		private sealed class DecoderState : IRestorable
		{
			private readonly WeakReference<object> decoder;

			private readonly bool originalFec;

			private readonly object gate = new object();

			internal DecoderState(object instance)
			{
				decoder = new WeakReference<object>(instance);
				originalFec = decoderFec(instance);
				Track(this);
			}

			internal void Apply(object instance)
			{
				lock (gate)
				{
					setDecoderFec(instance, ClearVoiceRuntime.Active ? ClearVoiceRuntime.UseFec : originalFec);
				}
			}

			public void Restore()
			{
				lock (gate)
				{
					if (decoder.TryGetTarget(out object target))
					{
						setDecoderFec(target, originalFec);
					}
				}
			}
		}

		private static readonly ConditionalWeakTable<object, EncoderState> Encoders = new ConditionalWeakTable<object, EncoderState>();

		private static readonly ConditionalWeakTable<object, DecoderState> Decoders = new ConditionalWeakTable<object, DecoderState>();

		private static readonly List<WeakReference<IRestorable>> States = new List<WeakReference<IRestorable>>();

		private static readonly List<(MethodBase Target, string? Prefix, string? Postfix, string? Finalizer)> Targets = new List<(MethodBase, string, string, string)>();

		private static Func<object, object> encoderHolder = null;

		private static Func<object, object> holderGate = null;

		private static Func<object, IntPtr> holderValue = null;

		private static Func<object, bool> decoderFec = null;

		private static Action<object, bool> setDecoderFec = null;

		private static MethodInfo occludeUpdate = null;

		[ThreadStatic]
		private static float[]? vadScratch;

		[ThreadStatic]
		private static int sendingBytes;

		internal static void Prepare()
		{
			Targets.Clear();
			Assembly assembly = typeof(DissonanceComms).Assembly;
			Type type = RequireType(assembly, "Dissonance.Audio.Codecs.Opus.OpusEncoder");
			Type type2 = RequireType(assembly, "Dissonance.Audio.Codecs.Opus.OpusNative+OpusEncoder");
			Type type3 = RequireType(assembly, "Dissonance.Audio.Codecs.Opus.OpusNative+OpusDecoder");
			Type type4 = RequireType(assembly, "Dissonance.Audio.Capture.WebRtcPreprocessingPipeline+WebRtcPreprocessor");
			Type type5 = RequireType(assembly, "Dissonance.Audio.Capture.WebRtcPreprocessingPipeline+RnnoisePreprocessor");
			Type type6 = RequireType(typeof(GameNetworkManager).Assembly, "Dissonance.Integrations.Unity_NFGO.NfgoConn");
			Type type7 = RequireType(assembly, "Dissonance.Networking.Client.VoiceSender`1").MakeGenericType(type6);
			FieldInfo fieldInfo = RequireField(type2, "_encoder");
			encoderHolder = Getter<object>(fieldInfo);
			holderGate = Getter<object>(RequireField(fieldInfo.FieldType, "_lockObject"));
			holderValue = Getter<IntPtr>(RequireField(fieldInfo.FieldType, "_value"));
			FieldInfo field = RequireField(type3, "<EnableForwardErrorCorrection>k__BackingField");
			decoderFec = Getter<bool>(field);
			setDecoderFec = Setter<bool>(field);
			occludeUpdate = RequireMethod(typeof(OccludeAudio), "Update", 0);
			Add(type.GetConstructor(new Type[3]
			{
				typeof(AudioQuality),
				typeof(FrameSize),
				typeof(bool)
			}) ?? throw new MissingMethodException(type.FullName, ".ctor"), null, "EncoderCreated");
			Add(RequireMethod(type2, "EncodeFloats", 2), "EncodePrefix");
			Add(RequireMethod(type3, "DecodeFloats", 2), "DecodePrefix");
			Add(RequireMethod(type4, "Process", 5), "AnalyzeCopy");
			Add(RequireMethod(type5, "Process", 3), "SkipProcessing");
			Add(RequireMethod(type5, "Reset", 0), "SkipProcessing");
			Add(RequireMethod(typeof(VoiceSettings), "get_DenoiseAmount", 0), null, "NoiseSuppression");
			Add(RequireMethod(typeof(VoiceSettings), "get_BackgroundSoundRemovalEnabled", 0), null, "BackgroundRemoval");
			Add(RequireMethod(typeof(VoiceSettings), "get_AecSuppressionAmount", 0), null, "EchoSuppression");
			Add(RequireMethod(typeof(VoiceSettings), "get_AecmRoutingMode", 0), null, "MobileEchoSuppression");
			Add(RequireMethod(typeof(VoiceSettings), "get_FrameSize", 0), null, "FrameLength");
			Add(RequireMethod(type7, "Send", 1), "BeginSend", null, "EndSend");
			Add(RequireMethod(type7, "GetVoiceDestinations", 1), null, "CheckPacketBudget");
		}

		internal static void Install(Harmony harmony)
		{
			foreach (var target in Targets)
			{
				harmony.Patch(target.Target, Method(target.Prefix), Method(target.Postfix), (HarmonyMethod)null, Method(target.Finalizer), (HarmonyMethod)null);
			}
			harmony.Patch((MethodBase)occludeUpdate, (HarmonyMethod)null, (HarmonyMethod)null, Method("ExpandNormalVoice"), (HarmonyMethod)null, (HarmonyMethod)null);
		}

		private static void Add(MethodBase target, string? prefix = null, string? postfix = null, string? finalizer = null)
		{
			Targets.Add((target, prefix, postfix, finalizer));
		}

		private static HarmonyMethod? Method(string? name)
		{
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_001b: Expected O, but got Unknown
			if (name != null)
			{
				return new HarmonyMethod(typeof(ClearVoicePatches).GetMethod(name, BindingFlags.Static | BindingFlags.NonPublic));
			}
			return null;
		}

		private static Type RequireType(Assembly assembly, string name)
		{
			return assembly.GetType(name, throwOnError: true);
		}

		private static FieldInfo RequireField(Type type, string name)
		{
			return AccessTools.Field(type, name) ?? throw new MissingFieldException(type.FullName, name);
		}

		private static MethodInfo RequireMethod(Type type, string name, int parameters)
		{
			MethodInfo[] array = (from method in type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
				where method.Name == name && method.GetParameters().Length == parameters
				select method).ToArray();
			if (array.Length != 1)
			{
				throw new MissingMethodException(type.FullName, name);
			}
			return array[0];
		}

		private static Func<object, T> Getter<T>(FieldInfo field)
		{
			ParameterExpression parameterExpression = Expression.Parameter(typeof(object), "instance");
			return Expression.Lambda<Func<object, T>>(Expression.Convert(Expression.Field(Expression.Convert(parameterExpression, field.DeclaringType), field), typeof(T)), new ParameterExpression[1] { parameterExpression }).Compile();
		}

		private static Action<object, T> Setter<T>(FieldInfo field)
		{
			ParameterExpression parameterExpression = Expression.Parameter(typeof(object), "instance");
			ParameterExpression parameterExpression2 = Expression.Parameter(typeof(T), "value");
			return Expression.Lambda<Action<object, T>>(Expression.Assign(Expression.Field(Expression.Convert(parameterExpression, field.DeclaringType), field), parameterExpression2), new ParameterExpression[2] { parameterExpression, parameterExpression2 }).Compile();
		}

		private static void Track(IRestorable state)
		{
			lock (States)
			{
				States.RemoveAll((WeakReference<IRestorable> reference) => !reference.TryGetTarget(out IRestorable _));
				States.Add(new WeakReference<IRestorable>(state));
			}
		}

		internal static void Restore()
		{
			lock (States)
			{
				Exception ex = null;
				foreach (WeakReference<IRestorable> state in States)
				{
					if (!state.TryGetTarget(out var target))
					{
						continue;
					}
					try
					{
						target.Restore();
					}
					catch (Exception ex2)
					{
						if (ex == null)
						{
							ex = ex2;
						}
					}
				}
				if (ex != null)
				{
					throw ex;
				}
			}
		}

		private static void EncoderCreated(object ____encoder)
		{
			if (!ClearVoiceRuntime.Active)
			{
				return;
			}
			try
			{
				Encoders.GetValue(____encoder, (object instance) => new EncoderState(instance)).Apply();
			}
			catch (Exception exception)
			{
				ClearVoiceRuntime.Instance?.Fail(exception);
			}
		}

		private static void EncodePrefix(object __instance, ref ArraySegment<byte> dstEncoded)
		{
			if (!Encoders.TryGetValue(__instance, out EncoderState value))
			{
				return;
			}
			try
			{
				if (!ClearVoiceRuntime.Active)
				{
					value.Restore();
					return;
				}
				value.Apply();
				if (dstEncoded.Count > 900)
				{
					dstEncoded = new ArraySegment<byte>(dstEncoded.Array, dstEncoded.Offset, 900);
				}
			}
			catch (Exception exception)
			{
				ClearVoiceRuntime.Instance?.Fail(exception);
				try
				{
					value.Restore();
				}
				catch
				{
				}
			}
		}

		private static void DecodePrefix(object __instance)
		{
			try
			{
				DecoderState value;
				if (ClearVoiceRuntime.Active)
				{
					Decoders.GetValue(__instance, (object instance) => new DecoderState(instance)).Apply(__instance);
				}
				else if (Decoders.TryGetValue(__instance, out value))
				{
					value.Restore();
				}
			}
			catch (Exception exception)
			{
				ClearVoiceRuntime.Instance?.Fail(exception);
			}
		}

		private static void AnalyzeCopy(ref float[] input, ref float[] output)
		{
			if (ClearVoiceRuntime.Active)
			{
				if (vadScratch == null || vadScratch.Length != input.Length)
				{
					vadScratch = new float[input.Length];
				}
				Array.Copy(input, vadScratch, input.Length);
				input = (output = vadScratch);
			}
		}

		private static bool SkipProcessing()
		{
			return !ClearVoiceRuntime.Active;
		}

		private static void NoiseSuppression(ref NoiseSuppressionLevels __result)
		{
			if (ClearVoiceRuntime.Active)
			{
				__result = (NoiseSuppressionLevels)(-1);
			}
		}

		private static void BackgroundRemoval(ref bool __result)
		{
			if (ClearVoiceRuntime.Active)
			{
				__result = false;
			}
		}

		private static void EchoSuppression(ref AecSuppressionLevels __result)
		{
			if (ClearVoiceRuntime.Active)
			{
				__result = (AecSuppressionLevels)(-1);
			}
		}

		private static void MobileEchoSuppression(ref AecmRoutingMode __result)
		{
			if (ClearVoiceRuntime.Active)
			{
				__result = (AecmRoutingMode)(-1);
			}
		}

		private static void FrameLength(ref FrameSize __result)
		{
			if (ClearVoiceRuntime.Active)
			{
				__result = (FrameSize)0;
			}
		}

		private static void BeginSend(ArraySegment<byte> encodedAudio, out int __state)
		{
			__state = sendingBytes;
			sendingBytes = (ClearVoiceRuntime.Active ? encodedAudio.Count : (-1));
		}

		private static Exception? EndSend(Exception? __exception, int __state)
		{
			sendingBytes = __state;
			return __exception;
		}

		private static void CheckPacketBudget(object openChannels, object __result)
		{
			if (ClearVoiceRuntime.Active && sendingBytes >= 0)
			{
				IList list = (IList)openChannels;
				IList list2 = (IList)__result;
				if (list2.Count != 0 && !ClearVoicePolicy.FitsPacket(sendingBytes, list.Count, list2.Count))
				{
					list2.Clear();
					ClearVoiceRuntime.Instance?.PacketDropped();
				}
			}
		}

		private static IEnumerable<CodeInstruction> ExpandNormalVoice(IEnumerable<CodeInstruction> instructions)
		{
			//IL_0085: Unknown result type (might be due to invalid IL or missing references)
			//IL_008f: Expected O, but got Unknown
			List<CodeInstruction> list = new List<CodeInstruction>();
			int num = 0;
			foreach (CodeInstruction instruction in instructions)
			{
				if (instruction.opcode == OpCodes.Ldc_R4 && instruction.operand is float num2 && num2 == 10000f)
				{
					instruction.opcode = OpCodes.Ldarg_0;
					instruction.operand = null;
					list.Add(instruction);
					list.Add(new CodeInstruction(OpCodes.Call, (object)AccessTools.Method(typeof(ClearVoicePatches), "NormalVoiceCutoff", (Type[])null, (Type[])null)));
					num++;
				}
				else
				{
					list.Add(instruction);
				}
			}
			if (num != 1)
			{
				throw new InvalidOperationException($"일반 대화 필터 패치 지점이 예상과 다릅니다: {num}");
			}
			return list;
		}

		private static float NormalVoiceCutoff(OccludeAudio instance)
		{
			if (!ClearVoiceRuntime.Active || !((Object)(object)((Component)instance).GetComponent<VoicePlayback>() != (Object)null))
			{
				return 10000f;
			}
			return 20000f;
		}
	}
	internal sealed class ClearVoiceRuntime : IDisposable
	{
		private static ClearVoiceRuntime? instance;

		private readonly ManualLogSource logger;

		private readonly bool enabledAtStartup;

		private Harmony? harmony;

		private int active;

		private int targetBitrate;

		private int useFec;

		private int actualBitrate;

		private int actualFec = -1;

		private long droppedPackets;

		private string? failure;

		private bool failureHandled;

		private bool packetDropWarningLogged;

		private bool disposed;

		internal static ClearVoiceRuntime? Instance => Volatile.Read(in instance);

		internal static bool Active
		{
			get
			{
				ClearVoiceRuntime clearVoiceRuntime = Instance;
				if (clearVoiceRuntime != null)
				{
					return Volatile.Read(in clearVoiceRuntime.active) != 0;
				}
				return false;
			}
		}

		internal static int TargetBitrate
		{
			get
			{
				ClearVoiceRuntime clearVoiceRuntime = Instance;
				if (clearVoiceRuntime != null)
				{
					return Volatile.Read(in clearVoiceRuntime.targetBitrate);
				}
				return 300000;
			}
		}

		internal static bool UseFec
		{
			get
			{
				ClearVoiceRuntime clearVoiceRuntime = Instance;
				if (clearVoiceRuntime != null)
				{
					return Volatile.Read(in clearVoiceRuntime.useFec) != 0;
				}
				return true;
			}
		}

		internal ConfigEntry<bool> Enabled { get; }

		internal ConfigEntry<int> BitrateKbps { get; }

		internal ConfigEntry<bool> FecEnabled { get; }

		internal bool RestartPending => Enabled.Value != enabledAtStartup;

		internal int ActualBitrate => Volatile.Read(in actualBitrate);

		internal int ActualFec => Volatile.Read(in actualFec);

		internal long DroppedPackets => Interlocked.Read(in droppedPackets);

		internal string? Failure => Volatile.Read(in failure);

		private ClearVoiceRuntime(ConfigFile config, ManualLogSource logger)
		{
			//IL_0056: Unknown result type (might be due to invalid IL or missing references)
			//IL_0060: Expected O, but got Unknown
			this.logger = logger;
			Enabled = config.Bind<bool>("ClearVoice", "Enabled", false, "클리어 보이스를 사용합니다. 게임 재시작 후 적용되며 음성 출력 분리와 독립적으로 동작합니다.");
			BitrateKbps = config.Bind<int>("ClearVoice", "BitrateKbps", 300, new ConfigDescription("모노 Opus 목표 비트레이트(kbps). 실제 전송량은 입력·VBR·FEC·패킷 제한에 따라 달라집니다.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(6, 300), Array.Empty<object>()));
			FecEnabled = config.Bind<bool>("ClearVoice", "FecEnabled", true, "클리어 보이스의 송신 FEC와 수신 FEC 복구 시도를 사용합니다. false여도 일반 패킷 손실 은폐(PLC)는 유지됩니다.");
			enabledAtStartup = Enabled.Value;
			ReadSettings();
			BitrateKbps.SettingChanged += SettingsChanged;
			FecEnabled.SettingChanged += SettingsChanged;
		}

		internal static void Start(ConfigFile config, ManualLogSource logger)
		{
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Expected O, but got Unknown
			if (Instance != null)
			{
				return;
			}
			ClearVoiceRuntime clearVoiceRuntime = new ClearVoiceRuntime(config, logger);
			Volatile.Write(ref instance, clearVoiceRuntime);
			if (!clearVoiceRuntime.enabledAtStartup)
			{
				return;
			}
			try
			{
				ClearVoicePatches.Prepare();
				ClearVoiceOpus.Verify();
				clearVoiceRuntime.harmony = new Harmony("io.lethalloopback.ClearVoice");
				ClearVoicePatches.Install(clearVoiceRuntime.harmony);
				Volatile.Write(ref clearVoiceRuntime.active, 1);
				logger.LogInfo((object)string.Format("Clear Voice enabled ({0} kbps, FEC {1}).", clearVoiceRuntime.BitrateKbps.Value, clearVoiceRuntime.FecEnabled.Value ? "on" : "off"));
			}
			catch (Exception exception)
			{
				clearVoiceRuntime.Fail(exception);
				clearVoiceRuntime.HandleFailure();
			}
		}

		private void SettingsChanged(object sender, EventArgs args)
		{
			ReadSettings();
		}

		private void ReadSettings()
		{
			Volatile.Write(ref targetBitrate, BitrateKbps.Value * 1000);
			Volatile.Write(ref useFec, FecEnabled.Value ? 1 : 0);
		}

		internal void ObserveEncoder(int bitrate, bool fec)
		{
			Volatile.Write(ref actualBitrate, bitrate);
			Volatile.Write(ref actualFec, fec ? 1 : 0);
		}

		internal void PacketDropped()
		{
			Interlocked.Increment(ref droppedPackets);
		}

		internal void Fail(Exception exception)
		{
			Interlocked.CompareExchange(ref failure, exception.GetBaseException().Message, null);
			Volatile.Write(ref active, 0);
		}

		internal void Update()
		{
			if (disposed)
			{
				return;
			}
			if (Failure != null && !failureHandled)
			{
				HandleFailure();
				DissonanceComms[] array = Object.FindObjectsOfType<DissonanceComms>();
				for (int i = 0; i < array.Length; i++)
				{
					array[i].ResetMicrophoneCapture();
				}
			}
			if (DroppedPackets > 0 && !packetDropWarningLogged)
			{
				packetDropWarningLogged = true;
				logger.LogWarning((object)"Clear Voice dropped oversized voice frames.");
			}
		}

		private void HandleFailure()
		{
			failureHandled = true;
			try
			{
				ClearVoicePatches.Restore();
			}
			catch
			{
				logger.LogWarning((object)"Clear Voice settings could not be restored.");
			}
			Harmony? obj2 = harmony;
			if (obj2 != null)
			{
				obj2.UnpatchSelf();
			}
			harmony = null;
			logger.LogError((object)"Clear Voice failed; feature disabled.");
		}

		internal string Status(bool korean)
		{
			if (Failure != null)
			{
				if (!korean)
				{
					return "PATCH FAILED / Original voice / See log";
				}
				return "적용 실패 · 기존 음성 사용: " + Failure;
			}
			return ((!Active) ? (korean ? "클리어 보이스 꺼짐 · 기존 음성 사용" : "CLEAR VOICE OFF / Original voice") : (korean ? "클리어 보이스 사용 중" : "CLEAR VOICE ACTIVE")) + ((!RestartPending) ? "" : (korean ? " · 변경은 게임 재시작 후 적용" : " / Restart to apply change"));
		}

		public void Dispose()
		{
			if (!disposed)
			{
				disposed = true;
				Volatile.Write(ref active, 0);
				BitrateKbps.SettingChanged -= SettingsChanged;
				FecEnabled.SettingChanged -= SettingsChanged;
				try
				{
					ClearVoicePatches.Restore();
				}
				catch
				{
					logger.LogWarning((object)"Clear Voice patches could not be restored during shutdown.");
				}
				Harmony? obj2 = harmony;
				if (obj2 != null)
				{
					obj2.UnpatchSelf();
				}
				Interlocked.CompareExchange(ref instance, null, this);
			}
		}
	}
	internal sealed class LethalConfigIntegration
	{
		private object? deviceDropdown;

		private object? gameDeviceDropdown;

		private object? microphoneDropdown;

		private bool refreshWarningLogged;

		private ManualLogSource logger;

		internal static LethalConfigIntegration? TryRegister(ConfigFile config, DeviceChoices choices, DeviceChoices gameChoices, MicrophoneChoices microphoneChoices, Action openMenu, Action refreshDevices, ManualLogSource logger)
		{
			if (!Chainloader.PluginInfos.ContainsKey("ainavt.lc.lethalconfig"))
			{
				return null;
			}
			try
			{
				return Register(config, choices, gameChoices, microphoneChoices, openMenu, refreshDevices, logger);
			}
			catch
			{
				logger.LogWarning((object)"LethalConfig integration failed; use F8 to open settings.");
				return null;
			}
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		private static LethalConfigIntegration Register(ConfigFile config, DeviceChoices choices, DeviceChoices gameChoices, MicrophoneChoices microphoneChoices, Action openMenu, Action refreshDevices, ManualLogSource logger)
		{
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			//IL_0032: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Expected O, but got Unknown
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_004e: Expected O, but got Unknown
			//IL_0054: Unknown result type (might be due to invalid IL or missing references)
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_0064: Unknown result type (might be due to invalid IL or missing references)
			//IL_006f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0076: Unknown result type (might be due to invalid IL or missing references)
			//IL_007d: Expected O, but got Unknown
			//IL_007d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0088: Expected O, but got Unknown
			//IL_008d: Expected O, but got Unknown
			//IL_0088: Unknown result type (might be due to invalid IL or missing references)
			//IL_0092: Expected O, but got Unknown
			//IL_0097: Unknown result type (might be due to invalid IL or missing references)
			//IL_009c: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00be: Expected O, but got Unknown
			//IL_00b9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c3: Expected O, but got Unknown
			//IL_00d5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00da: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fc: Expected O, but got Unknown
			//IL_00f7: Unknown result type (might be due to invalid IL or missing references)
			//IL_0101: Expected O, but got Unknown
			//IL_0113: Unknown result type (might be due to invalid IL or missing references)
			//IL_0118: Unknown result type (might be due to invalid IL or missing references)
			//IL_0123: Unknown result type (might be due to invalid IL or missing references)
			//IL_012e: Unknown result type (might be due to invalid IL or missing references)
			//IL_013a: Expected O, but got Unknown
			//IL_0135: Unknown result type (might be due to invalid IL or missing references)
			//IL_013f: Expected O, but got Unknown
			//IL_0152: Unknown result type (might be due to invalid IL or missing references)
			//IL_0157: Unknown result type (might be due to invalid IL or missing references)
			//IL_0162: Unknown result type (might be due to invalid IL or missing references)
			//IL_016d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0179: Expected O, but got Unknown
			//IL_019f: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a4: Unknown result type (might be due to invalid IL or missing references)
			//IL_01af: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ba: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c6: Expected O, but got Unknown
			//IL_01c1: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c7: Expected O, but got Unknown
			//IL_0210: Unknown result type (might be due to invalid IL or missing references)
			//IL_0215: Unknown result type (might be due to invalid IL or missing references)
			//IL_0220: Unknown result type (might be due to invalid IL or missing references)
			//IL_022b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0237: Expected O, but got Unknown
			//IL_0232: Unknown result type (might be due to invalid IL or missing references)
			//IL_0238: Expected O, but got Unknown
			//IL_0281: Unknown result type (might be due to invalid IL or missing references)
			//IL_0286: Unknown result type (might be due to invalid IL or missing references)
			//IL_0291: Unknown result type (might be due to invalid IL or missing references)
			//IL_029c: Unknown result type (might be due to invalid IL or missing references)
			//IL_02a8: Expected O, but got Unknown
			//IL_02a3: Unknown result type (might be due to invalid IL or missing references)
			//IL_02a9: Expected O, but got Unknown
			//IL_02e7: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ec: Unknown result type (might be due to invalid IL or missing references)
			//IL_02f7: Unknown result type (might be due to invalid IL or missing references)
			//IL_0302: Unknown result type (might be due to invalid IL or missing references)
			//IL_0309: Unknown result type (might be due to invalid IL or missing references)
			//IL_0314: Expected O, but got Unknown
			//IL_0314: Unknown result type (might be due to invalid IL or missing references)
			//IL_031f: Expected O, but got Unknown
			//IL_0324: Expected O, but got Unknown
			//IL_031f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0329: Expected O, but got Unknown
			//IL_033f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0344: Unknown result type (might be due to invalid IL or missing references)
			//IL_034f: Unknown result type (might be due to invalid IL or missing references)
			//IL_035a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0361: Unknown result type (might be due to invalid IL or missing references)
			//IL_036c: Expected O, but got Unknown
			//IL_036c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0377: Expected O, but got Unknown
			//IL_0377: Unknown result type (might be due to invalid IL or missing references)
			//IL_0387: Expected O, but got Unknown
			//IL_0382: Unknown result type (might be due to invalid IL or missing references)
			//IL_038c: Expected O, but got Unknown
			//IL_039f: Unknown result type (might be due to invalid IL or missing references)
			//IL_03a4: Unknown result type (might be due to invalid IL or missing references)
			//IL_03af: Unknown result type (might be due to invalid IL or missing references)
			//IL_03ba: Unknown result type (might be due to invalid IL or missing references)
			//IL_03c1: Unknown result type (might be due to invalid IL or missing references)
			//IL_03c9: Expected O, but got Unknown
			//IL_03c9: Unknown result type (might be due to invalid IL or missing references)
			//IL_03d4: Expected O, but got Unknown
			//IL_03d9: Expected O, but got Unknown
			//IL_03d4: Unknown result type (might be due to invalid IL or missing references)
			//IL_03de: Expected O, but got Unknown
			//IL_03fa: Unknown result type (might be due to invalid IL or missing references)
			//IL_0404: Expected O, but got Unknown
			//IL_03ff: Unknown result type (might be due to invalid IL or missing references)
			//IL_0409: Expected O, but got Unknown
			//IL_0425: Unknown result type (might be due to invalid IL or missing references)
			//IL_042f: Expected O, but got Unknown
			//IL_042a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0434: Expected O, but got Unknown
			LethalConfigIntegration obj = new LethalConfigIntegration
			{
				logger = logger
			};
			LethalConfigManager.SetModDescription("게임·음성 출력 장치와 송신 마이크를 선택하고, 클리어 보이스로 모노 음질과 FEC를 설정합니다. 클리어 보이스 활성화 변경은 게임 재시작 후 적용됩니다.");
			ClearVoiceRuntime? instance = ClearVoiceRuntime.Instance;
			LethalConfigManager.AddConfigItem((BaseConfigItem)new BoolCheckBoxConfigItem(instance.Enabled, new BoolCheckBoxOptions
			{
				Name = "클리어 보이스 사용",
				Section = "클리어 보이스",
				RequiresRestart = true
			}));
			ConfigEntry<int> bitrateKbps = instance.BitrateKbps;
			IntSliderOptions val = new IntSliderOptions
			{
				Name = "목표 비트레이트 (kbps)",
				Section = "클리어 보이스",
				RequiresRestart = false
			};
			((BaseRangeOptions<int>)val).Min = 6;
			((BaseRangeOptions<int>)val).Max = 300;
			LethalConfigManager.AddConfigItem((BaseConfigItem)new IntSliderConfigItem(bitrateKbps, val));
			LethalConfigManager.AddConfigItem((BaseConfigItem)new BoolCheckBoxConfigItem(instance.FecEnabled, new BoolCheckBoxOptions
			{
				Name = "송수신 FEC 사용",
				Section = "클리어 보이스",
				RequiresRestart = false
			}));
			LethalConfigManager.AddConfigItem((BaseConfigItem)new BoolCheckBoxConfigItem(config.Bind<bool>("General", "Enabled", true, (ConfigDescription)null), new BoolCheckBoxOptions
			{
				Name = "음성 출력 분리 사용",
				Section = "일반",
				RequiresRestart = false
			}));
			LethalConfigManager.AddConfigItem((BaseConfigItem)new BoolCheckBoxConfigItem(config.Bind<bool>("General", "FallbackToGameOutput", true, (ConfigDescription)null), new BoolCheckBoxOptions
			{
				Name = "음성 장치 오류 시 게임 출력으로 복구",
				Section = "일반",
				RequiresRestart = false
			}));
			LethalConfigManager.AddConfigItem((BaseConfigItem)(object)new EnumDropDownConfigItem<Key>(config.Bind<Key>("General", "MenuKey", (Key)101, (ConfigDescription)null), new EnumDropDownOptions
			{
				Name = "설정창 단축키",
				Section = "일반",
				RequiresRestart = false
			}));
			LethalConfigManager.AddConfigItem((BaseConfigItem)(obj.deviceDropdown = (object?)new TextDropDownConfigItem(config.Bind<string>("Output", "DeviceName", "Windows 기본 출력 장치", (ConfigDescription)null), new TextDropDownOptions(choices.Labels.ToArray())
			{
				Name = "음성 출력 장치",
				Section = "출력",
				RequiresRestart = false
			})));
			LethalConfigManager.SkipAutoGenFor((ConfigEntryBase)(object)config.Bind<string>("Output", "DeviceId", "default", (ConfigDescription)null));
			LethalConfigManager.AddConfigItem((BaseConfigItem)(obj.gameDeviceDropdown = (object?)new TextDropDownConfigItem(config.Bind<string>("GameOutput", "DeviceName", "기존 게임 출력", (ConfigDescription)null), new TextDropDownOptions(gameChoices.Labels.ToArray())
			{
				Name = "게임 소리 출력 장치",
				Section = "출력",
				RequiresRestart = false
			})));
			LethalConfigManager.SkipAutoGenFor((ConfigEntryBase)(object)config.Bind<string>("GameOutput", "DeviceId", "game", (ConfigDescription)null));
			LethalConfigManager.AddConfigItem((BaseConfigItem)(obj.microphoneDropdown = (object?)new TextDropDownConfigItem(config.Bind<string>("Input", "MicrophoneDeviceName", "게임 설정 사용", (ConfigDescription)null), new TextDropDownOptions(microphoneChoices.Labels.ToArray())
			{
				Name = "송신 마이크",
				Section = "입력",
				RequiresRestart = false
			})));
			LethalConfigManager.SkipAutoGenFor((ConfigEntryBase)(object)config.Bind<string>("Input", "MicrophoneName", "__lvr_game_setting__", (ConfigDescription)null));
			ConfigEntry<float> obj2 = config.Bind<float>("Output", "VoiceVolume", 1f, (ConfigDescription)null);
			FloatSliderOptions val2 = new FloatSliderOptions
			{
				Name = "음성 음량 배율",
				Section = "출력",
				RequiresRestart = false
			};
			((BaseRangeOptions<float>)val2).Min = 0f;
			((BaseRangeOptions<float>)val2).Max = 2f;
			LethalConfigManager.AddConfigItem((BaseConfigItem)new FloatSliderConfigItem(obj2, val2));
			ConfigEntry<float> obj3 = config.Bind<float>("Output", "LatencyMs", 40f, (ConfigDescription)null);
			FloatStepSliderOptions val3 = new FloatStepSliderOptions
			{
				Name = "출력 버퍼 (ms)",
				Section = "출력",
				RequiresRestart = false
			};
			((BaseRangeOptions<float>)val3).Min = 20f;
			((BaseRangeOptions<float>)val3).Max = 200f;
			val3.Step = 10f;
			LethalConfigManager.AddConfigItem((BaseConfigItem)new FloatStepSliderConfigItem(obj3, val3));
			ConfigEntry<int> obj4 = config.Bind<int>("Output", "MaximumQueueMs", 100, (ConfigDescription)null);
			IntSliderOptions val4 = new IntSliderOptions
			{
				Name = "최대 음성 대기열 (ms)",
				Section = "출력",
				RequiresRestart = false
			};
			((BaseRangeOptions<int>)val4).Min = 20;
			((BaseRangeOptions<int>)val4).Max = 500;
			LethalConfigManager.AddConfigItem((BaseConfigItem)new IntSliderConfigItem(obj4, val4));
			LethalConfigManager.AddConfigItem((BaseConfigItem)new GenericButtonConfigItem("출력", "장치 목록 새로고침", "새로 연결한 출력·입력 장치를 검색합니다.", "다시 검색", new GenericButtonHandler(refreshDevices.Invoke)));
			LethalConfigManager.AddConfigItem((BaseConfigItem)new GenericButtonConfigItem("출력", "별도 출력 설정창", "출력 상태와 장치 목록을 확인할 수 있는 설정창을 엽니다.", "설정창 열기", new GenericButtonHandler(openMenu.Invoke)));
			return obj;
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		internal void UpdateDevices(DeviceChoices choices, DeviceChoices gameChoices, MicrophoneChoices microphoneChoices)
		{
			UpdateDropdown(deviceDropdown, choices.Labels);
			UpdateDropdown(gameDeviceDropdown, gameChoices.Labels);
			UpdateDropdown(microphoneDropdown, microphoneChoices.Labels);
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		private void UpdateDropdown(object? item, IReadOnlyList<string> labels)
		{
			TextDropDownConfigItem val = (TextDropDownConfigItem)((item is TextDropDownConfigItem) ? item : null);
			if (val == null || val.Values.SequenceEqual(labels))
			{
				return;
			}
			val.Values = labels.ToList();
			try
			{
				RefreshOpenDropdown(val);
			}
			catch
			{
				if (!refreshWarningLogged)
				{
					refreshWarningLogged = true;
					logger.LogWarning((object)"LethalConfig device list refresh failed; reopen settings.");
				}
			}
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		private static void RefreshOpenDropdown(object dropdown)
		{
			Type type = typeof(LethalConfigManager).Assembly.GetType("LethalConfig.MonoBehaviours.Components.TextDropDownController");
			if (type == null)
			{
				return;
			}
			PropertyInfo property = type.GetProperty("BaseConfigItem", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			MethodInfo method = type.GetMethod("SetConfigItem", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (property == null || method == null)
			{
				return;
			}
			Object[] array = Object.FindObjectsOfType(type, true);
			foreach (Object obj in array)
			{
				if (property.GetValue(obj) == dropdown)
				{
					method.Invoke(obj, new object[1] { dropdown });
				}
			}
		}
	}
	internal static class LocalizationFont
	{
		internal static TMP_FontAsset? Resolve()
		{
			foreach (PluginInfo value2 in Chainloader.PluginInfos.Values)
			{
				if (value2.Metadata.GUID.IndexOf("LCKR", StringComparison.OrdinalIgnoreCase) < 0 && value2.Metadata.Name.IndexOf("LCKR", StringComparison.OrdinalIgnoreCase) < 0)
				{
					continue;
				}
				object instance = value2.Instance;
				if (instance == null)
				{
					continue;
				}
				Type type = instance.GetType();
				object? obj = type.GetField("font3270_Regular_SDF", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null);
				TMP_FontAsset val = (TMP_FontAsset)((obj is TMP_FontAsset) ? obj : null);
				if (val != null && (Object)(object)val != (Object)null)
				{
					return val;
				}
				FieldInfo[] fields = type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
				foreach (FieldInfo fieldInfo in fields)
				{
					if (fieldInfo.FieldType == typeof(TMP_FontAsset) && fieldInfo.Name.IndexOf("Regular", StringComparison.OrdinalIgnoreCase) >= 0)
					{
						object? value = fieldInfo.GetValue(null);
						TMP_FontAsset val2 = (TMP_FontAsset)((value is TMP_FontAsset) ? value : null);
						if (val2 != null && (Object)(object)val2 != (Object)null)
						{
							return val2;
						}
					}
				}
			}
			return null;
		}
	}
	internal sealed class MicrophoneChoices
	{
		internal const string GameSettingId = "__lvr_game_setting__";

		internal const string GameSettingLabel = "게임 설정 사용";

		private readonly Dictionary<string, string> ids = new Dictionary<string, string>(StringComparer.Ordinal);

		private readonly Dictionary<string, string> labels = new Dictionary<string, string>(StringComparer.Ordinal);

		internal IReadOnlyList<string> Labels => ids.Keys.ToArray();

		private MicrophoneChoices()
		{
		}

		internal static MicrophoneChoices Create(IEnumerable<string> devices, string currentId)
		{
			MicrophoneChoices microphoneChoices = new MicrophoneChoices();
			microphoneChoices.Add("__lvr_game_setting__", "게임 설정 사용");
			foreach (string item in devices.Where((string name) => !string.IsNullOrWhiteSpace(name)).Distinct<string>(StringComparer.Ordinal))
			{
				string label = ((item == "게임 설정 사용") ? (item + " [" + item + "]") : item);
				microphoneChoices.Add(item, label);
			}
			if (!string.IsNullOrWhiteSpace(currentId) && currentId != "__lvr_game_setting__" && !microphoneChoices.labels.ContainsKey(currentId))
			{
				microphoneChoices.Add(currentId, "연결되지 않은 입력 장치 [" + currentId + "]");
			}
			return microphoneChoices;
		}

		private void Add(string id, string label)
		{
			if (!labels.ContainsKey(id))
			{
				labels.Add(id, label);
				ids.Add(label, id);
			}
		}

		internal bool TryGetDeviceId(string label, out string id)
		{
			return ids.TryGetValue(label, out id);
		}

		internal string GetLabel(string id)
		{
			if (!labels.TryGetValue(id, out string value))
			{
				return "게임 설정 사용";
			}
			return value;
		}
	}
	internal sealed class MicrophoneLevelMeter : IMicrophoneSubscriber, IDisposable
	{
		private const int PreviewDurationSeconds = 1;

		private const int PreviewReadIntervalMilliseconds = 50;

		private const int PreviewMaximumFrames = 2400;

		private IMicrophoneCapture? capture;

		private AudioClip? previewClip;

		private string? previewDevice;

		private string? attemptedPreviewDevice;

		private bool previewAttempted;

		private bool previewFailed;

		private int previewReadPosition;

		private int previewSampleRate;

		private int previewChannels;

		private long previewStartedAt;

		private long nextPreviewReadAt;

		private float targetLevel;

		private float level;

		private long lastSampleAt;

		internal float Level => Volatile.Read(in level);

		internal bool IsRecording
		{
			get
			{
				IMicrophoneCapture? obj = capture;
				if (obj == null || !obj.IsRecording)
				{
					return IsPreviewRecording;
				}
				return true;
			}
		}

		internal bool PreviewFailed => previewFailed;

		internal string Device
		{
			get
			{
				IMicrophoneCapture? obj = capture;
				object obj2;
				if (obj == null || !obj.IsRecording)
				{
					if (!IsPreviewRecording)
					{
						return "";
					}
					obj2 = previewDevice;
					if (obj2 == null)
					{
						return "";
					}
				}
				else
				{
					obj2 = capture.Device ?? "";
				}
				return (string)obj2;
			}
		}

		private bool IsPreviewRecording
		{
			get
			{
				if ((Object)(object)previewClip == (Object)null)
				{
					return false;
				}
				try
				{
					return Microphone.IsRecording(previewDevice);
				}
				catch
				{
					return false;
				}
			}
		}

		internal void SetCapture(IMicrophoneCapture? next)
		{
			if (capture != next)
			{
				IMicrophoneCapture? obj = capture;
				if (obj != null)
				{
					obj.Unsubscribe((IMicrophoneSubscriber)(object)this);
				}
				capture = next;
				Reset();
				IMicrophoneCapture? obj2 = capture;
				if (obj2 != null)
				{
					obj2.Subscribe((IMicrophoneSubscriber)(object)this);
				}
			}
		}

		public void Reset()
		{
			Volatile.Write(ref targetLevel, 0f);
			Interlocked.Exchange(ref lastSampleAt, 0L);
			Volatile.Write(ref level, 0f);
		}

		public void ReceiveMicrophoneData(ArraySegment<float> samples, WaveFormat format)
		{
			float[] array = samples.Array;
			if (array != null && samples.Count != 0)
			{
				double num = 0.0;
				int num2 = samples.Offset + samples.Count;
				for (int i = samples.Offset; i < num2; i++)
				{
					float num3 = array[i];
					num += (double)(num3 * num3);
				}
				float num4 = (float)Math.Sqrt(num / (double)samples.Count);
				float num5 = ((num4 > 0f) ? (20f * (float)Math.Log10(num4)) : (-80f));
				Volatile.Write(ref targetLevel, Math.Clamp((num5 + 60f) / 60f, 0f, 1f));
				Interlocked.Exchange(ref lastSampleAt, Stopwatch.GetTimestamp());
			}
		}

		internal void Update(float deltaTime, bool previewEnabled = false, string? requestedPreviewDevice = null, bool microphoneNetworkActive = false)
		{
			IMicrophoneCapture? obj = capture;
			int num;
			if (obj == null)
			{
				num = 0;
			}
			else
			{
				num = (obj.IsRecording ? 1 : 0);
				if (num != 0)
				{
					StopPreview(endDevice: false, resetAttempt: true);
					goto IL_0037;
				}
			}
			if (previewEnabled)
			{
				UpdatePreview(requestedPreviewDevice);
			}
			else
			{
				StopPreview(!microphoneNetworkActive, resetAttempt: true);
			}
			goto IL_0037;
			IL_0037:
			long num2 = Interlocked.Read(in lastSampleAt);
			bool flag = num2 != 0L && Stopwatch.GetTimestamp() - num2 < Stopwatch.Frequency / 4;
			float num3 = (((num != 0 || IsPreviewRecording) && flag) ? Volatile.Read(in targetLevel) : 0f);
			float num4 = Volatile.Read(in level);
			float num5 = ((num3 > num4) ? 0.05f : 0.18f);
			float num6 = 1f - (float)Math.Exp((0f - Math.Max(0f, deltaTime)) / num5);
			Volatile.Write(ref level, num4 + (num3 - num4) * num6);
		}

		internal void ResetPreview(bool microphoneNetworkActive = false)
		{
			StopPreview(!microphoneNetworkActive, resetAttempt: true);
		}

		private void UpdatePreview(string? requestedDevice)
		{
			//IL_020a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0214: Expected O, but got Unknown
			if ((Object)(object)previewClip != (Object)null && !string.Equals(previewDevice, requestedDevice, StringComparison.Ordinal))
			{
				StopPreview(endDevice: true, resetAttempt: true);
			}
			if ((Object)(object)previewClip == (Object)null)
			{
				if (previewAttempted && string.Equals(attemptedPreviewDevice, requestedDevice, StringComparison.Ordinal))
				{
					return;
				}
				StartPreview(requestedDevice);
				if ((Object)(object)previewClip == (Object)null)
				{
					return;
				}
			}
			long timestamp = Stopwatch.GetTimestamp();
			if (!IsPreviewRecording)
			{
				if (timestamp - previewStartedAt > Stopwatch.Frequency)
				{
					previewFailed = true;
					StopPreview(endDevice: true, resetAttempt: false);
				}
			}
			else
			{
				if (timestamp < nextPreviewReadAt)
				{
					return;
				}
				nextPreviewReadAt = timestamp + Stopwatch.Frequency * 50 / 1000;
				int position;
				try
				{
					position = Microphone.GetPosition(previewDevice);
				}
				catch
				{
					previewFailed = true;
					StopPreview(endDevice: true, resetAttempt: false);
					return;
				}
				if (previewClip.samples <= 0 || position < 0)
				{
					return;
				}
				position %= previewClip.samples;
				int num = ((position >= previewReadPosition) ? (position - previewReadPosition) : (previewClip.samples - previewReadPosition + position));
				if (num <= 0)
				{
					return;
				}
				int num2 = Math.Min(num, 2400);
				int num3 = ((num > num2) ? ((position - num2 + previewClip.samples) % previewClip.samples) : previewReadPosition);
				int num4 = Math.Min(num2, previewClip.samples - num3);
				float[] array = new float[num2 * previewChannels];
				float[] array2 = ((num4 == num2) ? array : new float[num4 * previewChannels]);
				if (!previewClip.GetData(array2, num3))
				{
					return;
				}
				if (num4 != num2)
				{
					Array.Copy(array2, 0, array, 0, array2.Length);
					float[] array3 = new float[(num2 - num4) * previewChannels];
					if (!previewClip.GetData(array3, 0))
					{
						return;
					}
					Array.Copy(array3, 0, array, array2.Length, array3.Length);
				}
				previewReadPosition = position;
				ReceiveMicrophoneData(new ArraySegment<float>(array), new WaveFormat(previewSampleRate, previewChannels));
			}
		}

		private void StartPreview(string? device)
		{
			previewAttempted = true;
			attemptedPreviewDevice = device;
			previewDevice = device;
			previewFailed = false;
			try
			{
				int num = 48000;
				int num2 = default(int);
				int num3 = default(int);
				Microphone.GetDeviceCaps(device, ref num2, ref num3);
				if (num2 != 0 && num3 != 0)
				{
					num = Mathf.Clamp(num, num2, num3);
				}
				previewClip = Microphone.Start(device, true, 1, num);
				if ((Object)(object)previewClip == (Object)null)
				{
					previewFailed = true;
					previewDevice = null;
					return;
				}
				previewSampleRate = previewClip.frequency;
				previewChannels = Math.Max(1, previewClip.channels);
				previewReadPosition = Math.Max(0, Microphone.GetPosition(device)) % previewClip.samples;
				previewStartedAt = Stopwatch.GetTimestamp();
				nextPreviewReadAt = previewStartedAt;
			}
			catch
			{
				previewClip = null;
				previewDevice = null;
				previewFailed = true;
			}
		}

		private void StopPreview(bool endDevice, bool resetAttempt)
		{
			if ((Object)(object)previewClip != (Object)null && endDevice)
			{
				try
				{
					if (Microphone.IsRecording(previewDevice))
					{
						Microphone.End(previewDevice);
					}
				}
				catch
				{
				}
			}
			previewClip = null;
			previewDevice = null;
			previewReadPosition = 0;
			previewStartedAt = 0L;
			nextPreviewReadAt = 0L;
			if (resetAttempt)
			{
				attemptedPreviewDevice = null;
				previewAttempted = false;
				previewFailed = false;
			}
			Volatile.Write(ref targetLevel, 0f);
			Interlocked.Exchange(ref lastSampleAt, 0L);
		}

		public void Dispose()
		{
			Dispose(microphoneNetworkActive: false);
		}

		internal void Dispose(bool microphoneNetworkActive)
		{
			SetCapture(null);
			StopPreview(!microphoneNetworkActive, resetAttempt: true);
		}
	}
	internal sealed class OutputBufferRange : AcceptableValueRange<float>
	{
		internal const int Minimum = 20;

		internal const int Maximum = 200;

		internal const int Step = 10;

		internal OutputBufferRange()
			: base(20f, 200f)
		{
		}

		public override object Clamp(object value)
		{
			return MathF.Round((float)base.Clamp(value) / 10f, MidpointRounding.AwayFromZero) * 10f;
		}

		public override bool IsValid(object value)
		{
			if (base.IsValid(value))
			{
				return (float)value % 10f == 0f;
			}
			return false;
		}
	}
	[BepInPlugin("io.lethalloopback", "Lethal Voice Router", "0.5.0")]
	[BepInProcess("Lethal Company.exe")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string PluginId = "io.lethalloopback";

		public const string Version = "0.5.0";

		internal static VoiceRouter? Router => VoiceRouterRuntime.Router;

		private void Awake()
		{
			ClearVoiceRuntime.Start(((BaseUnityPlugin)this).Config, ((BaseUnityPlugin)this).Logger);
			RuntimeHost.Start(((BaseUnityPlugin)this).Config, ((BaseUnityPlugin)this).Logger);
		}
	}
	internal static class RuntimeHost
	{
		[CompilerGenerated]
		private static class <>O
		{
			public static UnityAction<Scene, LoadSceneMode> <0>__SceneLoaded;

			public static UnityAction <1>__EnsureStarted;

			public static Action <2>__Stop;
		}

		private static ConfigFile? configuration;

		private static ManualLogSource? logger;

		private static bool stopping;

		internal static void Start(ConfigFile config, ManualLogSource logSource)
		{
			//IL_003c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_0047: Expected O, but got Unknown
			configuration = config;
			logger = logSource;
			SceneManager.sceneLoaded += SceneLoaded;
			object obj = <>O.<1>__EnsureStarted;
			if (obj == null)
			{
				UnityAction val = EnsureStarted;
				<>O.<1>__EnsureStarted = val;
				obj = (object)val;
			}
			Application.onBeforeRender += (UnityAction)obj;
			Application.quitting += Stop;
		}

		private static void SceneLoaded(Scene scene, LoadSceneMode mode)
		{
			EnsureStarted();
		}

		private static void EnsureStarted()
		{
			//IL_0028: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Expected O, but got Unknown
			if (stopping || configuration == null || logger == null || (Object)(object)VoiceRouterRuntime.Instance != (Object)null)
			{
				return;
			}
			GameObject val = new GameObject("LethalVoiceRouter.Runtime");
			Object.DontDestroyOnLoad((Object)(object)val);
			try
			{
				val.AddComponent<VoiceRouterRuntime>().Initialize(configuration, logger);
			}
			catch (Exception ex)
			{
				Object.Destroy((Object)(object)val);
				logger.LogError((object)("Runtime initialization failed (" + ex.GetType().Name + ")."));
				Stop();
			}
		}

		private static void Stop()
		{
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Expected O, but got Unknown
			stopping = true;
			SceneManager.sceneLoaded -= SceneLoaded;
			object obj = <>O.<1>__EnsureStarted;
			if (obj == null)
			{
				UnityAction val = EnsureStarted;
				<>O.<1>__EnsureStarted = val;
				obj = (object)val;
			}
			Application.onBeforeRender -= (UnityAction)obj;
			Application.quitting -= Stop;
			ClearVoiceRuntime.Instance?.Dispose();
		}
	}
	[DefaultExecutionOrder(10000)]
	public sealed class VoiceCaptureFilter : MonoBehaviour
	{
		private static readonly Func<VoicePlayback, float>? PlaybackVolume = CreatePlaybackVolume();

		private VoicePlayback? playback;

		private AudioSource? source;

		private VoiceStream? stream;

		private SamplePlaybackComponent? input;

		private int inputAudible = 1;

		private DissonanceComms? comms;

		private string? volumeParameter;

		private string? playerName;

		private int playerIndex = -1;

		internal bool InputAudible => Volatile.Read(in inputAudible) != 0;

		internal void Bind(VoicePlayback voicePlayback)
		{
			playback = voicePlayback;
			source = voicePlayback.AudioSource;
			EnsureRegistered();
		}

		private void OnEnable()
		{
			EnsureRegistered();
		}

		private void EnsureRegistered()
		{
			if (!((Behaviour)this).isActiveAndEnabled || stream != null || Plugin.Router == null)
			{
				return;
			}
			if (playback == null)
			{
				playback = ((Component)this).GetComponent<VoicePlayback>();
			}
			source = ((Component)this).GetComponent<AudioSource>();
			if (!((Object)(object)playback == (Object)null) && !((Object)(object)source == (Object)null))
			{
				input = ((Component)this).GetComponent<SamplePlaybackComponent>();
				if (!((Object)(object)input == (Object)null))
				{
					VoiceStream val = Plugin.Router.RegisterSource();
					val.Muted = true;
					val.Gain = 0f;
					Volatile.Write(ref stream, val);
					Volatile.Write(ref inputAudible, 1);
					VoiceInputPatch.Track(input, this);
					comms = Object.FindObjectOfType<DissonanceComms>();
				}
			}
		}

		private void LateUpdate()
		{
			if (stream == null)
			{
				return;
			}
			if ((Object)(object)source == (Object)null || (Object)(object)playback == (Object)null)
			{
				stream.Muted = true;
				return;
			}
			if ((Object)(object)comms != (Object)null && playback.PlayerName == comms.LocalPlayerName)
			{
				stream.Dispose();
				stream = null;
				return;
			}
			if (playerName != playback.PlayerName)
			{
				stream.Muted = true;
				playerName = playback.PlayerName;
				volumeParameter = null;
				playerIndex = -1;
			}
			StartOfRound instance = StartOfRound.Instance;
			int num = -1;
			if ((Object)(object)instance != (Object)null && instance.allPlayerScripts != null)
			{
				PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts;
				foreach (PlayerControllerB val in allPlayerScripts)
				{
					if ((Object)(object)val != (Object)null && (val.isPlayerControlled || val.isPlayerDead) && (Object)(object)val.currentVoiceChatAudioSource == (Object)(object)source && val.voicePlayerState != null && (object)val.voicePlayerState.Playback == playback && val.playerClientId <= int.MaxValue)
					{
						num = (int)val.playerClientId;
						break;
					}
				}
			}
			if (playerIndex != num)
			{
				stream.Muted = true;
				playerIndex = num;
				volumeParameter = ((num >= 0) ? $"PlayerVolume{num}" : null);
			}
			AudioListener val2 = (((Object)(object)instance == (Object)null) ? null : instance.audioListener);
			if ((Object)(object)val2 == (Object)null || !((Behaviour)val2).isActiveAndEnabled)
			{
				val2 = Object.FindObjectOfType<AudioListener>();
			}
			float num2 = 1f;
			AudioMixerGroup outputAudioMixerGroup = source.outputAudioMixerGroup;
			if ((Object)(object)outputAudioMixerGroup != (Object)null)
			{
				AudioMixer audioMixer = outputAudioMixerGroup.audioMixer;
				float num3 = default(float);
				if (volumeParameter != null && audioMixer.GetFloat(volumeParameter, ref num3))
				{
					num2 *= Mathf.Pow(10f, num3 / 20f);
				}
				float num4 = default(float);
				if (audioMixer.GetFloat("DiageticVolume", ref num4))
				{
					num2 *= Mathf.Pow(10f, num4 / 20f);
				}
			}
			float num5 = PlaybackVolume?.Invoke(playback) ?? 0f;
			float[] array = (((Object)(object)SoundManager.Instance == (Object)null) ? null : SoundManager.Instance.playerVoiceVolumes);
			bool flag = array != null && playerIndex >= 0 && playerIndex < array.Length && array[playerIndex] <= -70f;
			int num6;
			if (!source.mute && source.volume > 0f && (Object)(object)val2 != (Object)null && ((Behaviour)val2).isActiveAndEnabled && !flag && ((Object)(object)instance == (Object)null || playerIndex >= 0) && (source.ignoreListenerVolume || AudioListener.volume > 0f) && (source.ignoreListenerPause || !AudioListener.pause) && InputAudible && ((Behaviour)source).isActiveAndEnabled && ((Behaviour)playback).isActiveAndEnabled && !string.IsNullOrEmpty(playerName) && num5 > 0f && !float.IsInfinity(num5) && num2 > 0f)
			{
				num6 = ((!float.IsInfinity(num2)) ? 1 : 0);
				if (num6 != 0)
				{
					goto IL_038a;
				}
			}
			else
			{
				num6 = 0;
			}
			stream.Muted = true;
			goto IL_038a;
			IL_038a:
			stream.Gain = num2;
			if (num6 != 0)
			{
				stream.Muted = false;
			}
		}

		internal void ObserveInput(float[] data)
		{
			bool flag = false;
			foreach (float num in data)
			{
				if (num != 0f && !float.IsNaN(num) && !float.IsInfinity(num))
				{
					flag = true;
					break;
				}
			}
			if (!flag)
			{
				VoiceStream? obj = Volatile.Read(in stream);
				if (obj != null)
				{
					obj.DiscardBufferedAudio();
				}
			}
			Volatile.Write(ref inputAudible, flag ? 1 : 0);
		}

		private static Func<VoicePlayback, float>? CreatePlaybackVolume()
		{
			try
			{
				Type type = typeof(VoicePlayback).Assembly.GetType("Dissonance.Audio.Playback.IVolumeProvider", throwOnError: true);
				MethodInfo getMethod = type.GetProperty("TargetVolume").GetGetMethod();
				ParameterExpression parameterExpression = Expression.Parameter(typeof(VoicePlayback), "playback");
				return Expression.Lambda<Func<VoicePlayback, float>>(Expression.Call(Expression.Convert(parameterExpression, type), getMethod), new ParameterExpression[1] { parameterExpression }).Compile();
			}
			catch
			{
				Debug.LogError((object)"Playback volume is unavailable; separate voice output is disabled.");
				return null;
			}
		}

		private void OnAudioFilterRead(float[] data, int channels)
		{
			VoiceStream? obj = Volatile.Read(in stream);
			if (obj != null)
			{
				obj.Process(data, channels);
			}
		}

		private void OnDisable()
		{
			if ((Object)(object)input != (Object)null)
			{
				VoiceInputPatch.Untrack(input);
			}
			input = null;
			VoiceStream? obj = stream;
			if (obj != null)
			{
				obj.Dispose();
			}
			stream = null;
			volumeParameter = null;
			playerName = null;
			playerIndex = -1;
		}

		private void OnDestroy()
		{
			OnDisable();
		}
	}
	[HarmonyPatch(typeof(SamplePlaybackComponent), "OnAudioFilterRead")]
	internal static class VoiceInputPatch
	{
		private static readonly ConditionalWeakTable<SamplePlaybackComponent, VoiceCaptureFilter> Filters = new ConditionalWeakTable<SamplePlaybackComponent, VoiceCaptureFilter>();

		internal static void Track(SamplePlaybackComponent input, VoiceCaptureFilter filter)
		{
			Filters.Remove(input);
			Filters.Add(input, filter);
		}

		internal static void Untrack(SamplePlaybackComponent input)
		{
			Filters.Remove(input);
		}

		[HarmonyPrefix]
		private static void Prefix(SamplePlaybackComponent __instance, float[] data)
		{
			if (Filters.TryGetValue(__instance, out VoiceCaptureFilter value))
			{
				value.ObserveInput(data);
			}
		}
	}
	[HarmonyPatch(typeof(VoicePlayback), "Awake")]
	internal static class VoicePlaybackPatch
	{
		[HarmonyPostfix]
		private static void Postfix(VoicePlayback __instance)
		{
			Attach(__instance);
		}

		internal static void Attach(VoicePlayback playback)
		{
			if (Plugin.Router != null)
			{
				VoiceCaptureFilter voiceCaptureFilter = ((Component)playback).GetComponent<VoiceCaptureFilter>();
				if ((Object)(object)voiceCaptureFilter == (Object)null)
				{
					voiceCaptureFilter = ((Component)playback).gameObject.AddComponent<VoiceCaptureFilter>();
				}
				voiceCaptureFilter.Bind(playback);
			}
		}
	}
	internal sealed class VoiceRouterMenuView : IDisposable
	{
		private static readonly Color Orange = new Color(1f, 48f / 85f, 0f, 1f);

		private static readonly Color Heading = Orange;

		private static readonly Color Muted = new Color(0.82f, 0.82f, 0.82f, 1f);

		private static readonly Color SectionFrame = new Color(0.62f, 0.62f, 0.62f, 1f);

		private static readonly Color Failure = new Color(0.94f, 0.32f, 0.3f, 1f);

		private readonly VoiceRouterRuntime runtime;

		private readonly GameObject root;

		private readonly EventSystem eventSystem;

		private readonly TMP_FontAsset font;

		private readonly bool korean;

		private readonly Sprite? frameSprite;

		private readonly Sprite? checkedSprite;

		private readonly AudioSource audio;

		private readonly TextMeshProUGUI status;

		private readonly TextMeshProUGUI gameStatus;

		private readonly TextMeshProUGUI explanation;

		private readonly TextMeshProUGUI title;

		private readonly TextMeshProUGUI voiceDeviceHint;

		private readonly TextMeshProUGUI volumeValue;

		private readonly TextMeshProUGUI latencyValue;

		private readonly TextMeshProUGUI shortcut;

		private readonly TextMeshProUGUI microphoneStatus;

		private readonly TextMeshProUGUI microphoneLevelStatus;

		private readonly RectTransform microphoneLevelFill;

		private readonly Slider volumeSlider;

		private readonly Slider latencySlider;

		private readonly Image splitCheck;

		private readonly TextMeshProUGUI splitState;

		private readonly Image fallbackCheck;

		private readonly TextMeshProUGUI fallbackState;

		private readonly RectTransform deviceContent;

		private readonly CanvasGroup voiceDeviceGroup;

		private readonly RectTransform gameDeviceContent;

		private readonly RectTransform microphoneContent;

		private readonly Button closeButton;

		private readonly GameObject outputPage;

		private readonly GameObject inputPage;

		private readonly GameObject clearVoicePage;

		private readonly NativeMenuButton outputTab;

		private readonly NativeMenuButton inputTab;

		private readonly NativeMenuButton clearVoiceTab;

		private readonly Image clearVoiceCheck;

		private readonly TextMeshProUGUI clearVoiceToggleState;

		private readonly Image fecCheck;

		private readonly TextMeshProUGUI fecToggleState;

		private readonly Slider bitrateSlider;

		private readonly TextMeshProUGUI bitrateValue;

		private readonly TextMeshProUGUI clearVoiceStatus;

		private readonly TextMeshProUGUI clearVoiceApplied;

		private readonly TextMeshProUGUI clearVoiceDetails;

		private readonly List<(string Id, NativeMenuButton Feedback, Image Check)> deviceRows = new List<(string, NativeMenuButton, Image)>();

		private readonly List<(string Id, NativeMenuButton Feedback, Image Check)> gameDeviceRows = new List<(string, NativeMenuButton, Image)>();

		private readonly List<(string Id, NativeMenuButton Feedback, Image Check)> microphoneRows = new List<(string, NativeMenuButton, Image)>();

		private string deviceSignature = "";

		private string microphoneSignature = "";

		private string? selectedDevice;

		private string? selectedGameDevice;

		private string? selectedMicrophoneDevice;

		private int selectedPage;

		private bool disposed;

		internal VoiceRouterMenuView(Transform parent, VoiceRouterRuntime runtime)
		{
			//IL_0148: Unknown result type (might be due to invalid IL or missing references)
			//IL_0152: Expected O, but got Unknown
			//IL_01a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_0223: Unknown result type (might be due to invalid IL or missing references)
			//IL_022a: Expected O, but got Unknown
			//IL_029e: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ec: Unknown result type (might be due to invalid IL or missing references)
			//IL_02f3: Unknown result type (might be due to invalid IL or missing references)
			//IL_02f5: Unknown result type (might be due to invalid IL or missing references)
			//IL_02f6: Unknown result type (might be due to invalid IL or missing references)
			//IL_02fd: Unknown result type (might be due to invalid IL or missing references)
			//IL_0306: Unknown result type (might be due to invalid IL or missing references)
			//IL_0312: Unknown result type (might be due to invalid IL or missing references)
			//IL_0357: Unknown result type (might be due to invalid IL or missing references)
			//IL_0392: Unknown result type (might be due to invalid IL or missing references)
			//IL_04c5: Unknown result type (might be due to invalid IL or missing references)
			//IL_0500: Unknown result type (might be due to invalid IL or missing references)
			//IL_0539: Unknown result type (might be due to invalid IL or missing references)
			//IL_0579: Unknown result type (might be due to invalid IL or missing references)
			//IL_05c6: Unknown result type (might be due to invalid IL or missing references)
			//IL_0606: Unknown result type (might be due to invalid IL or missing references)
			//IL_063d: Unknown result type (might be due to invalid IL or missing references)
			//IL_06c2: Unknown result type (might be due to invalid IL or missing references)
			//IL_0702: Unknown result type (might be due to invalid IL or missing references)
			//IL_077f: Unknown result type (might be due to invalid IL or missing references)
			//IL_07b6: Unknown result type (might be due to invalid IL or missing references)
			//IL_0829: Unknown result type (might be due to invalid IL or missing references)
			//IL_085f: Unknown result type (might be due to invalid IL or missing references)
			//IL_08a0: Unknown result type (might be due to invalid IL or missing references)
			//IL_08d7: Unknown result type (might be due to invalid IL or missing references)
			//IL_0998: Unknown result type (might be due to invalid IL or missing references)
			//IL_09e4: Unknown result type (might be due to invalid IL or missing references)
			//IL_0a7d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0b2f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0b6f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0bb2: Unknown result type (might be due to invalid IL or missing references)
			//IL_0bf2: Unknown result type (might be due to invalid IL or missing references)
			//IL_0c33: Unknown result type (might be due to invalid IL or missing references)
			//IL_0c79: Unknown result type (might be due to invalid IL or missing references)
			//IL_0cba: Unknown result type (might be due to invalid IL or missing references)
			//IL_0cfc: Unknown result type (might be due to invalid IL or missing references)
			//IL_0d43: Unknown result type (might be due to invalid IL or missing references)
			//IL_0d85: Unknown result type (might be due to invalid IL or missing references)
			//IL_0d9f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0db9: Unknown result type (might be due to invalid IL or missing references)
			//IL_0dc9: Unknown result type (might be due to invalid IL or missing references)
			//IL_0e3a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0eae: Unknown result type (might be due to invalid IL or missing references)
			//IL_0eee: Unknown result type (might be due to invalid IL or missing references)
			//IL_0f6b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0fb1: Unknown result type (might be due to invalid IL or missing references)
			//IL_0fe8: Unknown result type (might be due to invalid IL or missing references)
			//IL_105b: Unknown result type (might be due to invalid IL or missing references)
			//IL_1091: Unknown result type (might be due to invalid IL or missing references)
			//IL_1102: Unknown result type (might be due to invalid IL or missing references)
			//IL_1142: Unknown result type (might be due to invalid IL or missing references)
			//IL_1179: Unknown result type (might be due to invalid IL or missing references)
			//IL_11c0: Unknown result type (might be due to invalid IL or missing references)
			//IL_1207: Unknown result type (might be due to invalid IL or missing references)
			//IL_1258: Unknown result type (might be due to invalid IL or missing references)
			VoiceRouterMenuView voiceRouterMenuView = this;
			this.runtime = runtime;
			Sprite[] source = Resources.FindObjectsOfTypeAll<Sprite>();
			frameSprite = ((IEnumerable<Sprite>)source).FirstOrDefault((Func<Sprite, bool>)((Sprite sprite) => ((Object)sprite).name == "DialogueBoxSimple"));
			checkedSprite = ((IEnumerable<Sprite>)source).FirstOrDefault((Func<Sprite, bool>)((Sprite sprite) => ((Object)sprite).name == "DialogueBoxSimple 1"));
			TMP_FontAsset val = LocalizationFont.Resolve();
			korean = (Object)(object)val != (Object)null;
			TMP_FontAsset val2 = val ?? ((IEnumerable<TMP_FontAsset>)Resources.FindObjectsOfTypeAll<TMP_FontAsset>()).FirstOrDefault((Func<TMP_FontAsset, bool>)((TMP_FontAsset candidate) => ((Object)candidate).name == "3270-Regular SDF")) ?? TMP_Settings.defaultFontAsset;
			font = Object.Instantiate<TMP_FontAsset>(val2);
			font.fallbackFontAssetTable = new List<TMP_FontAsset>(val2.fallbackFontAssetTable ?? new List<TMP_FontAsset>());
			root = new GameObject("LethalVoiceRouter.Menu", new Type[1] { typeof(RectTransform) });
			root.SetActive(false);
			root.transform.SetParent(parent, false);
			Canvas obj = root.AddComponent<Canvas>();
			obj.renderMode = (RenderMode)0;
			obj.sortingOrder = 32767;
			CanvasScaler obj2 = root.AddComponent<CanvasScaler>();
			obj2.uiScaleMode = (ScaleMode)1;
			obj2.referenceResolution = new Vector2(1280f, 720f);
			obj2.screenMatchMode = (ScreenMatchMode)1;
			root.AddComponent<GraphicRaycaster>();
			audio = ((Component)parent).gameObject.AddComponent<AudioSource>();
			audio.playOnAwake = false;
			audio.spatialBlend = 0f;
			audio.ignoreListenerPause = true;
			GameObject val3 = new GameObject("MenuInput", new Type[2]
			{
				typeof(EventSystem),
				typeof(InputSystemUIInputModule)
			});
			val3.transform.SetParent(root.transform, false);
			eventSystem = val3.GetComponent<EventSystem>();
			val3.GetComponent<InputSystemUIInputModule>().AssignDefaultActions();
			RectTransform rect = Rect(root.transform, "Backdrop", 0f, 0f, 0f, 0f);
			Stretch(rect);
			Fill(rect, new Color(0f, 0f, 0f, 0.96f));
			RectTransform val4 = Rect(root.transform, "SettingsPanel", 0f, 0f, 1180f, 570f);
			Vector2 val5 = default(Vector2);
			((Vector2)(ref val5))..ctor(0.5f, 0.5f);
			val4.pivot = val5;
			val4.anchorMin = (val4.anchorMax = val5);
			val4.anchoredPosition = Vector2.zero;
			Fill(val4, Color.black);
			Frame(val4);
			title = Text((Transform)(object)val4, "Title", "LETHAL VOICE ROUTER", 28f, 17f, 600f, 36f, 28f, Color.white, (TextAlignmentOptions)4097);
			shortcut = Text((Transform)(object)val4, "Shortcut", "", 438f, 22f, 704f, 28f, 16f, Orange, (TextAlignmentOptions)4097);
			outputTab = ((Component)ActionButton(val4, "OutputTabButton", L("> 출력", "> Output"), 28f, 61f, 220f, delegate
			{
				voiceRouterMenuView.SelectPage(0);
			})).GetComponent<NativeMenuButton>();
			inputTab = ((Component)ActionButton(val4, "InputTabButton", L("> 마이크 입력", "> Microphone Input"), 268f, 61f, 220f, delegate
			{
				voiceRouterMenuView.SelectPage(1);
			})).GetComponent<NativeMenuButton>();
			clearVoiceTab = ((Component)ActionButton(val4, "ClearVoiceTabButton", L("> 클리어 보이스", "> Clear Voice"), 508f, 61f, 280f, delegate
			{
				voiceRouterMenuView.SelectPage(2);
			})).GetComponent<NativeMenuButton>();
			RectTransform val7 = Rect((Transform)(object)val4, "OutputTab", 0f, 42f, 1180f, 520f);
			outputPage = ((Component)val7).gameObject;
			gameStatus = Text((Transform)(object)val7, "GameOutputStatus", "", 29f, 348f, 1122f, 24f, 14f, Muted, (TextAlignmentOptions)4097);
			status = Text((Transform)(object)val7, "OutputStatus", "", 29f, 372f, 1122f, 24f, 14f, Muted, (TextAlignmentOptions)4097);
			RectTransform val8 = Rect((Transform)(object)val7, "GameOutputDevices", 28f, 66f, 376f, 274f);
			Frame(val8, SectionFrame);
			Text((Transform)(object)val8, "Heading", L("게임 소리 출력 장치", "GAME OUTPUT DEVICE"), 14f, 8f, 347f, 28f, 18f, Heading, (TextAlignmentOptions)4097);
			gameDeviceContent = DeviceList(val8);
			RectTransform val9 = Rect((Transform)(object)val7, "OutputDevices", 428f, 66f, 376f, 274f);
			Frame(val9, SectionFrame);
			Text((Transform)(object)val9, "Heading", L("음성 출력 장치", "VOICE OUTPUT DEVICE"), 14f, 8f, 347f, 28f, 18f, Heading, (TextAlignmentOptions)4097);
			voiceDeviceHint = Text((Transform)(object)val9, "VoiceDeviceHint", "", 14f, 38f, 347f, 22f, 13f, Muted, (TextAlignmentOptions)4097);
			((TMP_Text)voiceDeviceHint).enableWordWrapping = true;
			((Component)voiceDeviceHint).gameObject.AddComponent<CanvasGroup>().ignoreParentGroups = true;
			deviceContent = DeviceList(val9, 62f, 197f);
			voiceDeviceGroup = ((Component)val9).gameObject.AddComponent<CanvasGroup>();
			RectTransform val10 = Rect((Transform)(object)val7, "OutputSettings", 828f, 66f, 324f, 274f);
			Frame(val10, SectionFrame);
			Text((Transform)(object)val10, "Heading", L("출력 설정", "AUDIO SETTINGS"), 14f, 8f, 296f, 28f, 18f, Heading, (TextAlignmentOptions)4097);
			splitCheck = Toggle(val10, "SplitEnabled", L("음성 분리 사용", "Separate voice"), 44f, out splitState, delegate
			{
				runtime.SplitEnabled = !runtime.SplitEnabled;
			});
			Text((Transform)(object)val10, "VolumeLabel", L("음성 음량", "Voice volume:"), 15f, 88f, 190f, 24f, 15f, Color.white, (TextAlignmentOptions)4097);
			volumeValue = Text((Transform)(object)val10, "VolumeValue", "", 215f, 88f, 94f, 24f, 15f, Orange, (TextAlignmentOptions)4100);
			volumeSlider = Slider(val10, "VoiceVolume", 15f, 122f, 292f, 0f, 2f, whole: false, delegate(float value)
			{
				runtime.VoiceVolume = Mathf.Round(value * 100f) / 100f;
			});
			Text((Transform)(object)val10, "VolumeMinimum", "0%", 15f, 141f, 60f, 14f, 11f, Muted, (TextAlignmentOptions)4097);
			Text((Transform)(object)val10, "VolumeMaximum", "200%", 247f, 141f, 60f, 14f, 11f, Muted, (TextAlignmentOptions)4100);
			Text((Transform)(object)val10, "LatencyLabel", L("출력 버퍼", "Output buffer:"), 15f, 161f, 190f, 24f, 15f, Color.white, (TextAlignmentOptions)4097);
			latencyValue = Text((Transform)(object)val10, "LatencyValue", "", 209f, 161f, 100f, 24f, 15f, Orange, (TextAlignmentOptions)4100);
			latencySlider = Slider(val10, "OutputLatency", 15f, 195f, 292f, 2f, 20f, whole: true, delegate(float value)
			{
				runtime.Latency = Mathf.RoundToInt(value) * 10;
			});
			int[] array = new int[5] { 20, 50, 100, 150, 200 };
			for (int num = 0; num < array.Length; num++)
			{
				int num2 = array[num];
				float num3 = 9f + 274f * (float)(num2 - 20) / 180f;
				Fill(Rect(((Component)latencySlider).transform, "MajorTick_" + num2, num3 - 0.5f, 15f, 1f, 5f), Muted, raycast: false);
				Text(((Component)latencySlider).transform, "TickLabel_" + num2, num2.ToString(), num3 - 18f, 20f, 36f, 13f, 10f, Muted, (TextAlignmentOptions)514);
			}
			((Transform)latencySlider.handleRect).parent.SetAsLastSibling();
			fallbackCheck = Toggle(val10, "FallbackEnabled", L("음성 오류 시 게임 출력 복구", "Voice error: use game output"), 228f, out fallbackState, delegate
			{
				runtime.FallbackEnabled = !runtime.FallbackEnabled;
			}, 13);
			explanation = Text((Transform)(object)val7, "Explanation", "", 29f, 410f, 1121f, 40f, 14f, Muted, (TextAlignmentOptions)4097);
			((TMP_Text)explanation).enableWordWrapping = true;
			ActionButton(val7, "RefreshDevices", L("> 장치 새로고침", "> Refresh devices"), 29f, 470f, 252f, runtime.RefreshMenuDevices);
			RectTransform val11 = Rect((Transform)(object)val4, "InputTab", 28f, 108f, 1124f, 385f);
			inputPage = ((Component)val11).gameObject;
			RectTransform val12 = Rect((Transform)(object)val11, "MicrophoneDevices", 0f, 0f, 690f, 306f);
			Frame(val12, SectionFrame);
			Text((Transform)(object)val12, "Heading", L("송신 마이크 입력 장치", "VOICE INPUT DEVICE"), 14f, 8f, 660f, 28f, 18f, Heading, (TextAlignmentOptions)4097);
			microphoneContent = MicrophoneList(val12);
			RectTransform val13 = Rect((Transform)(object)val11, "MicrophoneHelp", 710f, 0f, 414f, 306f);
			Frame(val13, SectionFrame);
			Text((Transform)(object)val13, "Heading", L("사용 안내", "ABOUT INPUT"), 16f, 8f, 380f, 28f, 18f, Heading, (TextAlignmentOptions)4097);
			((TMP_Text)Text((Transform)(object)val13, "Help", L("게임 설정 사용을 선택하면 게임의 마이크 설정을 따릅니다. 다른 장치를 선택하면 본인 음성 송신에 사용합니다. 입력 장치를 바꾸면 음성 캡처가 잠시 재시작됩니다.", "Use game setting to follow the game's microphone choice. Select another device to use it for your voice. Changing the input briefly restarts voice capture."), 16f, 48f, 380f, 115f, 14f, Color.white, (TextAlignmentOptions)4097)).enableWordWrapping = true;
			Text((Transform)(object)val13, "DisconnectedHelp", L("연결이 끊긴 선택은 보존됩니다. 다시 연결한 뒤 목록을 새로고침하면 캡처도 재시도합니다.", "Disconnected selections are kept. Refresh after reconnecting to retry microphone capture."), 16f, 180f, 380f, 75f, 13f, Muted, (TextAlignmentOptions)4097);
			Text((Transform)(object)val13, "MicrophoneLevelLabel", L("마이크 입력 레벨", "MICROPHONE INPUT LEVEL"), 16f, 260f, 245f, 18f, 12f, Muted, (TextAlignmentOptions)4097);
			microphoneLevelStatus = Text((Transform)(object)val13, "MicrophoneLevelStatus", L("입력 대기", "WAITING"), 261f, 260f, 135f, 18f, 12f, Muted, (TextAlignmentOptions)4100);
			RectTransform val14 = Rect((Transform)(object)val13, "MicrophoneLevelTrack", 16f, 282f, 380f, 12f);
			Fill(val14, new Color(0.16f, 0.16f, 0.16f), raycast: false);
			microphoneLevelFill = Rect((Transform)(object)val14, "MicrophoneLevelFill", 0f, 0f, 0f, 12f);
			microphoneLevelFill.anchorMin = new Vector2(0f, 0f);
			microphoneLevelFill.anchorMax = new Vector2(0f, 1f);
			microphoneLevelFill.pivot = new Vector2(0f, 0.5f);
			Fill(microphoneLevelFill, Orange, raycast: false);
			ActionButton(val11, "RefreshMicrophones", L("> 마이크 목록 새로고침", "> Refresh microphones"), 0f, 337f, 254f, runtime.RefreshMenuMicrophones);
			microphoneStatus = Text((Transform)(object)val11, "MicrophoneStatus", "", 272f, 337f, 852f, 34f, 13f, Muted, (TextAlignmentOptions)4097);
			((TMP_Text)microphoneStatus).enableWordWrapping = true;
			RectTransform val15 = Rect((Transform)(object)val4, "ClearVoiceTab", 28f, 108f, 1124f, 385f);
			clearVoicePage = ((Component)val15).gameObject;
			RectTransform val16 = Rect((Transform)(object)val15, "ClearVoiceSettings", 0f, 0f, 356f, 290f);
			Frame(val16, SectionFrame);
			Text((Transform)(object)val16, "Heading", L("클리어 보이스 설정", "CLEAR VOICE SETTINGS"), 14f, 8f, 328f, 28f, 18f, Heading, (TextAlignmentOptions)4097);
			clearVoiceCheck = Toggle(val16, "ClearVoiceEnabled", L("클리어 보이스 사용", "Enable Clear Voice"), 44f, out clearVoiceToggleState, delegate
			{
				runtime.ClearVoice.Enabled.Value = !runtime.ClearVoice.Enabled.Value;
			});
			((TMP_Text)Text((Transform)(object)val16, "RestartNotice", L("사용 여부 변경은 게임 재시작 후 적용됩니다.", "Enable / disable changes require a game restart."), 15f, 78f, 326f, 42f, 14f, Muted, (TextAlignmentOptions)4097)).enableWordWrapping = true;
			Text((Transform)(object)val16, "BitrateLabel", L("목표 비트레이트", "Target bitrate:"), 15f, 132f, 200f, 24f, 15f, Color.white, (TextAlignmentOptions)4097);
			bitrateValue = Text((Transform)(object)val16, "BitrateValue", "", 213f, 132f, 128f, 24f, 15f, Orange, (TextAlignmentOptions)4100);
			bitrateSlider = Slider(val16, "BitrateKbps", 15f, 170f, 326f, 6f, 300f, whole: true, delegate(float value)
			{
				runtime.ClearVoice.BitrateKbps.Value = Mathf.RoundToInt(value);
			});
			Text((Transform)(object)val16, "MinimumBitrate", "6 kbps", 15f, 195f, 100f, 19f, 13f, Muted, (TextAlignmentOptions)4097);
			Text((Transform)(object)val16, "MaximumBitrate", "300 kbps", 241f, 195f, 100f, 19f, 13f, Muted, (TextAlignmentOptions)4100);
			fecCheck = Toggle(val16, "FecEnabled", L("송수신 오류 정정(FEC)", "Send / receive FEC"), 235f, out fecToggleState, delegate
			{
				runtime.ClearVoice.FecEnabled.Value = !runtime.ClearVoice.FecEnabled.Value;
			});
			RectTransform val17 = Rect((Transform)(object)val15, "ClearVoiceStatus", 380f, 0f, 744f, 290f);
			Frame(val17, SectionFrame);
			Text((Transform)(object)val17, "Heading", L("적용 상태", "APPLIED STATUS"), 16f, 8f, 712f, 28f, 18f, Heading, (TextAlignmentOptions)4097);
			clearVoiceStatus = Text((Transform)(object)val17, "Status", "", 16f, 48f, 712f, 38f, 18f, Orange, (TextAlignmentOptions)4097);
			((TMP_Text)clearVoiceStatus).enableWordWrapping = true;
			clearVoiceApplied = Text((Transform)(object)val17, "Encoder", "", 16f, 94f, 712f, 52f, 16f, Color.white, (TextAlignmentOptions)4097);
			((TMP_Text)clearVoiceApplied).enableWordWrapping = true;
			clearVoiceDetails = Text((Transform)(object)val17, "Details", "", 16f, 151f, 712f, 122f, 16f, Muted, (TextAlignmentOptions)4097);
			((TMP_Text)clearVoiceDetails).enableWordWrapping = true;
			((TMP_Text)Text((Transform)(object)val15, "ClearVoiceHelp", L("게임의 마이크 잡음 억제·RNNoise(신경망 기반 잡음 억제)·자동 음량 보정·에코 제거를 우회합니다.\n일반 음성 필터는 20 kHz로 확장하고 무전기·상황 효과는 유지합니다.\n상대방도 클리어 보이스를 켜야 송신 음질이 개선됩니다.", "Bypasses input noise suppression, RNNoise, automatic gain and echo processing.\nExtends the normal voice filter to 20 kHz; radio and situational effects remain.\nOther players need Clear Voice enabled for improved transmission quality."), 0f, 310f, 1124f, 72f, 16f, Muted, (TextAlignmentOptions)4097)).enableWordWrapping = true;
			closeButton = ActionButton(val4, "CloseMenu", L("> 닫기", "> Back"), 832f, 512f, 320f, runtime.CloseMenu);
			SelectPage(0);
			Refresh();
		}

		private void SelectPage(int page)
		{
			selectedPage = page;
			outputPage.SetActive(page == 0);
			inputPage.SetActive(page == 1);
			clearVoicePage.SetActive(page == 2);
			outputTab.SetChosen(page == 0);
			inputTab.SetChosen(page == 1);
			clearVoiceTab.SetChosen(page == 2);
			((Component)title).gameObject.SetActive(true);
			((Behaviour)title).enabled = true;
			((TMP_Text)title).transform.SetAsLastSibling();
		}

		internal void Show()
		{
			root.SetActive(true);
			SelectPage(selectedPage);
			Refresh();
			eventSystem.SetSelectedGameObject(((Component)closeButton).gameObject);
		}

		internal void Hide()
		{
			root.SetActive(false);
		}

		internal void Refresh()
		{
			//IL_02ca: Unknown result type (might be due to invalid IL or missing references)
			//IL_02c3: Unknown result type (might be due to invalid IL or missing references)
			//IL_049f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0498: Unknown result type (might be due to invalid IL or missing references)
			//IL_0581: Unknown result type (might be due to invalid IL or missing references)
			//IL_05f4: Unknown result type (might be due to invalid IL or missing references)
			//IL_05ed: Unknown result type (might be due to invalid IL or missing references)
			((TMP_Text)shortcut).text = L("열기: " + runtime.MenuKeyName + " · 닫기: ESC", "Open: " + runtime.MenuKeyName + " · Close: ESC");
			((TMP_Text)status).text = (korean ? runtime.StatusMessage : EnglishStatus());
			((TMP_Text)gameStatus).text = (korean ? runtime.GameStatusMessage : EnglishGameStatus());
			((TMP_Text)voiceDeviceHint).text = (runtime.SplitEnabled ? L("분리된 음성을 출력할 장치를 선택합니다.", "Choose a device for separated voice.") : L("음성 분리를 켜면 장치를 선택할 수 있습니다.", "Enable voice separation to choose a device."));
			((TMP_Text)volumeValue).text = $"{runtime.VoiceVolume * 100f:0}%";
			((TMP_Text)latencyValue).text = $"{runtime.Latency} ms";
			volumeSlider.SetValueWithoutNotify(runtime.VoiceVolume);
			latencySlider.SetValueWithoutNotify((float)runtime.Latency / 10f);
			SetCheck(splitCheck, runtime.SplitEnabled);
			SetToggleState(splitState, runtime.SplitEnabled);
			voiceDeviceGroup.interactable = runtime.SplitEnabled;
			voiceDeviceGroup.blocksRaycasts = runtime.SplitEnabled;
			voiceDeviceGroup.alpha = (runtime.SplitEnabled ? 1f : 0.5f);
			SetCheck(fallbackCheck, runtime.FallbackEnabled);
			SetToggleState(fallbackState, runtime.FallbackEnabled);
			ClearVoiceRuntime clearVoice = runtime.ClearVoice;
			SetCheck(clearVoiceCheck, clearVoice.Enabled.Value);
			SetToggleState(clearVoiceToggleState, clearVoice.Enabled.Value);
			SetCheck(fecCheck, clearVoice.FecEnabled.Value);
			SetToggleState(fecToggleState, clearVoice.FecEnabled.Value);
			bitrateSlider.SetValueWithoutNotify((float)clearVoice.BitrateKbps.Value);
			((TMP_Text)bitrateValue).text = $"{clearVoice.BitrateKbps.Value} kbps";
			((TMP_Text)clearVoiceStatus).text = clearVoice.Status(korean);
			((Graphic)clearVoiceStatus).color = ((clearVoice.Failure == null) ? Orange : Failure);
			string arg = ((clearVoice.ActualBitrate == 0) ? L("송신 대기", "Waiting for voice") : $"{clearVoice.ActualBitrate / 1000} kbps");
			string arg2 = (clearVoice.FecEnabled.Value ? L("켜짐", "On") : L("꺼짐", "Off"));
			string arg3 = ((clearVoice.ActualFec < 0) ? L("송신 대기", "Waiting for voice") : ((clearVoice.ActualFec == 1) ? L("켜짐", "On") : L("꺼짐", "Off")));
			((TMP_Text)clearVoiceApplied).text = (ClearVoiceRuntime.Active ? L($"목표 비트레이트: {clearVoice.BitrateKbps.Value} kbps\n인코더 비트레이트: {arg}", $"Target bitrate: {clearVoice.BitrateKbps.Value} kbps\nEncoder bitrate: {arg}") : L("비활성 · 게임 기본 음성 설정 사용", "Off · using game defaults"));
			((TMP_Text)clearVoiceDetails).text = (ClearVoiceRuntime.Active ? L($"송신 FEC(전방 오류 정정): 설정 {arg2} · 인코더 보고: {arg3}\n음성 송신을 시작하면 실제 인코더 값이 표시됩니다.\n모노 · 48 kHz · 20 ms\n실제 전송량은 입력·가변 비트레이트(VBR)·FEC에 따라 달라집니다.\nFEC를 꺼도 패킷 손실 은폐(PLC)는 유지됩니다.\n패킷 제한으로 차단된 프레임: {clearVoice.DroppedPackets}", $"Send FEC (forward error correction): {arg2} · Encoder report: {arg3}\nActual encoder values appear when voice transmission starts.\nMono · 48 kHz · 20 ms\nTraffic varies with input, variable bitrate (VBR) and FEC.\nPacket loss concealment (PLC) remains enabled when FEC is off.\nFrames blocked by the packet limit: {clearVoice.DroppedPackets}") : L("클리어 보이스를 켜면 실제 인코더 비트레이트와 오류 정정(FEC) 상태를 표시합니다.\n저장된 비트레이트와 FEC는 기능을 활성화한 뒤 적용됩니다.", "Enable Clear Voice to display the encoder bitrate and FEC status.\nSaved bitrate and FEC settings take effect after the feature is enabled."));
			string discoveryError = runtime.DiscoveryError;
			((TMP_Text)explanation).text = ((discoveryError != null) ? OutputDiscoveryGuidance(discoveryError) : (runtime.FallbackEnabled ? L("게임 소리와 음성의 장치를 각각 선택하세요. 음성 오류 시 게임 출력으로 복구합니다. 출력 버퍼는 분리된 음성에 적용됩니다.", "Select game and voice devices separately. Voice errors use game output. The output buffer applies to voice.") : L("게임 소리와 음성의 장치를 각각 선택하세요. 음성 오류 시 음소거합니다. 출력 버퍼는 분리된 음성에 적용됩니다.", "Select game and voice devices separately. Voice errors mute voice. The output buffer applies to voice.")));
			((Graphic)explanation).color = ((discoveryError == null) ? Muted : Failure);
			string activeMicrophoneDeviceName = runtime.ActiveMicrophoneDeviceName;
			bool microphoneCaptureRecording = runtime.MicrophoneCaptureRecording;
			((TMP_Text)microphoneStatus).text = ((runtime.MicrophoneDiscoveryError != null) ? L(runtime.MicrophoneDiscoveryError, "Microphone device discovery failed. Check the game log.") : (runtime.MicrophonePreviewFailed ? L("선택한 마이크를 미리보기로 열지 못했습니다. 장치를 확인한 뒤 목록 새로고침으로 다시 시도하세요.", "Could not open the selected microphone for preview. Check the device and refresh the list to retry.") : ((!microphoneCaptureRecording) ? L("마이크 캡처가 실행 중이 아닙니다. 목록 새로고침으로 다시 시도하세요.", "Microphone capture is not running. Refresh the list to retry.") : (string.IsNullOrWhiteSpace(activeMicrophoneDeviceName) ? L("마이크 캡처는 진행 중이며 장치 이름을 확인할 수 없습니다.", "Microphone capture is running, but the device name is unavailable.") : L("현재 사용 중: " + activeMicrophoneDeviceName, "Current input: " + activeMicrophoneDeviceName)))));
			float microphoneInputLevel = runtime.MicrophoneInputLevel;
			microphoneLevelFill.sizeDelta = new Vector2(380f * microphoneInputLevel, 0f);
			bool flag = microphoneInputLevel >= 0.08f;
			((TMP_Text)microphoneLevelStatus).text = ((!microphoneCaptureRecording) ? L("캡처 대기", "CAPTURE WAITING") : (flag ? L("입력 감지", "SIGNAL DETECTED") : L("입력 대기", "WAITING")));
			((Graphic)microphoneLevelStatus).color = ((microphoneCaptureRecording && flag) ? Orange : Muted);
			AudioDevice defaultOutputDevice = runtime.DefaultOutputDevice;
			string text = string.Join("\n", runtime.Devices.Select((AudioDevice device) => device.Id + "=" + device.Name)) + "\n" + ((defaultOutputDevice == null) ? "" : (defaultOutputDevice.Id + "=" + defaultOutputDevice.Name)) + "\n" + (runtime.Devices.Any((AudioDevice device) => device.Id == runtime.SelectedDeviceId) ? "" : runtime.SelectedDeviceId) + "\n" + (runtime.Devices.Any((AudioDevice device) => device.Id == runtime.SelectedGameDeviceId) ? "" : runtime.SelectedGameDeviceId);
			if (text != deviceSignature || deviceRows.Count == 0)
			{
				deviceSignature = text;
				RebuildDevices(deviceContent, deviceRows, game: false, defaultOutputDevice);
				RebuildDevices(gameDeviceContent, gameDeviceRows, game: true, defaultOutputDevice);
				selectedDevice = null;
				selectedGameDevice = null;
			}
			if (selectedDevice != runtime.SelectedDeviceId)
			{
				selectedDevice = runtime.SelectedDeviceId;
				foreach (var deviceRow in deviceRows)
				{
					bool flag2 = deviceRow.Id == selectedDevice;
					deviceRow.Feedback.SetChosen(flag2);
					SetCheck(deviceRow.Check, flag2);
				}
			}
			if (selectedGameDevice != runtime.SelectedGameDeviceId)
			{
				selectedGameDevice = runtime.SelectedGameDeviceId;
				foreach (var gameDeviceRow in gameDeviceRows)
				{
					bool flag3 = gameDeviceRow.Id == selectedGameDevice;
					gameDeviceRow.Feedback.SetChosen(flag3);
					SetCheck(gameDeviceRow.Check, flag3);
				}
			}
			string selectedMicrophoneDeviceId = runtime.SelectedMicrophoneDeviceId;
			string text2 = string.Join("\n", runtime.MicrophoneDevices) + "\n" + selectedMicrophoneDeviceId;
			if (text2 != microphoneSignature || microphoneRows.Count == 0)
			{
				microphoneSignature = text2;
				RebuildMicrophoneDevices();
				selectedMicrophoneDevice = null;
			}
			if (!(selectedMicrophoneDevice != selectedMicrophoneDeviceId))
			{
				return;
			}
			selectedMicrophoneDevice = selectedMicrophoneDeviceId;
			foreach (var microphoneRow in microphoneRows)
			{
				bool flag4 = microphoneRow.Id == selectedMicrophoneDevice;
				microphoneRow.Feedback.SetChosen(flag4);
				SetCheck(microphoneRow.Check, flag4);
			}
		}

		private void RebuildDevices(RectTransform content, List<(string Id, NativeMenuButton Feedback, Image Check)> rows, bool game, AudioDevice? defaultOutput)
		{
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_0063: Expected O, but got Unknown
			//IL_0083: Unknown result type (might be due to invalid IL or missing references)
			//IL_008d: Expected O, but got Unknown
			//IL_00ef: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f9: Expected O, but got Unknown
			//IL_0171: Unknown result type (might be due to invalid IL or missing references)
			//IL_037d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0234: Unknown result type (might be due to invalid IL or missing references)
			//IL_0310: Unknown result type (might be due to invalid IL or missing references)
			//IL_0335: Unknown result type (might be due to invalid IL or missing references)
			//IL_033f: Expected O, but got Unknown
			//IL_029f: Unknown result type (might be due to invalid IL or missing references)
			for (int num = ((Transform)content).childCount - 1; num >= 0; num--)
			{
				Object.Destroy((Object)(object)((Component)((Transform)content).GetChild(num)).gameObject);
			}
			rows.Clear();
			List<AudioDevice> list = new List<AudioDevice>
			{
				new AudioDevice("default", L("Windows 기본 출력 장치", "Default output device"))
			};
			if (game)
			{
				list.Insert(0, new AudioDevice("game", L("기존 게임 출력", "Original game output")));
			}
			list.AddRange(runtime.Devices);
			string currentId = (game ? runtime.SelectedGameDeviceId : runtime.SelectedDeviceId);
			if (list.All((AudioDevice device) => device.Id != currentId))
			{
				list.Add(new AudioDevice(currentId, L("연결되지 않은 장치", "Disconnected device")));
			}
			DeviceChoices val = DeviceChoices.Create((IReadOnlyList<AudioDevice>)runtime.Devices, currentId, game);
			for (int num2 = 0; num2 < list.Count; num2++)
			{
				AudioDevice choice = list[num2];
				RectTransform val2 = Rect((Transform)(object)content, "Device_" + choice.Id, 0f, num2 * 34, 334f, 31f);
				Image targetGraphic = Fill(val2, Color.white);
				string value = (korean ? val.GetLabel(choice.Id) : (choice.Name.Any((char character) => character > '\u007f') ? $"Output device {num2}" : choice.Name));
				bool flag = choice.Id == "default";
				TextMeshProUGUI val3 = Text((Transform)(object)val2, "DeviceName", value, 40f, 1f, flag ? 136 : 285, 29f, 14f, Color.white, (TextAlignmentOptions)4097);
				((TMP_Text)val3).enableAutoSizing = true;
				((TMP_Text)val3).fontSizeMin = 10f;
				((TMP_Text)val3).fontSizeMax = 14f;
				if (flag && defaultOutput != null)
				{
					TextMeshProUGUI obj = Text((Transform)(object)val2, "CurrentDefaultOutput", "· " + defaultOutput.Name, 180f, 1f, 144f, 29f, 10f, Muted, (TextAlignmentOptions)4097);
					((TMP_Text)obj).enableAutoSizing = true;
					((TMP_Text)obj).fontSizeMin = 8f;
					((TMP_Text)obj).fontSizeMax = 10f;
				}
				Image val4 = Check(val2, 8f, 7f, 17f);
				Button val5 = ((Component)val2).gameObject.AddComponent<Button>();
				((Selectable)val5).targetGraphic = (Graphic)(object)targetGraphic;
				NativeMenuButton nativeMenuButton = ((Component)val2).gameObject.AddComponent<NativeMenuButton>();
				nativeMenuButton.Initialize(val5, (TMP_Text)(object)val3, val4, Color.white, PlayHover);
				((UnityEvent)val5.onClick).AddListener((UnityAction)delegate
				{
					PlayConfirm();
					if (game)
					{
						runtime.SelectedGameDeviceId = choice.Id;
					}
					else
					{
						runtime.SelectedDeviceId = choice.Id;
					}
					Refresh();
				});
				rows.Add((choice.Id, nativeMenuButton, val4));
			}
			content.sizeDelta = new Vector2(0f, (float)(list.Count * 34));
		}

		private RectTransform DeviceList(RectTransform parent, float top = 44f, float height = 215f)
		{
			//IL_0066: Unknown result type (might be due to invalid IL or missing references)
			//IL_007b: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e2: Unknown result type (might be due to invalid IL or missing references)
			//IL_0115: Unknown result type (might be due to invalid IL or missing references)
			RectTransform obj = Rect((Transform)(object)parent, "DeviceList", 13f, top, 350f, height);
			RectTransform val = Rect((Transform)(object)obj, "Viewport", 0f, 0f, 335f, height);
			((Component)val).gameObject.AddComponent<RectMask2D>();
			RectTransform val2 = Rect((Transform)(object)val, "Content", 0f, 0f, 0f, height);
			val2.anchorMin = new Vector2(0f, 1f);
			val2.anchorMax = new Vector2(1f, 1f);
			ScrollRect val3 = ((Component)obj).gameObject.AddComponent<ScrollRect>();
			val3.viewport = val;
			val3.content = val2;
			val3.horizontal = false;
			val3.movementType = (MovementType)2;
			val3.scrollSensitivity = 24f;
			RectTransform obj2 = Rect((Transform)(object)obj, "Scrollbar", 342f, 0f, 6f, height);
			Fill(obj2, new Color(0.18f, 0.18f, 0.18f));
			RectTransform val4 = Rect((Transform)(object)obj2, "Handle", 0f, 0f, 0f, 0f);
			Stretch(val4);
			Image targetGraphic = Fill(val4,