using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using Dissonance; using Dissonance.Audio.Capture; using HarmonyLib; using Microsoft.CodeAnalysis; using NAudio.Wave; using Newtonsoft.Json; using SpeechRecognitionAPI.Patches; using UnityEngine; using Vosk; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("JS03")] [assembly: AssemblyConfiguration("Debug")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+9248eb8c7794f7211b32ff17374b70fbdc2ac5fe")] [assembly: AssemblyProduct("WhisperLC")] [assembly: AssemblyTitle("SpeechRecognitionAPI")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.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 SpeechRecognitionAPI { public class Engine : MonoBehaviour { [CompilerGenerated] private sealed class d__10 : IEnumerator, IEnumerator, IDisposable { private int <>1__state; private object <>2__current; public Engine <>4__this; private DissonanceComms 5__1; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__10(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { 5__1 = null; <>1__state = -2; } private bool MoveNext() { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; 5__1 = Object.FindObjectOfType(); break; case 1: <>1__state = -1; break; } if (!Object.op_Implicit((Object)(object)5__1) || 5__1.MicrophoneCapture == null) { 5__1 = Object.FindObjectOfType(); <>2__current = (object)new WaitForSeconds(0.5f); <>1__state = 1; return true; } <>4__this._micCapture = new MicrophoneCapture(); 5__1.MicrophoneCapture.Subscribe((IMicrophoneSubscriber)(object)<>4__this._micCapture); Plugin.mls.LogInfo((object)"Subscribed to Dissonance microphone stream"); return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private string[] models = new string[2] { "vosk-model-small-en-us-0.15", "vosk-model-small-es-0.42" }; private VoskRecognizer _recognizer; private Model _model; private MicrophoneCapture _micCapture; private int _micSampleRate; public static event EventHandler SpeechRecognized; public void StartEngine() { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Expected O, but got Unknown //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Expected O, but got Unknown Object.DontDestroyOnLoad((Object)(object)this); string text = Path.Combine(Plugin.pluginDir, "models", models[(int)Plugin.language.Value]); _model = new Model(text); _recognizer = new VoskRecognizer(_model, 16000f); Plugin.mls.LogInfo((object)"Vosk ASR ready."); } public void StartMicCapture() { ((MonoBehaviour)this).StartCoroutine(WaitForDissonance()); } [IteratorStateMachine(typeof(d__10))] private IEnumerator WaitForDissonance() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__10(0) { <>4__this = this }; } public void StopMicCapture() { DissonanceComms val = Object.FindObjectOfType(); if ((Object)(object)val == (Object)null) { Plugin.mls.LogError((object)"DissonanceComms not found"); } else { val.MicrophoneCapture.Unsubscribe((IMicrophoneSubscriber)(object)_micCapture); } } private float[] Resample(float[] input, int inputRate, int outputRate) { if (inputRate == outputRate) { return input; } int num = (int)((long)input.Length * (long)outputRate / inputRate); float[] array = new float[num]; float num2 = (float)input.Length / (float)num; for (int i = 0; i < num; i++) { float num3 = (float)i * num2; int num4 = (int)num3; float num5 = num3 - (float)num4; float num6 = input[num4]; float num7 = ((num4 + 1 < input.Length) ? input[num4 + 1] : num6); array[i] = num6 + (num7 - num6) * num5; } return array; } public void ReceiveAudio(ArraySegment buffer, int sampleRate) { float[] array = new float[buffer.Count]; if (buffer.Array == null) { return; } Array.Copy(buffer.Array, buffer.Offset, array, 0, buffer.Count); float[] array2 = Resample(array, sampleRate, 16000); byte[] array3 = new byte[array2.Length * 2]; for (int i = 0; i < array2.Length; i++) { short num = (short)(array2[i] * 32767f); array3[i * 2] = (byte)((uint)num & 0xFFu); array3[i * 2 + 1] = (byte)(num >> 8); } if (!_recognizer.AcceptWaveform(array3, array3.Length)) { return; } string text = _recognizer.Result(); string text2 = JsonConvert.DeserializeObject(text)?.text; if (!string.IsNullOrEmpty(text2)) { if (Plugin.logging.Value) { Plugin.mls.LogInfo((object)("Recognized: " + text2)); } if (Speech.phrases.Count > 0) { Speech.GetBestMatch(text2); } Engine.SpeechRecognized?.Invoke(this, new SpeechEventArgs(text2)); } } } public enum Languages { English, Spanish } public class MicrophoneCapture : IMicrophoneSubscriber { public void ReceiveMicrophoneData(ArraySegment buffer, WaveFormat format) { Plugin.SpeechEngine.ReceiveAudio(buffer, format.SampleRate); } public void Reset() { } } [BepInPlugin("JS03.SpeechRecognitionAPI", "SpeechRecognitionAPI", "1.0.0")] public class Plugin : BaseUnityPlugin { private const string modGUID = "JS03.SpeechRecognitionAPI"; private const string modName = "SpeechRecognitionAPI"; private const string modVersion = "1.0.0"; public static Engine SpeechEngine; private static readonly string[] _libraries = new string[5] { "libgcc_s_seh-1", "libstdc++-6", "libvosk", "libwinpthread-1", "Vosk" }; internal static string? pluginDir; private readonly Harmony harmony = new Harmony("JS03.SpeechRecognitionAPI"); public static Plugin Instance; internal static ManualLogSource mls; public static ConfigEntry logging; public static ConfigEntry language; [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr LoadLibrary(string lpFileName); private void Awake() { if ((Object)(object)Instance == (Object)null) { Instance = this; } mls = Logger.CreateLogSource("JS03.SpeechRecognitionAPI"); mls.LogInfo((object)"Starting SpeechRecognitionAPI"); language = ((BaseUnityPlugin)this).Config.Bind("General", "Language", Languages.English, "Language to be used for speech recognition"); logging = ((BaseUnityPlugin)this).Config.Bind("General", "Log recognized speech", true, "Shows the speech recognition output"); Speech.phrases = new List(); pluginDir = Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location); Environment.SetEnvironmentVariable("PATH", pluginDir + ";" + Environment.GetEnvironmentVariable("PATH")); string[] libraries = _libraries; foreach (string text in libraries) { string text2 = Path.Combine(pluginDir, text + ".dll"); if (File.Exists(text2)) { LoadLibrary(text2); } else { mls.LogError((object)(text + ".dll not found at: " + text2)); } } harmony.PatchAll(typeof(GameNetworkManagerPatch)); harmony.PatchAll(typeof(StartOfRoundPatch)); } } public class Speech { internal static List phrases; public static string bestMatch; private static double bestScore; internal static float GetSimilarity(string phrase, string recognized) { if (string.IsNullOrEmpty(phrase) || string.IsNullOrEmpty(recognized)) { return 0f; } int num = Math.Max(phrase.Length, recognized.Length); if (num == 0) { return 1f; } int num2 = LevenshteinDistance(phrase, recognized); return (float)Math.Round(1.0 - (double)num2 / (double)num, 2); } internal static void GetBestMatch(string recognized) { float num = float.MinValue; foreach (string phrase in phrases) { float similarity = GetSimilarity(phrase, recognized); if (similarity > num) { num = similarity; bestMatch = phrase; } } bestScore = num; if (Plugin.logging.Value) { Plugin.mls.LogDebug((object)("Best match: " + bestMatch)); Plugin.mls.LogDebug((object)$"Best similarity score: {bestScore}"); } } public static bool IsAboveThreshold(string[] phrases, double similarityThreshold) { return phrases.Contains(bestMatch) && bestScore >= similarityThreshold; } public static void RegisterPhrases(string[] phrases) { Speech.phrases.AddRange(phrases); } public static EventHandler RegisterCustomHandler(EventHandler callback) { Engine.SpeechRecognized += callback; return callback; } private static int LevenshteinDistance(string s1, string s2) { s1 = s1.ToLower(); s2 = s2.ToLower(); int[] array = new int[s2.Length + 1]; int[] array2 = new int[s2.Length + 1]; for (int i = 0; i <= s2.Length; i++) { array[i] = i; } for (int j = 1; j <= s1.Length; j++) { array2[0] = j; for (int k = 1; k <= s2.Length; k++) { int num = ((s1[j - 1] != s2[k - 1]) ? 1 : 0); array2[k] = Math.Min(Math.Min(array[k] + 1, array2[k - 1] + 1), array[k - 1] + num); } int[] array3 = array2; array2 = array; array = array3; } return array[s2.Length]; } } public class SpeechEventArgs : EventArgs { public string Text { get; } public SpeechEventArgs(string text) { Text = text; } } internal class VoskResult { public string text; } } namespace SpeechRecognitionAPI.Patches { [HarmonyPatch(typeof(GameNetworkManager))] internal class GameNetworkManagerPatch { [HarmonyPatch("Start")] [HarmonyPostfix] private static void StartModel() { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown if (Plugin.pluginDir == null) { Debug.LogError((object)"[SpeechRecognitionAPI] Plugin did not initialize correctly, skipping engine start."); return; } GameObject val = new GameObject("SpeechRecognitionAPIEngine"); Plugin.SpeechEngine = val.AddComponent(); Plugin.SpeechEngine.StartEngine(); } [HarmonyPatch("Disconnect")] [HarmonyPostfix] private static void StopMicCapture() { Plugin.SpeechEngine.StopMicCapture(); } } [HarmonyPatch(typeof(StartOfRound))] public class StartOfRoundPatch { [HarmonyPatch("Start")] [HarmonyPostfix] private static void StartMicCapture() { Plugin.SpeechEngine.StartMicCapture(); } } }