Decompiled source of MultisyllableCurse v1.4.5

MultisyllableCurse/MultisyllableCurse.dll

Decompiled a week ago
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
using Photon.Voice.Unity;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: AssemblyTitle("MultisyllableCurse")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Org")]
[assembly: AssemblyProduct("MultisyllableCurse")]
[assembly: AssemblyCopyright("Copyright © Org 2026")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("e1cf5710-aeff-4084-9894-a4eeac8fb5ef")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace MultisyllableCurse;

[BepInPlugin("twin.multisyllablecurse", "Multisyllable Curse", "1.4.5")]
public class Plugin : BaseUnityPlugin
{
	private class LocalVoiceSession
	{
		public IntPtr Recognizer;

		public int InputSampleRate;

		public int InputChannels;

		public volatile bool Removed;

		public bool IsSpeaking;

		public int SilenceFrames;

		public object AudioSource;

		public EventInfo AudioEvent;

		public Delegate AudioCallback;

		public readonly Queue<short[]> PreRoll = new Queue<short[]>();

		public readonly List<short> AudioBuffer = new List<short>();

		public LocalVoiceSession(IntPtr recognizer, int sampleRate, int channels)
		{
			Recognizer = recognizer;
			InputSampleRate = sampleRate;
			InputChannels = channels;
		}
	}

	private class AudioPacket
	{
		public LocalVoiceSession Session;

		public float[] Samples;

		public int Channels;

		public bool Ended;

		public AudioPacket(LocalVoiceSession session, float[] samples, int channels, bool ended)
		{
			Session = session;
			Samples = samples;
			Channels = channels;
			Ended = ended;
		}
	}

	internal static ManualLogSource Log;

	internal static Plugin Instance;

	private static IntPtr VoskModel = IntPtr.Zero;

	private const int TargetSampleRate = 16000;

	private const float SpeechThreshold = 0.02f;

	private const int PreRollFrames = 5;

	private const int SilenceFramesThreshold = 25;

	private const int MinimumSpeechSamples = 16000;

	private readonly ConcurrentQueue<AudioPacket> audioQueue = new ConcurrentQueue<AudioPacket>();

	private LocalVoiceSession localSession;

	private Harmony harmony;

	private bool micHookLogged;

	private volatile bool gameplayEnabled;

	private void Awake()
	{
		//IL_0018: Unknown result type (might be due to invalid IL or missing references)
		//IL_0022: Expected O, but got Unknown
		Log = ((BaseUnityPlugin)this).Logger;
		Instance = this;
		harmony = new Harmony("twin.multisyllablecurse");
		harmony.PatchAll();
		Log.LogInfo((object)"Multisyllable Curse v1.4.5 loaded!");
		try
		{
			string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
			string text = Path.Combine(directoryName, "libvosk.dll");
			string text2 = Path.Combine(directoryName, "model", "vosk-model-small-en-us-0.15");
			string[] array = new string[4] { "libvosk.dll", "libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll" };
			string[] array2 = array;
			foreach (string text3 in array2)
			{
				string text4 = Path.Combine(directoryName, text3);
				if (!File.Exists(text4))
				{
					throw new FileNotFoundException("Required Vosk file missing: " + text3, text4);
				}
			}
			if (!Directory.Exists(text2))
			{
				throw new DirectoryNotFoundException("Vosk model folder not found: " + text2);
			}
			if (!SetDllDirectory(directoryName))
			{
				throw new Exception("Could not set native DLL directory.");
			}
			Log.LogInfo((object)("Loading Vosk from: " + text));
			VoskNative.Load(text);
			Log.LogInfo((object)"Vosk native library loaded!");
			VoskNative.vosk_set_log_level(-1);
			VoskModel = VoskNative.vosk_model_new(text2);
			if (VoskModel == IntPtr.Zero)
			{
				throw new Exception("Vosk model failed to load!");
			}
			Log.LogInfo((object)"Vosk model loaded successfully!");
			Log.LogInfo((object)("Vosk loaded from: " + directoryName));
		}
		catch (Exception ex)
		{
			Log.LogError((object)("Vosk initialization failed: " + ex));
		}
	}

	private bool IsGameplayAllowed()
	{
		try
		{
			return SemiFunc.RunIsLevel();
		}
		catch (Exception ex)
		{
			Log.LogError((object)("Could not check current game area: " + ex));
			return false;
		}
	}

	private void ResetAudioState()
	{
		AudioPacket result;
		while (audioQueue.TryDequeue(out result))
		{
		}
		LocalVoiceSession localVoiceSession = localSession;
		if (localVoiceSession != null)
		{
			localVoiceSession.IsSpeaking = false;
			localVoiceSession.SilenceFrames = 0;
			localVoiceSession.PreRoll.Clear();
			localVoiceSession.AudioBuffer.Clear();
		}
	}

	private void UpdateGameplayState()
	{
		bool flag = IsGameplayAllowed();
		if (flag != gameplayEnabled)
		{
			gameplayEnabled = flag;
			ResetAudioState();
			if (flag)
			{
				Log.LogInfo((object)"Speech detection enabled: normal level.");
			}
			else
			{
				Log.LogInfo((object)"Speech detection paused: outside normal gameplay.");
			}
		}
	}

	internal void RegisterLocalRecorder(GameObject recorderObject)
	{
		if ((Object)(object)recorderObject == (Object)null || VoskModel == IntPtr.Zero)
		{
			return;
		}
		try
		{
			LocalRecorderListener localRecorderListener = recorderObject.GetComponent<LocalRecorderListener>();
			if ((Object)(object)localRecorderListener == (Object)null)
			{
				localRecorderListener = recorderObject.AddComponent<LocalRecorderListener>();
			}
			localRecorderListener.Initialize(this);
		}
		catch (Exception ex)
		{
			Log.LogError((object)("Could not register local recorder: " + ex));
		}
	}

	internal void RegisterMicrophone(object payload)
	{
		if (payload == null || VoskModel == IntPtr.Zero)
		{
			return;
		}
		try
		{
			PropertyInfo property = payload.GetType().GetProperty("AudioDesc");
			if (property == null)
			{
				Log.LogWarning((object)"PhotonVoiceCreated had no AudioDesc property.");
				return;
			}
			object value = property.GetValue(payload, null);
			if (value == null)
			{
				Log.LogWarning((object)"Photon Voice supplied a null audio source.");
				return;
			}
			Type type = value.GetType();
			PropertyInfo property2 = type.GetProperty("SamplingRate");
			PropertyInfo property3 = type.GetProperty("Channels");
			if (property2 == null || property3 == null)
			{
				Log.LogWarning((object)("Audio source has no SamplingRate or Channels property: " + type.FullName));
				return;
			}
			int num = Convert.ToInt32(property2.GetValue(value, null));
			int num2 = Convert.ToInt32(property3.GetValue(value, null));
			if (num <= 0 || num2 <= 0)
			{
				Log.LogWarning((object)("Invalid microphone format. Rate: " + num + ", channels: " + num2));
				return;
			}
			if (value is MicWrapper)
			{
				RegisterMicWrapper(value, num, num2);
				return;
			}
			EventInfo eventInfo = type.GetEvent("OnAudioFrame", BindingFlags.Instance | BindingFlags.Public);
			if (eventInfo == null)
			{
				Log.LogWarning((object)("Unsupported microphone source type: " + type.FullName));
				return;
			}
			RemoveLocalSession();
			IntPtr intPtr = CreateRecognizer();
			if (intPtr == IntPtr.Zero)
			{
				throw new Exception("Could not create local recognizer.");
			}
			LocalVoiceSession session = new LocalVoiceSession(intPtr, num, num2);
			Action<float[], int> action = delegate(float[] samples, int frameChannels)
			{
				if (!gameplayEnabled || session.Removed || samples == null || samples.Length == 0)
				{
					return;
				}
				try
				{
					float[] array = new float[samples.Length];
					Array.Copy(samples, array, samples.Length);
					audioQueue.Enqueue(new AudioPacket(session, array, frameChannels, ended: false));
				}
				catch
				{
				}
			};
			eventInfo.AddEventHandler(value, action);
			session.AudioSource = value;
			session.AudioEvent = eventInfo;
			session.AudioCallback = action;
			localSession = session;
			Log.LogInfo((object)("Local microphone connected through audio event! Sample rate: " + num));
		}
		catch (Exception ex)
		{
			Log.LogError((object)("Microphone registration failed: " + ex));
		}
	}

	private void RegisterMicWrapper(object audioSource, int sampleRate, int channels)
	{
		RemoveLocalSession();
		IntPtr intPtr = CreateRecognizer();
		if (intPtr == IntPtr.Zero)
		{
			throw new Exception("Could not create MicWrapper recognizer.");
		}
		LocalVoiceSession localVoiceSession = new LocalVoiceSession(intPtr, sampleRate, channels);
		localVoiceSession.AudioSource = audioSource;
		localSession = localVoiceSession;
		Log.LogInfo((object)("MicWrapper detected! Read() capture registered. Sample rate: " + sampleRate + ", channels: " + channels));
	}

	internal void OnMicWrapperRead(object source, float[] samples, bool readSucceeded)
	{
		if (!gameplayEnabled || !readSucceeded || source == null || samples == null || samples.Length == 0)
		{
			return;
		}
		try
		{
			LocalVoiceSession localVoiceSession = localSession;
			if (localVoiceSession != null && !localVoiceSession.Removed && localVoiceSession.AudioSource == source)
			{
				float[] array = new float[samples.Length];
				Array.Copy(samples, array, samples.Length);
				audioQueue.Enqueue(new AudioPacket(localVoiceSession, array, localVoiceSession.InputChannels, ended: false));
				if (!micHookLogged)
				{
					Log.LogInfo((object)"MicWrapper.Read() is successfully supplying audio samples!");
					micHookLogged = true;
				}
			}
		}
		catch
		{
		}
	}

	private IntPtr CreateRecognizer()
	{
		return VoskNative.vosk_recognizer_new(VoskModel, 16000f);
	}

	private short[] ConvertToPcm16(float[] input, int sampleRate, int channels)
	{
		if (channels <= 0 || input.Length < channels)
		{
			return new short[0];
		}
		int num = input.Length / channels;
		if (num == 0)
		{
			return new short[0];
		}
		float[] array = new float[num];
		for (int i = 0; i < num; i++)
		{
			float num2 = 0f;
			for (int j = 0; j < channels; j++)
			{
				num2 += input[i * channels + j];
			}
			array[i] = num2 / (float)channels;
		}
		int num3 = Math.Max(1, (int)((long)num * 16000L / sampleRate));
		short[] array2 = new short[num3];
		for (int k = 0; k < num3; k++)
		{
			double num4 = (double)k * (double)sampleRate / 16000.0;
			int num5 = (int)num4;
			int num6 = Math.Min(num5 + 1, num - 1);
			float num7 = (float)(num4 - (double)num5);
			float val = array[num5] + (array[num6] - array[num5]) * num7;
			val = Math.Max(-1f, Math.Min(1f, val));
			array2[k] = (short)(val * 32767f);
		}
		return array2;
	}

	private float CalculateRms(float[] samples)
	{
		if (samples.Length == 0)
		{
			return 0f;
		}
		double num = 0.0;
		foreach (float num2 in samples)
		{
			num += (double)(num2 * num2);
		}
		return (float)Math.Sqrt(num / (double)samples.Length);
	}

	private void ProcessAudioFrame(LocalVoiceSession session, float[] samples, int channels)
	{
		if (!gameplayEnabled || session.Removed || session.Recognizer == IntPtr.Zero)
		{
			return;
		}
		float num = CalculateRms(samples);
		short[] array = ConvertToPcm16(samples, session.InputSampleRate, channels);
		if (array.Length == 0)
		{
			return;
		}
		if (!session.IsSpeaking)
		{
			if (num > 0.02f)
			{
				session.IsSpeaking = true;
				while (session.PreRoll.Count > 0)
				{
					session.AudioBuffer.AddRange(session.PreRoll.Dequeue());
				}
				session.AudioBuffer.AddRange(array);
				session.SilenceFrames = 0;
			}
			else
			{
				short[] array2 = new short[array.Length];
				Array.Copy(array, array2, array.Length);
				session.PreRoll.Enqueue(array2);
				while (session.PreRoll.Count > 5)
				{
					session.PreRoll.Dequeue();
				}
			}
			return;
		}
		session.AudioBuffer.AddRange(array);
		if (num > 0.02f)
		{
			session.SilenceFrames = 0;
			return;
		}
		session.SilenceFrames++;
		if (session.SilenceFrames > 25)
		{
			FinishSpeech(session);
		}
	}

	private void FinishSpeech(LocalVoiceSession session)
	{
		if (gameplayEnabled && session.IsSpeaking)
		{
			session.IsSpeaking = false;
			session.SilenceFrames = 0;
			session.PreRoll.Clear();
			if (session.AudioBuffer.Count >= 16000)
			{
				ProcessSpeech(session, session.AudioBuffer.ToArray());
			}
			session.AudioBuffer.Clear();
		}
	}

	private void ProcessSpeech(LocalVoiceSession session, short[] audioData)
	{
		if (!gameplayEnabled || VoskModel == IntPtr.Zero || session.Removed)
		{
			return;
		}
		try
		{
			byte[] array = new byte[audioData.Length * 2];
			Buffer.BlockCopy(audioData, 0, array, 0, array.Length);
			IntPtr ptr = ((VoskNative.vosk_recognizer_accept_waveform(session.Recognizer, array, array.Length) != 0) ? VoskNative.vosk_recognizer_result(session.Recognizer) : VoskNative.vosk_recognizer_final_result(session.Recognizer));
			string json = Marshal.PtrToStringAnsi(ptr);
			LogRecognizedText(json);
		}
		catch (Exception ex)
		{
			Log.LogError((object)("Speech processing error: " + ex));
		}
	}

	private void LogRecognizedText(string json)
	{
		if (gameplayEnabled && !string.IsNullOrEmpty(json))
		{
			Log.LogInfo((object)("Local recognition: " + json));
			string text = ExtractRecognizedText(json);
			if (!string.IsNullOrWhiteSpace(text))
			{
				DetectMultisyllableWords(text);
			}
		}
	}

	private string ExtractRecognizedText(string json)
	{
		Match match = Regex.Match(json, "\"text\"\\s*:\\s*\"((?:\\\\.|[^\"\\\\])*)\"");
		if (!match.Success)
		{
			return "";
		}
		return DecodeJsonString(match.Groups[1].Value);
	}

	private string DecodeJsonString(string value)
	{
		StringBuilder stringBuilder = new StringBuilder();
		for (int i = 0; i < value.Length; i++)
		{
			char c = value[i];
			if (c != '\\' || i + 1 >= value.Length)
			{
				stringBuilder.Append(c);
				continue;
			}
			i++;
			char c2 = value[i];
			switch (c2)
			{
			case '"':
				stringBuilder.Append('"');
				break;
			case '\\':
				stringBuilder.Append('\\');
				break;
			case '/':
				stringBuilder.Append('/');
				break;
			case 'b':
				stringBuilder.Append('\b');
				break;
			case 'f':
				stringBuilder.Append('\f');
				break;
			case 'n':
				stringBuilder.Append('\n');
				break;
			case 'r':
				stringBuilder.Append('\r');
				break;
			case 't':
				stringBuilder.Append('\t');
				break;
			case 'u':
			{
				if (i + 4 < value.Length && int.TryParse(value.Substring(i + 1, 4), NumberStyles.HexNumber, null, out var result))
				{
					stringBuilder.Append((char)result);
					i += 4;
				}
				break;
			}
			default:
				stringBuilder.Append(c2);
				break;
			}
		}
		return stringBuilder.ToString();
	}

	private void DetectMultisyllableWords(string text)
	{
		if (!gameplayEnabled)
		{
			return;
		}
		MatchCollection matchCollection = Regex.Matches(text.ToLowerInvariant(), "[a-z]+(?:'[a-z]+)?");
		foreach (Match item in matchCollection)
		{
			if (!gameplayEnabled)
			{
				break;
			}
			string value = item.Value;
			int num = CountSyllables(value);
			if (num > 1)
			{
				Log.LogWarning((object)("[MULTISYLLABLE DETECTED] You said \"" + value + "\" (" + num + " estimated syllables)"));
				ApplyInstantDeath(value);
				break;
			}
		}
	}

	private void ApplyInstantDeath(string word)
	{
		if (!gameplayEnabled)
		{
			return;
		}
		try
		{
			if ((Object)(object)GameDirector.instance == (Object)null)
			{
				return;
			}
			foreach (PlayerAvatar player in GameDirector.instance.PlayerList)
			{
				if ((Object)(object)player == (Object)null || (Object)(object)player.photonView == (Object)null || !player.photonView.IsMine)
				{
					continue;
				}
				if (!((Object)(object)player.playerHealth == (Object)null))
				{
					Log.LogWarning((object)("[PUNISHMENT] Killing your character for saying \"" + word + "\"!"));
					player.PlayerDeath(-1);
				}
				return;
			}
			Log.LogWarning((object)"Could not find your local PlayerAvatar.");
		}
		catch (Exception ex)
		{
			Log.LogError((object)("Instant death failed: " + ex));
		}
	}

	private int CountSyllables(string word)
	{
		if (string.IsNullOrEmpty(word))
		{
			return 0;
		}
		word = word.ToLowerInvariant().Replace("'", "");
		if (word.Length <= 2)
		{
			return 1;
		}
		int num = 0;
		bool flag = false;
		for (int i = 0; i < word.Length; i++)
		{
			bool flag2 = IsVowel(word[i]);
			if (flag2 && !flag)
			{
				num++;
			}
			flag = flag2;
		}
		if (word.EndsWith("e") && !word.EndsWith("le") && num > 1)
		{
			num--;
		}
		if (word.EndsWith("le") && word.Length > 2 && !IsVowel(word[word.Length - 3]))
		{
			num++;
		}
		if (word.EndsWith("ed") && word.Length > 2 && word[word.Length - 3] != 't' && word[word.Length - 3] != 'd' && num > 1)
		{
			num--;
		}
		return Math.Max(1, num);
	}

	private bool IsVowel(char c)
	{
		return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' || c == 'y';
	}

	private void Update()
	{
		UpdateGameplayState();
		if (!gameplayEnabled)
		{
			AudioPacket result;
			while (audioQueue.TryDequeue(out result))
			{
			}
			return;
		}
		int num = 0;
		AudioPacket result2;
		while (num < 100 && audioQueue.TryDequeue(out result2))
		{
			num++;
			if (!gameplayEnabled)
			{
				ResetAudioState();
				break;
			}
			if (result2.Session == null || result2.Session.Removed)
			{
				continue;
			}
			try
			{
				if (result2.Ended)
				{
					FinishSpeech(result2.Session);
				}
				else
				{
					ProcessAudioFrame(result2.Session, result2.Samples, result2.Channels);
				}
			}
			catch (Exception ex)
			{
				Log.LogError((object)("Audio processing error: " + ex));
			}
		}
	}

	internal void RemoveLocalSession()
	{
		if (localSession == null)
		{
			return;
		}
		LocalVoiceSession localVoiceSession = localSession;
		localSession = null;
		localVoiceSession.Removed = true;
		try
		{
			if (localVoiceSession.AudioSource != null && localVoiceSession.AudioEvent != null && (object)localVoiceSession.AudioCallback != null)
			{
				localVoiceSession.AudioEvent.RemoveEventHandler(localVoiceSession.AudioSource, localVoiceSession.AudioCallback);
			}
		}
		catch (Exception ex)
		{
			Log.LogWarning((object)("Could not detach microphone callback: " + ex));
		}
		if (localVoiceSession.Recognizer != IntPtr.Zero)
		{
			VoskNative.vosk_recognizer_free(localVoiceSession.Recognizer);
			localVoiceSession.Recognizer = IntPtr.Zero;
		}
	}

	private void OnDestroy()
	{
		gameplayEnabled = false;
		RemoveLocalSession();
		AudioPacket result;
		while (audioQueue.TryDequeue(out result))
		{
		}
		if (harmony != null)
		{
			harmony.UnpatchSelf();
		}
		if (VoskModel != IntPtr.Zero)
		{
			VoskNative.vosk_model_free(VoskModel);
			VoskModel = IntPtr.Zero;
		}
		VoskNative.Unload();
		Instance = null;
	}

	[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
	private static extern bool SetDllDirectory(string path);
}
public class LocalRecorderListener : MonoBehaviour
{
	private Plugin plugin;

	internal void Initialize(Plugin owner)
	{
		plugin = owner;
	}

	public void PhotonVoiceCreated(object payload)
	{
		if ((Object)(object)plugin != (Object)null)
		{
			plugin.RegisterMicrophone(payload);
		}
	}

	private void OnDestroy()
	{
		if ((Object)(object)plugin != (Object)null)
		{
			plugin.RemoveLocalSession();
		}
	}
}
[HarmonyPatch]
public static class LocalRecorderPatch
{
	private static MethodBase TargetMethod()
	{
		return AccessTools.Method(typeof(Recorder), "SendPhotonVoiceCreatedMessage", (Type[])null, (Type[])null);
	}

	private static void Prefix(Recorder __instance)
	{
		if ((Object)(object)Plugin.Instance != (Object)null && (Object)(object)__instance != (Object)null)
		{
			Plugin.Instance.RegisterLocalRecorder(((Component)__instance).gameObject);
		}
	}
}
[HarmonyPatch]
public static class MicWrapperReadPatch
{
	private static MethodBase TargetMethod()
	{
		return AccessTools.Method(typeof(MicWrapper), "Read", new Type[1] { typeof(float[]) }, (Type[])null);
	}

	private static void Postfix(MicWrapper __instance, float[] __0, bool __result)
	{
		if (!((Object)(object)Plugin.Instance == (Object)null))
		{
			Plugin.Instance.OnMicWrapperRead(__instance, __0, __result);
		}
	}
}
internal static class VoskNative
{
	[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
	private delegate void SetLogLevelDelegate(int level);

	[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
	private delegate IntPtr ModelNewDelegate([MarshalAs(UnmanagedType.LPStr)] string modelPath);

	[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
	private delegate void ModelFreeDelegate(IntPtr model);

	[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
	private delegate IntPtr RecognizerNewDelegate(IntPtr model, float sampleRate);

	[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
	private delegate void RecognizerFreeDelegate(IntPtr recognizer);

	[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
	private delegate int AcceptWaveformDelegate(IntPtr recognizer, [In] byte[] data, int length);

	[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
	private delegate IntPtr RecognizerResultDelegate(IntPtr recognizer);

	private static IntPtr libraryHandle = IntPtr.Zero;

	private static SetLogLevelDelegate setLogLevel;

	private static ModelNewDelegate modelNew;

	private static ModelFreeDelegate modelFree;

	private static RecognizerNewDelegate recognizerNew;

	private static RecognizerFreeDelegate recognizerFree;

	private static AcceptWaveformDelegate acceptWaveform;

	private static RecognizerResultDelegate recognizerResult;

	private static RecognizerResultDelegate recognizerFinalResult;

	[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
	private static extern IntPtr LoadLibraryW(string fileName);

	[DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
	private static extern IntPtr GetProcAddress(IntPtr module, string procedureName);

	[DllImport("kernel32.dll", SetLastError = true)]
	private static extern bool FreeLibrary(IntPtr module);

	internal static void Load(string fullPath)
	{
		if (libraryHandle != IntPtr.Zero)
		{
			throw new InvalidOperationException("Vosk is already loaded.");
		}
		libraryHandle = LoadLibraryW(fullPath);
		if (libraryHandle == IntPtr.Zero)
		{
			int lastWin32Error = Marshal.GetLastWin32Error();
			throw new Exception("Windows could not load libvosk.dll. Error " + lastWin32Error + ": " + new Win32Exception(lastWin32Error).Message);
		}
		try
		{
			setLogLevel = GetFunction<SetLogLevelDelegate>("vosk_set_log_level");
			modelNew = GetFunction<ModelNewDelegate>("vosk_model_new");
			modelFree = GetFunction<ModelFreeDelegate>("vosk_model_free");
			recognizerNew = GetFunction<RecognizerNewDelegate>("vosk_recognizer_new");
			recognizerFree = GetFunction<RecognizerFreeDelegate>("vosk_recognizer_free");
			acceptWaveform = GetFunction<AcceptWaveformDelegate>("vosk_recognizer_accept_waveform");
			recognizerResult = GetFunction<RecognizerResultDelegate>("vosk_recognizer_result");
			recognizerFinalResult = GetFunction<RecognizerResultDelegate>("vosk_recognizer_final_result");
		}
		catch
		{
			Unload();
			throw;
		}
	}

	private static T GetFunction<T>(string name) where T : class
	{
		IntPtr procAddress = GetProcAddress(libraryHandle, name);
		if (procAddress == IntPtr.Zero)
		{
			throw new Exception("Could not find Vosk function " + name + ". Windows error: " + Marshal.GetLastWin32Error());
		}
		return Marshal.GetDelegateForFunctionPointer(procAddress, typeof(T)) as T;
	}

	internal static void vosk_set_log_level(int level)
	{
		EnsureLoaded();
		setLogLevel(level);
	}

	internal static IntPtr vosk_model_new(string modelPath)
	{
		EnsureLoaded();
		return modelNew(modelPath);
	}

	internal static void vosk_model_free(IntPtr model)
	{
		EnsureLoaded();
		modelFree(model);
	}

	internal static IntPtr vosk_recognizer_new(IntPtr model, float sampleRate)
	{
		EnsureLoaded();
		return recognizerNew(model, sampleRate);
	}

	internal static void vosk_recognizer_free(IntPtr recognizer)
	{
		EnsureLoaded();
		recognizerFree(recognizer);
	}

	internal static int vosk_recognizer_accept_waveform(IntPtr recognizer, byte[] data, int length)
	{
		EnsureLoaded();
		return acceptWaveform(recognizer, data, length);
	}

	internal static IntPtr vosk_recognizer_result(IntPtr recognizer)
	{
		EnsureLoaded();
		return recognizerResult(recognizer);
	}

	internal static IntPtr vosk_recognizer_final_result(IntPtr recognizer)
	{
		EnsureLoaded();
		return recognizerFinalResult(recognizer);
	}

	private static void EnsureLoaded()
	{
		if (libraryHandle == IntPtr.Zero)
		{
			throw new InvalidOperationException("Vosk native library is not loaded.");
		}
	}

	internal static void Unload()
	{
		if (!(libraryHandle == IntPtr.Zero))
		{
			setLogLevel = null;
			modelNew = null;
			modelFree = null;
			recognizerNew = null;
			recognizerFree = null;
			acceptWaveform = null;
			recognizerResult = null;
			recognizerFinalResult = null;
			FreeLibrary(libraryHandle);
			libraryHandle = IntPtr.Zero;
		}
	}
}