399 lines
16 KiB
C#
399 lines
16 KiB
C#
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<string> 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); }
|
|
}
|
|
}
|
|
}
|