Decompiled source of PeakTTS v1.0.2

PeakTTS.dll

Decompiled 2 days 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.Versioning;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;
using UnityEngine.SceneManagement;

[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("PeakTTS")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.2.0")]
[assembly: AssemblyInformationalVersion("1.0.2")]
[assembly: AssemblyProduct("PeakTTS")]
[assembly: AssemblyTitle("PeakTTS")]
[assembly: AssemblyVersion("1.0.2.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[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 PeakTTS
{
	internal static class GameInputBridge
	{
		private static Type _guiManagerType;

		private static PropertyInfo _instanceProperty;

		private static FieldInfo _instanceField;

		private static PropertyInfo _windowBlockingInputProperty;

		public static bool InstallPatch(Harmony harmony)
		{
			//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00be: Expected O, but got Unknown
			try
			{
				_guiManagerType = AccessTools.TypeByName("GUIManager");
				if (_guiManagerType == null)
				{
					ManualLogSource logSource = Plugin.LogSource;
					if (logSource != null)
					{
						logSource.LogWarning((object)"GUIManager not found; the TTS box will still work, but PEAK input may not be blocked while typing");
					}
					return false;
				}
				_instanceProperty = AccessTools.Property(_guiManagerType, "instance");
				_instanceField = AccessTools.Field(_guiManagerType, "instance");
				_windowBlockingInputProperty = AccessTools.Property(_guiManagerType, "windowBlockingInput");
				MethodInfo methodInfo = AccessTools.Method(_guiManagerType, "UpdateWindowStatus", (Type[])null, (Type[])null);
				if (methodInfo == null)
				{
					ManualLogSource logSource2 = Plugin.LogSource;
					if (logSource2 != null)
					{
						logSource2.LogWarning((object)"GUIManager.UpdateWindowStatus not found; inputblock disabled");
					}
					return false;
				}
				HarmonyMethod val = new HarmonyMethod(typeof(GameInputBridge), "UpdateWindowStatusPostfix", (Type[])null);
				harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				return true;
			}
			catch (Exception ex)
			{
				ManualLogSource logSource3 = Plugin.LogSource;
				if (logSource3 != null)
				{
					logSource3.LogWarning((object)("Could not install PEAK inputblock: " + ex.Message));
				}
				return false;
			}
		}

		public static bool IsBlockedByGame()
		{
			try
			{
				EnsureReflection();
				object instance = GetInstance();
				if (instance == null || _windowBlockingInputProperty == null)
				{
					return false;
				}
				return Convert.ToBoolean(_windowBlockingInputProperty.GetValue(instance, null));
			}
			catch
			{
				return false;
			}
		}

		private static void UpdateWindowStatusPostfix()
		{
			if ((Object)(object)Plugin.Instance == (Object)null || !Plugin.Instance.IsInputOpen)
			{
				return;
			}
			try
			{
				EnsureReflection();
				object instance = GetInstance();
				if (instance != null && !(_windowBlockingInputProperty == null) && _windowBlockingInputProperty.CanWrite)
				{
					_windowBlockingInputProperty.SetValue(instance, true, null);
				}
			}
			catch
			{
			}
		}

		private static object GetInstance()
		{
			object obj = _instanceProperty?.GetValue(null, null);
			if (obj == null)
			{
				FieldInfo instanceField = _instanceField;
				if ((object)instanceField == null)
				{
					return null;
				}
				obj = instanceField.GetValue(null);
			}
			return obj;
		}

		private static void EnsureReflection()
		{
			if (!(_guiManagerType != null))
			{
				_guiManagerType = AccessTools.TypeByName("GUIManager");
				if (!(_guiManagerType == null))
				{
					_instanceProperty = AccessTools.Property(_guiManagerType, "instance");
					_instanceField = AccessTools.Field(_guiManagerType, "instance");
					_windowBlockingInputProperty = AccessTools.Property(_guiManagerType, "windowBlockingInput");
				}
			}
		}
	}
	internal static class PhotonVoiceBridge
	{
		private static readonly object Sync = new object();

		private static object _primaryVoice;

		private static int _sampleRate = 24000;

		private static int _channels = 1;

		private static int _frameSize = 480;

		private static int _frameDurationUs = 20000;

		private static bool _loggedEncoderStage;

		private static bool _loggedSkippedStage;

		public static void RefreshPrimaryVoice()
		{
			try
			{
				Type type = AccessTools.TypeByName("Photon.Voice.PUN.PunVoiceClient");
				if (type == null)
				{
					return;
				}
				object obj = AccessTools.Property(type, "Instance")?.GetValue(null, null);
				if (obj == null)
				{
					return;
				}
				object obj2 = AccessTools.Property(obj.GetType(), "PrimaryRecorder")?.GetValue(obj, null);
				if (obj2 == null)
				{
					return;
				}
				object obj3 = AccessTools.Field(obj2.GetType(), "voice")?.GetValue(obj2);
				if (obj3 == null)
				{
					return;
				}
				int num = 24000;
				int num2 = 1;
				int num3 = 480;
				int num4 = 20000;
				object obj4 = AccessTools.Property(obj3.GetType(), "Info")?.GetValue(obj3, null);
				if (obj4 != null)
				{
					num = ReadIntProperty(obj4, "SamplingRate", num);
					num2 = Math.Max(1, ReadIntProperty(obj4, "Channels", num2));
					num3 = Math.Max(num2, ReadIntProperty(obj4, "FrameSize", num3));
					num4 = Math.Max(1000, ReadIntProperty(obj4, "FrameDurationUs", num4));
				}
				lock (Sync)
				{
					_primaryVoice = obj3;
					_sampleRate = num;
					_channels = num2;
					_frameSize = num3;
					_frameDurationUs = num4;
				}
			}
			catch (Exception ex)
			{
				ManualLogSource logSource = Plugin.LogSource;
				if (logSource != null)
				{
					logSource.LogDebug((object)("Primary voice refresh failed: " + ex.Message));
				}
			}
		}

		public static bool IsPrimaryVoice(object voice)
		{
			lock (Sync)
			{
				return _primaryVoice == null || _primaryVoice == voice;
			}
		}

		public static void GetFormat(out int sampleRate, out int channels, out int frameSize)
		{
			lock (Sync)
			{
				sampleRate = _sampleRate;
				channels = _channels;
				frameSize = _frameSize;
			}
		}

		public static void ResolveProcessedBufferFormat(int bufferLength, out int sampleRate, out int channels)
		{
			int sampleRate2;
			int num;
			lock (Sync)
			{
				sampleRate2 = _sampleRate;
				channels = Math.Max(1, _channels);
				num = Math.Max(1000, _frameDurationUs);
			}
			sampleRate = ((sampleRate2 > 0) ? sampleRate2 : 24000);
			if (bufferLength <= 0)
			{
				return;
			}
			int num2 = bufferLength / channels;
			if (num2 > 0)
			{
				int num3 = SnapToVoiceRate((int)Math.Round((double)num2 * 1000000.0 / (double)num));
				if (num3 > 0)
				{
					sampleRate = num3;
				}
			}
		}

		public static bool IsEncoderStage(int bufferLength, out int sampleRate, out int channels)
		{
			int num;
			int num2;
			int num3;
			lock (Sync)
			{
				num = ((_sampleRate > 0) ? _sampleRate : 24000);
				num2 = Math.Max(1, _frameSize);
				channels = Math.Max(1, _channels);
				num3 = Math.Max(1000, _frameDurationUs);
			}
			ResolveProcessedBufferFormat(bufferLength, out sampleRate, out channels);
			int num4 = ((bufferLength > 0) ? (bufferLength / channels) : 0);
			bool num5 = Math.Abs(sampleRate - num) <= Math.Max(100, num / 100);
			bool flag = Math.Abs(num4 - num2) <= Math.Max(1, num2 / 100);
			bool flag2 = num5 && flag;
			if (flag2 && !_loggedEncoderStage)
			{
				_loggedEncoderStage = true;
				ManualLogSource logSource = Plugin.LogSource;
				if (logSource != null)
				{
					logSource.LogInfo((object)($"PeakTTS encoder stage selected: {bufferLength} values, {channels} channel(s), " + $"{(double)num3 / 1000.0:0.###} ms => {sampleRate} Hz (VoiceInfo {num} Hz, frame {num2})"));
				}
			}
			else if (!flag2 && !_loggedSkippedStage)
			{
				_loggedSkippedStage = true;
				ManualLogSource logSource2 = Plugin.LogSource;
				if (logSource2 != null)
				{
					logSource2.LogInfo((object)($"PeakTTS ignoring upstream Photon PCM stage: {bufferLength} values, {channels} channel(s), " + $"{(double)num3 / 1000.0:0.###} ms => {sampleRate} Hz; encoder expects {num} Hz / {num2} samples"));
				}
			}
			return flag2;
		}

		private static int SnapToVoiceRate(int value)
		{
			int[] obj = new int[5] { 8000, 12000, 16000, 24000, 48000 };
			int num = 0;
			int num2 = int.MaxValue;
			int[] array = obj;
			foreach (int num3 in array)
			{
				int num4 = Math.Abs(value - num3);
				if (num4 < num2)
				{
					num = num3;
					num2 = num4;
				}
			}
			if (num2 > Math.Max(500, num / 20))
			{
				if (value < 4000 || value > 96000)
				{
					return 0;
				}
				return value;
			}
			return num;
		}

		private static int ReadIntProperty(object value, string name, int fallback)
		{
			try
			{
				PropertyInfo propertyInfo = AccessTools.Property(value.GetType(), name);
				if (propertyInfo == null)
				{
					return fallback;
				}
				return Convert.ToInt32(propertyInfo.GetValue(value, null));
			}
			catch
			{
				return fallback;
			}
		}
	}
	internal static class PhotonVoicePatches
	{
		private sealed class TransmitPatchState
		{
			public bool Applied;

			public bool Previous;

			public PropertyInfo Property;
		}

		public static int Install(Harmony harmony, ManualLogSource log)
		{
			Type type = AccessTools.TypeByName("Photon.Voice.LocalVoiceFramed`1");
			if (type == null)
			{
				log.LogError((object)"Could not find Photon.Voice.LocalVoiceFramed`1");
				return 0;
			}
			return 0 + PatchSampleType(harmony, type, typeof(float), "ProcessPostfixFloat", log) + PatchSampleType(harmony, type, typeof(short), "ProcessPostfixShort", log);
		}

		private static int PatchSampleType(Harmony harmony, Type openType, Type sampleType, string processPostfixName, ManualLogSource log)
		{
			//IL_009a: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a7: Expected O, but got Unknown
			//IL_015f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0166: Unknown result type (might be due to invalid IL or missing references)
			//IL_0173: Expected O, but got Unknown
			//IL_0173: Expected O, but got Unknown
			int num = 0;
			Type type;
			try
			{
				type = openType.MakeGenericType(sampleType);
			}
			catch (Exception ex)
			{
				log.LogWarning((object)("Could not close LocalVoiceFramed<" + sampleType.Name + ">: " + ex.Message));
				return 0;
			}
			BindingFlags bindingAttr = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
			MethodInfo methodInfo = type.GetMethods(bindingAttr).FirstOrDefault((MethodInfo m) => m.Name == "processFrame" && m.ReturnType == sampleType.MakeArrayType());
			if (methodInfo != null)
			{
				MethodInfo method = typeof(PhotonVoicePatches).GetMethod(processPostfixName, BindingFlags.Static | BindingFlags.NonPublic);
				harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(method), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				log.LogInfo((object)("Patched " + type.FullName + "." + methodInfo.Name + " -> " + processPostfixName));
				num++;
			}
			else
			{
				log.LogWarning((object)("No compatible processFrame method found on " + type.FullName));
			}
			MethodInfo methodInfo2 = type.GetMethods(bindingAttr).FirstOrDefault((MethodInfo m) => m.Name == "PushData" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == sampleType.MakeArrayType());
			if (methodInfo2 != null)
			{
				MethodInfo method2 = typeof(PhotonVoicePatches).GetMethod("PushPrefix", BindingFlags.Static | BindingFlags.NonPublic);
				MethodInfo method3 = typeof(PhotonVoicePatches).GetMethod("PushPostfix", BindingFlags.Static | BindingFlags.NonPublic);
				harmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(method2), new HarmonyMethod(method3), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				log.LogInfo((object)("Patched " + type.FullName + ".PushData for TTS-only transmit override"));
				num++;
			}
			else
			{
				log.LogWarning((object)("No compatible PushData method found on " + type.FullName));
			}
			return num;
		}

		private static void PushPrefix(object __instance, ref TransmitPatchState __state)
		{
			__state = null;
			if (!TtsMixer.HasAudio || !PhotonVoiceBridge.IsPrimaryVoice(__instance))
			{
				return;
			}
			try
			{
				PropertyInfo propertyInfo = AccessTools.Property(__instance.GetType(), "TransmitEnabled");
				if (!(propertyInfo == null) && propertyInfo.CanRead && propertyInfo.CanWrite)
				{
					bool flag = Convert.ToBoolean(propertyInfo.GetValue(__instance, null));
					__state = new TransmitPatchState
					{
						Applied = true,
						Previous = flag,
						Property = propertyInfo
					};
					if (!flag)
					{
						propertyInfo.SetValue(__instance, true, null);
					}
				}
			}
			catch (Exception ex)
			{
				ManualLogSource logSource = Plugin.LogSource;
				if (logSource != null)
				{
					logSource.LogDebug((object)("Transmit override failed: " + ex.Message));
				}
			}
		}

		private static void PushPostfix(object __instance, TransmitPatchState __state)
		{
			if (__state == null || !__state.Applied || __state.Previous)
			{
				return;
			}
			try
			{
				__state.Property.SetValue(__instance, false, null);
			}
			catch
			{
			}
		}

		private static void ProcessPostfixFloat(object __instance, ref float[] __result)
		{
			if (TtsMixer.HasAudio && PhotonVoiceBridge.IsPrimaryVoice(__instance))
			{
				PhotonVoiceBridge.GetFormat(out var _, out var channels, out var frameSize);
				if (__result == null)
				{
					__result = new float[Math.Max(channels, frameSize)];
				}
				if (PhotonVoiceBridge.IsEncoderStage(__result.Length, out var sampleRate2, out channels))
				{
					TtsMixer.Mix(__result, sampleRate2, channels);
				}
			}
		}

		private static void ProcessPostfixShort(object __instance, ref short[] __result)
		{
			if (TtsMixer.HasAudio && PhotonVoiceBridge.IsPrimaryVoice(__instance))
			{
				PhotonVoiceBridge.GetFormat(out var _, out var channels, out var frameSize);
				if (__result == null)
				{
					__result = new short[Math.Max(channels, frameSize)];
				}
				if (PhotonVoiceBridge.IsEncoderStage(__result.Length, out var sampleRate2, out channels))
				{
					TtsMixer.Mix(__result, sampleRate2, channels);
				}
			}
		}
	}
	[BepInPlugin("ahun.PeakTTS", "PeakTTS", "1.0.2")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string PluginGuid = "ahun.PeakTTS";

		public const string PluginName = "PeakTTS";

		public const string PluginVersion = "1.0.2";

		internal static ManualLogSource LogSource;

		internal static Plugin Instance;

		private ConfigEntry<KeyCode> _openKey;

		private ConfigEntry<float> _ttsGain;

		private ConfigEntry<float> _transmitGain;

		private ConfigEntry<bool> _monitorPlayback;

		private ConfigEntry<float> _monitorVolume;

		private ConfigEntry<int> _speechRate;

		private ConfigEntry<string> _voiceName;

		private ConfigEntry<int> _maxCharacters;

		private ConfigEntry<string> _powershellPath;

		private Harmony _harmony;

		private bool _inputOpen;

		private string _text = string.Empty;

		private bool _focusNextFrame;

		private volatile string _status = "Ready";

		private int _pendingJobs;

		private float _nextVoiceRefreshAt;

		private int _keyboardHandledFrame = -1;

		private readonly object _monitorSync = new object();

		private readonly Queue<SpeechClip> _monitorQueue = new Queue<SpeechClip>();

		private AudioSource _monitorSource;

		private AudioClip _monitorClip;

		private volatile bool _monitorPlaying;

		private volatile bool _monitorEnabled;

		private GUIStyle _boxStyle;

		private GUIStyle _labelStyle;

		internal bool IsInputOpen => _inputOpen;

		private bool HasMonitorAudio
		{
			get
			{
				if (_monitorPlaying)
				{
					return true;
				}
				lock (_monitorSync)
				{
					return _monitorQueue.Count > 0;
				}
			}
		}

		private void Awake()
		{
			//IL_01a1: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ab: Expected O, but got Unknown
			Instance = this;
			LogSource = ((BaseUnityPlugin)this).Logger;
			_openKey = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Input", "OpenKey", (KeyCode)289, "Key used to toggle the TTS text box open/closed");
			_ttsGain = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "TTSGain", 0.85f, "Legacy TTS gain setting retained for compatibility. Local monitor volume is controlled separately; outgoing voice uses TransmitGain");
			_transmitGain = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "TransmitGain", 0.22f, "Outgoing TTS gain sent to other players, from 0.0 to 1.0. Lower this if the transmitted voice is too loud");
			_monitorPlayback = ((BaseUnityPlugin)this).Config.Bind<bool>("Audio", "MonitorPlayback", true, "Play your own generated TTS locally so you can hear when it starts and ends");
			_monitorVolume = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "MonitorVolume", 0.85f, "Local TTS monitoring volume, from 0.0 to 1.0");
			_speechRate = ((BaseUnityPlugin)this).Config.Bind<int>("Voice", "SpeechRate", 0, "Windows TTS speaking rate, from -10 to 10");
			_voiceName = ((BaseUnityPlugin)this).Config.Bind<string>("Voice", "VoiceName", string.Empty, "Optional installed Windows voice name. Empty uses the default voice");
			_maxCharacters = ((BaseUnityPlugin)this).Config.Bind<int>("Input", "MaxCharacters", 220, "Maximum text length per TTS message");
			_powershellPath = ((BaseUnityPlugin)this).Config.Bind<string>("Advanced", "PowerShellPath", "powershell.exe", "PowerShell executable used for Windows System.Speech TTS");
			TtsMixer.SetGain(Mathf.Clamp01(_transmitGain.Value));
			_monitorEnabled = _monitorPlayback.Value;
			SetupMonitorAudio();
			_transmitGain.SettingChanged += delegate
			{
				TtsMixer.SetGain(Mathf.Clamp01(_transmitGain.Value));
			};
			_harmony = new Harmony("ahun.PeakTTS");
			int num = PhotonVoicePatches.Install(_harmony, ((BaseUnityPlugin)this).Logger);
			bool flag = GameInputBridge.InstallPatch(_harmony);
			((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("{0} {1} loaded. Installed {2} Photon Voice patches. Inputblocking: {3}", "PeakTTS", "1.0.2", num, flag));
			if (num == 0)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"Photon Voice was not found yet. PEAK may have changed its Photon Voice version; check LogOutput.log");
			}
			SceneManager.activeSceneChanged += OnActiveSceneChanged;
		}

		private void OnDestroy()
		{
			SceneManager.activeSceneChanged -= OnActiveSceneChanged;
			try
			{
				Harmony harmony = _harmony;
				if (harmony != null)
				{
					harmony.UnpatchSelf();
				}
			}
			catch
			{
			}
			ClearMonitorPlayback();
		}

		private void Update()
		{
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			UpdateMonitorPlayback();
			if (Time.unscaledTime >= _nextVoiceRefreshAt)
			{
				_nextVoiceRefreshAt = Time.unscaledTime + 2f;
				PhotonVoiceBridge.RefreshPrimaryVoice();
			}
			if (Input.GetKeyDown(_openKey.Value))
			{
				SetInputOpen(!_inputOpen);
				_keyboardHandledFrame = Time.frameCount;
			}
		}

		private void OnActiveSceneChanged(Scene previous, Scene next)
		{
			if (_inputOpen)
			{
				_text = string.Empty;
				SetInputOpen(open: false);
			}
		}

		private void SetInputOpen(bool open)
		{
			if (_inputOpen != open)
			{
				_inputOpen = open;
				_focusNextFrame = open;
				ManualLogSource logSource = LogSource;
				if (logSource != null)
				{
					logSource.LogInfo((object)("TTS input overlay " + (open ? "opened" : "closed")));
				}
			}
		}

		private void OnGUI()
		{
			//IL_00e6: 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_0133: Unknown result type (might be due to invalid IL or missing references)
			//IL_01bc: Unknown result type (might be due to invalid IL or missing references)
			//IL_006c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0072: Invalid comparison between Unknown and I4
			//IL_003f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0075: Unknown result type (might be due to invalid IL or missing references)
			//IL_0080: Unknown result type (might be due to invalid IL or missing references)
			//IL_029f: Unknown result type (might be due to invalid IL or missing references)
			EnsureStyles();
			if (!_inputOpen)
			{
				if (TtsMixer.HasAudio || HasMonitorAudio || Volatile.Read(in _pendingJobs) > 0)
				{
					GUI.Label(new Rect(18f, 18f, 520f, 28f), "PeakTTS: " + _status, _labelStyle);
				}
				return;
			}
			Event current = Event.current;
			if (current != null && (int)current.type == 4 && current.keyCode == _openKey.Value && _keyboardHandledFrame != Time.frameCount)
			{
				SetInputOpen(open: false);
				_keyboardHandledFrame = Time.frameCount;
				current.Use();
				return;
			}
			float num = Mathf.Min(720f, (float)Screen.width - 40f);
			float num2 = ((float)Screen.width - num) * 0.5f;
			Rect val = default(Rect);
			((Rect)(ref val))..ctor(num2, 55f, num, 130f);
			GUI.Box(val, GUIContent.none, _boxStyle);
			GUI.Label(new Rect(((Rect)(ref val)).x + 14f, ((Rect)(ref val)).y + 10f, ((Rect)(ref val)).width - 28f, 24f), $"PeakTTS — Enter: speak  {_openKey.Value}: close  ({_text.Length}/{Math.Max(1, _maxCharacters.Value)})", _labelStyle);
			GUI.SetNextControlName("PeakTTSText");
			int num3 = Math.Max(1, _maxCharacters.Value);
			string text = GUI.TextArea(new Rect(((Rect)(ref val)).x + 14f, ((Rect)(ref val)).y + 42f, ((Rect)(ref val)).width - 28f, 34f), _text ?? string.Empty, num3 + 1);
			if (text.IndexOf('\n') >= 0 || text.IndexOf('\r') >= 0)
			{
				_text = text.Replace("\r", string.Empty).Replace("\n", string.Empty);
				if (_text.Length > num3)
				{
					_text = _text.Substring(0, num3);
				}
				if (current != null)
				{
					current.Use();
				}
				SubmitCurrentText();
			}
			else
			{
				_text = ((text.Length > num3) ? text.Substring(0, num3) : text);
				GUI.Label(new Rect(((Rect)(ref val)).x + 14f, ((Rect)(ref val)).y + 89f, ((Rect)(ref val)).width - 28f, 24f), _status, _labelStyle);
				if (_focusNextFrame)
				{
					GUI.FocusControl("PeakTTSText");
					_focusNextFrame = false;
				}
				else if (GUI.GetNameOfFocusedControl() != "PeakTTSText")
				{
					GUI.FocusControl("PeakTTSText");
				}
			}
		}

		private void SubmitCurrentText()
		{
			string text = (_text ?? string.Empty).Trim();
			if (text.Length == 0)
			{
				_status = "Type something first";
				return;
			}
			_text = string.Empty;
			SetInputOpen(open: false);
			_status = "Generating speech...";
			Interlocked.Increment(ref _pendingJobs);
			string voice = _voiceName.Value ?? string.Empty;
			int rate = Math.Max(-10, Math.Min(10, _speechRate.Value));
			string powershell = (string.IsNullOrWhiteSpace(_powershellPath.Value) ? "powershell.exe" : _powershellPath.Value);
			Task.Run(delegate
			{
				try
				{
					SpeechClip clip = WindowsTts.Synthesize(text, voice, rate, powershell);
					_status = "Speaking: " + Shorten(text, 72);
					TtsMixer.Enqueue(clip);
				}
				catch (Exception arg)
				{
					_status = "TTS failed - check BepInEx/LogOutput.log";
					LogSource.LogError((object)$"TTS generation failed: {arg}");
				}
				finally
				{
					Interlocked.Decrement(ref _pendingJobs);
				}
			});
		}

		internal static void NotifyPlaybackStarted(SpeechClip clip)
		{
			Instance?.QueueMonitorPlayback(clip);
		}

		internal static void NotifyPlaybackFinished()
		{
			Instance?.TrySetReady();
		}

		private void SetupMonitorAudio()
		{
			_monitorSource = ((Component)this).gameObject.AddComponent<AudioSource>();
			_monitorSource.playOnAwake = false;
			_monitorSource.loop = false;
			_monitorSource.spatialBlend = 0f;
			_monitorSource.dopplerLevel = 0f;
			_monitorSource.ignoreListenerPause = true;
			_monitorSource.volume = Mathf.Clamp01(_monitorVolume.Value);
			_monitorVolume.SettingChanged += delegate
			{
				if ((Object)(object)_monitorSource != (Object)null)
				{
					_monitorSource.volume = Mathf.Clamp01(_monitorVolume.Value);
				}
			};
			_monitorPlayback.SettingChanged += delegate
			{
				_monitorEnabled = _monitorPlayback.Value;
				if (!_monitorEnabled)
				{
					ClearMonitorPlayback();
				}
			};
		}

		private void QueueMonitorPlayback(SpeechClip clip)
		{
			if (!_monitorEnabled || clip == null || clip.MonoSamples.Length == 0)
			{
				return;
			}
			lock (_monitorSync)
			{
				_monitorQueue.Enqueue(clip);
			}
		}

		private void UpdateMonitorPlayback()
		{
			if ((Object)(object)_monitorSource == (Object)null || !_monitorEnabled)
			{
				return;
			}
			if (_monitorSource.isPlaying)
			{
				_monitorPlaying = true;
				return;
			}
			_monitorPlaying = false;
			if ((Object)(object)_monitorClip != (Object)null)
			{
				Object.Destroy((Object)(object)_monitorClip);
				_monitorClip = null;
				ManualLogSource logSource = LogSource;
				if (logSource != null)
				{
					logSource.LogDebug((object)"Local monitor playback finished");
				}
				TrySetReady();
			}
			SpeechClip speechClip = null;
			lock (_monitorSync)
			{
				if (_monitorQueue.Count > 0)
				{
					speechClip = _monitorQueue.Dequeue();
				}
			}
			if (speechClip == null)
			{
				return;
			}
			try
			{
				_monitorClip = AudioClip.Create("PeakTTS_Monitor", speechClip.MonoSamples.Length, 1, Math.Max(8000, speechClip.SampleRate), false);
				_monitorClip.SetData(speechClip.MonoSamples, 0);
				_monitorSource.clip = _monitorClip;
				_monitorSource.volume = Mathf.Clamp01(_monitorVolume.Value);
				_monitorSource.Play();
				_monitorPlaying = true;
				ManualLogSource logSource2 = LogSource;
				if (logSource2 != null)
				{
					logSource2.LogDebug((object)$"Local monitor playback started ({speechClip.MonoSamples.Length} samples @ {speechClip.SampleRate} Hz)");
				}
			}
			catch (Exception ex)
			{
				ManualLogSource logSource3 = LogSource;
				if (logSource3 != null)
				{
					logSource3.LogWarning((object)("Could not play local TTS monitor audio: " + ex.Message));
				}
				_monitorPlaying = false;
				if ((Object)(object)_monitorClip != (Object)null)
				{
					Object.Destroy((Object)(object)_monitorClip);
					_monitorClip = null;
				}
				TrySetReady();
			}
		}

		private void ClearMonitorPlayback()
		{
			_monitorPlaying = false;
			if ((Object)(object)_monitorSource != (Object)null)
			{
				_monitorSource.Stop();
				_monitorSource.clip = null;
			}
			if ((Object)(object)_monitorClip != (Object)null)
			{
				Object.Destroy((Object)(object)_monitorClip);
				_monitorClip = null;
			}
			lock (_monitorSync)
			{
				_monitorQueue.Clear();
			}
			TrySetReady();
		}

		private void TrySetReady()
		{
			if (Volatile.Read(in _pendingJobs) == 0 && !TtsMixer.HasAudio && !HasMonitorAudio)
			{
				_status = "Ready";
			}
		}

		private static string Shorten(string value, int max)
		{
			if (string.IsNullOrEmpty(value) || value.Length <= max)
			{
				return value;
			}
			return value.Substring(0, max - 1) + "...";
		}

		private void EnsureStyles()
		{
			//IL_0014: Unknown result type (might be due to invalid IL or missing references)
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0028: Expected O, but got Unknown
			//IL_002d: Expected O, but got Unknown
			//IL_0038: 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_0045: Unknown result type (might be due to invalid IL or missing references)
			//IL_0051: Expected O, but got Unknown
			if (_boxStyle == null)
			{
				_boxStyle = new GUIStyle(GUI.skin.box)
				{
					padding = new RectOffset(8, 8, 8, 8)
				};
				_labelStyle = new GUIStyle(GUI.skin.label)
				{
					fontSize = 15,
					wordWrap = false
				};
			}
		}
	}
	internal sealed class SpeechClip
	{
		public readonly float[] MonoSamples;

		public readonly int SampleRate;

		public double Position;

		public SpeechClip(float[] monoSamples, int sampleRate)
		{
			MonoSamples = monoSamples ?? Array.Empty<float>();
			SampleRate = ((sampleRate > 0) ? sampleRate : 22050);
		}
	}
	internal static class TtsMixer
	{
		private static readonly object Sync = new object();

		private static readonly Queue<SpeechClip> Queue = new Queue<SpeechClip>();

		private static SpeechClip _current;

		private static float _gain = 0.22f;

		public static bool HasAudio
		{
			get
			{
				lock (Sync)
				{
					return _current != null || Queue.Count > 0;
				}
			}
		}

		public static void SetGain(float gain)
		{
			lock (Sync)
			{
				_gain = Mathf.Clamp01(gain);
			}
		}

		public static void Enqueue(SpeechClip clip)
		{
			if (clip == null || clip.MonoSamples.Length == 0)
			{
				return;
			}
			lock (Sync)
			{
				Queue.Enqueue(clip);
			}
		}

		public static bool Mix(float[] target, int targetRate, int channels)
		{
			if (target == null || target.Length == 0)
			{
				return false;
			}
			if (channels <= 0)
			{
				channels = 1;
			}
			if (targetRate <= 0)
			{
				targetRate = 24000;
			}
			lock (Sync)
			{
				if (!EnsureCurrent())
				{
					return false;
				}
				bool result = false;
				int num = target.Length / channels;
				for (int i = 0; i < num; i++)
				{
					if (!EnsureCurrent())
					{
						break;
					}
					float num2 = ReadCurrentSample(targetRate) * _gain;
					for (int j = 0; j < channels; j++)
					{
						int num3 = i * channels + j;
						target[num3] = Mathf.Clamp(target[num3] + num2, -1f, 1f);
					}
					result = true;
				}
				if (!HasAudioUnsafe())
				{
					Plugin.NotifyPlaybackFinished();
				}
				return result;
			}
		}

		public static bool Mix(short[] target, int targetRate, int channels)
		{
			if (target == null || target.Length == 0)
			{
				return false;
			}
			if (channels <= 0)
			{
				channels = 1;
			}
			if (targetRate <= 0)
			{
				targetRate = 24000;
			}
			lock (Sync)
			{
				if (!EnsureCurrent())
				{
					return false;
				}
				bool result = false;
				int num = target.Length / channels;
				for (int i = 0; i < num; i++)
				{
					if (!EnsureCurrent())
					{
						break;
					}
					float num2 = ReadCurrentSample(targetRate) * _gain;
					for (int j = 0; j < channels; j++)
					{
						int num3 = i * channels + j;
						float num4 = Mathf.Clamp((float)target[num3] / 32768f + num2, -1f, 1f);
						target[num3] = (short)Mathf.RoundToInt(num4 * 32767f);
					}
					result = true;
				}
				if (!HasAudioUnsafe())
				{
					Plugin.NotifyPlaybackFinished();
				}
				return result;
			}
		}

		private static float ReadCurrentSample(int targetRate)
		{
			if (_current == null || _current.MonoSamples.Length == 0)
			{
				return 0f;
			}
			float[] monoSamples = _current.MonoSamples;
			double position = _current.Position;
			int num = (int)position;
			if (num >= monoSamples.Length)
			{
				return 0f;
			}
			int num2 = Math.Min(num + 1, monoSamples.Length - 1);
			float num3 = (float)(position - (double)num);
			float result = monoSamples[num] + (monoSamples[num2] - monoSamples[num]) * num3;
			_current.Position += (double)_current.SampleRate / (double)targetRate;
			if (_current.Position >= (double)monoSamples.Length)
			{
				_current = null;
			}
			return result;
		}

		private static bool EnsureCurrent()
		{
			while (_current == null && Queue.Count > 0)
			{
				_current = Queue.Dequeue();
				if (_current.MonoSamples.Length == 0)
				{
					_current = null;
				}
				else
				{
					Plugin.NotifyPlaybackStarted(_current);
				}
			}
			return _current != null;
		}

		private static bool HasAudioUnsafe()
		{
			if (_current == null)
			{
				return Queue.Count > 0;
			}
			return true;
		}
	}
	internal static class WavReader
	{
		public static SpeechClip ReadMono(string path)
		{
			using FileStream fileStream = File.OpenRead(path);
			using BinaryReader binaryReader = new BinaryReader(fileStream, Encoding.UTF8, leaveOpen: false);
			if (ReadFourCc(binaryReader) != "RIFF")
			{
				throw new InvalidDataException("Not a RIFF WAV file");
			}
			binaryReader.ReadUInt32();
			if (ReadFourCc(binaryReader) != "WAVE")
			{
				throw new InvalidDataException("Not a WAVE file");
			}
			ushort num = 0;
			ushort num2 = 0;
			int num3 = 0;
			ushort num4 = 0;
			byte[] array = null;
			while (fileStream.Position + 8 <= fileStream.Length)
			{
				string text = ReadFourCc(binaryReader);
				uint num5 = binaryReader.ReadUInt32();
				long num6 = fileStream.Position + num5;
				if (text == "fmt ")
				{
					num = binaryReader.ReadUInt16();
					num2 = binaryReader.ReadUInt16();
					num3 = binaryReader.ReadInt32();
					binaryReader.ReadInt32();
					binaryReader.ReadUInt16();
					num4 = binaryReader.ReadUInt16();
				}
				else if (text == "data")
				{
					array = binaryReader.ReadBytes(checked((int)num5));
				}
				fileStream.Position = num6 + (num5 & 1);
				if (array != null && num != 0)
				{
					break;
				}
			}
			if (array == null)
			{
				throw new InvalidDataException("WAV data chunk not found");
			}
			if (num2 == 0 || num3 <= 0)
			{
				throw new InvalidDataException("Invalid WAV format");
			}
			if (num != 1 && num != 3)
			{
				throw new NotSupportedException($"Unsupported WAV format code {num}");
			}
			int num7 = num4 / 8;
			if (num7 <= 0)
			{
				throw new InvalidDataException("Invalid WAV bit depth");
			}
			int num8 = array.Length / num7 / num2;
			float[] array2 = new float[num8];
			int offset = 0;
			for (int i = 0; i < num8; i++)
			{
				double num9 = 0.0;
				for (int j = 0; j < num2; j++)
				{
					num9 += (double)ReadSample(array, ref offset, num, num4);
				}
				array2[i] = (float)(num9 / (double)(int)num2);
			}
			return new SpeechClip(array2, num3);
		}

		private static float ReadSample(byte[] data, ref int offset, ushort format, ushort bits)
		{
			if (format == 3 && bits == 32)
			{
				float v = BitConverter.ToSingle(data, offset);
				offset += 4;
				return Clamp(v);
			}
			switch (bits)
			{
			case 8:
				return (float)(data[offset++] - 128) / 128f;
			case 16:
			{
				short num3 = (short)(data[offset] | (data[offset + 1] << 8));
				offset += 2;
				return (float)num3 / 32768f;
			}
			case 24:
			{
				int num2 = data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16);
				if ((num2 & 0x800000) != 0)
				{
					num2 |= -16777216;
				}
				offset += 3;
				return (float)num2 / 8388608f;
			}
			case 32:
			{
				int num = BitConverter.ToInt32(data, offset);
				offset += 4;
				return (float)num / 2.1474836E+09f;
			}
			default:
				throw new NotSupportedException($"Unsupported PCM bit depth {bits}");
			}
		}

		private static string ReadFourCc(BinaryReader br)
		{
			return Encoding.ASCII.GetString(br.ReadBytes(4));
		}

		private static float Clamp(float v)
		{
			if (v > 1f)
			{
				return 1f;
			}
			if (v < -1f)
			{
				return -1f;
			}
			return v;
		}
	}
	internal static class WindowsTts
	{
		public static SpeechClip Synthesize(string text, string voiceName, int rate, string powershellPath)
		{
			if (string.IsNullOrWhiteSpace(text))
			{
				throw new ArgumentException("Text is empty", "text");
			}
			string text2 = Guid.NewGuid().ToString("N");
			string text3 = Path.Combine(Path.GetTempPath(), "PeakTTS");
			Directory.CreateDirectory(text3);
			string text4 = Path.Combine(text3, text2 + ".wav");
			string text5 = Path.Combine(text3, text2 + ".ps1");
			try
			{
				string text6 = Convert.ToBase64String(Encoding.UTF8.GetBytes(text));
				string voice = Convert.ToBase64String(Encoding.UTF8.GetBytes(voiceName ?? string.Empty));
				string path = Convert.ToBase64String(Encoding.UTF8.GetBytes(text4));
				string contents = BuildScript(text6, voice, path, Math.Max(-10, Math.Min(10, rate)));
				File.WriteAllText(text5, contents, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
				using (Process process = Process.Start(new ProcessStartInfo
				{
					FileName = powershellPath,
					Arguments = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"" + text5 + "\"",
					UseShellExecute = false,
					CreateNoWindow = true,
					RedirectStandardOutput = true,
					RedirectStandardError = true,
					WorkingDirectory = text3
				}))
				{
					if (process == null)
					{
						throw new InvalidOperationException("Could not start PowerShell");
					}
					string arg = process.StandardOutput.ReadToEnd();
					string arg2 = process.StandardError.ReadToEnd();
					process.WaitForExit();
					if (process.ExitCode != 0)
					{
						throw new InvalidOperationException($"PowerShell TTS exited with code {process.ExitCode}: {arg2}\n{arg}");
					}
				}
				if (!File.Exists(text4))
				{
					throw new FileNotFoundException("PowerShell did not produce a WAV file", text4);
				}
				return WavReader.ReadMono(text4);
			}
			finally
			{
				TryDelete(text5);
				TryDelete(text4);
			}
		}

		private static string BuildScript(string text64, string voice64, string path64, int rate)
		{
			return $"$ErrorActionPreference = 'Stop'\nAdd-Type -AssemblyName System.Speech\n$text = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('{text64}'))\n$voice = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('{voice64}'))\n$outPath = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('{path64}'))\n$s = New-Object System.Speech.Synthesis.SpeechSynthesizer\ntry {{\n    if (-not [string]::IsNullOrWhiteSpace($voice)) {{\n        try {{ $s.SelectVoice($voice) }} catch {{ }}\n    }}\n    $s.Rate = {rate}\n    $s.Volume = 100\n    $s.SetOutputToWaveFile($outPath)\n    $s.Speak($text)\n}} finally {{\n    $s.Dispose()\n}}\n";
		}

		private static void TryDelete(string path)
		{
			try
			{
				if (!string.IsNullOrEmpty(path) && File.Exists(path))
				{
					File.Delete(path);
				}
			}
			catch
			{
			}
		}
	}
}