2 using Concentus.Structs;
3 using Microsoft.Xna.Framework;
6 using System.Collections.Generic;
8 using System.Runtime.InteropServices;
9 using System.Threading;
22 private readonly IntPtr captureDevice;
24 private Thread captureThread;
26 private bool capturing;
28 private readonly OpusEncoder encoder;
44 get {
return GameSettings.CurrentConfig.Audio.MicrophoneVolume; }
65 public static void Create(
string deviceName, UInt16? storedBufferID =
null)
69 throw new Exception(
"Tried to instance more than one VoipCapture object");
76 if (capture.captureDevice != IntPtr.Zero)
86 encoder = VoipConfig.CreateEncoder();
89 captureDevice =
Alc.CaptureOpenDevice(deviceName, VoipConfig.FREQUENCY,
Al.
FormatMono16, VoipConfig.BUFFER_SIZE * 5);
91 if (captureDevice == IntPtr.Zero)
93 DebugConsole.NewMessage(
"Alc.CaptureOpenDevice attempt 1 failed: error code " +
Alc.
GetError(IntPtr.Zero).ToString(), Color.Orange);
95 captureDevice =
Alc.CaptureOpenDevice(deviceName, VoipConfig.FREQUENCY,
Al.
FormatMono16, VoipConfig.BUFFER_SIZE * 2);
98 if (captureDevice == IntPtr.Zero)
100 DebugConsole.NewMessage(
"Alc.CaptureOpenDevice attempt 2 failed: error code " +
Alc.
GetError(IntPtr.Zero).ToString(), Color.Orange);
102 captureDevice =
Alc.CaptureOpenDevice(
"", VoipConfig.FREQUENCY,
Al.
FormatMono16, VoipConfig.BUFFER_SIZE * 2);
105 if (captureDevice == IntPtr.Zero)
107 string errorCode =
Alc.
GetError(IntPtr.Zero).ToString();
108 if (!GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as
string ==
"capturedevicenotfound"))
111 new GUIMessageBox(TextManager.Get(
"Error"),
112 (TextManager.Get(
"VoipCaptureDeviceNotFound").Fallback(
"Could not start voice capture, suitable capture device not found.")) +
" (" + errorCode +
")")
114 UserData =
"capturedevicenotfound"
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);
131 throw new Exception(
"Failed to open capture device: " + alcError.ToString() +
" (ALC)");
135 throw new Exception(
"Failed to open capture device: " + alError.ToString() +
" (AL)");
142 throw new Exception(
"Error determining if disconnect can be detected: " + alcError.ToString());
149 throw new Exception(
"Failed to start capturing: " + alcError.ToString());
153 captureThread =
new Thread(UpdateCapture)
158 captureThread.Start();
167 Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID);
176 readonly
short[] uncompressedBuffer =
new short[VoipConfig.BUFFER_SIZE];
177 readonly
short[] prevUncompressedBuffer =
new short[VoipConfig.BUFFER_SIZE];
178 bool prevCaptured =
true;
181 private void UpdateCapture()
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);
198 throw new Exception(
"Failed to determine if capture device is connected: " + alcError.ToString());
201 if (isConnected == 0)
203 DebugConsole.ThrowError(
"Capture device has been disconnected. You can select another available device in the settings.");
214 throw new Exception(
"Failed to capture samples: " + alcError.ToString());
217 double maxAmplitude = 0.0f;
218 for (
int i = 0; i < VoipConfig.BUFFER_SIZE; i++)
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));
224 double dB = Math.Min(20 * Math.Log10(maxAmplitude), 0.0);
229 bool allowEnqueue = overrideSound !=
null;
230 if (GameMain.WindowActive && SettingsMenu.Instance is
null)
232 bool usingLocalMode = PlayerInput.KeyDown(
InputType.LocalVoice);
233 bool usingRadioMode = PlayerInput.KeyDown(
InputType.RadioVoice);
234 if (GameSettings.CurrentConfig.Audio.VoiceSetting ==
VoiceMode.Activity)
236 bool pttDown = (usingLocalMode || usingRadioMode) && GUI.KeyboardDispatcher.Subscriber ==
null;
246 if (dB > GameSettings.CurrentConfig.Audio.NoiseGateThreshold)
251 else if (GameSettings.CurrentConfig.Audio.VoiceSetting ==
VoiceMode.PushToTalk)
254 bool usingActiveMode = PlayerInput.KeyDown(
InputType.Voice);
255 bool pttDown = (usingActiveMode || usingLocalMode || usingRadioMode) && GUI.KeyboardDispatcher.Subscriber ==
null;
258 ForceLocal = (usingActiveMode && GameMain.ActiveChatMode ==
ChatMode.Local) || usingLocalMode;
264 if (Screen.Selected is ModDownloadScreen)
266 allowEnqueue =
false;
270 if (allowEnqueue || captureTimer > 0)
273 if (GameMain.Client?.Character !=
null)
276 if (GameMain.Client.Character.IsDead) { messageType =
ChatMessageType.Dead; }
278 GameMain.Client.Character.ShowTextlessSpeechBubble(1.25f,
ChatMessage.MessageColor[(
int)messageType]);
285 int compressedCountPrev = encoder.Encode(prevUncompressedBuffer, 0, VoipConfig.BUFFER_SIZE,
BufferToQueue, 0, VoipConfig.MAX_COMPRESSED_SIZE);
288 int compressedCount = encoder.Encode(uncompressedBuffer, 0, VoipConfig.BUFFER_SIZE,
BufferToQueue, 0, VoipConfig.MAX_COMPRESSED_SIZE);
291 captureTimer -= (VoipConfig.BUFFER_SIZE * 1000) / VoipConfig.FREQUENCY;
294 captureTimer = GameSettings.CurrentConfig.Audio.VoiceChatCutoffPrevention;
301 prevCaptured =
false;
312 DebugConsole.ThrowError($
"VoipCapture threw an exception. Disabling capture...", e);
317 Marshal.FreeHGlobal(nativeBuffer);
321 private Sound overrideSound;
322 private int overridePos;
323 private readonly
short[] overrideBuf =
new short[VoipConfig.BUFFER_SIZE];
325 private void FillBuffer()
327 if (overrideSound !=
null)
329 int totalSampleCount = 0;
330 while (totalSampleCount < VoipConfig.BUFFER_SIZE)
333 overridePos += sampleCount * 2;
334 Array.Copy(overrideBuf, 0, uncompressedBuffer, totalSampleCount, Math.Min(sampleCount, uncompressedBuffer.Length - totalSampleCount));
335 totalSampleCount += sampleCount;
337 if (sampleCount == 0)
342 int sleepMs = VoipConfig.BUFFER_SIZE * 800 / VoipConfig.FREQUENCY;
343 Thread.Sleep(sleepMs - 1);
349 while (sampleCount < VoipConfig.BUFFER_SIZE)
356 throw new Exception(
"Failed to determine sample count: " + alcError.ToString());
359 if (sampleCount < VoipConfig.BUFFER_SIZE)
361 int sleepMs = (VoipConfig.BUFFER_SIZE - sampleCount) * 800 / VoipConfig.FREQUENCY;
364 Thread.Sleep(sleepMs);
368 if (!capturing) {
return; }
372 Marshal.Copy(nativeBuffer, uncompressedBuffer, 0, uncompressedBuffer.Length);
379 if (
string.IsNullOrEmpty(fileName))
381 overrideSound =
null;
391 DebugConsole.ThrowError($
"Failed to load the sound {fileName}.", e);
400 captureThread?.Join();
401 captureThread =
null;
static Sounds.SoundManager SoundManager
void SetOverrideSound(string fileName)
static void Create(string deviceName, UInt16? storedBufferID=null)
static IReadOnlyList< string > GetCaptureDeviceNames()
static void ChangeCaptureDevice(string deviceName)
readonly bool CanDetectDisconnect
DateTime LastEnqueueAudio
static VoipCapture Instance
void EnqueueBuffer(int length)
abstract int FillStreamBuffer(int samplePos, short[] buffer)
Sound LoadSound(string filename, bool stream=false)
const int CaptureDeviceSpecifier
static IReadOnlyList< string > GetStringList(IntPtr device, int param)
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)
const int EnumCaptureSamples
static bool IsExtensionPresent(IntPtr device, string extname)