using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Splatform; using TorchSharp; using TorchSharp.Utils; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyVersion("0.0.0.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 BroheimSileroTts { internal static class AccentorV5 { private struct MlpLayer { public int Rows; public int Cols; public float[] W; public float[] B; } private static bool _loaded; private static string _vowels; private static string _sos; private static string _eos; private static string _allowed; private static Dictionary _ngram; private static float[] _emb; private static int _embRows; private static Dictionary _symToId; private static Dictionary _exceptions; private static MlpLayer[] _stressClf; private static MlpLayer[] _yoClf; private static readonly Regex _splitRe = new Regex("([\\s.,!?;:<>=()/\\\\]+)", RegexOptions.Compiled); private static readonly Dictionary _lat2cyr = new Dictionary { { 'a', "а" }, { 'b', "б" }, { 'c', "к" }, { 'd', "д" }, { 'e', "е" }, { 'f', "ф" }, { 'g', "г" }, { 'h', "х" }, { 'i', "и" }, { 'j', "дж" }, { 'k', "к" }, { 'l', "л" }, { 'm', "м" }, { 'n', "н" }, { 'o', "о" }, { 'p', "п" }, { 'q', "к" }, { 'r', "р" }, { 's', "с" }, { 't', "т" }, { 'u', "у" }, { 'v', "в" }, { 'w', "в" }, { 'x', "кс" }, { 'y', "и" }, { 'z', "з" } }; public static void EnsureLoaded(string assetPath) { if (_loaded) { return; } using (BinaryReader binaryReader = new BinaryReader(File.OpenRead(assetPath))) { byte[] bytes = binaryReader.ReadBytes(8); if (Encoding.ASCII.GetString(bytes, 0, 7) != "BRACC02") { throw new InvalidDataException("Bad v5 accentor asset magic"); } _vowels = ReadShortString(binaryReader); _sos = ReadShortString(binaryReader); _eos = ReadShortString(binaryReader); binaryReader.ReadSingle(); binaryReader.ReadSingle(); _allowed = ReadShortString(binaryReader); int num = binaryReader.ReadInt32(); for (int i = 0; i < num; i++) { ReadShortString(binaryReader); } int num2 = binaryReader.ReadInt32(); _ngram = new Dictionary(num2); for (int j = 0; j < num2; j++) { string key = ReadShortString(binaryReader); int value = binaryReader.ReadInt32(); _ngram[key] = value; } _embRows = num2; _emb = new float[num2 * 16]; for (int k = 0; k < _emb.Length; k++) { _emb[k] = binaryReader.ReadSingle(); } int num3 = binaryReader.ReadInt32(); _symToId = new Dictionary(num3); for (int l = 0; l < num3; l++) { int key2 = binaryReader.ReadInt32(); int value2 = binaryReader.ReadInt32(); _symToId[key2] = value2; } int num4 = binaryReader.ReadInt32(); for (int m = 0; m < num4; m++) { binaryReader.ReadInt32(); binaryReader.ReadInt32(); } int num5 = binaryReader.ReadInt32(); _exceptions = new Dictionary(num5); for (int n = 0; n < num5; n++) { string key3 = ReadShortString(binaryReader); int item = binaryReader.ReadInt32(); int item2 = binaryReader.ReadInt32(); _exceptions[key3] = ValueTuple.Create(item, item2); } _stressClf = ReadMlp(binaryReader); _yoClf = ReadMlp(binaryReader); } _loaded = true; } private static string ReadShortString(BinaryReader br) { int count = br.ReadUInt16(); return Encoding.UTF8.GetString(br.ReadBytes(count)); } private static MlpLayer[] ReadMlp(BinaryReader br) { int num = br.ReadInt32(); MlpLayer[] array = new MlpLayer[num]; for (int i = 0; i < num; i++) { int num2 = br.ReadInt32(); int num3 = br.ReadInt32(); float[] array2 = new float[num2 * num3]; for (int j = 0; j < array2.Length; j++) { array2[j] = br.ReadSingle(); } float[] array3 = new float[num2]; for (int k = 0; k < num2; k++) { array3[k] = br.ReadSingle(); } array[i] = new MlpLayer { Rows = num2, Cols = num3, W = array2, B = array3 }; } return array; } public static void BuildInputs(string text, int speakerId, out long[] sequence, out float[] dursRate, out float[] pitchCoefs) { string text2 = Accentor(PrepareTextInput(text)); sequence = PreprocessTacotron(text2); dursRate = Ones(sequence.Length); pitchCoefs = Ones(sequence.Length); } private static string ExpandNumbers(string text) { return Regex.Replace(text, "\\d+([.,]\\d+)?", delegate(Match m) { string value = m.Value; string text2 = null; int num = value.IndexOfAny(new char[2] { '.', ',' }); string s; if (num >= 0) { s = value.Substring(0, num); text2 = value.Substring(num + 1); } else { s = value; } StringBuilder stringBuilder = new StringBuilder(); if (!long.TryParse(s, out var result)) { return value; } stringBuilder.Append(NumberToRussian(result)); if (!string.IsNullOrEmpty(text2)) { stringBuilder.Append(" запятая "); string text3 = text2; foreach (char c in text3) { stringBuilder.Append(NumberToRussian(c - 48)).Append(' '); } } return stringBuilder.ToString().Trim(); }); } private static string NumberToRussian(long n) { if (n == 0L) { return "ноль"; } if (n < 0) { return "минус " + NumberToRussian(-n); } string[] u = new string[10] { "", "один", "два", "три", "четыре", "пять", "шесть", "семь", "восемь", "девять" }; string[] uF = new string[10] { "", "одна", "две", "три", "четыре", "пять", "шесть", "семь", "восемь", "девять" }; string[] teens = new string[10] { "десять", "одиннадцать", "двенадцать", "тринадцать", "четырнадцать", "пятнадцать", "шестнадцать", "семнадцать", "восемнадцать", "девятнадцать" }; string[] tens = new string[10] { "", "", "двадцать", "тридцать", "сорок", "пятьдесят", "шестьдесят", "семьдесят", "восемьдесят", "девяносто" }; string[] hundreds = new string[10] { "", "сто", "двести", "триста", "четыреста", "пятьсот", "шестьсот", "семьсот", "восемьсот", "девятьсот" }; string[][] array = new string[5][] { new string[3] { "", "", "" }, new string[3] { "тысяча", "тысячи", "тысяч" }, new string[3] { "миллион", "миллиона", "миллионов" }, new string[3] { "миллиард", "миллиарда", "миллиардов" }, new string[3] { "триллион", "триллиона", "триллионов" } }; int[] array2 = new int[5] { 0, 1, 0, 0, 0 }; List list = new List(); int num = 0; while (n > 0 && num < array.Length) { int num2 = (int)(n % 1000); n /= 1000; if (num2 > 0) { string text = ThreeDigitToRussian(num2, u, uF, teens, tens, hundreds, (num > 0) ? array2[num] : 0); if (num > 0) { int num3 = num2 % 100; if (num3 >= 11 && num3 <= 14) { num3 = 0; } int num4 = num2 % 10; int num5 = ((num4 != 1 || num3 == 11) ? ((num4 >= 2 && num4 <= 4 && (num3 < 11 || num3 > 14)) ? 1 : 2) : 0); text = text + " " + array[num][num5]; } list.Add(text); } num++; } list.Reverse(); return string.Join(" ", list); } private static string ThreeDigitToRussian(int chunk, string[] u, string[] uF, string[] teens, string[] tens, string[] hundreds, int gender) { StringBuilder stringBuilder = new StringBuilder(); int num = chunk / 100; if (num > 0) { stringBuilder.Append(hundreds[num]).Append(' '); } int num2 = chunk % 100; if (num2 >= 10 && num2 < 20) { stringBuilder.Append(teens[num2 - 10]); } else { int num3 = num2 / 10; if (num3 > 0) { stringBuilder.Append(tens[num3]).Append(' '); } int num4 = num2 % 10; if (num4 > 0) { stringBuilder.Append((gender == 1) ? uF[num4] : u[num4]); } } return stringBuilder.ToString().Trim(); } private static string TransliterateLatin(string text) { return Regex.Replace(text, "[a-z]+", delegate(Match m) { string text2 = m.Value.Replace("th", "з").Replace("ch", "ч").Replace("sh", "ш") .Replace("ph", "ф") .Replace("ck", "к") .Replace("oo", "у") .Replace("ee", "и") .Replace("ya", "я") .Replace("yo", "ё"); StringBuilder stringBuilder = new StringBuilder(text2.Length); string text3 = text2; foreach (char c in text3) { if (_lat2cyr.TryGetValue(c, out var value)) { stringBuilder.Append(value); } else { stringBuilder.Append(c); } } return stringBuilder.ToString(); }); } private static string PrepareTextInput(string text) { text = text.ToLowerInvariant(); text = text.Replace('—', '–').Replace('‑', '-'); text = ExpandNumbers(text); text = TransliterateLatin(text); HashSet hashSet = new HashSet(_allowed); hashSet.Add('^'); StringBuilder stringBuilder = new StringBuilder(); string text2 = text; foreach (char c in text2) { if (hashSet.Contains(c)) { stringBuilder.Append(c); } } return CleanStarText(Regex.Replace(stringBuilder.ToString(), "\\s+", " ").Trim()); } private static string CleanStarText(string text) { text = text.Replace("^", " ^"); text = Regex.Replace(text, "\\s+", " ").Trim(); text = text.Replace("^ ", "^"); text = Regex.Replace(text, "\\^+", "^").Trim(); if (text.Length != 0 && text[text.Length - 1] == '^') { text = text.Substring(0, text.Length - 1).Trim(); } return text; } private static void Tokenize(string sentence, List tokens, List modelInputs, List mask) { string[] array = _splitRe.Split(sentence); for (int i = 0; i < array.Length; i++) { string[] array2 = array[i].Split('-'); List list; bool[] array3; if (array2.Length == 1) { list = new List { array2[0] }; array3 = new bool[1] { true }; } else { list = new List(); for (int j = 0; j < array2.Length - 1; j++) { list.Add(array2[j] + "-"); } list.Add(array2[^1]); array3 = new bool[array2.Length]; for (int k = 0; k < array2.Length - 1; k++) { array3[k] = true; } array3[array2.Length - 1] = array2[^1] != "то"; } string[] array4 = new string[list.Count]; for (int l = 0; l < list.Count; l++) { array4[l] = StripNonCyrillic(list[l].ToLowerInvariant()); } int num = Math.Min(array4.Length, array3.Length); for (int m = 0; m < num; m++) { array3[m] = array3[m] && array4[m].Length > 0; } for (int n = 0; n < list.Count; n++) { tokens.Add(list[n]); } for (int num2 = 0; num2 < array4.Length; num2++) { modelInputs.Add(array4[num2]); } for (int num3 = 0; num3 < num; num3++) { mask.Add(array3[num3]); } } } private static string StripNonCyrillic(string s) { StringBuilder stringBuilder = new StringBuilder(s.Length); foreach (char c in s) { if ((c >= 'А' && c <= 'я') || c == 'ё' || c == 'Ё') { stringBuilder.Append(c); } } return stringBuilder.ToString(); } private static List WordNgrams(string word) { string text = "<" + word + ">"; int num = word.Length + 3; List list = new List(); for (int i = 1; i <= num; i++) { for (int j = 0; j <= text.Length - i; j++) { list.Add(text.Substring(j, i)); } } if (word.Length < 1) { list.Add(word); } return list; } private static float[,] Embedding(List words) { float[,] array = new float[words.Count, 16]; int num = _ngram["UNK"]; for (int i = 0; i < words.Count; i++) { List list = WordNgrams(words[i]); int num2 = 0; float[] array2 = new float[16]; for (int j = 0; j < list.Count; j++) { if (_ngram.TryGetValue(list[j], out var value)) { int num3 = value * 16; for (int k = 0; k < 16; k++) { array2[k] += _emb[num3 + k]; } num2++; } } if (num2 == 0) { int num4 = num * 16; for (int l = 0; l < 16; l++) { array2[l] += _emb[num4 + l]; } num2 = 1; } for (int m = 0; m < 16; m++) { array[i, m] = array2[m] / (float)num2; } } return array; } private static float[] Linear(MlpLayer L, float[] x) { float[] array = new float[L.Rows]; for (int i = 0; i < L.Rows; i++) { float num = L.B[i]; int num2 = i * L.Cols; for (int j = 0; j < L.Cols; j++) { num += L.W[num2 + j] * x[j]; } array[i] = num; } return array; } private static float[] Relu(float[] x) { float[] array = new float[x.Length]; for (int i = 0; i < x.Length; i++) { array[i] = ((x[i] < 0f) ? 0f : x[i]); } return array; } private static float[] RunMlp(MlpLayer[] layers, float[] x) { float[] array = x; for (int i = 0; i < layers.Length; i++) { array = Linear(layers[i], array); if (i != layers.Length - 1) { array = Relu(array); } } return array; } private static void GetModelPreds(List words, out float[,] stressProb, out int[] stressPred, out float[,] yoProb, out int[] yoPred) { int count = words.Count; stressProb = new float[count, 10]; yoProb = new float[count, 7]; stressPred = new int[count]; yoPred = new int[count]; if (count == 0) { return; } float[,] array = Embedding(words); for (int i = 0; i < count; i++) { float[] array2 = new float[16]; for (int j = 0; j < 16; j++) { array2[j] = array[i, j]; } float[] logits = RunMlp(_stressClf, array2); float[] logits2 = RunMlp(_yoClf, array2); SoftmaxArgmax(logits, out stressPred[i], out var probAtArgmax); for (int k = 0; k < 10; k++) { stressProb[i, k] = probAtArgmax; } SoftmaxArgmax(logits2, out yoPred[i], out var probAtArgmax2); for (int l = 0; l < 7; l++) { yoProb[i, l] = probAtArgmax2; } } } private static void SoftmaxArgmax(float[] logits, out int argmax, out float probAtArgmax) { float num = float.NegativeInfinity; argmax = 0; for (int i = 0; i < logits.Length; i++) { if (logits[i] > num) { num = logits[i]; argmax = i; } } double num2 = 0.0; for (int j = 0; j < logits.Length; j++) { num2 += Math.Exp(logits[j] - num); } probAtArgmax = (float)(1.0 / num2); } private static void GetPositions(string word, int[] stressedVowelIds, int[] yoVowelIds, out List stressPos, out List yoPos, out int numVowels, out int firstVowel) { List list = new List(); List list2 = new List(); for (int i = 0; i < word.Length; i++) { char c = word[i]; if (_vowels.IndexOf(c) >= 0) { list.Add(i); } if (c == 'е') { list2.Add(i); } } stressPos = new List(); foreach (int num in stressedVowelIds) { if (num < list.Count && list.Count > 0) { stressPos.Add(list[num]); } } yoPos = new List(); foreach (int num2 in yoVowelIds) { if (num2 > 0 && num2 - 1 < list2.Count && list2.Count > 0) { yoPos.Add(list2[num2 - 1]); } } numVowels = list.Count; firstVowel = ((numVowels > 0) ? list[0] : (-1)); } private static string AccentuateException(string cleanWord, string rawWord, bool haveStress) { var (num, num2) = _exceptions[cleanWord]; if (haveStress) { List list = new List(); for (int i = 0; i < rawWord.Length; i++) { if (rawWord[i] == '+') { list.Add(i); } } string text = rawWord.Replace("+", ""); if (num2 != -1) { bool flag = false; for (int j = 0; j < list.Count; j++) { if (list[j] == num2 + 1) { flag = true; break; } } if (flag && num2 < text.Length) { char c = text[num2]; text = text.Substring(0, num2) + ((char.ToLowerInvariant(c) == c) ? 'ё' : 'Ё') + text.Substring(num2 + 1); } } { foreach (int item in list) { text = text.Substring(0, item) + "+" + text.Substring(item); } return text; } } if (num2 != -1 && num2 < rawWord.Length) { char c2 = rawWord[num2]; rawWord = rawWord.Substring(0, num2) + ((char.ToLowerInvariant(c2) == c2) ? 'ё' : 'Ё') + rawWord.Substring(num2 + 1); } return rawWord.Substring(0, num) + "+" + rawWord.Substring(num); } private static string Accentor(string sentence, bool putStress = true, bool putYo = true, bool stressSingleVowel = true) { if (!(putStress || putYo)) { return sentence; } List list = new List(); List list2 = new List(); List list3 = new List(); Tokenize(sentence, list, list2, list3); GetModelPreds(list2, out var stressProb, out var stressPred, out var yoProb, out var yoPred); List list4 = new List(); int num = Math.Min(Math.Min(list.Count, list2.Count), list3.Count); for (int i = 0; i < num; i++) { string text = list[i]; string text2 = list2[i]; bool num2 = list3[i]; string text3 = text.ToLowerInvariant(); if (!num2) { list4.Add(text); continue; } bool flag = text3.IndexOf('+') >= 0; bool flag2 = text3.IndexOf('ё') >= 0; if (flag && flag2) { list4.Add(text); continue; } if (!flag && flag2 && putStress) { int num3 = 0; string text4 = text3; foreach (char value in text4) { if (_vowels.IndexOf(value) >= 0) { num3++; } } if (num3 == 1 && !stressSingleVowel) { list4.Add(text); continue; } List list5 = new List(); for (int k = 0; k < text3.Length; k++) { if (text3[k] == 'ё') { list5.Add(k); } } string text5 = text; for (int l = 0; l < list5.Count; l++) { int num4 = list5[l] + l; text5 = text5.Substring(0, num4) + "+" + text5.Substring(num4); } list4.Add(text5); continue; } if (_exceptions.ContainsKey(text2)) { list4.Add(AccentuateException(text2, text, flag)); continue; } int[] array = new int[1] { stressPred[i] }; bool flag3 = stressProb[i, 0] > (putStress ? 0.5f : 1f) && !flag; int[] yoVowelIds = new int[1] { yoPred[i] }; bool flag4 = yoProb[i, 0] > (putYo ? 0.5f : 1f); int[] stressedVowelIds; if (flag) { string[] array2 = text3.Split('+'); List list6 = new List(); for (int m = 0; m < array2.Length; m++) { int num5 = 0; for (int n = 0; n < array2[m].Length; n++) { if (_vowels.IndexOf(array2[m][n]) >= 0) { num5++; } } list6.Add(num5); } stressedVowelIds = list6.ToArray(); } else { stressedVowelIds = array; } GetPositions(text3, stressedVowelIds, yoVowelIds, out var stressPos, out var yoPos, out var numVowels, out var firstVowel); if (numVowels == 0) { list4.Add(text); continue; } string text6 = text; for (int num6 = 0; num6 < yoPos.Count; num6++) { int num7 = yoPos[num6]; if (stressPos.Contains(num7) && flag4 && num7 < text3.Length && text3[num7] == 'е') { char c = text6[num7]; text6 = text6.Substring(0, num7) + ((char.ToLowerInvariant(c) == c) ? 'ё' : 'Ё') + text6.Substring(num7 + 1); } } if (numVowels == 1) { stressPos = new List { firstVowel }; flag3 = stressSingleVowel && putStress; } if (!flag && flag3) { for (int num8 = 0; num8 < stressPos.Count; num8++) { int num9 = stressPos[num8] + num8; text6 = text6.Substring(0, num9) + "+" + text6.Substring(num9); } } list4.Add(text6); } return string.Join("", list4); } private static long[] PreprocessTacotron(string text) { string text2 = _sos + text + _eos; List list = new List(text2.Length); string text3 = text2; foreach (char c in text3) { if (_symToId.TryGetValue(c, out var value)) { list.Add(value); continue; } throw new InvalidDataException($"Symbol '{c}' (U+{(int)c:X4}) not in symbol_to_id"); } return list.ToArray(); } private static float[] Ones(int n) { float[] array = new float[n]; for (int i = 0; i < n; i++) { array[i] = 1f; } return array; } } [BepInPlugin("Broheim.SileroTTS", "Broheim Silero TTS", "1.3.0")] public class Plugin : BaseUnityPlugin { public enum Language { Russian, English, German } public enum OverlapMode { Queue, Interrupt, Overlap } public enum Channel { Chat, Center, Runes, Npc, Dreams } public const string Guid = "Broheim.SileroTTS"; public const string Name = "Broheim Silero TTS"; public const string Version = "1.3.0"; internal static ManualLogSource Log; internal static ConfigEntry VoiceLanguage; internal static ConfigEntry VoiceSpeaker; internal static ConfigEntry VoicePitch; internal static ConfigEntry VoiceSpeed; internal static ConfigEntry VoiceVolume; internal static ConfigEntry VoiceOverlap; internal static ConfigEntry ChatVolume; internal static ConfigEntry CenterVolume; internal static ConfigEntry RunesVolume; internal static ConfigEntry NpcVolume; internal static ConfigEntry DreamsVolume; internal static ConfigEntry PreRollMs; internal static ConfigEntry TrimTrailingSilence; internal static ConfigEntry NarrateChat; internal static ConfigEntry SkipOwnChat; internal static ConfigEntry AnnounceSpeaker; internal static ConfigEntry NarrateCenter; internal static ConfigEntry NarrateRunes; internal static ConfigEntry NarrateNpc; internal static ConfigEntry NarrateDreams; private static SileroEngine _engine; private Harmony _harmony; internal static SileroEngine Engine => _engine; internal static float VolumeScaled { get { if (VoiceVolume != null) { if (!(VoiceVolume.Value < 0f)) { if (!(VoiceVolume.Value > 3f)) { return VoiceVolume.Value; } return 3f; } return 0f; } return 1f; } } private void Awake() { //IL_02e9: Unknown result type (might be due to invalid IL or missing references) //IL_02f3: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; VoiceLanguage = ((BaseUnityPlugin)this).Config.Bind("Voice", "Language", Language.Russian, "Bundled Silero model. Russian = v5_4_ru (auto-stress + ё via n-gram accentor, 4 voices). English = v3_en (119 voices). German = v3_de (5 voices, umlaut handling)."); VoiceSpeaker = ((BaseUnityPlugin)this).Config.Bind("Voice", "Speaker", "aidar", "Speaker name for the chosen language.\nRussian (v5_4_ru): aidar (male), baya, kseniya, xenia (eugene -> xenia fallback)\nEnglish: en_0 .. en_117 (curated: en_0, en_20, en_27, en_42, en_46, en_70, en_80, en_91, en_48, en_116, en_82, en_67, en_95)\nGerman: bernd_ungerer, eva_k, friedrich, hokuspokus, karlsson"); VoicePitch = ((BaseUnityPlugin)this).Config.Bind("Voice", "Pitch", 1f, "Pitch multiplier. 1.0 = model default. Below 1.0 = lower / rougher / deeper voice (e.g. 0.8). Above 1.0 = higher / brighter. Range 0.5 – 2.0. Applied to the model's pitch contour, so it shifts the voice without retraining."); VoiceSpeed = ((BaseUnityPlugin)this).Config.Bind("Voice", "Speed", 1f, "Speech speed multiplier. 1.0 = model default. Below 1.0 = slower (e.g. 0.85). Above 1.0 = faster. Range 0.5 – 2.0. Controls token duration scaling."); VoiceVolume = ((BaseUnityPlugin)this).Config.Bind("Voice", "Volume", 1f, "Output volume multiplier. 1.0 = model default. Above 1.0 amplifies the signal (e.g. 1.5 = noticeably louder, 2.0 = very loud; hard-clipped to avoid distortion). Below 1.0 = quieter. Range 0.0 – 3.0. Independent of the game's master volume."); VoiceOverlap = ((BaseUnityPlugin)this).Config.Bind("Voice", "OverlapMode", OverlapMode.Queue, "How overlapping narration lines are handled.\nQueue = wait for the current line to finish, then play the next (no overlap, recommended).\nInterrupt = cut off the current line and play the new one immediately.\nOverlap = let multiple lines play at once (mix)."); PreRollMs = ((BaseUnityPlugin)this).Config.Bind("Voice", "PreRollMs", 120, "Leading silence (ms) added before each utterance. Fixes 'first letters swallowed' — the audio source/mixer drops the very first buffer on start, so a short lead-in lets the first phoneme land clean. 0 = off. Range 0 – 500. Default 120."); TrimTrailingSilence = ((BaseUnityPlugin)this).Config.Bind("Voice", "TrimTrailingSilence", true, "Cut trailing silence from the end of each utterance so narration feels snappy (the model often pads a long quiet tail)."); ChatVolume = ((BaseUnityPlugin)this).Config.Bind("Channel Volume", "ChatVolume", 1f, "Volume multiplier for chat narration (on top of [Voice] Volume). Range 0.0 – 3.0."); CenterVolume = ((BaseUnityPlugin)this).Config.Bind("Channel Volume", "CenterVolume", 1f, "Volume multiplier for center-screen messages (Huginn tips, events). Range 0.0 – 3.0."); RunesVolume = ((BaseUnityPlugin)this).Config.Bind("Channel Volume", "RunesVolume", 1f, "Volume multiplier for rune/raven/intro text. Range 0.0 – 3.0."); NpcVolume = ((BaseUnityPlugin)this).Config.Bind("Channel Volume", "NpcVolume", 1f, "Volume multiplier for NPC dialogue. Range 0.0 – 3.0."); DreamsVolume = ((BaseUnityPlugin)this).Config.Bind("Channel Volume", "DreamsVolume", 1f, "Volume multiplier for dream/portal text. Range 0.0 – 3.0."); NarrateChat = ((BaseUnityPlugin)this).Config.Bind("Triggers", "NarrateChat", true, "Speak incoming chat messages."); SkipOwnChat = ((BaseUnityPlugin)this).Config.Bind("Triggers", "SkipOwnChat", true, "Do not speak your own outgoing chat messages."); AnnounceSpeaker = ((BaseUnityPlugin)this).Config.Bind("Triggers", "AnnounceSpeaker", false, "Prepend the speaker name and verb to chat narration (e.g. \"Haldor said in chat: ...\"). Off = voice only the message body."); NarrateCenter = ((BaseUnityPlugin)this).Config.Bind("Triggers", "NarrateCenter", true, "Speak center-screen messages (Huginn tips, boss defeats, events)."); NarrateRunes = ((BaseUnityPlugin)this).Config.Bind("Triggers", "NarrateRunes", false, "Speak rune, raven and intro/tutorial text."); NarrateNpc = ((BaseUnityPlugin)this).Config.Bind("Triggers", "NarrateNpc", false, "Speak NPC dialogue, positioned near the speaker."); NarrateDreams = ((BaseUnityPlugin)this).Config.Bind("Triggers", "NarrateDreams", false, "Speak the dream/portal text shown between sessions."); _engine = new SileroEngine((!string.IsNullOrEmpty(((BaseUnityPlugin)this).Info.Location)) ? Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location) : Environment.CurrentDirectory); _harmony = new Harmony("Broheim.SileroTTS"); _harmony.PatchAll(Assembly.GetExecutingAssembly()); Log.LogInfo((object)string.Format("{0} {1} loaded — voice {2}/{3}.", "Broheim Silero TTS", "1.3.0", VoiceLanguage.Value, VoiceSpeaker.Value)); } private void OnDestroy() { SpeechQueue.Reset(); Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } PlayerVoiceSource.Forget(); } internal static string LangCode() { return SileroEngine.LangCodeFromName(VoiceLanguage.Value.ToString()); } internal static void Narrate(string text, Channel ch) { //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) if (!string.IsNullOrEmpty(text)) { Log.LogDebug((object)$"Narrate: 2D ch={ch}, len={text.Length}, text='{Snip(text)}'"); SpeechQueue.Enqueue(new SpeechReq(text, ch, sp: false, default(Vector3))); } } internal static void NarrateAt(string text, Vector3 pos, Channel ch) { //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) if (!string.IsNullOrEmpty(text)) { Log.LogDebug((object)$"Narrate: spatial ch={ch}, len={text.Length}, pos={pos}, text='{Snip(text)}'"); SpeechQueue.Enqueue(new SpeechReq(text, ch, sp: true, pos)); } } internal static float ChannelVolume(Channel ch) { float num = ch switch { Channel.Chat => ChatVolume.Value, Channel.Center => CenterVolume.Value, Channel.Runes => RunesVolume.Value, Channel.Npc => NpcVolume.Value, Channel.Dreams => DreamsVolume.Value, _ => 1f, }; if (!(num < 0f)) { if (!(num > 3f)) { return num; } return 3f; } return 0f; } private static string Snip(string s) { if (s.Length <= 60) { return s; } return s.Substring(0, 60) + "…"; } internal static float ClampPitch(float v) { if (!(v < 0.5f)) { if (!(v > 2f)) { return v; } return 2f; } return 0.5f; } internal static float ClampSpeed(float v) { if (!(v < 0.5f)) { if (!(v > 2f)) { return v; } return 2f; } return 0.5f; } } internal sealed class Preprocessor { private readonly Dictionary _symbolToId = new Dictionary(); private readonly Dictionary _asciiMap = new Dictionary(); private readonly Dictionary _speakerToId = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly HashSet _allowed = new HashSet(); private int _sosId; private int _eosId; public string Language { get; private set; } public Preprocessor(string datPath) { Load(datPath); } private void Load(string path) { using BinaryReader binaryReader = new BinaryReader(File.OpenRead(path), Encoding.UTF8); byte[] bytes = binaryReader.ReadBytes(4); if (Encoding.ASCII.GetString(bytes, 0, 4) != "BSP1") { throw new InvalidDataException("Bad preprocess.dat magic"); } byte[] bytes2 = binaryReader.ReadBytes(2); Language = Encoding.ASCII.GetString(bytes2, 0, 2); string text = ReadStr(binaryReader); for (int i = 3; i < text.Length; i++) { _allowed.Add(text[i]); } int num = binaryReader.ReadInt32(); for (int j = 0; j < num; j++) { string text2 = ReadStr(binaryReader); int value = binaryReader.ReadInt32(); if (text2.Length == 1) { _symbolToId[text2[0]] = value; } } _sosId = binaryReader.ReadInt32(); _eosId = binaryReader.ReadInt32(); int num2 = binaryReader.ReadInt32(); for (int k = 0; k < num2; k++) { string text3 = ReadStr(binaryReader); string value2 = ReadStr(binaryReader); if (text3.Length == 1) { _asciiMap[text3[0]] = value2; } } int num3 = binaryReader.ReadInt32(); for (int l = 0; l < num3; l++) { string key = ReadStr(binaryReader); int value3 = binaryReader.ReadInt32(); _speakerToId[key] = value3; } } public int SpeakerToId(string speaker) { if (speaker != null && _speakerToId.TryGetValue(speaker, out var value)) { return value; } if (speaker != null) { int num = speaker.LastIndexOf('_'); if (num >= 0 && int.TryParse(speaker.Substring(num + 1), out var result) && _speakerToId.ContainsValue(result)) { return result; } } return 0; } public bool HasSpeaker(string speaker) { if (speaker != null) { return _speakerToId.ContainsKey(speaker); } return false; } public void BuildInputs(string text, out long[] sequence, out float[] dursRate, out float[] pitchCoefs) { sequence = null; dursRate = null; pitchCoefs = null; if (string.IsNullOrEmpty(text)) { return; } string text2 = text.ToLowerInvariant(); text2 = text2.Replace('—', '–').Replace('‑', '-'); StringBuilder stringBuilder = new StringBuilder(text2.Length); string text3 = text2; foreach (char c in text3) { if (_allowed.Contains(c)) { stringBuilder.Append(c); } } text2 = stringBuilder.ToString(); text2 = CollapseWhitespace(text2); if (string.IsNullOrEmpty(text2)) { return; } StringBuilder stringBuilder2 = new StringBuilder(text2.Length); text3 = text2; for (int i = 0; i < text3.Length; i++) { char key = text3[i]; stringBuilder2.Append(_asciiMap.TryGetValue(key, out var value) ? value : key.ToString()); } string text4 = stringBuilder2.ToString(); List list = new List(text4.Length + 2) { _sosId }; text3 = text4; foreach (char key2 in text3) { if (_symbolToId.TryGetValue(key2, out var value2)) { list.Add(value2); } } list.Add(_eosId); int count = list.Count; sequence = list.ToArray(); dursRate = new float[count]; pitchCoefs = new float[count]; for (int j = 0; j < count; j++) { dursRate[j] = 1f; pitchCoefs[j] = 1f; } } private static string CollapseWhitespace(string s) { StringBuilder stringBuilder = new StringBuilder(s.Length); bool flag = false; foreach (char c in s) { if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { if (!flag) { stringBuilder.Append(' '); } flag = true; } else { stringBuilder.Append(c); flag = false; } } return stringBuilder.ToString().Trim(); } private static string ReadStr(BinaryReader br) { int count = br.ReadUInt16(); byte[] array = br.ReadBytes(count); return Encoding.UTF8.GetString(array, 0, array.Length); } } internal sealed class SileroEngine { private sealed class LangBundle { public string Lang; public ScriptModule Model; public Preprocessor Preproc; public bool UseAccentor; } public const string LangRu = "ru"; public const string LangEn = "en"; public const string LangDe = "de"; private readonly string _assetDir; private readonly object _lock = new object(); private bool _nativesOnPath; private readonly Dictionary _bundles = new Dictionary(StringComparer.Ordinal); public SileroEngine(string modDir) { _assetDir = Path.Combine(modDir, "silero_assets"); } private static int RuSpeakerIndex(string speaker) { return (speaker ?? "xenia").ToLowerInvariant() switch { "aidar" => 0, "baya" => 1, "kseniya" => 2, "xenia" => 3, "eugene" => 3, _ => 2, }; } public static string LangCodeFromName(string name) { switch ((name ?? "").ToLowerInvariant()) { case "russian": case "русский": case "ru": return "ru"; case "english": case "en": case "английский": return "en"; case "deutsch": case "de": case "german": case "немецкий": return "de"; default: return "ru"; } } private LangBundle LoadBundle(string lang) { string path = Path.Combine(_assetDir, lang); string text = Path.Combine(path, "tts_runner.pt"); if (!File.Exists(text)) { throw new FileNotFoundException("Silero tts_runner.pt missing for '" + lang + "' at " + text); } PrepareNativePath(); LangBundle langBundle = new LangBundle { Lang = lang, UseAccentor = (lang == "ru") }; if (lang == "ru") { string text2 = Path.Combine(path, "silero_accentor_v5.dat"); if (!File.Exists(text2)) { throw new FileNotFoundException("silero_accentor_v5.dat missing at " + text2); } AccentorV5.EnsureLoaded(text2); } else { string text3 = Path.Combine(path, "preprocess.dat"); if (!File.Exists(text3)) { throw new FileNotFoundException("preprocess.dat missing for '" + lang + "' at " + text3); } langBundle.Preproc = new Preprocessor(text3); } Plugin.Log.LogInfo((object)("Loading Silero model [" + lang + "]...")); langBundle.Model = jit.load(text, (DeviceType)0, -1L); ((Module)langBundle.Model).eval(); Plugin.Log.LogInfo((object)("Silero model [" + lang + "] ready.")); return langBundle; } private LangBundle Bundle(string lang) { lock (_lock) { if (_bundles.TryGetValue(lang, out var value)) { return value; } value = LoadBundle(lang); _bundles[lang] = value; return value; } } private void PrepareNativePath() { if (!_nativesOnPath) { string text = Environment.GetEnvironmentVariable("PATH") ?? ""; if (!text.Contains(_assetDir)) { Environment.SetEnvironmentVariable("PATH", _assetDir + Path.PathSeparator + text); } _nativesOnPath = true; } } public async Task SynthesizeAsync(string text, string lang, string speaker, float pitchScale, float speedScale) { if (string.IsNullOrEmpty(text)) { return null; } await Task.Yield(); lang = ((!string.IsNullOrEmpty(lang)) ? lang : "ru"); speaker = speaker ?? "aidar"; if (pitchScale <= 0f) { pitchScale = 1f; } if (speedScale <= 0f) { speedScale = 1f; } LangBundle langBundle; try { langBundle = Bundle(lang); } catch (Exception ex) { Plugin.Log.LogError((object)("Silero load [" + lang + "] failed: " + ex.Message)); return null; } try { int num; long[] sequence; float[] dursRate; float[] pitchCoefs; if (langBundle.UseAccentor) { num = RuSpeakerIndex(speaker); AccentorV5.BuildInputs(text, num, out sequence, out dursRate, out pitchCoefs); } else { num = langBundle.Preproc.SpeakerToId(speaker); langBundle.Preproc.BuildInputs(text, out sequence, out dursRate, out pitchCoefs); } if (sequence == null || sequence.Length == 0) { Plugin.Log.LogWarning((object)("Silero [" + lang + "]: empty token sequence, skipping.")); return null; } if (pitchScale != 1f) { for (int i = 0; i < pitchCoefs.Length; i++) { pitchCoefs[i] *= pitchScale; } } if (speedScale != 1f) { float num2 = 1f / speedScale; for (int j = 0; j < dursRate.Length; j++) { dursRate[j] *= num2; } } Plugin.Log.LogDebug((object)("Silero [" + lang + "]: seqLen=" + sequence.Length + " spk=" + num + " pitch=" + pitchScale.ToString("0.00") + " speed=" + speedScale.ToString("0.00"))); using (torch.no_grad()) { Tensor val = torch.tensor(sequence, (ScalarType?)null, (Device)null, false, (string[])null).unsqueeze(0L); Tensor val2 = torch.tensor(new long[1] { num }, (ScalarType?)null, (Device)null, false, (string[])null); Tensor val3 = torch.tensor(dursRate, (ScalarType?)null, (Device)null, false, (string[])null).unsqueeze(0L); Tensor val4 = torch.tensor(pitchCoefs, (ScalarType?)null, (Device)null, false, (string[])null).unsqueeze(0L); Tensor obj = langBundle.Model.invoke("forward", new object[4] { val, val2, val3, val4 }).reshape(new long[1] { -1L }).cpu(); int num3 = (int)obj.shape[0]; float[] array = new float[num3]; TensorAccessor val5 = obj.data(); try { for (int k = 0; k < num3; k++) { array[k] = val5[new long[1] { k }]; } } finally { ((IDisposable)val5)?.Dispose(); } Plugin.Log.LogDebug((object)("Silero [" + lang + "]: produced " + num3 + " samples.")); return PcmToWav(array, 24000); } } catch (Exception ex2) { Plugin.Log.LogError((object)("Silero synth [" + lang + "] failed: " + ex2)); return null; } } private static byte[] PcmToWav(float[] samples, int sampleRate) { short[] array = new short[samples.Length]; for (int i = 0; i < samples.Length; i++) { float num = samples[i]; if (num > 1f) { num = 1f; } else if (num < -1f) { num = -1f; } array[i] = (short)(num * 32767f); } int num2 = array.Length * 2; using MemoryStream memoryStream = new MemoryStream(44 + num2); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream); binaryWriter.Write(Encoding.ASCII.GetBytes("RIFF")); binaryWriter.Write(36 + num2); binaryWriter.Write(Encoding.ASCII.GetBytes("WAVE")); binaryWriter.Write(Encoding.ASCII.GetBytes("fmt ")); binaryWriter.Write(16); binaryWriter.Write((short)1); binaryWriter.Write((short)1); binaryWriter.Write(sampleRate); binaryWriter.Write(sampleRate * 2); binaryWriter.Write((short)2); binaryWriter.Write((short)16); binaryWriter.Write(Encoding.ASCII.GetBytes("data")); binaryWriter.Write(num2); byte[] array2 = new byte[num2]; Buffer.BlockCopy(array, 0, array2, 0, num2); binaryWriter.Write(array2); return memoryStream.ToArray(); } } internal static class TextClean { private static readonly Regex Tags = new Regex("<[^>]+>", RegexOptions.Compiled); public static string Clean(string s) { if (!string.IsNullOrEmpty(s)) { return Tags.Replace(s, string.Empty).Trim(); } return s; } } [HarmonyPatch(typeof(Player), "SetLocalPlayer")] internal static class LocalPlayerAudioHook { private static void Postfix(Player __instance) { PlayerVoiceSource.EnsureOnLocalPlayer(__instance); Plugin.Log.LogDebug((object)"LocalPlayerAudioHook: ensured voice AudioSource on local player."); } } [HarmonyPatch(typeof(TextViewer), "ShowText")] internal static class LoreTextHook { private static void Postfix(TextViewer __instance, Style style, string topic, string textId) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Expected I4, but got Unknown //IL_00bd: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)Player.m_localPlayer == (Object)null) && Plugin.NarrateRunes.Value) { string text; string text2; switch ((int)style) { case 0: text = __instance.m_topic.text; text2 = __instance.m_text.text; break; case 1: text = __instance.m_introTopic.text; text2 = __instance.m_introText.text; break; case 2: text = __instance.m_ravenTopic.text; text2 = __instance.m_ravenText.text; break; default: text = Localization.instance.Localize(topic); text2 = Localization.instance.Localize(textId); break; } string text3 = TextClean.Clean(string.IsNullOrEmpty(text) ? text2 : (text + " " + text2)); Plugin.Log.LogDebug((object)$"LoreTextHook: style={style}, lineLen={text3?.Length ?? 0}, enabled={Plugin.NarrateRunes.Value}"); if (!string.IsNullOrEmpty(text3)) { Plugin.Narrate(text3, Plugin.Channel.Runes); } } } } [HarmonyPatch(typeof(MessageHud), "ShowMessage")] internal static class CenterMessageHook { private const float Cooldown = 1.5f; private static string _lastText; private static float _lastTime; private static void Postfix(MessageType type, string text) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Invalid comparison between Unknown and I4 if ((Object)(object)Player.m_localPlayer == (Object)null || (int)type != 2 || !Plugin.NarrateCenter.Value) { return; } string text2 = TextClean.Clean(Localization.instance.Localize(text)); if (!string.IsNullOrEmpty(text2)) { float time = Time.time; if (text2 == _lastText && time - _lastTime < 1.5f) { Plugin.Log.LogDebug((object)"CenterMessageHook: debounced duplicate."); return; } _lastText = text2; _lastTime = time; Plugin.Log.LogDebug((object)$"CenterMessageHook: lineLen={text2.Length}"); Plugin.Narrate(text2, Plugin.Channel.Center); } } } [HarmonyPatch(typeof(Chat), "SetNpcText")] internal static class NpcDialogueHook { private static void Postfix(GameObject talker, string topic, string text) { //IL_0098: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)Player.m_localPlayer == (Object)null) && Plugin.NarrateNpc.Value && !((Object)(object)talker == (Object)null)) { string text2 = TextClean.Clean(string.IsNullOrEmpty(topic) ? Localization.instance.Localize(text) : (Localization.instance.Localize(topic) + " " + Localization.instance.Localize(text))); Plugin.Log.LogDebug((object)$"NpcDialogueHook: talker={((Object)talker).name}, bodyLen={text2?.Length ?? 0}"); if (!string.IsNullOrEmpty(text2)) { Plugin.NarrateAt(text2, talker.transform.position, Plugin.Channel.Npc); } } } } [HarmonyPatch(typeof(Terminal), "AddString", new Type[] { typeof(PlatformUserID), typeof(string), typeof(Type), typeof(bool) })] internal static class ChatLineHook { private static void Postfix(PlatformUserID user, string text, Type type) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Expected I4, but got Unknown if (!Plugin.NarrateChat.Value || (Object)(object)Player.m_localPlayer == (Object)null) { return; } string text2 = null; PlayerInfo val = default(PlayerInfo); if (ZNet.TryGetPlayerByPlatformUserID(user, ref val)) { text2 = CensorShittyWords.FilterUGC(val.m_name, (UGCType)2, user, 0L); } bool flag = text2 != null && text2 == Player.m_localPlayer.GetPlayerName(); Plugin.Log.LogDebug((object)string.Format("ChatLineHook: type={0}, name={1}, isSelf={2}, textLen={3}", type, text2 ?? "?", flag, text?.Length ?? 0)); if (flag && Plugin.SkipOwnChat.Value) { Plugin.Log.LogDebug((object)"ChatLineHook: skipped own message."); return; } string s; if (Plugin.AnnounceSpeaker.Value && text2 != null) { string text3 = (int)type switch { 2 => "shouted", 0 => "whispered", 3 => "pinged", _ => "said", }; s = text2 + " " + text3 + " in chat: " + text; } else { s = text; } Plugin.Narrate(TextClean.Clean(s), Plugin.Channel.Chat); } } [HarmonyPatch(typeof(DreamTexts), "GetRandomDreamText")] internal static class DreamTextHook { private static void Postfix(ref DreamText __result) { if (__result != null && !((Object)(object)Player.m_localPlayer == (Object)null) && Plugin.NarrateDreams.Value) { string text = TextClean.Clean(Localization.instance.Localize(__result.m_text)); Plugin.Log.LogDebug((object)$"DreamTextHook: lineLen={text?.Length ?? 0}"); if (!string.IsNullOrEmpty(text)) { Plugin.Narrate(text, Plugin.Channel.Dreams); } } } } internal readonly struct SpeechReq { public readonly string Text; public readonly Plugin.Channel Ch; public readonly bool Spatial; public readonly Vector3 Pos; public SpeechReq(string t, Plugin.Channel c, bool sp, Vector3 p) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) Text = t; Ch = c; Spatial = sp; Pos = p; } } internal static class SpeechQueue { private static readonly ConcurrentQueue _q = new ConcurrentQueue(); private static readonly SemaphoreSlim _sem = new SemaphoreSlim(1, 1); private static CancellationTokenSource _playCts; private static AudioSource _cur2D; private static AudioSource _curTransient; private static int _drainRunning; public static void Enqueue(SpeechReq r) { if (string.IsNullOrEmpty(r.Text)) { return; } Plugin.OverlapMode value = Plugin.VoiceOverlap.Value; if (value == Plugin.OverlapMode.Overlap) { PlayOverlapAsync(r); return; } if (value == Plugin.OverlapMode.Interrupt) { SpeechReq result; while (_q.TryDequeue(out result)) { } StopCurrent(); _playCts?.Cancel(); } _q.Enqueue(r); StartDrain(); } public static void Reset() { SpeechReq result; while (_q.TryDequeue(out result)) { } StopCurrent(); _playCts?.Cancel(); } private static void StartDrain() { if (Interlocked.CompareExchange(ref _drainRunning, 1, 0) == 0) { DrainAsync(); } } private static async Task DrainAsync() { await _sem.WaitAsync(); try { SpeechReq r; while (_q.TryDequeue(out r)) { _playCts?.Dispose(); _playCts = new CancellationTokenSource(); CancellationToken ct = _playCts.Token; byte[] array; try { array = await Plugin.Engine.SynthesizeAsync(r.Text, Plugin.LangCode(), Plugin.VoiceSpeaker.Value, Plugin.ClampPitch(Plugin.VoicePitch.Value), Plugin.ClampSpeed(Plugin.VoiceSpeed.Value)); } catch (Exception ex) { Plugin.Log.LogError((object)("queue synth threw: " + ex)); continue; } if (array == null || array.Length <= 44 || ct.IsCancellationRequested) { continue; } float num = Plugin.VolumeScaled * Plugin.ChannelVolume(r.Ch); int num2 = Mathf.Clamp(Plugin.PreRollMs.Value, 0, 500); bool value = Plugin.TrimTrailingSilence.Value; AudioClip val = WavPlayer.ToClip(array, num, num2, value); if (!((Object)(object)val == (Object)null)) { float length = val.length; if (r.Spatial) { _curTransient = WavPlayer.PlaySpatialClip(val, r.Pos); } else { _cur2D = WavPlayer.Play2DClip(val); } Plugin.Log.LogDebug((object)$"queue: ch={r.Ch} dur={length:F1}s vol={num:F2} pre={num2}ms trim={value}"); try { await Task.Delay((int)(length * 1000f) + 70, ct); } catch (TaskCanceledException) { } } } } finally { _drainRunning = 0; _sem.Release(); if (!_q.IsEmpty) { StartDrain(); } } } private static async Task PlayOverlapAsync(SpeechReq r) { byte[] array; try { array = await Plugin.Engine.SynthesizeAsync(r.Text, Plugin.LangCode(), Plugin.VoiceSpeaker.Value, Plugin.ClampPitch(Plugin.VoicePitch.Value), Plugin.ClampSpeed(Plugin.VoiceSpeed.Value)); } catch (Exception ex) { Plugin.Log.LogError((object)("overlap synth threw: " + ex)); return; } if (array == null || array.Length <= 44) { return; } float num = Plugin.VolumeScaled * Plugin.ChannelVolume(r.Ch); int preRollMs = Mathf.Clamp(Plugin.PreRollMs.Value, 0, 500); bool value = Plugin.TrimTrailingSilence.Value; AudioClip val = WavPlayer.ToClip(array, num, preRollMs, value); if (!((Object)(object)val == (Object)null)) { if (r.Spatial) { WavPlayer.PlaySpatialClip(val, r.Pos); } else { WavPlayer.Play2DTransient(val); } Plugin.Log.LogDebug((object)$"overlap: ch={r.Ch} dur={val.length:F1}s vol={num:F2}"); } } private static void StopCurrent() { try { if ((Object)(object)_cur2D != (Object)null) { _cur2D.Stop(); _cur2D = null; } } catch { } try { if ((Object)(object)_curTransient != (Object)null) { GameObject gameObject = ((Component)_curTransient).gameObject; _curTransient = null; Object.Destroy((Object)(object)gameObject); } } catch { } } } internal static class WavPlayer { private const float DefaultSpatial = 0f; private const float SilenceThreshold = 0.012f; private const int KeepTailMs = 40; public static AudioClip ToClip(byte[] wav, float vol, int preRollMs, bool trimTrailing, string name = "sileroVoice") { if (wav == null || wav.Length < 44) { return null; } int num = ReadInt16(wav, 22); int num2 = ReadInt32(wav, 24); int num3 = ReadInt16(wav, 34); if (num <= 0 || num2 <= 0) { return null; } if (num3 != 16 && num3 != 8) { Plugin.Log.LogWarning((object)"WavPlayer: only 8/16-bit PCM supported."); return null; } int i = 12; int num4 = -1; int num5 = 0; int num6; for (; i + 8 <= wav.Length; i += 8 + num6) { string text = Encoding.ASCII.GetString(wav, i, 4); num6 = ReadInt32(wav, i + 4); if (text == "data") { num4 = i + 8; num5 = num6; break; } } if (num4 < 0) { return null; } if (num4 + num5 > wav.Length) { num5 = wav.Length - num4; } int num7 = num3 / 8; int num8 = num5 / num7; bool flag = vol != 1f; float[] array = new float[num8]; if (num3 == 16) { for (int j = 0; j < num8; j++) { float num9 = (float)ReadInt16(wav, num4 + j * 2) / 32768f; if (flag) { num9 *= vol; if (num9 > 1f) { num9 = 1f; } else if (num9 < -1f) { num9 = -1f; } } array[j] = num9; } } else { for (int k = 0; k < num8; k++) { float num10 = (float)(wav[num4 + k] - 128) / 128f; if (flag) { num10 *= vol; if (num10 > 1f) { num10 = 1f; } else if (num10 < -1f) { num10 = -1f; } } array[k] = num10; } } int num11 = array.Length; if (trimTrailing && num11 > 0) { int num12 = 40 * num2 / 1000 * num; int num13 = num11; while (num13 > num12 && Mathf.Abs(array[num13 - 1]) < 0.012f) { num13--; } if (num13 < num11) { num11 = num13; } } int num14 = ((preRollMs > 0) ? (preRollMs * num2 / 1000 * num) : 0); if (num14 > 0 || num11 != array.Length) { float[] array2 = new float[num14 + num11]; Array.Copy(array, 0, array2, num14, num11); array = array2; num11 = array2.Length; } AudioClip val = AudioClip.Create(name, num11 / num, num, num2, false); val.SetData(array, 0); Plugin.Log.LogDebug((object)$"WavPlayer.ToClip: samples={num11}, ch={num}, sr={num2}, bits={num3}, vol={vol:F2}, pre={preRollMs}ms, trim={trimTrailing}"); return val; } public static AudioSource Play2DClip(AudioClip clip) { if ((Object)(object)clip == (Object)null) { return null; } AudioSource val = PlayerVoiceSource.Find(); if ((Object)(object)val == (Object)null) { Plugin.Log.LogDebug((object)"Play2DClip: player source missing, using transient 2D source."); return SpawnTransient(clip, 0f, null, autoDestroy: false); } val.Stop(); val.clip = clip; val.time = 0f; val.Play(); Plugin.Log.LogDebug((object)$"Play2DClip: clipLen={clip.length}s"); return val; } public static AudioSource Play2DTransient(AudioClip clip) { if ((Object)(object)clip == (Object)null) { return null; } return SpawnTransient(clip, 0f, null, autoDestroy: true); } public static AudioSource PlaySpatialClip(AudioClip clip, Vector3 worldPos) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)clip == (Object)null) { return null; } Plugin.Log.LogDebug((object)$"PlaySpatialClip: clipLen={clip.length}s, pos={worldPos}"); return SpawnTransient(clip, 1f, worldPos, autoDestroy: true); } private static AudioSource SpawnTransient(AudioClip clip, float spatial, Vector3? pos, bool autoDestroy) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown //IL_001c: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("SileroTtsVoice"); if (pos.HasValue) { val.transform.position = pos.Value; } AudioSource obj = val.AddComponent(); obj.clip = clip; obj.spatialBlend = spatial; obj.bypassEffects = true; obj.bypassReverbZones = true; obj.bypassListenerEffects = true; obj.ignoreListenerVolume = true; obj.ignoreListenerPause = true; obj.volume = 1f; obj.Play(); if (autoDestroy) { Object.Destroy((Object)(object)val, clip.length + 0.2f); } return obj; } private static int ReadInt32(byte[] b, int o) { return b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24); } private static short ReadInt16(byte[] b, int o) { return (short)(b[o] | (b[o + 1] << 8)); } } internal static class PlayerVoiceSource { private const string ChildName = "SileroVoice"; private static AudioSource _cached; public static AudioSource Find() { if ((Object)(object)_cached != (Object)null) { return _cached; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return null; } Transform val = ((Component)localPlayer).transform.Find("SileroVoice"); _cached = (((Object)(object)val != (Object)null) ? ((Component)val).GetComponent() : null); return _cached; } public static void EnsureOnLocalPlayer(Player p) { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Expected O, but got Unknown if (!((Object)(object)p == (Object)null)) { Transform transform = ((Component)p).transform; if ((Object)(object)transform.Find("SileroVoice") != (Object)null) { _cached = null; Find(); return; } GameObject val = new GameObject("SileroVoice"); AudioSource obj = val.AddComponent(); obj.spatialBlend = 0f; obj.dopplerLevel = 0f; obj.rolloffMode = (AudioRolloffMode)1; obj.minDistance = 1f; obj.maxDistance = 100f; obj.ignoreListenerVolume = true; obj.ignoreListenerPause = true; obj.bypassEffects = true; obj.bypassReverbZones = true; val.transform.SetParent(transform, false); _cached = obj; } } public static void Forget() { _cached = null; } } }