using Barotrauma.Sounds; using Concentus.Structs; using Microsoft.Xna.Framework; using OpenAL; using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Threading; namespace Barotrauma.Networking { class VoipCapture : VoipQueue, IDisposable { public static VoipCapture Instance { get; private set; } private readonly IntPtr captureDevice; private Thread captureThread; private bool capturing; private readonly OpusEncoder encoder; public double LastdB { get; private set; } public double LastAmplitude { get; private set; } public float Gain { get { return GameSettings.CurrentConfig.Audio.MicrophoneVolume; } } public DateTime LastEnqueueAudio; public override byte QueueID { get { return GameMain.Client?.SessionId ?? 0; } protected set { //do nothing } } public readonly bool CanDetectDisconnect; public bool Disconnected { get; private set; } public static void Create(string deviceName, UInt16? storedBufferID = null) { if (Instance != null) { throw new Exception("Tried to instance more than one VoipCapture object"); } var capture = new VoipCapture(deviceName) { LatestBufferID = storedBufferID ?? BUFFER_COUNT - 1 }; if (capture.captureDevice != IntPtr.Zero) { Instance = capture; } } private VoipCapture(string deviceName) : base(GameMain.Client?.SessionId ?? 0, true, false) { Disconnected = false; encoder = VoipConfig.CreateEncoder(); //set up capture device captureDevice = Alc.CaptureOpenDevice(deviceName, VoipConfig.FREQUENCY, Al.FormatMono16, VoipConfig.BUFFER_SIZE * 5); if (captureDevice == IntPtr.Zero) { DebugConsole.NewMessage("Alc.CaptureOpenDevice attempt 1 failed: error code " + Alc.GetError(IntPtr.Zero).ToString(), Color.Orange); //attempt using a smaller buffer size captureDevice = Alc.CaptureOpenDevice(deviceName, VoipConfig.FREQUENCY, Al.FormatMono16, VoipConfig.BUFFER_SIZE * 2); } if (captureDevice == IntPtr.Zero) { DebugConsole.NewMessage("Alc.CaptureOpenDevice attempt 2 failed: error code " + Alc.GetError(IntPtr.Zero).ToString(), Color.Orange); //attempt using the default device captureDevice = Alc.CaptureOpenDevice("", VoipConfig.FREQUENCY, Al.FormatMono16, VoipConfig.BUFFER_SIZE * 2); } if (captureDevice == IntPtr.Zero) { string errorCode = Alc.GetError(IntPtr.Zero).ToString(); if (!GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "capturedevicenotfound")) { //GUI.SettingsMenuOpen = false; new GUIMessageBox(TextManager.Get("Error"), (TextManager.Get("VoipCaptureDeviceNotFound").Fallback("Could not start voice capture, suitable capture device not found.")) + " (" + errorCode + ")") { UserData = "capturedevicenotfound" }; } GameAnalyticsManager.AddErrorEventOnce("Alc.CaptureDeviceOpenFailed", GameAnalyticsManager.ErrorSeverity.Error, "Alc.CaptureDeviceOpen(" + deviceName + ") failed. Error code: " + errorCode); var config = GameSettings.CurrentConfig; config.Audio.VoiceSetting = VoiceMode.Disabled; GameSettings.SetCurrentConfig(config); Instance?.Dispose(); Instance = null; return; } int alError = Al.GetError(); int alcError = Alc.GetError(captureDevice); if (alcError != Alc.NoError) { throw new Exception("Failed to open capture device: " + alcError.ToString() + " (ALC)"); } if (alError != Al.NoError) { throw new Exception("Failed to open capture device: " + alError.ToString() + " (AL)"); } CanDetectDisconnect = Alc.IsExtensionPresent(captureDevice, "ALC_EXT_disconnect"); alcError = Alc.GetError(captureDevice); if (alcError != Alc.NoError) { throw new Exception("Error determining if disconnect can be detected: " + alcError.ToString()); } Alc.CaptureStart(captureDevice); alcError = Alc.GetError(captureDevice); if (alcError != Alc.NoError) { throw new Exception("Failed to start capturing: " + alcError.ToString()); } capturing = true; captureThread = new Thread(UpdateCapture) { IsBackground = true, Name = "VoipCapture" }; captureThread.Start(); } public static void ChangeCaptureDevice(string deviceName) { if (Instance == null) { return; } UInt16 storedBufferID = Instance.LatestBufferID; Instance.Dispose(); Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); } public static IReadOnlyList GetCaptureDeviceNames() { return Alc.GetStringList(IntPtr.Zero, OpenAL.Alc.CaptureDeviceSpecifier); } IntPtr nativeBuffer; readonly short[] uncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; readonly short[] prevUncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; bool prevCaptured = true; int captureTimer; private void UpdateCapture() { Array.Copy(uncompressedBuffer, 0, prevUncompressedBuffer, 0, VoipConfig.BUFFER_SIZE); Array.Clear(uncompressedBuffer, 0, VoipConfig.BUFFER_SIZE); nativeBuffer = Marshal.AllocHGlobal(VoipConfig.BUFFER_SIZE * 2); try { while (capturing) { int alcError; if (CanDetectDisconnect) { Alc.GetInteger(captureDevice, Alc.EnumConnected, out int isConnected); alcError = Alc.GetError(captureDevice); if (alcError != Alc.NoError) { throw new Exception("Failed to determine if capture device is connected: " + alcError.ToString()); } if (isConnected == 0) { DebugConsole.ThrowError("Capture device has been disconnected. You can select another available device in the settings."); Disconnected = true; break; } } FillBuffer(); alcError = Alc.GetError(captureDevice); if (alcError != Alc.NoError) { throw new Exception("Failed to capture samples: " + alcError.ToString()); } double maxAmplitude = 0.0f; for (int i = 0; i < VoipConfig.BUFFER_SIZE; i++) { uncompressedBuffer[i] = (short)MathHelper.Clamp((uncompressedBuffer[i] * Gain), -short.MaxValue, short.MaxValue); double sampleVal = uncompressedBuffer[i] / (double)short.MaxValue; maxAmplitude = Math.Max(maxAmplitude, Math.Abs(sampleVal)); } double dB = Math.Min(20 * Math.Log10(maxAmplitude), 0.0); LastdB = dB; LastAmplitude = maxAmplitude; bool allowEnqueue = overrideSound != null; if (GameMain.WindowActive && SettingsMenu.Instance is null) { bool usingLocalMode = PlayerInput.KeyDown(InputType.LocalVoice); bool usingRadioMode = PlayerInput.KeyDown(InputType.RadioVoice); if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Activity) { bool pttDown = (usingLocalMode || usingRadioMode) && GUI.KeyboardDispatcher.Subscriber == null; if (pttDown) { ForceLocal = usingLocalMode; } //in Activity mode, we default to the active mode UNLESS a specific ptt key is held else { ForceLocal = GameMain.ActiveChatMode == ChatMode.Local; } if (dB > GameSettings.CurrentConfig.Audio.NoiseGateThreshold) { allowEnqueue = true; } } else if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.PushToTalk) { //in push-to-talk mode, InputType.Voice uses the active chat mode bool usingActiveMode = PlayerInput.KeyDown(InputType.Voice); bool pttDown = (usingActiveMode || usingLocalMode || usingRadioMode) && GUI.KeyboardDispatcher.Subscriber == null; if (pttDown) { ForceLocal = (usingActiveMode && GameMain.ActiveChatMode == ChatMode.Local) || usingLocalMode; allowEnqueue = true; } } } if (Screen.Selected is ModDownloadScreen) { allowEnqueue = false; captureTimer = 0; } if (allowEnqueue || captureTimer > 0) { LastEnqueueAudio = DateTime.Now; if (GameMain.Client?.Character != null) { var messageType = !ForceLocal && ChatMessage.CanUseRadio(GameMain.Client.Character, out _) ? ChatMessageType.Radio : ChatMessageType.Default; if (GameMain.Client.Character.IsDead) { messageType = ChatMessageType.Dead; } GameMain.Client.Character.ShowTextlessSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); } //encode audio and enqueue it lock (buffers) { if (!prevCaptured) //enqueue the previous buffer if not sent to avoid cutoff { int compressedCountPrev = encoder.Encode(prevUncompressedBuffer, 0, VoipConfig.BUFFER_SIZE, BufferToQueue, 0, VoipConfig.MAX_COMPRESSED_SIZE); EnqueueBuffer(compressedCountPrev); } int compressedCount = encoder.Encode(uncompressedBuffer, 0, VoipConfig.BUFFER_SIZE, BufferToQueue, 0, VoipConfig.MAX_COMPRESSED_SIZE); EnqueueBuffer(compressedCount); } captureTimer -= (VoipConfig.BUFFER_SIZE * 1000) / VoipConfig.FREQUENCY; if (allowEnqueue) { captureTimer = GameSettings.CurrentConfig.Audio.VoiceChatCutoffPrevention; } prevCaptured = true; } else { captureTimer = 0; prevCaptured = false; //enqueue silence lock (buffers) { EnqueueBuffer(0); } } } } catch (Exception e) { DebugConsole.ThrowError($"VoipCapture threw an exception. Disabling capture...", e); capturing = false; } finally { Marshal.FreeHGlobal(nativeBuffer); } } private Sound overrideSound; private int overridePos; private short[] overrideBuf = new short[VoipConfig.BUFFER_SIZE]; private void FillBuffer() { if (overrideSound != null) { int totalSampleCount = 0; while (totalSampleCount < VoipConfig.BUFFER_SIZE) { int sampleCount = overrideSound.FillStreamBuffer(overridePos, overrideBuf); overridePos += sampleCount * 2; Array.Copy(overrideBuf, 0, uncompressedBuffer, totalSampleCount, sampleCount); totalSampleCount += sampleCount; if (sampleCount == 0) { overridePos = 0; } } int sleepMs = VoipConfig.BUFFER_SIZE * 800 / VoipConfig.FREQUENCY; Thread.Sleep(sleepMs - 1); } else { int sampleCount = 0; while (sampleCount < VoipConfig.BUFFER_SIZE) { Alc.GetInteger(captureDevice, Alc.EnumCaptureSamples, out sampleCount); int alcError = Alc.GetError(captureDevice); if (alcError != Alc.NoError) { throw new Exception("Failed to determine sample count: " + alcError.ToString()); } if (sampleCount < VoipConfig.BUFFER_SIZE) { int sleepMs = (VoipConfig.BUFFER_SIZE - sampleCount) * 800 / VoipConfig.FREQUENCY; if (sleepMs >= 1) { Thread.Sleep(sleepMs); } } if (!capturing) { return; } } Alc.CaptureSamples(captureDevice, nativeBuffer, VoipConfig.BUFFER_SIZE); Marshal.Copy(nativeBuffer, uncompressedBuffer, 0, uncompressedBuffer.Length); } } public void SetOverrideSound(string fileName) { overrideSound?.Dispose(); if (string.IsNullOrEmpty(fileName)) { overrideSound = null; } else { overrideSound = GameMain.SoundManager.LoadSound(fileName, true); } } public override void Dispose() { Instance = null; capturing = false; captureThread?.Join(); captureThread = null; if (captureDevice != IntPtr.Zero) { Alc.CaptureCloseDevice(captureDevice); } } } }