Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of RemoteVoiceSplit v0.1.0
RemoteVoiceSplit.dll
Decompiled 6 hours agousing System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Pipes; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using System.Threading; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Microsoft.Win32.SafeHandles; using RemoteVoiceSplit.Core; using RemoteVoiceSplit.Interop.Game; using RemoteVoiceSplit.Interop.ProcessAudio; 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: AssemblyCompany("aoirint")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Moves remote Lethal Company player voices to an OBS-capturable audio process.")] [assembly: AssemblyFileVersion("0.1.0.0")] [assembly: AssemblyInformationalVersion("0.1.0+405c323fff7319af684519e269fe3edb7308eeeb")] [assembly: AssemblyProduct("Remote Voice Split")] [assembly: AssemblyTitle("RemoteVoiceSplit")] [assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/aoirint/RemoteVoiceSplit")] [assembly: AssemblyVersion("0.1.0.0")] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [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] [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; } } } namespace RemoteVoiceSplit { [BepInPlugin("com.aoirint.remotevoicesplit", "Remote Voice Split", "0.1.0")] [BepInProcess("Lethal Company.exe")] public sealed class Plugin : BaseUnityPlugin { private void Awake() { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { ((BaseUnityPlugin)this).Logger.LogError((object)"Remote Voice Split requires Windows process audio capture and has been disabled on this platform."); return; } try { ConfigEntry<bool> enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Set to false to disable this mod. Changes made through BepInEx configuration APIs apply immediately."); ConfigEntry<bool> fallbackToGameOutput = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "FallbackToGameOutput", false, "Keep remote voices on the normal game output whenever separate process output cannot accept them. The default false value prevents remote voice from leaking into the game-audio recording track, but also makes remote voice inaudible until separate output recovers. Changes made through BepInEx configuration APIs apply immediately."); int num = AudioSettings.outputSampleRate; if (num <= 0) { num = 48000; ((BaseUnityPlugin)this).Logger.LogWarning((object)"Unity did not report an output sample rate; falling back to 48000 Hz for remote voice process output."); } string audioHostPath = Path.Combine(Path.GetDirectoryName(typeof(Plugin).Assembly.Location) ?? throw new InvalidOperationException("The plugin assembly directory is unavailable."), "RemoteVoiceSplit.AudioHost.exe"); if (PluginRuntime.Initialize(((BaseUnityPlugin)this).Logger, num, Process.GetCurrentProcess().Id, audioHostPath, enabled, fallbackToGameOutput)) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"Remote Voice Split 0.1.0 loaded."); } else { ((BaseUnityPlugin)this).Logger.LogInfo((object)"Remote Voice Split process-lifetime routing was already initialized."); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("Remote Voice Split could not initialize and was disabled: " + ex.GetType().Name + ": " + ex.Message)); } } } public static class MyPluginInfo { public const string PLUGIN_GUID = "com.aoirint.remotevoicesplit"; public const string PLUGIN_NAME = "Remote Voice Split"; public const string PLUGIN_VERSION = "0.1.0"; } } namespace RemoteVoiceSplit.Interop.ProcessAudio { internal static class DetachedAudioHostLauncher { private struct StartupInfo { public uint Size; public IntPtr Reserved; public IntPtr Desktop; public IntPtr Title; public uint X; public uint Y; public uint XSize; public uint YSize; public uint XCountChars; public uint YCountChars; public uint FillAttribute; public uint Flags; public ushort ShowWindow; public ushort ReservedSize; public IntPtr ReservedData; public IntPtr StandardInput; public IntPtr StandardOutput; public IntPtr StandardError; } private struct StartupInfoEx { public StartupInfo StartupInfo; public IntPtr AttributeList; } private struct ProcessInformation { public IntPtr Process; public IntPtr Thread; public uint ProcessId; public uint ThreadId; } private const uint ProcessCreateProcess = 128u; private const uint QueryLimitedInformation = 4096u; private const uint ExtendedStartupInfoPresent = 524288u; private const int ParentProcessAttribute = 131072; public static int Launch(string executablePath, string pipeName, int gameProcessId) { ValidateArguments(executablePath, pipeName, gameProcessId); uint shellProcessId = GetShellProcessId(); using SafeProcessHandle safeProcessHandle = OpenProcess(4224u, inheritHandle: false, shellProcessId); if (safeProcessHandle.IsInvalid) { throw CreateWin32Exception("Windows could not open the desktop shell as the audio host parent."); } VerifyShellImage(safeProcessHandle); IntPtr intPtr = IntPtr.Zero; IntPtr intPtr2 = IntPtr.Zero; bool flag = false; try { IntPtr size = IntPtr.Zero; InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0u, ref size); if (size == IntPtr.Zero) { throw CreateWin32Exception("Windows could not size the audio host process attribute list."); } intPtr = Marshal.AllocHGlobal(size); if (!InitializeProcThreadAttributeList(intPtr, 1, 0u, ref size)) { throw CreateWin32Exception("Windows could not initialize the audio host process attribute list."); } flag = true; intPtr2 = Marshal.AllocHGlobal(IntPtr.Size); Marshal.WriteIntPtr(intPtr2, safeProcessHandle.DangerousGetHandle()); if (!UpdateProcThreadAttribute(intPtr, 0u, new IntPtr(131072), intPtr2, new IntPtr(IntPtr.Size), IntPtr.Zero, IntPtr.Zero)) { throw CreateWin32Exception("Windows could not assign the desktop shell as the audio host parent."); } return LaunchWithAttributes(executablePath, pipeName, gameProcessId, intPtr); } finally { if (flag) { DeleteProcThreadAttributeList(intPtr); } if (intPtr2 != IntPtr.Zero) { Marshal.FreeHGlobal(intPtr2); } if (intPtr != IntPtr.Zero) { Marshal.FreeHGlobal(intPtr); } } } private static void ValidateArguments(string executablePath, string pipeName, int gameProcessId) { if (string.IsNullOrWhiteSpace(executablePath)) { throw new ArgumentException("The audio host path is required.", "executablePath"); } if (!File.Exists(executablePath)) { throw new FileNotFoundException("The packaged audio host was not found.", executablePath); } if (string.IsNullOrWhiteSpace(pipeName)) { throw new ArgumentException("The pipe name is required.", "pipeName"); } if (gameProcessId <= 0) { throw new ArgumentOutOfRangeException("gameProcessId"); } } private static uint GetShellProcessId() { IntPtr shellWindow = GetShellWindow(); if (shellWindow == IntPtr.Zero) { throw new InvalidOperationException("Windows did not report an interactive desktop shell."); } GetWindowThreadProcessId(shellWindow, out var processId); if (processId == 0) { throw CreateWin32Exception("Windows did not report the desktop shell process."); } return processId; } private static void VerifyShellImage(SafeProcessHandle shellProcess) { string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.Windows); if (string.IsNullOrWhiteSpace(folderPath)) { throw new InvalidOperationException("Windows did not report its installation directory."); } string fullPath = Path.GetFullPath(Path.Combine(folderPath, "explorer.exe")); string fullPath2 = Path.GetFullPath(ProcessImagePath.Get(shellProcess)); if (!string.Equals(fullPath, fullPath2, StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException("The interactive desktop shell is not the Windows Explorer executable."); } } private static int LaunchWithAttributes(string executablePath, string pipeName, int gameProcessId, IntPtr attributeList) { string fullPath = Path.GetFullPath(executablePath); StringBuilder commandLine = new StringBuilder(string.Format(CultureInfo.InvariantCulture, "{0} --pipe {1} --game-process-id {2}", QuoteCommandLineArgument(fullPath), QuoteCommandLineArgument(pipeName), gameProcessId)); checked { StartupInfoEx startupInfo = new StartupInfoEx { StartupInfo = new StartupInfo { Size = (uint)Marshal.SizeOf<StartupInfoEx>() }, AttributeList = attributeList }; if (!CreateProcess(fullPath, commandLine, IntPtr.Zero, IntPtr.Zero, inheritHandles: false, 524288u, IntPtr.Zero, Path.GetDirectoryName(fullPath), ref startupInfo, out var processInformation)) { throw CreateWin32Exception("Windows could not start the audio host process."); } CloseHandle(processInformation.Thread); CloseHandle(processInformation.Process); return (int)processInformation.ProcessId; } } private static string QuoteCommandLineArgument(string value) { StringBuilder stringBuilder = new StringBuilder(value.Length + 2); stringBuilder.Append('"'); int num = 0; foreach (char c in value) { switch (c) { case '\\': num++; break; case '"': stringBuilder.Append('\\', checked(num * 2 + 1)); stringBuilder.Append(c); num = 0; break; default: stringBuilder.Append('\\', num); num = 0; stringBuilder.Append(c); break; } } stringBuilder.Append('\\', checked(num * 2)); stringBuilder.Append('"'); return stringBuilder.ToString(); } private static Win32Exception CreateWin32Exception(string message) { return new Win32Exception(Marshal.GetLastWin32Error(), message); } [DllImport("user32.dll")] private static extern IntPtr GetShellWindow(); [DllImport("user32.dll", SetLastError = true)] private static extern uint GetWindowThreadProcessId(IntPtr window, out uint processId); [DllImport("kernel32.dll", SetLastError = true)] private static extern SafeProcessHandle OpenProcess(uint desiredAccess, [MarshalAs(UnmanagedType.Bool)] bool inheritHandle, uint processId); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool InitializeProcThreadAttributeList(IntPtr attributeList, int attributeCount, uint flags, ref IntPtr size); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool UpdateProcThreadAttribute(IntPtr attributeList, uint flags, IntPtr attribute, IntPtr value, IntPtr size, IntPtr previousValue, IntPtr returnSize); [DllImport("kernel32.dll")] private static extern void DeleteProcThreadAttributeList(IntPtr attributeList); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CreateProcess(string applicationName, StringBuilder commandLine, IntPtr processAttributes, IntPtr threadAttributes, [MarshalAs(UnmanagedType.Bool)] bool inheritHandles, uint creationFlags, IntPtr environment, string? currentDirectory, ref StartupInfoEx startupInfo, out ProcessInformation processInformation); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CloseHandle(IntPtr handle); } internal static class NamedPipeServerIdentity { public static int GetServerProcessId(NamedPipeClientStream pipe) { if (pipe == null) { throw new ArgumentNullException("pipe"); } if (!GetNamedPipeServerProcessId(pipe.SafePipeHandle, out var serverProcessId)) { throw new Win32Exception(Marshal.GetLastWin32Error(), "Windows could not identify the audio host pipe server."); } return checked((int)serverProcessId); } [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool GetNamedPipeServerProcessId(SafePipeHandle pipe, out uint serverProcessId); } internal static class ProcessImagePath { private const uint QueryLimitedInformation = 4096u; public static string Get(int processId) { using SafeProcessHandle safeProcessHandle = OpenProcess(4096u, inheritHandle: false, checked((uint)processId)); if (safeProcessHandle.IsInvalid) { throw new Win32Exception(Marshal.GetLastWin32Error(), "Windows could not open a process for image-path verification."); } return Get(safeProcessHandle); } public static string Get(SafeProcessHandle process) { if (process == null) { throw new ArgumentNullException("process"); } if (process.IsInvalid || process.IsClosed) { throw new ArgumentException("A live process handle is required.", "process"); } StringBuilder stringBuilder = new StringBuilder(32768); int size = stringBuilder.Capacity; if (!QueryFullProcessImageName(process, 0u, stringBuilder, ref size)) { throw new Win32Exception(Marshal.GetLastWin32Error(), "Windows could not read a process executable path."); } return stringBuilder.ToString(); } [DllImport("kernel32.dll", SetLastError = true)] private static extern SafeProcessHandle OpenProcess(uint desiredAccess, [MarshalAs(UnmanagedType.Bool)] bool inheritHandle, uint processId); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool QueryFullProcessImageName(SafeProcessHandle process, uint flags, StringBuilder executableName, ref int size); } internal static class ProcessTreeSnapshot { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct ProcessEntry32 { public uint Size; public uint Usage; public uint ProcessId; public IntPtr DefaultHeapId; public uint ModuleId; public uint Threads; public uint ParentProcessId; public int BasePriority; public uint Flags; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string ExecutableFile; } private sealed class SafeSnapshotHandle : SafeHandleZeroOrMinusOneIsInvalid { private SafeSnapshotHandle() : base(ownsHandle: true) { } protected override bool ReleaseHandle() { return CloseHandle(handle); } } private const uint SnapshotProcesses = 2u; public static bool IsSelfOrDescendant(int candidateProcessId, int ancestorProcessId) { return ProcessAncestry.IsSelfOrDescendant(candidateProcessId, ancestorProcessId, Capture()); } private static Dictionary<int, int> Capture() { checked { using SafeSnapshotHandle safeSnapshotHandle = CreateToolhelp32Snapshot(2u, 0u); if (safeSnapshotHandle.IsInvalid) { throw new InvalidOperationException($"Windows could not snapshot the process tree. Error {Marshal.GetLastWin32Error()}."); } Dictionary<int, int> dictionary = new Dictionary<int, int>(); ProcessEntry32 entry = new ProcessEntry32 { Size = (uint)Marshal.SizeOf<ProcessEntry32>() }; if (!Process32First(safeSnapshotHandle, ref entry)) { throw new InvalidOperationException($"Windows could not read the process tree. Error {Marshal.GetLastWin32Error()}."); } do { dictionary[(int)entry.ProcessId] = (int)entry.ParentProcessId; entry.Size = (uint)Marshal.SizeOf<ProcessEntry32>(); } while (Process32Next(safeSnapshotHandle, ref entry)); int lastWin32Error = Marshal.GetLastWin32Error(); if (lastWin32Error != 18) { throw new InvalidOperationException($"Windows process enumeration failed. Error {lastWin32Error}."); } return dictionary; } } [DllImport("kernel32.dll", SetLastError = true)] private static extern SafeSnapshotHandle CreateToolhelp32Snapshot(uint flags, uint processId); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool Process32First(SafeSnapshotHandle snapshot, ref ProcessEntry32 entry); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool Process32Next(SafeSnapshotHandle snapshot, ref ProcessEntry32 entry); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CloseHandle(IntPtr handle); } internal sealed class VoiceProcessRouter : IDisposable { private const int CaptureCapacitySamples = 131072; private static readonly TimeSpan SessionRetryDelay = TimeSpan.FromSeconds(1.0); private readonly ManualLogSource _logger; private readonly VoiceAudioMixer _mixer = new VoiceAudioMixer(); private readonly ManualResetEvent _stop = new ManualResetEvent(initialState: false); private readonly RoutingSessionGate _routingSession = new RoutingSessionGate(); private readonly Thread _worker; private readonly int _sampleRate; private readonly int _gameProcessId; private readonly string _audioHostPath; private readonly RemoteVoiceSettingsState _settings; private bool _disposed; public bool IsReady => _routingSession.IsReady; public VoiceProcessRouter(ManualLogSource logger, int sampleRate, int gameProcessId, string audioHostPath, RemoteVoiceSettingsState settings) { _logger = logger ?? throw new ArgumentNullException("logger"); if (sampleRate <= 0) { throw new ArgumentOutOfRangeException("sampleRate"); } if (gameProcessId <= 0) { throw new ArgumentOutOfRangeException("gameProcessId"); } if (string.IsNullOrWhiteSpace(audioHostPath)) { throw new ArgumentException("The audio host path is required.", "audioHostPath"); } _sampleRate = sampleRate; _gameProcessId = gameProcessId; _audioHostPath = audioHostPath; _settings = settings ?? throw new ArgumentNullException("settings"); _worker = new Thread(WorkerMain) { IsBackground = true, Name = "RemoteVoiceSplit pipe sender" }; _worker.Start(); } public VoiceCaptureStream RegisterCapture() { ThrowIfDisposed(); return _mixer.Register(131072); } public void UnregisterCapture(VoiceCaptureStream stream) { _mixer.Unregister(stream); } public RoutingSubmissionLease? TrySubmit(VoiceCaptureStream stream, float[] samples, int channels) { RoutingSubmissionLease routingSubmissionLease = _routingSession.TryBeginSubmission(); if (routingSubmissionLease == null) { return null; } if (!stream.TryWrite(samples, channels)) { routingSubmissionLease.Dispose(); return null; } return routingSubmissionLease; } public void Dispose() { if (!_disposed) { _disposed = true; SetNotReady(); _stop.Set(); if (_worker.Join(TimeSpan.FromSeconds(7.0))) { _stop.Dispose(); } else { TryLog((LogLevel)4, "The remote voice audio-host thread did not stop within seven seconds; its stop handle was left intact."); } } } private void WorkerMain() { string pipeName = $"RemoteVoiceSplit-{_gameProcessId}-{Guid.NewGuid():N}"; int num = 0; string a = null; while (!_stop.WaitOne(0)) { try { if (!IsExpectedHostProcess(num)) { num = DetachedAudioHostLauncher.Launch(_audioHostPath, pipeName, _gameProcessId); } RunSession(pipeName, num); a = null; } catch (Exception ex) { string text = ex.GetType().Name + ": " + ex.Message; if (!string.Equals(a, text, StringComparison.Ordinal)) { TryLog((LogLevel)2, (!_settings.Enabled) ? ("Remote voice process output is unavailable while Remote Voice Split is disabled; Unity output remains enabled. " + text) : (_settings.FallbackToGameOutput ? ("Remote voice process output is unavailable; Unity output remains enabled. " + text) : ("Remote voice process output is unavailable; remote voice remains silent until it recovers. " + text))); a = text; } } finally { SetNotReady(); } _stop.WaitOne(SessionRetryDelay); } } private void RunSession(string pipeName, int expectedHostProcessId) { using NamedPipeClientStream namedPipeClientStream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); namedPipeClientStream.Connect(5000); using BinaryReader reader = new BinaryReader(namedPipeClientStream, Encoding.UTF8, leaveOpen: true); using BinaryWriter writer = new BinaryWriter(namedPipeClientStream, Encoding.UTF8, leaveOpen: true); AudioHostProtocol.WriteClientHello(writer, _sampleRate); int num = AudioHostProtocol.ReadServerReady(reader); if (num != expectedHostProcessId) { throw new InvalidOperationException("The audio host handshake did not match the launched process."); } int serverProcessId = NamedPipeServerIdentity.GetServerProcessId(namedPipeClientStream); if (num != serverProcessId) { throw new InvalidOperationException("The audio host handshake did not match the actual pipe server process."); } VerifyExpectedHostProcess(num); SetReady(); TryLog((LogLevel)16, "Remote voice process output is ready. Select 'Lethal Company Remote Voice Split' in OBS Application Audio Capture."); SendAudio(namedPipeClientStream, writer); } private bool IsExpectedHostProcess(int processId) { if (processId <= 0) { return false; } try { VerifyExpectedHostProcess(processId); return true; } catch { return false; } } private void VerifyExpectedHostProcess(int processId) { string fullPath = Path.GetFullPath(_audioHostPath); string fullPath2 = Path.GetFullPath(ProcessImagePath.Get(processId)); if (!string.Equals(fullPath, fullPath2, StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException("The actual pipe server is not the packaged Remote Voice Split audio host."); } if (ProcessTreeSnapshot.IsSelfOrDescendant(processId, _gameProcessId)) { throw new InvalidOperationException("The audio host remained inside the game process tree and cannot provide an isolated OBS source."); } } private void SendAudio(NamedPipeClientStream pipe, BinaryWriter writer) { checked { int num = Math.Max(1, unchecked(_sampleRate / 100)) * 2; if (num > 16384) { throw new InvalidOperationException("The runtime sample rate exceeds the audio host protocol block limit."); } float[] array = new float[num]; byte[] array2 = new byte[num * 4]; Stopwatch stopwatch = Stopwatch.StartNew(); while (!_stop.WaitOne(TimeSpan.FromMilliseconds(10.0))) { if (_mixer.Mix(array, num)) { Buffer.BlockCopy(array, 0, array2, 0, array2.Length); writer.Write(num); writer.Write(array2, 0, array2.Length); writer.Flush(); stopwatch.Restart(); } else if (stopwatch.Elapsed >= TimeSpan.FromSeconds(1.0)) { writer.Write(0); writer.Flush(); stopwatch.Restart(); } if (!pipe.IsConnected) { throw new EndOfStreamException("The audio host disconnected."); } } } } private void SetReady() { _routingSession.Activate(); } private void SetNotReady() { _routingSession.Deactivate(); _mixer.Clear(); } private void TryLog(LogLevel level, string message) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) try { _logger.Log(level, (object)message); } catch { } } private void ThrowIfDisposed() { if (_disposed) { throw new ObjectDisposedException("VoiceProcessRouter"); } } } } namespace RemoteVoiceSplit.Interop.Game { internal static class GameReflection { public static Type StartOfRoundType { get; } = RequireType("StartOfRound"); public static FieldInfo AllPlayerScriptsField { get; } = RequireField(StartOfRoundType, "allPlayerScripts"); public static FieldInfo LocalPlayerControllerField { get; } = RequireField(StartOfRoundType, "localPlayerController"); public static FieldInfo IsPlayerControlledField { get; } = RequireField(RequireType("GameNetcodeStuff.PlayerControllerB"), "isPlayerControlled"); public static FieldInfo IsPlayerDeadField { get; } = RequireField(RequireType("GameNetcodeStuff.PlayerControllerB"), "isPlayerDead"); public static FieldInfo CurrentVoiceSourceField { get; } = RequireField(RequireType("GameNetcodeStuff.PlayerControllerB"), "currentVoiceChatAudioSource"); public static MethodInfo StartOfRoundRefreshMethod { get; } = RequireMethod(StartOfRoundType, "RefreshPlayerVoicePlaybackObjects"); private static Type RequireType(string name) { return AccessTools.TypeByName(name) ?? throw new TypeLoadException("Lethal Company v81 type '" + name + "' was not found."); } private static FieldInfo RequireField(Type type, string name) { return AccessTools.Field(type, name) ?? throw new MissingFieldException(type.FullName, name); } private static MethodInfo RequireMethod(Type type, string name) { return AccessTools.Method(type, name, Type.EmptyTypes, (Type[])null) ?? throw new MissingMethodException(type.FullName, name); } } internal static class IntegrationContext { private static ManualLogSource? _logger; private static VoiceRoutingContext? _routing; public static void Initialize(ManualLogSource logger, VoiceProcessRouter router, RemoteVoiceSettingsState settings) { _logger = logger; _routing = new VoiceRoutingContext(router, settings); } public static void Clear() { _routing = null; _logger = null; } public static bool TryGetRouting(out VoiceRoutingContext? routing) { routing = _routing; return routing != null; } public static void RunGuarded(string operation, Action action) { try { action(); } catch (Exception ex) { TryLog((LogLevel)2, operation + " failed without interrupting the game: " + ex.GetType().Name + ": " + ex.Message); } } private static void TryLog(LogLevel level, string message) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) try { ManualLogSource? logger = _logger; if (logger != null) { logger.Log(level, (object)message); } } catch { } } } internal sealed class VoiceRoutingContext { public VoiceProcessRouter Router { get; } public RemoteVoiceSettingsState Settings { get; } public VoiceRoutingContext(VoiceProcessRouter router, RemoteVoiceSettingsState settings) { Router = router; Settings = settings; } } internal static class PluginRuntime { private static readonly object Gate = new object(); private static Harmony? _harmony; private static ManualLogSource? _logger; private static VoiceProcessRouter? _router; private static RemoteVoiceConfiguration? _configuration; private static bool _initialized; public static bool Initialize(ManualLogSource logger, int sampleRate, int gameProcessId, string audioHostPath, ConfigEntry<bool> enabled, ConfigEntry<bool> fallbackToGameOutput) { //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Expected O, but got Unknown if (logger == null) { throw new ArgumentNullException("logger"); } if (enabled == null) { throw new ArgumentNullException("enabled"); } if (fallbackToGameOutput == null) { throw new ArgumentNullException("fallbackToGameOutput"); } lock (Gate) { if (_initialized) { return false; } VoiceProcessRouter router = null; Harmony val = null; RemoteVoiceConfiguration remoteVoiceConfiguration = null; try { remoteVoiceConfiguration = new RemoteVoiceConfiguration(enabled, fallbackToGameOutput, logger); router = new VoiceProcessRouter(logger, sampleRate, gameProcessId, audioHostPath, remoteVoiceConfiguration.State); IntegrationContext.Initialize(logger, router, remoteVoiceConfiguration.State); val = new Harmony("com.aoirint.remotevoicesplit"); val.PatchAll(typeof(Plugin).Assembly); _logger = logger; _router = router; _harmony = val; _configuration = remoteVoiceConfiguration; Application.quitting += OnApplicationQuitting; _initialized = true; return true; } catch { IntegrationContext.Clear(); TryUnpatch(val, logger); TryDisposeRouter(router, logger); TryDisposeConfiguration(remoteVoiceConfiguration, logger); _harmony = null; _router = null; _logger = null; _configuration = null; throw; } } } private static void OnApplicationQuitting() { lock (Gate) { if (_initialized) { _initialized = false; Application.quitting -= OnApplicationQuitting; ManualLogSource logger = _logger; Harmony? harmony = _harmony; VoiceProcessRouter router = _router; RemoteVoiceConfiguration configuration = _configuration; _harmony = null; _router = null; _logger = null; _configuration = null; TryLog(logger, (LogLevel)16, "Remote Voice Split is stopping because the game application is quitting."); IntegrationContext.Clear(); TryUnpatch(harmony, logger); TryDisposeRouter(router, logger); TryDisposeConfiguration(configuration, logger); } } } private static void TryUnpatch(Harmony? harmony, ManualLogSource? logger) { try { if (harmony != null) { harmony.UnpatchSelf(); } } catch (Exception ex) { TryLog(logger, (LogLevel)4, "Harmony cleanup failed during application shutdown: " + ex.GetType().Name + ": " + ex.Message); } } private static void TryDisposeRouter(VoiceProcessRouter? router, ManualLogSource? logger) { try { router?.Dispose(); } catch (Exception ex) { TryLog(logger, (LogLevel)4, "Audio-router cleanup failed during application shutdown: " + ex.GetType().Name + ": " + ex.Message); } } private static void TryDisposeConfiguration(RemoteVoiceConfiguration? configuration, ManualLogSource? logger) { try { configuration?.Dispose(); } catch (Exception ex) { TryLog(logger, (LogLevel)4, "Configuration cleanup failed during application shutdown: " + ex.GetType().Name + ": " + ex.Message); } } private static void TryLog(ManualLogSource? logger, LogLevel level, string message) { //IL_0004: Unknown result type (might be due to invalid IL or missing references) try { if (logger != null) { logger.Log(level, (object)message); } } catch { } } } internal sealed class RemoteVoiceConfiguration : IDisposable { private readonly ConfigEntry<bool> _enabledEntry; private readonly ConfigEntry<bool> _fallbackEntry; private readonly ManualLogSource _logger; private bool _disposed; public RemoteVoiceSettingsState State { get; } public RemoteVoiceConfiguration(ConfigEntry<bool> enabledEntry, ConfigEntry<bool> fallbackEntry, ManualLogSource logger) { _enabledEntry = enabledEntry ?? throw new ArgumentNullException("enabledEntry"); _fallbackEntry = fallbackEntry ?? throw new ArgumentNullException("fallbackEntry"); _logger = logger ?? throw new ArgumentNullException("logger"); State = new RemoteVoiceSettingsState(enabledEntry.Value, fallbackEntry.Value); _enabledEntry.SettingChanged += OnEnabledSettingChanged; _fallbackEntry.SettingChanged += OnFallbackSettingChanged; State.Update(_enabledEntry.Value, _fallbackEntry.Value); } public void Dispose() { if (!_disposed) { _disposed = true; _enabledEntry.SettingChanged -= OnEnabledSettingChanged; _fallbackEntry.SettingChanged -= OnFallbackSettingChanged; } } private void OnEnabledSettingChanged(object sender, EventArgs args) { if (TryApply()) { TryLog((LogLevel)16, State.Enabled ? "Remote Voice Split is enabled." : "Remote Voice Split is disabled; remote voices remain on the normal game output."); } } private void OnFallbackSettingChanged(object sender, EventArgs args) { if (TryApply()) { TryLog((LogLevel)16, State.FallbackToGameOutput ? "Remote voice fallback to normal game output is enabled." : "Remote voice fallback to normal game output is disabled; unavailable remote voice will remain silent."); } } private bool TryApply() { try { State.Update(_enabledEntry.Value, _fallbackEntry.Value); return true; } catch (Exception ex) { TryLog((LogLevel)2, "Remote voice configuration could not be applied: " + ex.GetType().Name + ": " + ex.Message); return false; } } private void TryLog(LogLevel level, string message) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) try { _logger.Log(level, (object)message); } catch { } } } internal sealed class VoiceCaptureFilter : MonoBehaviour { private sealed class CaptureRegistration { public VoiceProcessRouter Router { get; } public VoiceCaptureStream Stream { get; } public RemoteVoiceSettingsState Settings { get; } public AtomicCommitLease CommitLease { get; } = new AtomicCommitLease(); public CaptureRegistration(VoiceProcessRouter router, VoiceCaptureStream stream, RemoteVoiceSettingsState settings) { Router = router; Stream = stream; Settings = settings; } } private readonly AtomicRegistration<CaptureRegistration> _registration = new AtomicRegistration<CaptureRegistration>(); public void Initialize(VoiceProcessRouter router, RemoteVoiceSettingsState settings) { CaptureRegistration captureRegistration = _registration.Read(); if (captureRegistration != null && captureRegistration.Router == router && captureRegistration.Settings == settings) { ((Behaviour)this).enabled = true; return; } Deactivate(); CaptureRegistration value = new CaptureRegistration(router, router.RegisterCapture(), settings); _registration.Exchange(value); ((Behaviour)this).enabled = true; } public void Deactivate() { ((Behaviour)this).enabled = false; CaptureRegistration captureRegistration = _registration.Exchange(null); if (captureRegistration != null) { captureRegistration.CommitLease.Retire(); captureRegistration.Router.UnregisterCapture(captureRegistration.Stream); } } private void OnAudioFilterRead(float[] data, int channels) { CaptureRegistration captureRegistration = _registration.Read(); if (captureRegistration == null) { return; } bool enabled = captureRegistration.Settings.Enabled; if (!enabled) { captureRegistration.Stream.Clear(); return; } using RoutingSubmissionLease routingSubmissionLease = captureRegistration.Router.TrySubmit(captureRegistration.Stream, data, channels); bool flag = routingSubmissionLease != null; if (!RemoteVoiceOutputPolicy.ShouldClearUnityOutput(enabled, flag, captureRegistration.Settings.FallbackToGameOutput)) { return; } if (!flag) { captureRegistration.Stream.Clear(); } if (!captureRegistration.CommitLease.TryBegin()) { captureRegistration.Stream.Clear(); return; } try { if (!_registration.IsCurrent(captureRegistration)) { captureRegistration.Stream.Clear(); } else { Array.Clear(data, 0, data.Length); } } finally { captureRegistration.CommitLease.End(); } } private void OnDestroy() { Deactivate(); } } internal static class VoicePlaybackIntegration { public static void Refresh(object startOfRound, VoiceProcessRouter router, RemoteVoiceSettingsState settings) { object value = GameReflection.LocalPlayerControllerField.GetValue(startOfRound); if (value == null || !(GameReflection.AllPlayerScriptsField.GetValue(startOfRound) is Array array)) { return; } HashSet<AudioSource> hashSet = new HashSet<AudioSource>(); foreach (object item in array) { if (item != null) { bool isLocalPlayer = IsSameUnityObject(item, value); bool isPlayerControlled = (bool)(GameReflection.IsPlayerControlledField.GetValue(item) ?? ((object)false)); bool isPlayerDead = (bool)(GameReflection.IsPlayerDeadField.GetValue(item) ?? ((object)false)); object? value2 = GameReflection.CurrentVoiceSourceField.GetValue(item); AudioSource val = (AudioSource)((value2 is AudioSource) ? value2 : null); if (RemoteVoiceSelectionPolicy.ShouldCapture(isLocalPlayer, isPlayerControlled, isPlayerDead, (Object)(object)val != (Object)null) && !((Object)(object)val == (Object)null)) { hashSet.Add(val); (((Component)val).GetComponent<VoiceCaptureFilter>() ?? ((Component)val).gameObject.AddComponent<VoiceCaptureFilter>()).Initialize(router, settings); } } } VoiceCaptureFilter[] array2 = Object.FindObjectsOfType<VoiceCaptureFilter>(true); foreach (VoiceCaptureFilter voiceCaptureFilter in array2) { AudioSource component = ((Component)voiceCaptureFilter).GetComponent<AudioSource>(); if ((Object)(object)component == (Object)null || !hashSet.Contains(component)) { voiceCaptureFilter.Deactivate(); } } } private static bool IsSameUnityObject(object left, object right) { if (left == right) { return true; } Object val = (Object)((left is Object) ? left : null); if (val != null) { Object val2 = (Object)((right is Object) ? right : null); if (val2 != null) { return val == val2; } } return false; } } [HarmonyPatch] internal static class RefreshPlayerVoicePlaybackObjectsPatch { private static MethodInfo TargetMethod() { return GameReflection.StartOfRoundRefreshMethod; } [HarmonyPostfix] private static void Postfix(object __instance) { IntegrationContext.RunGuarded("Remote voice source attachment", delegate { if (IntegrationContext.TryGetRouting(out VoiceRoutingContext routing) && routing != null) { VoicePlaybackIntegration.Refresh(__instance, routing.Router, routing.Settings); } }); } } } namespace RemoteVoiceSplit.Core { internal sealed class AtomicRegistration<T> where T : class { private T? _current; public T? Read() { return Volatile.Read(in _current); } public T? Exchange(T? value) { return Interlocked.Exchange(ref _current, value); } public bool IsCurrent(T value) { return Volatile.Read(in _current) == value; } } internal sealed class AtomicCommitLease { private const int Active = 0; private const int Committing = 1; private const int Retired = 2; private int _state; public bool TryBegin() { return Interlocked.CompareExchange(ref _state, 1, 0) == 0; } public void End() { if (Interlocked.CompareExchange(ref _state, 0, 1) != 1) { throw new InvalidOperationException("The commit lease is not active."); } } public void Retire() { SpinWait spinWait = default(SpinWait); while (Interlocked.CompareExchange(ref _state, 2, 0) != 0 && Volatile.Read(in _state) != 2) { spinWait.SpinOnce(); } } } internal sealed class AtomicUsageLease { private const int RetiredMask = int.MinValue; private const int UsageMask = int.MaxValue; private int _state; public bool TryBegin() { int num; do { num = Volatile.Read(in _state); if ((num & int.MinValue) != 0) { return false; } if ((num & 0x7FFFFFFF) == int.MaxValue) { throw new InvalidOperationException("The usage lease is exhausted."); } } while (Interlocked.CompareExchange(ref _state, num + 1, num) != num); return true; } public void End() { if ((Interlocked.Decrement(ref _state) & 0x7FFFFFFF) == int.MaxValue) { throw new InvalidOperationException("The usage lease was released without a matching acquisition."); } } public void Retire() { int num; do { num = Volatile.Read(in _state); } while ((num & int.MinValue) == 0 && Interlocked.CompareExchange(ref _state, num | int.MinValue, num) != num); SpinWait spinWait = default(SpinWait); while ((Volatile.Read(in _state) & 0x7FFFFFFF) != 0) { spinWait.SpinOnce(); } } } internal static class AudioHostProtocol { public const int Magic = 827610706; public const int Version = 1; public const int Channels = 2; public const int Ready = 1; public const int MaximumBlockSamples = 16384; public static void WriteClientHello(BinaryWriter writer, int sampleRate) { if (writer == null) { throw new ArgumentNullException("writer"); } if (sampleRate <= 0) { throw new ArgumentOutOfRangeException("sampleRate"); } writer.Write(827610706); writer.Write(1); writer.Write(sampleRate); writer.Write(2); writer.Flush(); } public static int ReadClientHello(BinaryReader reader) { if (reader == null) { throw new ArgumentNullException("reader"); } RequireEqual(827610706, reader.ReadInt32(), "magic"); RequireEqual(1, reader.ReadInt32(), "version"); int num = reader.ReadInt32(); if (num <= 0) { throw new InvalidDataException("The audio host protocol sample rate is invalid."); } RequireEqual(2, reader.ReadInt32(), "channel count"); return num; } public static void WriteServerReady(BinaryWriter writer, int processId) { if (writer == null) { throw new ArgumentNullException("writer"); } if (processId <= 0) { throw new ArgumentOutOfRangeException("processId"); } writer.Write(827610706); writer.Write(1); writer.Write(1); writer.Write(processId); writer.Flush(); } public static int ReadServerReady(BinaryReader reader) { if (reader == null) { throw new ArgumentNullException("reader"); } RequireEqual(827610706, reader.ReadInt32(), "magic"); RequireEqual(1, reader.ReadInt32(), "version"); RequireEqual(1, reader.ReadInt32(), "ready state"); int num = reader.ReadInt32(); if (num <= 0) { throw new InvalidDataException("The audio host process identifier is invalid."); } return num; } private static void RequireEqual(int expected, int actual, string field) { if (actual != expected) { throw new InvalidDataException("The audio host protocol " + field + " is incompatible."); } } } internal sealed class AudioRingBuffer { private readonly float[] _samples; private readonly int _mask; private int _readSequence; private int _writeSequence; public int CapacitySamples => _samples.Length; public int AvailableSamples { get { int num = Volatile.Read(in _writeSequence); int num2 = Volatile.Read(in _readSequence); uint num3 = (uint)(num - num2); if (num3 > _samples.Length) { return 0; } return (int)num3; } } public AudioRingBuffer(int capacitySamples) { if (capacitySamples < 2 || (capacitySamples & (capacitySamples - 1)) != 0) { throw new ArgumentOutOfRangeException("capacitySamples", "Capacity must be a power of two."); } _samples = new float[capacitySamples]; _mask = capacitySamples - 1; } public bool TryWriteStereo(float[] source, int channels) { return TryWriteStereo(source, (source != null) ? source.Length : 0, channels); } public bool TryWriteStereo(float[] source, int sourceSampleCount, int channels) { if (source == null) { throw new ArgumentNullException("source"); } if (sourceSampleCount < 0 || sourceSampleCount > source.Length || channels <= 0 || sourceSampleCount % channels != 0) { return false; } int num = sourceSampleCount / channels; int num2 = checked(num * 2); int writeSequence = _writeSequence; int num3 = Volatile.Read(in _readSequence); uint num4 = (uint)(writeSequence - num3); if (num4 > _samples.Length || num2 > _samples.Length - (int)num4) { return false; } int num5 = writeSequence & _mask; for (int i = 0; i < num; i++) { int num6 = i * channels; float num7 = source[num6]; float num8 = ((channels == 1) ? num7 : source[num6 + 1]); _samples[num5] = num7; _samples[(num5 + 1) & _mask] = num8; num5 = (num5 + 2) & _mask; } Volatile.Write(ref _writeSequence, writeSequence + num2); return true; } public int Read(float[] destination, int sampleCount) { return Read(destination, sampleCount, null); } internal int Read(float[] destination, int sampleCount, Action? beforePublish) { if (destination == null) { throw new ArgumentNullException("destination"); } if (sampleCount < 0 || sampleCount > destination.Length) { throw new ArgumentOutOfRangeException("sampleCount"); } int num = Volatile.Read(in _readSequence); uint num2 = (uint)(Volatile.Read(in _writeSequence) - num); if (num2 > _samples.Length) { return 0; } int num3 = Math.Min(sampleCount, (int)num2); int num4 = num & _mask; int num5 = Math.Min(num3, _samples.Length - num4); Array.Copy(_samples, num4, destination, 0, num5); Array.Copy(_samples, 0, destination, num5, num3 - num5); beforePublish?.Invoke(); if (Interlocked.CompareExchange(ref _readSequence, num + num3, num) != num) { Array.Clear(destination, 0, num3); return 0; } return num3; } public void Clear() { int value = Volatile.Read(in _writeSequence); Interlocked.Exchange(ref _readSequence, value); } } internal static class ProcessAncestry { public static bool IsSelfOrDescendant(int candidateProcessId, int ancestorProcessId, IReadOnlyDictionary<int, int> parentByProcessId) { if (candidateProcessId <= 0) { throw new ArgumentOutOfRangeException("candidateProcessId"); } if (ancestorProcessId <= 0) { throw new ArgumentOutOfRangeException("ancestorProcessId"); } if (parentByProcessId == null) { throw new ArgumentNullException("parentByProcessId"); } int value = candidateProcessId; HashSet<int> hashSet = new HashSet<int>(); while (value > 0 && hashSet.Add(value)) { if (value == ancestorProcessId) { return true; } if (!parentByProcessId.TryGetValue(value, out value)) { return false; } } return false; } } internal static class RemoteVoiceOutputPolicy { public const bool DefaultEnabled = true; public const bool DefaultFallbackToGameOutput = false; public static bool ShouldClearUnityOutput(bool enabled, bool submissionAccepted, bool fallbackToGameOutput) { if (enabled) { if (!submissionAccepted) { return !fallbackToGameOutput; } return true; } return false; } } internal sealed class RemoteVoiceSettingsState { private int _enabled; private int _fallbackToGameOutput; public bool Enabled => Volatile.Read(in _enabled) != 0; public bool FallbackToGameOutput => Volatile.Read(in _fallbackToGameOutput) != 0; public RemoteVoiceSettingsState(bool enabled, bool fallbackToGameOutput) { Update(enabled, fallbackToGameOutput); } public void Update(bool enabled, bool fallbackToGameOutput) { Volatile.Write(ref _enabled, enabled ? 1 : 0); Volatile.Write(ref _fallbackToGameOutput, fallbackToGameOutput ? 1 : 0); } } internal static class RemoteVoiceSelectionPolicy { public static bool ShouldCapture(bool isLocalPlayer, bool isPlayerControlled, bool isPlayerDead, bool hasVoiceSource) { return !isLocalPlayer && (isPlayerControlled || isPlayerDead) && hasVoiceSource; } } internal sealed class RoutingSessionGate { private sealed class RoutingSessionEpoch { public AtomicUsageLease Usage { get; } = new AtomicUsageLease(); } private readonly object _transitionGate = new object(); private readonly AtomicRegistration<RoutingSessionEpoch> _current = new AtomicRegistration<RoutingSessionEpoch>(); public bool IsReady => _current.Read() != null; public void Activate() { lock (_transitionGate) { if (_current.Read() != null) { throw new InvalidOperationException("A routing session was already active."); } _current.Exchange(new RoutingSessionEpoch()); } } public RoutingSubmissionLease? TryBeginSubmission() { RoutingSessionEpoch routingSessionEpoch = _current.Read(); if (routingSessionEpoch == null || !routingSessionEpoch.Usage.TryBegin()) { return null; } if (!_current.IsCurrent(routingSessionEpoch)) { routingSessionEpoch.Usage.End(); return null; } return new RoutingSubmissionLease(routingSessionEpoch.Usage); } public void Deactivate() { lock (_transitionGate) { _current.Exchange(null)?.Usage.Retire(); } } } internal sealed class RoutingSubmissionLease : IDisposable { private AtomicUsageLease? _usage; public RoutingSubmissionLease(AtomicUsageLease usage) { _usage = usage; } public void Dispose() { Interlocked.Exchange(ref _usage, null)?.End(); } } internal sealed class VoiceAudioMixer { private readonly object _gate = new object(); private VoiceCaptureStream[] _streams = Array.Empty<VoiceCaptureStream>(); private float[] _scratch = Array.Empty<float>(); public VoiceCaptureStream Register(int capacitySamples) { VoiceCaptureStream voiceCaptureStream = new VoiceCaptureStream(capacitySamples); lock (_gate) { VoiceCaptureStream[] streams = _streams; VoiceCaptureStream[] array = new VoiceCaptureStream[streams.Length + 1]; Array.Copy(streams, array, streams.Length); array[streams.Length] = voiceCaptureStream; Volatile.Write(ref _streams, array); return voiceCaptureStream; } } public void Unregister(VoiceCaptureStream stream) { if (stream == null) { return; } lock (_gate) { VoiceCaptureStream[] streams = _streams; int num = Array.IndexOf(streams, stream); if (num < 0) { return; } VoiceCaptureStream[] array = new VoiceCaptureStream[streams.Length - 1]; Array.Copy(streams, 0, array, 0, num); Array.Copy(streams, num + 1, array, num, streams.Length - num - 1); Volatile.Write(ref _streams, array); } stream.Clear(); } public bool Mix(float[] destination, int sampleCount) { if (destination == null) { throw new ArgumentNullException("destination"); } if (sampleCount < 0 || sampleCount > destination.Length || (sampleCount & 1) != 0) { throw new ArgumentOutOfRangeException("sampleCount"); } Array.Clear(destination, 0, sampleCount); EnsureScratchCapacity(sampleCount); bool flag = false; VoiceCaptureStream[] array = Volatile.Read(in _streams); for (int i = 0; i < array.Length; i++) { int num = array[i].Read(_scratch, sampleCount); flag = flag || num > 0; for (int j = 0; j < num; j++) { destination[j] += _scratch[j]; } } if (flag) { for (int k = 0; k < sampleCount; k++) { destination[k] = Math.Max(-1f, Math.Min(1f, destination[k])); } } return flag; } public void Clear() { VoiceCaptureStream[] array = Volatile.Read(in _streams); for (int i = 0; i < array.Length; i++) { array[i].Clear(); } } private void EnsureScratchCapacity(int sampleCount) { if (_scratch.Length < sampleCount) { _scratch = new float[sampleCount]; } } } internal sealed class VoiceCaptureStream { private readonly AudioRingBuffer _buffer; public VoiceCaptureStream(int capacitySamples) { _buffer = new AudioRingBuffer(capacitySamples); } public bool TryWrite(float[] samples, int channels) { return _buffer.TryWriteStereo(samples, channels); } public int Read(float[] destination, int sampleCount) { int num = _buffer.Read(destination, sampleCount); if ((num & 1) != 0) { throw new InvalidOperationException("A stereo stream returned a partial frame."); } return num; } public void Clear() { _buffer.Clear(); } } }