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
{
}
}
}
}