Decompiled source of REPOSoundBoard v0.1.1

REPOSoundBoard.dll

Decompiled a week ago
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.Versioning;
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Photon.Pun;
using Photon.Voice.Unity;
using REPOSoundBoard.Config;
using REPOSoundBoard.Hotkeys;
using REPOSoundBoard.Patches;
using REPOSoundBoard.Sound;
using UnityEngine;
using UnityEngine.Networking;

[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("REPOSoundBoard")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+d5cf5ba6d1d300a00d41a7ff3192bca516660c18")]
[assembly: AssemblyProduct("REPOSoundBoard")]
[assembly: AssemblyTitle("REPOSoundBoard")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace REPOSoundBoard
{
	[BepInPlugin("com.moli.repo-soundboard", "REPOSoundBoard", "0.1.1")]
	public class REPOSoundBoard : BaseUnityPlugin
	{
		private const string GUID = "com.moli.repo-soundboard";

		private const string NAME = "REPOSoundBoard";

		private const string VERSION = "0.1.1";

		private static Harmony _harmony = new Harmony("com.moli.repo-soundboard");

		public HotkeyManager HotkeyManager;

		public SoundBoard SoundBoard;

		public static REPOSoundBoard Instance { get; private set; }

		public ManualLogSource LOG => ((BaseUnityPlugin)this).Logger;

		public AppConfig Config { get; private set; }

		private void Awake()
		{
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Expected O, but got Unknown
			if (!((Object)(object)Instance != (Object)null))
			{
				Instance = this;
				Config = AppConfig.LoadConfig();
				GameObject val = new GameObject("REPOSoundBoardMod");
				Object.DontDestroyOnLoad((Object)(object)val);
				((Object)val).hideFlags = (HideFlags)61;
				HotkeyManager = val.AddComponent<HotkeyManager>();
				SoundBoard = val.AddComponent<SoundBoard>();
				SoundBoard.LoadConfig(Config.SoundBoard);
				_harmony.PatchAll(typeof(PlayerVoiceChatPatch));
			}
		}
	}
}
namespace REPOSoundBoard.Sound
{
	public static class AudioExtractor
	{
		private static bool IsFfmpegInstalled()
		{
			try
			{
				using Process process = Process.Start(new ProcessStartInfo
				{
					FileName = "ffmpeg",
					Arguments = "-version",
					RedirectStandardOutput = true,
					RedirectStandardError = true,
					UseShellExecute = false,
					CreateNoWindow = true
				});
				if (process == null)
				{
					return false;
				}
				process.WaitForExit(2000);
				return process.ExitCode == 0 || process.ExitCode == 1;
			}
			catch
			{
				return false;
			}
		}

		public static bool ExtractAudioFromVideo(string videoPath, string outputAudioPath)
		{
			if (!IsFfmpegInstalled())
			{
				REPOSoundBoard.Instance.LOG.LogError((object)"Cannot extract audio from video: ffmpeg is not installed. You can download ffmpeg from https://www.ffmpeg.org/download.html");
				return false;
			}
			ProcessStartInfo processStartInfo = new ProcessStartInfo();
			processStartInfo.FileName = "ffmpeg";
			processStartInfo.Arguments = "-i \"" + videoPath + "\" -vn -acodec pcm_s16le -ar 48000 -ac 1 \"" + outputAudioPath + "\"";
			processStartInfo.UseShellExecute = false;
			processStartInfo.CreateNoWindow = true;
			processStartInfo.RedirectStandardOutput = true;
			processStartInfo.RedirectStandardError = true;
			ProcessStartInfo startInfo = processStartInfo;
			try
			{
				using Process process = Process.Start(startInfo);
				process.WaitForExit();
				return process.ExitCode == 0;
			}
			catch (Exception ex)
			{
				REPOSoundBoard.Instance.LOG.LogError((object)("Failed to extract audio with ffmpeg: " + ex.Message));
				return false;
			}
		}
	}
	public class MediaClip
	{
		[CompilerGenerated]
		private sealed class <Load>d__18 : IEnumerator<object>, IEnumerator, IDisposable
		{
			private int <>1__state;

			private object <>2__current;

			public MediaClip <>4__this;

			object IEnumerator<object>.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			object IEnumerator.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			[DebuggerHidden]
			public <Load>d__18(int <>1__state)
			{
				this.<>1__state = <>1__state;
			}

			[DebuggerHidden]
			void IDisposable.Dispose()
			{
				<>1__state = -2;
			}

			private bool MoveNext()
			{
				int num = <>1__state;
				MediaClip mediaClip = <>4__this;
				switch (num)
				{
				default:
					return false;
				case 0:
					<>1__state = -1;
					if (mediaClip.IsVideoFile())
					{
						<>2__current = mediaClip.LoadVideo();
						<>1__state = 1;
						return true;
					}
					<>2__current = mediaClip.LoadAudio("file:///" + mediaClip._source);
					<>1__state = 2;
					return true;
				case 1:
					<>1__state = -1;
					break;
				case 2:
					<>1__state = -1;
					break;
				}
				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();
			}
		}

		[CompilerGenerated]
		private sealed class <LoadAudio>d__20 : IEnumerator<object>, IEnumerator, IDisposable
		{
			private int <>1__state;

			private object <>2__current;

			public string source;

			public MediaClip <>4__this;

			private UnityWebRequest <www>5__2;

			object IEnumerator<object>.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			object IEnumerator.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			[DebuggerHidden]
			public <LoadAudio>d__20(int <>1__state)
			{
				this.<>1__state = <>1__state;
			}

			[DebuggerHidden]
			void IDisposable.Dispose()
			{
				int num = <>1__state;
				if (num == -3 || num == 1)
				{
					try
					{
					}
					finally
					{
						<>m__Finally1();
					}
				}
				<www>5__2 = null;
				<>1__state = -2;
			}

			private bool MoveNext()
			{
				//IL_0029: Unknown result type (might be due to invalid IL or missing references)
				//IL_002e: Unknown result type (might be due to invalid IL or missing references)
				//IL_002f: 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_009f: Unknown result type (might be due to invalid IL or missing references)
				//IL_00a5: Invalid comparison between Unknown and I4
				bool result;
				try
				{
					int num = <>1__state;
					MediaClip mediaClip = <>4__this;
					switch (num)
					{
					default:
						result = false;
						break;
					case 0:
					{
						<>1__state = -1;
						AudioType audioTypeFromExtension = GetAudioTypeFromExtension(source);
						if ((int)audioTypeFromExtension == 0)
						{
							REPOSoundBoard.Instance.LOG.LogError((object)("Unsupported media format for file: " + mediaClip._source));
							result = false;
							break;
						}
						<www>5__2 = UnityWebRequestMultimedia.GetAudioClip(source, audioTypeFromExtension);
						<>1__state = -3;
						<>2__current = <www>5__2.SendWebRequest();
						<>1__state = 1;
						result = true;
						break;
					}
					case 1:
						<>1__state = -3;
						if ((int)<www>5__2.result == 1)
						{
							AudioClip content = DownloadHandlerAudioClip.GetContent(<www>5__2);
							((Object)content).name = Path.GetFileName(mediaClip._source);
							AudioClip val = SoundConverter.ConvertStereoToMono(content);
							if ((Object)(object)val == (Object)null)
							{
								mediaClip.IsLoaded = false;
								mediaClip.FailedToLoad = true;
								result = false;
								<>m__Finally1();
								break;
							}
							mediaClip.AudioClip = val;
							mediaClip.IsLoaded = true;
							mediaClip.FailedToLoad = false;
						}
						else
						{
							REPOSoundBoard.Instance.LOG.LogError((object)("Failed to load media: " + <www>5__2.error + ". Path: " + source));
							mediaClip.IsLoaded = false;
							mediaClip.FailedToLoad = true;
						}
						<>m__Finally1();
						<www>5__2 = null;
						result = false;
						break;
					}
				}
				catch
				{
					//try-fault
					((IDisposable)this).Dispose();
					throw;
				}
				return result;
			}

			bool IEnumerator.MoveNext()
			{
				//ILSpy generated this explicit interface implementation from .override directive in MoveNext
				return this.MoveNext();
			}

			private void <>m__Finally1()
			{
				<>1__state = -1;
				if (<www>5__2 != null)
				{
					((IDisposable)<www>5__2).Dispose();
				}
			}

			[DebuggerHidden]
			void IEnumerator.Reset()
			{
				throw new NotSupportedException();
			}
		}

		[CompilerGenerated]
		private sealed class <LoadVideo>d__19 : IEnumerator<object>, IEnumerator, IDisposable
		{
			private int <>1__state;

			private object <>2__current;

			public MediaClip <>4__this;

			object IEnumerator<object>.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			object IEnumerator.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			[DebuggerHidden]
			public <LoadVideo>d__19(int <>1__state)
			{
				this.<>1__state = <>1__state;
			}

			[DebuggerHidden]
			void IDisposable.Dispose()
			{
				<>1__state = -2;
			}

			private bool MoveNext()
			{
				int num = <>1__state;
				MediaClip mediaClip = <>4__this;
				switch (num)
				{
				default:
					return false;
				case 0:
					<>1__state = -1;
					if (!File.Exists(mediaClip._videoAudioPath))
					{
						REPOSoundBoard.Instance.LOG.LogInfo((object)("Found video. Extracting audio to " + mediaClip._videoAudioPath + "..."));
						if (!AudioExtractor.ExtractAudioFromVideo(mediaClip._source, mediaClip._videoAudioPath))
						{
							mediaClip.IsLoaded = false;
							mediaClip.FailedToLoad = true;
							return false;
						}
						REPOSoundBoard.Instance.LOG.LogInfo((object)"Audio successfully extracted");
					}
					<>2__current = mediaClip.LoadAudio("file:///" + mediaClip._videoAudioPath);
					<>1__state = 1;
					return true;
				case 1:
					<>1__state = -1;
					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 readonly string _source;

		private static readonly string[] VideoExtensions = new string[5] { ".mp4", ".webm", ".avi", ".mov", ".ogg" };

		private string _videoAudioPath;

		public bool IsLoaded { get; private set; }

		public bool FailedToLoad { get; private set; }

		public AudioClip AudioClip { get; private set; }

		public MediaClip(string source)
		{
			_source = source;
			IsLoaded = false;
			FailedToLoad = false;
			_videoAudioPath = Path.Combine(Path.GetDirectoryName(_source), "audio_" + Path.GetFileName(_source) + ".wav");
		}

		private static AudioType GetAudioTypeFromExtension(string file)
		{
			//IL_0037: 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_003c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			return (AudioType)(Path.GetExtension(file).ToLowerInvariant() switch
			{
				".wav" => 20, 
				".mp3" => 13, 
				".aiff" => 2, 
				_ => 0, 
			});
		}

		private bool IsVideoFile()
		{
			string value = Path.GetExtension(_source).ToLowerInvariant();
			return VideoExtensions.Contains(value);
		}

		[IteratorStateMachine(typeof(<Load>d__18))]
		public IEnumerator Load()
		{
			//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
			return new <Load>d__18(0)
			{
				<>4__this = this
			};
		}

		[IteratorStateMachine(typeof(<LoadVideo>d__19))]
		private IEnumerator LoadVideo()
		{
			//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
			return new <LoadVideo>d__19(0)
			{
				<>4__this = this
			};
		}

		[IteratorStateMachine(typeof(<LoadAudio>d__20))]
		private IEnumerator LoadAudio(string source)
		{
			//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
			return new <LoadAudio>d__20(0)
			{
				<>4__this = this,
				source = source
			};
		}
	}
	public class SoundBoard : MonoBehaviour
	{
		[CompilerGenerated]
		private sealed class <Play>d__11 : IEnumerator<object>, IEnumerator, IDisposable
		{
			private int <>1__state;

			private object <>2__current;

			public SoundBoard <>4__this;

			public SoundButton soundButton;

			object IEnumerator<object>.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			object IEnumerator.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			[DebuggerHidden]
			public <Play>d__11(int <>1__state)
			{
				this.<>1__state = <>1__state;
			}

			[DebuggerHidden]
			void IDisposable.Dispose()
			{
				<>1__state = -2;
			}

			private bool MoveNext()
			{
				//IL_0103: Unknown result type (might be due to invalid IL or missing references)
				//IL_010d: Expected O, but got Unknown
				int num = <>1__state;
				SoundBoard soundBoard = <>4__this;
				switch (num)
				{
				default:
					return false;
				case 0:
					<>1__state = -1;
					if ((Object)(object)soundBoard._recorder == (Object)null || (Object)(object)soundBoard._audioSource == (Object)null || !soundButton.Clip.IsLoaded)
					{
						return false;
					}
					if (soundBoard._isPlaying)
					{
						soundBoard.StopCurrent();
					}
					soundBoard._isPlaying = true;
					soundBoard._currentSoundButton = soundButton;
					soundBoard._recorder.TransmitEnabled = false;
					soundBoard._recorder.SourceType = (InputSourceType)1;
					soundBoard._recorder.AudioClip = soundButton.Clip.AudioClip;
					soundBoard._recorder.TransmitEnabled = true;
					soundBoard._audioSource.clip = soundButton.Clip.AudioClip;
					soundBoard._audioSource.volume = soundButton.Volume;
					soundBoard._audioSource.Play();
					<>2__current = (object)new WaitForSeconds(soundButton.Clip.AudioClip.length);
					<>1__state = 1;
					return true;
				case 1:
					<>1__state = -1;
					soundBoard.StopCurrent();
					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 List<SoundButton> _soundButtons = new List<SoundButton>();

		private Recorder _recorder;

		private AudioSource _audioSource;

		private bool _isPlaying;

		private SoundButton _currentSoundButton;

		private Coroutine _playCoroutine;

		public void LoadConfig(SoundBoardConfig config)
		{
			config.StopHotkey.OnPressed(StopCurrent);
			REPOSoundBoard.Instance.HotkeyManager.RegisterHotkey(config.StopHotkey);
			foreach (SoundBoardConfig.SoundButtonConfig soundButton2 in config.SoundButtons)
			{
				if (soundButton2.Hotkey != null && soundButton2.Path != null)
				{
					MediaClip mediaClip = new MediaClip(soundButton2.Path);
					((MonoBehaviour)this).StartCoroutine(mediaClip.Load());
					SoundButton soundButton = new SoundButton(mediaClip, soundButton2.Hotkey, soundButton2.Volume);
					AddSoundButton(soundButton);
				}
			}
		}

		public void AddSoundButton(SoundButton soundButton)
		{
			soundButton.Hotkey.OnPressed(delegate
			{
				_playCoroutine = ((MonoBehaviour)this).StartCoroutine(Play(soundButton));
			});
			REPOSoundBoard.Instance.HotkeyManager.RegisterHotkey(soundButton.Hotkey);
			_soundButtons.Add(soundButton);
		}

		public void RemoveSoundButton(SoundButton soundButton)
		{
			_soundButtons.Remove(soundButton);
			REPOSoundBoard.Instance.HotkeyManager.UnregisterHotkey(soundButton.Hotkey);
		}

		public void ChangeRecorder(Recorder recorder)
		{
			StopCurrent();
			_recorder = recorder;
		}

		public void ChangeAudioSource(AudioSource source)
		{
			StopCurrent();
			_audioSource = source;
		}

		[IteratorStateMachine(typeof(<Play>d__11))]
		private IEnumerator Play(SoundButton soundButton)
		{
			//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
			return new <Play>d__11(0)
			{
				<>4__this = this,
				soundButton = soundButton
			};
		}

		private void StopCurrent()
		{
			if (!((Object)(object)_recorder == (Object)null) && _isPlaying)
			{
				if (_playCoroutine != null)
				{
					((MonoBehaviour)this).StopCoroutine(_playCoroutine);
					_playCoroutine = null;
				}
				_recorder.SourceType = (InputSourceType)0;
				_recorder.AudioClip = null;
				_recorder.TransmitEnabled = true;
				if ((Object)(object)_audioSource != (Object)null)
				{
					_audioSource.Stop();
				}
				_isPlaying = false;
				_currentSoundButton = null;
			}
		}
	}
	public class SoundButton
	{
		public MediaClip Clip { get; private set; }

		public Hotkey Hotkey { get; private set; }

		public float Volume { get; private set; }

		public SoundButton(MediaClip clip, Hotkey hotkey, float volume)
		{
			Clip = clip;
			Hotkey = hotkey;
			Volume = volume;
		}
	}
	public class SoundConverter
	{
		public static AudioClip ConvertStereoToMono(AudioClip stereoClip)
		{
			if (stereoClip.channels == 1)
			{
				return stereoClip;
			}
			float[] array = new float[stereoClip.samples * stereoClip.channels];
			stereoClip.GetData(array, 0);
			int samples = stereoClip.samples;
			float[] array2 = new float[samples];
			for (int i = 0; i < samples; i++)
			{
				array2[i] = (array[i * 2] + array[i * 2 + 1]) * 0.5f;
			}
			try
			{
				AudioClip obj = AudioClip.Create(((Object)stereoClip).name + " (Mono)", samples, 1, stereoClip.frequency, false);
				obj.SetData(array2, 0);
				return obj;
			}
			catch (Exception ex)
			{
				REPOSoundBoard.Instance.LOG.LogWarning((object)("Failed to convert clip " + ((Object)stereoClip).name + " to mono. Error: " + ex.Message));
				return null;
			}
		}
	}
}
namespace REPOSoundBoard.Patches
{
	public class PlayerVoiceChatPatch
	{
		[HarmonyPatch(typeof(PlayerVoiceChat), "Start")]
		[HarmonyPostfix]
		public static void PostStart(PlayerVoiceChat __instance, ref Recorder ___recorder, ref PhotonView ___photonView)
		{
			if (___photonView.IsMine)
			{
				AudioSource source = ((Component)__instance).gameObject.AddComponent<AudioSource>();
				REPOSoundBoard.Instance.SoundBoard.ChangeRecorder(___recorder);
				REPOSoundBoard.Instance.SoundBoard.ChangeAudioSource(source);
			}
		}
	}
}
namespace REPOSoundBoard.Hotkeys
{
	public class Hotkey
	{
		[JsonProperty(ItemConverterType = typeof(StringEnumConverter))]
		public List<KeyCode> Keys { get; set; } = new List<KeyCode>();


		[CanBeNull]
		[JsonIgnore]
		private Action Callback { get; set; }

		[JsonIgnore]
		public bool IsPressed { get; set; }

		public Hotkey()
		{
		}

		public Hotkey(KeyCode key, [CanBeNull] Action callback)
		{
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			Keys.Add(key);
			Callback = callback;
		}

		public Hotkey(List<KeyCode> keys, [CanBeNull] Action callback)
		{
			Keys = keys;
		}

		public Hotkey OnPressed(Action callback)
		{
			Callback = callback;
			return this;
		}

		public void Trigger()
		{
			if (Callback != null)
			{
				Callback();
			}
		}
	}
	public class HotkeyManager : MonoBehaviour
	{
		private List<Hotkey> _hotkeys = new List<Hotkey>();

		public void RegisterHotkey(Hotkey hotkey)
		{
			_hotkeys.Add(hotkey);
		}

		public void UnregisterHotkey(Hotkey hotkey)
		{
			_hotkeys.Remove(hotkey);
		}

		public void Update()
		{
			foreach (Hotkey hotkey in _hotkeys)
			{
				if (hotkey.IsPressed)
				{
					HandlePressedHotkey(hotkey);
				}
				else
				{
					HandleReleasedHotkey(hotkey);
				}
			}
		}

		private static void HandlePressedHotkey(Hotkey hotkey)
		{
			if (hotkey.Keys.Any((KeyCode key) => !Input.GetKey(key)))
			{
				hotkey.IsPressed = false;
			}
		}

		private static void HandleReleasedHotkey(Hotkey hotkey)
		{
			if (hotkey.Keys.All((KeyCode key) => Input.GetKey(key)))
			{
				hotkey.IsPressed = true;
				hotkey.Trigger();
			}
		}
	}
}
namespace REPOSoundBoard.Config
{
	public class AppConfig
	{
		private static string ConfigFilePath;

		public SoundBoardConfig SoundBoard { get; set; }

		public AppConfig()
		{
			SoundBoard = new SoundBoardConfig();
		}

		public static AppConfig LoadConfig()
		{
			ConfigFilePath = Path.Combine(Paths.ConfigPath, "Moli.REPOSoundBoard.json");
			AppConfig result = new AppConfig();
			try
			{
				result = ConfigSerializer.DeserializeConfig(File.ReadAllText(ConfigFilePath));
				return result;
			}
			catch (Exception ex)
			{
				REPOSoundBoard.Instance.LOG.LogWarning((object)("Failed to read config file" + ex.Message));
				return result;
			}
		}

		public void SaveToFile()
		{
			string contents = ConfigSerializer.SerializeConfig(this);
			File.WriteAllText(ConfigFilePath, contents);
		}
	}
	public class ConfigSerializer
	{
		public static string SerializeConfig(AppConfig config)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0014: Expected O, but got Unknown
			JsonSerializerSettings val = new JsonSerializerSettings
			{
				Formatting = (Formatting)1,
				NullValueHandling = (NullValueHandling)1
			};
			return JsonConvert.SerializeObject((object)config, val);
		}

		public static AppConfig DeserializeConfig(string json)
		{
			return JsonConvert.DeserializeObject<AppConfig>(json);
		}
	}
	public class SoundBoardConfig
	{
		public class SoundButtonConfig
		{
			public string Path { get; set; }

			public float Volume { get; set; }

			public Hotkey Hotkey { get; set; }
		}

		public Hotkey StopHotkey { get; set; }

		public List<SoundButtonConfig> SoundButtons { get; set; }

		public SoundBoardConfig()
		{
			SoundButtons = new List<SoundButtonConfig>();
			StopHotkey = new Hotkey((KeyCode)104, null);
		}
	}
}