Client LuaCsForBarotrauma
VoipCapture.cs
1 using Barotrauma.Sounds;
2 using Concentus.Structs;
3 using Microsoft.Xna.Framework;
4 using OpenAL;
5 using System;
6 using System.Collections.Generic;
7 using System.Linq;
8 using System.Runtime.InteropServices;
9 using System.Threading;
10 
11 namespace Barotrauma.Networking
12 {
13  class VoipCapture : VoipQueue, IDisposable
14  {
15  public static VoipCapture Instance
16  {
17  get;
18  private set;
19  }
20 
21 
22  private readonly IntPtr captureDevice;
23 
24  private Thread captureThread;
25 
26  private bool capturing;
27 
28  private readonly OpusEncoder encoder;
29 
30  public double LastdB
31  {
32  get;
33  private set;
34  }
35 
36  public double LastAmplitude
37  {
38  get;
39  private set;
40  }
41 
42  public float Gain
43  {
44  get { return GameSettings.CurrentConfig.Audio.MicrophoneVolume; }
45  }
46 
47  public DateTime LastEnqueueAudio;
48 
49  public override byte QueueID
50  {
51  get
52  {
53  return GameMain.Client?.SessionId ?? 0;
54  }
55  protected set
56  {
57  //do nothing
58  }
59  }
60 
61  public readonly bool CanDetectDisconnect;
62 
63  public bool Disconnected { get; private set; }
64 
65  public static void Create(string deviceName, UInt16? storedBufferID = null)
66  {
67  if (Instance != null)
68  {
69  throw new Exception("Tried to instance more than one VoipCapture object");
70  }
71 
72  var capture = new VoipCapture(deviceName)
73  {
74  LatestBufferID = storedBufferID ?? BUFFER_COUNT - 1
75  };
76  if (capture.captureDevice != IntPtr.Zero)
77  {
78  Instance = capture;
79  }
80  }
81 
82  private VoipCapture(string deviceName) : base(GameMain.Client?.SessionId ?? 0, true, false)
83  {
84  Disconnected = false;
85 
86  encoder = VoipConfig.CreateEncoder();
87 
88  //set up capture device
89  captureDevice = Alc.CaptureOpenDevice(deviceName, VoipConfig.FREQUENCY, Al.FormatMono16, VoipConfig.BUFFER_SIZE * 5);
90 
91  if (captureDevice == IntPtr.Zero)
92  {
93  DebugConsole.NewMessage("Alc.CaptureOpenDevice attempt 1 failed: error code " + Alc.GetError(IntPtr.Zero).ToString(), Color.Orange);
94  //attempt using a smaller buffer size
95  captureDevice = Alc.CaptureOpenDevice(deviceName, VoipConfig.FREQUENCY, Al.FormatMono16, VoipConfig.BUFFER_SIZE * 2);
96  }
97 
98  if (captureDevice == IntPtr.Zero)
99  {
100  DebugConsole.NewMessage("Alc.CaptureOpenDevice attempt 2 failed: error code " + Alc.GetError(IntPtr.Zero).ToString(), Color.Orange);
101  //attempt using the default device
102  captureDevice = Alc.CaptureOpenDevice("", VoipConfig.FREQUENCY, Al.FormatMono16, VoipConfig.BUFFER_SIZE * 2);
103  }
104 
105  if (captureDevice == IntPtr.Zero)
106  {
107  string errorCode = Alc.GetError(IntPtr.Zero).ToString();
108  if (!GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "capturedevicenotfound"))
109  {
110  //GUI.SettingsMenuOpen = false;
111  new GUIMessageBox(TextManager.Get("Error"),
112  (TextManager.Get("VoipCaptureDeviceNotFound").Fallback("Could not start voice capture, suitable capture device not found.")) + " (" + errorCode + ")")
113  {
114  UserData = "capturedevicenotfound"
115  };
116  }
117  GameAnalyticsManager.AddErrorEventOnce("Alc.CaptureDeviceOpenFailed", GameAnalyticsManager.ErrorSeverity.Error,
118  "Alc.CaptureDeviceOpen(" + deviceName + ") failed. Error code: " + errorCode);
119  var config = GameSettings.CurrentConfig;
120  config.Audio.VoiceSetting = VoiceMode.Disabled;
121  GameSettings.SetCurrentConfig(config);
122  Instance?.Dispose();
123  Instance = null;
124  return;
125  }
126 
127  int alError = Al.GetError();
128  int alcError = Alc.GetError(captureDevice);
129  if (alcError != Alc.NoError)
130  {
131  throw new Exception("Failed to open capture device: " + alcError.ToString() + " (ALC)");
132  }
133  if (alError != Al.NoError)
134  {
135  throw new Exception("Failed to open capture device: " + alError.ToString() + " (AL)");
136  }
137 
138  CanDetectDisconnect = Alc.IsExtensionPresent(captureDevice, "ALC_EXT_disconnect");
139  alcError = Alc.GetError(captureDevice);
140  if (alcError != Alc.NoError)
141  {
142  throw new Exception("Error determining if disconnect can be detected: " + alcError.ToString());
143  }
144 
145  Alc.CaptureStart(captureDevice);
146  alcError = Alc.GetError(captureDevice);
147  if (alcError != Alc.NoError)
148  {
149  throw new Exception("Failed to start capturing: " + alcError.ToString());
150  }
151 
152  capturing = true;
153  captureThread = new Thread(UpdateCapture)
154  {
155  IsBackground = true,
156  Name = "VoipCapture"
157  };
158  captureThread.Start();
159  }
160 
161  public static void ChangeCaptureDevice(string deviceName)
162  {
163  if (Instance == null) { return; }
164 
165  UInt16 storedBufferID = Instance.LatestBufferID;
166  Instance.Dispose();
167  Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID);
168  }
169 
170  public static IReadOnlyList<string> GetCaptureDeviceNames()
171  {
172  return Alc.GetStringList(IntPtr.Zero, OpenAL.Alc.CaptureDeviceSpecifier);
173  }
174 
175  IntPtr nativeBuffer;
176  readonly short[] uncompressedBuffer = new short[VoipConfig.BUFFER_SIZE];
177  readonly short[] prevUncompressedBuffer = new short[VoipConfig.BUFFER_SIZE];
178  bool prevCaptured = true;
179  int captureTimer;
180 
181  private void UpdateCapture()
182  {
183  Array.Copy(uncompressedBuffer, 0, prevUncompressedBuffer, 0, VoipConfig.BUFFER_SIZE);
184  Array.Clear(uncompressedBuffer, 0, VoipConfig.BUFFER_SIZE);
185  nativeBuffer = Marshal.AllocHGlobal(VoipConfig.BUFFER_SIZE * 2);
186  try
187  {
188  while (capturing)
189  {
190  int alcError;
191 
193  {
194  Alc.GetInteger(captureDevice, Alc.EnumConnected, out int isConnected);
195  alcError = Alc.GetError(captureDevice);
196  if (alcError != Alc.NoError)
197  {
198  throw new Exception("Failed to determine if capture device is connected: " + alcError.ToString());
199  }
200 
201  if (isConnected == 0)
202  {
203  DebugConsole.ThrowError("Capture device has been disconnected. You can select another available device in the settings.");
204  Disconnected = true;
205  break;
206  }
207  }
208 
209  FillBuffer();
210 
211  alcError = Alc.GetError(captureDevice);
212  if (alcError != Alc.NoError)
213  {
214  throw new Exception("Failed to capture samples: " + alcError.ToString());
215  }
216 
217  double maxAmplitude = 0.0f;
218  for (int i = 0; i < VoipConfig.BUFFER_SIZE; i++)
219  {
220  uncompressedBuffer[i] = (short)MathHelper.Clamp((uncompressedBuffer[i] * Gain), -short.MaxValue, short.MaxValue);
221  double sampleVal = uncompressedBuffer[i] / (double)short.MaxValue;
222  maxAmplitude = Math.Max(maxAmplitude, Math.Abs(sampleVal));
223  }
224  double dB = Math.Min(20 * Math.Log10(maxAmplitude), 0.0);
225 
226  LastdB = dB;
227  LastAmplitude = maxAmplitude;
228 
229  bool allowEnqueue = overrideSound != null;
230  if (GameMain.WindowActive && SettingsMenu.Instance is null)
231  {
232  bool usingLocalMode = PlayerInput.KeyDown(InputType.LocalVoice);
233  bool usingRadioMode = PlayerInput.KeyDown(InputType.RadioVoice);
234  if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Activity)
235  {
236  bool pttDown = (usingLocalMode || usingRadioMode) && GUI.KeyboardDispatcher.Subscriber == null;
237  if (pttDown)
238  {
239  ForceLocal = usingLocalMode;
240  }
241  //in Activity mode, we default to the active mode UNLESS a specific ptt key is held
242  else
243  {
244  ForceLocal = GameMain.ActiveChatMode == ChatMode.Local;
245  }
246  if (dB > GameSettings.CurrentConfig.Audio.NoiseGateThreshold)
247  {
248  allowEnqueue = true;
249  }
250  }
251  else if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.PushToTalk)
252  {
253  //in push-to-talk mode, InputType.Voice uses the active chat mode
254  bool usingActiveMode = PlayerInput.KeyDown(InputType.Voice);
255  bool pttDown = (usingActiveMode || usingLocalMode || usingRadioMode) && GUI.KeyboardDispatcher.Subscriber == null;
256  if (pttDown)
257  {
258  ForceLocal = (usingActiveMode && GameMain.ActiveChatMode == ChatMode.Local) || usingLocalMode;
259  allowEnqueue = true;
260  }
261  }
262  }
263 
264  if (Screen.Selected is ModDownloadScreen)
265  {
266  allowEnqueue = false;
267  captureTimer = 0;
268  }
269 
270  if (allowEnqueue || captureTimer > 0)
271  {
272  LastEnqueueAudio = DateTime.Now;
273  if (GameMain.Client?.Character != null)
274  {
275  var messageType = !ForceLocal && ChatMessage.CanUseRadio(GameMain.Client.Character, out _) ? ChatMessageType.Radio : ChatMessageType.Default;
276  if (GameMain.Client.Character.IsDead) { messageType = ChatMessageType.Dead; }
277 
278  GameMain.Client.Character.ShowTextlessSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]);
279  }
280  //encode audio and enqueue it
281  lock (buffers)
282  {
283  if (!prevCaptured) //enqueue the previous buffer if not sent to avoid cutoff
284  {
285  int compressedCountPrev = encoder.Encode(prevUncompressedBuffer, 0, VoipConfig.BUFFER_SIZE, BufferToQueue, 0, VoipConfig.MAX_COMPRESSED_SIZE);
286  EnqueueBuffer(compressedCountPrev);
287  }
288  int compressedCount = encoder.Encode(uncompressedBuffer, 0, VoipConfig.BUFFER_SIZE, BufferToQueue, 0, VoipConfig.MAX_COMPRESSED_SIZE);
289  EnqueueBuffer(compressedCount);
290  }
291  captureTimer -= (VoipConfig.BUFFER_SIZE * 1000) / VoipConfig.FREQUENCY;
292  if (allowEnqueue)
293  {
294  captureTimer = GameSettings.CurrentConfig.Audio.VoiceChatCutoffPrevention;
295  }
296  prevCaptured = true;
297  }
298  else
299  {
300  captureTimer = 0;
301  prevCaptured = false;
302  //enqueue silence
303  lock (buffers)
304  {
305  EnqueueBuffer(0);
306  }
307  }
308  }
309  }
310  catch (Exception e)
311  {
312  DebugConsole.ThrowError($"VoipCapture threw an exception. Disabling capture...", e);
313  capturing = false;
314  }
315  finally
316  {
317  Marshal.FreeHGlobal(nativeBuffer);
318  }
319  }
320 
321  private Sound overrideSound;
322  private int overridePos;
323  private readonly short[] overrideBuf = new short[VoipConfig.BUFFER_SIZE];
324 
325  private void FillBuffer()
326  {
327  if (overrideSound != null)
328  {
329  int totalSampleCount = 0;
330  while (totalSampleCount < VoipConfig.BUFFER_SIZE)
331  {
332  int sampleCount = overrideSound.FillStreamBuffer(overridePos, overrideBuf);
333  overridePos += sampleCount * 2;
334  Array.Copy(overrideBuf, 0, uncompressedBuffer, totalSampleCount, Math.Min(sampleCount, uncompressedBuffer.Length - totalSampleCount));
335  totalSampleCount += sampleCount;
336 
337  if (sampleCount == 0)
338  {
339  overridePos = 0;
340  }
341  }
342  int sleepMs = VoipConfig.BUFFER_SIZE * 800 / VoipConfig.FREQUENCY;
343  Thread.Sleep(sleepMs - 1);
344  }
345  else
346  {
347  int sampleCount = 0;
348 
349  while (sampleCount < VoipConfig.BUFFER_SIZE)
350  {
351  Alc.GetInteger(captureDevice, Alc.EnumCaptureSamples, out sampleCount);
352 
353  int alcError = Alc.GetError(captureDevice);
354  if (alcError != Alc.NoError)
355  {
356  throw new Exception("Failed to determine sample count: " + alcError.ToString());
357  }
358 
359  if (sampleCount < VoipConfig.BUFFER_SIZE)
360  {
361  int sleepMs = (VoipConfig.BUFFER_SIZE - sampleCount) * 800 / VoipConfig.FREQUENCY;
362  if (sleepMs >= 1)
363  {
364  Thread.Sleep(sleepMs);
365  }
366  }
367 
368  if (!capturing) { return; }
369  }
370 
371  Alc.CaptureSamples(captureDevice, nativeBuffer, VoipConfig.BUFFER_SIZE);
372  Marshal.Copy(nativeBuffer, uncompressedBuffer, 0, uncompressedBuffer.Length);
373  }
374  }
375 
376  public void SetOverrideSound(string fileName)
377  {
378  overrideSound?.Dispose();
379  if (string.IsNullOrEmpty(fileName))
380  {
381  overrideSound = null;
382  }
383  else
384  {
385  try
386  {
387  overrideSound = GameMain.SoundManager.LoadSound(fileName, true);
388  }
389  catch (Exception e)
390  {
391  DebugConsole.ThrowError($"Failed to load the sound {fileName}.", e);
392  }
393  }
394  }
395 
396  public override void Dispose()
397  {
398  Instance = null;
399  capturing = false;
400  captureThread?.Join();
401  captureThread = null;
402  if (captureDevice != IntPtr.Zero) { Alc.CaptureCloseDevice(captureDevice); }
403  }
404  }
405 }
static GameClient Client
Definition: GameMain.cs:188
static Sounds.SoundManager SoundManager
Definition: GameMain.cs:80
void SetOverrideSound(string fileName)
Definition: VoipCapture.cs:376
static void Create(string deviceName, UInt16? storedBufferID=null)
Definition: VoipCapture.cs:65
static IReadOnlyList< string > GetCaptureDeviceNames()
Definition: VoipCapture.cs:170
static void ChangeCaptureDevice(string deviceName)
Definition: VoipCapture.cs:161
void EnqueueBuffer(int length)
Definition: VoipQueue.cs:87
abstract int FillStreamBuffer(int samplePos, short[] buffer)
virtual void Dispose()
Definition: Sound.cs:158
Sound LoadSound(string filename, bool stream=false)
Definition: Al.cs:38
static int GetError()
const int FormatMono16
Definition: Al.cs:84
const int NoError
Definition: Al.cs:98
const int CaptureDeviceSpecifier
Definition: Alc.cs:108
const int EnumConnected
Definition: Alc.cs:111
static IReadOnlyList< string > GetStringList(IntPtr device, int param)
Definition: Alc.cs:225
static void CaptureSamples(IntPtr device, IntPtr buffer, int samples)
static int GetError(IntPtr device)
static void CaptureStart(IntPtr device)
static bool CaptureCloseDevice(IntPtr device)
static void GetInteger(IntPtr device, int param, out int data)
Definition: Alc.cs:259
const int EnumCaptureSamples
Definition: Alc.cs:110
const int NoError
Definition: Alc.cs:93
static bool IsExtensionPresent(IntPtr device, string extname)
Definition: Al.cs:36