• R/O
  • SSH

提交

标签
No Tags

Frequently used words (click to add to your profile)

javac++androidlinuxc#windowsobjective-ccocoa誰得qtpythonphprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

This is a fork of Zandronum used on servers hosted by The Sentinels Playground (TSPG), Euroboros (EB), and Down Under Doomers (DUD).


Commit MetaInfo

修订版d44f57100df3e673f6364879cfe53190aa92cf0d (tree)
时间2023-09-04 01:33:20
作者Adam Kaminski <kaminskiadam9@gmai...>
CommiterAdam Kaminski

Log Message

Added support for voice chat:
- Audio is encoded and decoded using Opus, allowing it to be transmitted over the network with minimal bandwidth usage and decent quality.
- Noise suppression, using RNNoise, cleans the audio of any unwanted noise before it's sent to the server.
- Includes the option to transmit VoIP audio packets using either push-to-talk or voice activity. For the latter, the sensitivity, in decibels, can be adjusted to suit the user's needs.

更改概述

差异

diff -r e3c6f5883952 -r d44f57100df3 CMakeLists.txt
--- a/CMakeLists.txt Sun Sep 03 12:19:24 2023 -0400
+++ b/CMakeLists.txt Sun Sep 03 12:33:20 2023 -0400
@@ -251,6 +251,11 @@
251251 add_subdirectory( wadsrc_st )
252252 add_subdirectory( src )
253253
254+# [AK] Library for noise suppression for VoIP.
255+if ( NOT NO_SOUND )
256+ add_subdirectory( rnnoise )
257+endif ( NOT NO_SOUND )
258+
254259 if( NOT WIN32 AND NOT APPLE )
255260 # [BB] We don't need output_sdl (only used for sound), if we are just building the server.
256261 if ( NOT SERVERONLY )
diff -r e3c6f5883952 -r d44f57100df3 FindOpus.cmake
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/FindOpus.cmake Sun Sep 03 12:33:20 2023 -0400
@@ -0,0 +1,21 @@
1+# [AK] Find Opus
2+# Find the native Opus includes and library.
3+#
4+# OPUS_INCLUDE_DIR - Where to find opus.h.
5+# OPUS_LIBRARIES - List of libraries when using Opus.
6+# OPUS_FOUND - True if Opus found.
7+
8+IF ( OPUS_INCLUDE_DIR AND OPUS_LIBRARIES )
9+ # Already in cache, be silent.
10+ SET( OPUS_FIND_QUIETLY TRUE )
11+ENDIF ( OPUS_INCLUDE_DIR AND OPUS_LIBRARIES )
12+
13+FIND_PATH( OPUS_INCLUDE_DIR opus.h PATH_SUFFIXES opus )
14+
15+FIND_LIBRARY( OPUS_LIBRARIES NAMES opus libopus )
16+MARK_AS_ADVANCED( OPUS_LIBRARIES OPUS_INCLUDE_DIR )
17+
18+# Handle the QUIETLY and REQUIRED arguments and set OPUS_FOUND to TRUE if
19+# all listed variables are TRUE.
20+INCLUDE( FindPackageHandleStandardArgs )
21+FIND_PACKAGE_HANDLE_STANDARD_ARGS( Opus DEFAULT_MSG OPUS_LIBRARIES OPUS_INCLUDE_DIR )
diff -r e3c6f5883952 -r d44f57100df3 docs/zandronum-history.txt
--- a/docs/zandronum-history.txt Sun Sep 03 12:19:24 2023 -0400
+++ b/docs/zandronum-history.txt Sun Sep 03 12:33:20 2023 -0400
@@ -18,6 +18,7 @@
1818 *+ - Added the new AUTHINFO lump, allowing modders to define their own list of lumps to be authenticated. [Kaminsky]
1919 *+ - Revamped the scoreboard, and added the new SCORINFO lump that allows full customization of the scoreboard. [Kaminsky]
2020 *+ - Added support for custom vote types using the new VOTEINFO lump. [Dusk]
21+*+ - Added support for voice chat. Audio is encoded and decoded using Opus, allowing it to be transmitted over the network with minimal bandwidth usage and decent quality. Any unwanted noise in the audio is also removed with RNNoise before it's sent to the server. [Kaminsky]
2122 + - Added the +NOMORPHLIMITATIONS flag, allowing morphs to switch weapons, play sounds, and be affected by speed powerups. [geNia/Binary]
2223 + - Added CVars: "con_interpolate" and "con_speed" which interpolates and controls how fast the console moves. Based on featues from ZCC. [Kaminsky]
2324 + - Added ACS functions: "GetCurrentMapPosition", "GetEventResult", "GetActorSectorLocation", and "ChangeTeamScore". [Kaminsky]
diff -r e3c6f5883952 -r d44f57100df3 gzdoom.vcproj
--- a/gzdoom.vcproj Sun Sep 03 12:19:24 2023 -0400
+++ b/gzdoom.vcproj Sun Sep 03 12:33:20 2023 -0400
@@ -1201,6 +1201,10 @@
12011201 >
12021202 </File>
12031203 <File
1204+ RelativePath=".\src\voicechat.cpp"
1205+ >
1206+ </File>
1207+ <File
12041208 RelativePath=".\src\w_wad.cpp"
12051209 >
12061210 </File>
@@ -1850,6 +1854,10 @@
18501854 >
18511855 </File>
18521856 <File
1857+ RelativePath=".\src\voicechat.h"
1858+ >
1859+ </File>
1860+ <File
18531861 RelativePath=".\src\w_wad.h"
18541862 >
18551863 </File>
diff -r e3c6f5883952 -r d44f57100df3 rnnoise/CMakeLists.txt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rnnoise/CMakeLists.txt Sun Sep 03 12:33:20 2023 -0400
@@ -0,0 +1,3 @@
1+cmake_minimum_required( VERSION 2.4 )
2+
3+add_library( rnnoise denoise.c rnn.c rnn_data.c rnn_reader.c pitch.c kiss_fft.c celt_lpc.c )
diff -r e3c6f5883952 -r d44f57100df3 src/CMakeLists.txt
--- a/src/CMakeLists.txt Sun Sep 03 12:19:24 2023 -0400
+++ b/src/CMakeLists.txt Sun Sep 03 12:33:20 2023 -0400
@@ -671,6 +671,13 @@
671671 set ( ZDOOM_LIBS ${ZDOOM_LIBS} crypt32 )
672672 endif ( WIN32 )
673673
674+# [AK] We need Opus for encoding/decoding VoIP audio packets, and RNNoise for noise suppression.
675+if ( NOT NO_SOUND )
676+ find_package( Opus REQUIRED )
677+ include_directories( ${OPUS_INCLUDE_DIR} )
678+ set( ZDOOM_LIBS ${ZDOOM_LIBS} ${OPUS_LIBRARIES} rnnoise )
679+endif ( NOT NO_SOUND )
680+
674681 if( NOT DYN_FLUIDSYNTH)
675682 if( FLUIDSYNTH_FOUND )
676683 set( ZDOOM_LIBS ${ZDOOM_LIBS} "${FLUIDSYNTH_LIBRARIES}" )
@@ -1245,6 +1252,7 @@
12451252 v_pfx.cpp
12461253 v_text.cpp
12471254 v_video.cpp
1255+ voicechat.cpp #ZA
12481256 w_wad.cpp
12491257 wi_stuff.cpp
12501258 za_database.cpp #ZA
@@ -1464,6 +1472,7 @@
14641472 xlat
14651473 ../gdtoa
14661474 ../dumb/include
1475+ ../rnnoise/ #ZA
14671476 ../sqlite/ #ZA
14681477 ../upnpnat/ #ST
14691478 ${CMAKE_BINARY_DIR}/gdtoa
diff -r e3c6f5883952 -r d44f57100df3 src/c_dispatch.cpp
--- a/src/c_dispatch.cpp Sun Sep 03 12:19:24 2023 -0400
+++ b/src/c_dispatch.cpp Sun Sep 03 12:33:20 2023 -0400
@@ -143,7 +143,8 @@
143143 Button_User1, Button_User2, Button_User3, Button_User4,
144144 Button_AM_PanLeft, Button_AM_PanRight, Button_AM_PanDown, Button_AM_PanUp,
145145 Button_AM_ZoomIn, Button_AM_ZoomOut,
146- Button_ShowMedals; // [BC] Added the "show medals" button.
146+ Button_ShowMedals, // [BC] Added the "show medals" button.
147+ Button_VoiceRecord; // [AK] Added the "voicerecord" button.
147148
148149
149150 bool ParsingKeyConf, UnsafeExecutionContext;
@@ -173,7 +174,8 @@
173174
174175 FActionMap ActionMaps[] =
175176 {
176- { &Button_ShowMedals, 0x03fe31c3, "showmedals" }, // [BC] New "show medals" button.
177+ { &Button_ShowMedals, 0x03fe31c3, "showmedals" }, // [BC] New "show medals" button.
178+ { &Button_VoiceRecord, 0x0719c77f, "voicerecord" }, // [AK] Added the "voicerecord" button.
177179 { &Button_AM_PanLeft, 0x0d52d67b, "am_panleft"},
178180 { &Button_User2, 0x125f5226, "user2" },
179181 { &Button_Jump, 0x1eefa611, "jump" },
diff -r e3c6f5883952 -r d44f57100df3 src/c_dispatch.h
--- a/src/c_dispatch.h Sun Sep 03 12:19:24 2023 -0400
+++ b/src/c_dispatch.h Sun Sep 03 12:33:20 2023 -0400
@@ -203,7 +203,8 @@
203203 Button_User1, Button_User2, Button_User3, Button_User4,
204204 Button_AM_PanLeft, Button_AM_PanRight, Button_AM_PanDown, Button_AM_PanUp,
205205 Button_AM_ZoomIn, Button_AM_ZoomOut,
206- Button_ShowMedals; // [BC] New "show medals" button.
206+ Button_ShowMedals, // [BC] New "show medals" button.
207+ Button_VoiceRecord; // [AK] Added the "voicerecord" button.
207208 extern bool ParsingKeyConf, UnsafeExecutionContext;
208209
209210 void ResetButtonTriggers (); // Call ResetTriggers for all buttons
diff -r e3c6f5883952 -r d44f57100df3 src/cl_commands.cpp
--- a/src/cl_commands.cpp Sun Sep 03 12:19:24 2023 -0400
+++ b/src/cl_commands.cpp Sun Sep 03 12:33:20 2023 -0400
@@ -896,3 +896,16 @@
896896 CLIENT_GetLocalBuffer( )->ByteStream.WriteString( cvarName );
897897 CLIENT_GetLocalBuffer( )->ByteStream.WriteString( cvarValue );
898898 }
899+
900+//*****************************************************************************
901+// [AK]
902+void CLIENTCOMMANDS_VoIPAudioPacket( const unsigned int frame, const unsigned char *data, const unsigned int length )
903+{
904+ if (( data == nullptr ) || ( length == 0 ))
905+ return;
906+
907+ CLIENT_GetLocalBuffer( )->ByteStream.WriteByte( CLC_VOIPAUDIOPACKET );
908+ CLIENT_GetLocalBuffer( )->ByteStream.WriteLong( frame );
909+ CLIENT_GetLocalBuffer( )->ByteStream.WriteByte( length );
910+ CLIENT_GetLocalBuffer( )->ByteStream.WriteBuffer( data, length );
911+}
diff -r e3c6f5883952 -r d44f57100df3 src/cl_commands.h
--- a/src/cl_commands.h Sun Sep 03 12:19:24 2023 -0400
+++ b/src/cl_commands.h Sun Sep 03 12:33:20 2023 -0400
@@ -115,5 +115,6 @@
115115 void CLIENTCOMMANDS_SetWantHideAccount( bool wantHideCountry );
116116 void CLIENTCOMMANDS_SetVideoResolution();
117117 void CLIENTCOMMANDS_RCONSetCVar( const char *cvarName, const char *cvarValue );
118+void CLIENTCOMMANDS_VoIPAudioPacket( const unsigned int frame, const unsigned char *data, const unsigned int length );
118119
119120 #endif // __CL_COMMANDS_H__
diff -r e3c6f5883952 -r d44f57100df3 src/cl_main.cpp
--- a/src/cl_main.cpp Sun Sep 03 12:19:24 2023 -0400
+++ b/src/cl_main.cpp Sun Sep 03 12:33:20 2023 -0400
@@ -130,6 +130,7 @@
130130 #include "v_text.h"
131131 #include "maprotation.h"
132132 #include "st_hud.h"
133+#include "voicechat.h"
133134
134135 //*****************************************************************************
135136 // MISC CRAP THAT SHOULDN'T BE HERE BUT HAS TO BE BECAUSE OF SLOPPY CODING
@@ -199,6 +200,7 @@
199200 // [BB] Does not work with the latest ZDoom changes. Check if it's still necessary.
200201 //static void client_SetPlayerPieces( BYTESTREAM_s *pByteStream );
201202 static void client_IgnorePlayer( BYTESTREAM_s *pByteStream );
203+static void client_PlayerVoIPAudioPacket( BYTESTREAM_s *byteStream );
202204
203205 // Game commands.
204206 static void client_SetGameMode( BYTESTREAM_s *pByteStream );
@@ -429,6 +431,9 @@
429431
430432 // [AK] Clear out saved chat messages from the players.
431433 CHAT_ClearChatMessages( ulIdx );
434+
435+ // [AK] Delete this player's VoIP channel if it exists.
436+ VOIPController::GetInstance( ).RemoveVoIPChannel( ulIdx );
432437 }
433438
434439 // [AK] Also clear out saved chat messages from the server.
@@ -1908,6 +1913,11 @@
19081913 client_IgnorePlayer( pByteStream );
19091914 break;
19101915
1916+ case SVC_PLAYERVOIPAUDIOPACKET:
1917+
1918+ client_PlayerVoIPAudioPacket( pByteStream );
1919+ break;
1920+
19111921 case SVC_EXTENDEDCOMMAND:
19121922 {
19131923 const LONG lExtCommand = pByteStream->ReadByte();
@@ -4144,6 +4154,8 @@
41444154 // [CK] We do compressed bitfields now.
41454155 else if ( name == NAME_CL_ClientFlags )
41464156 player->userinfo.ClientFlagsChanged ( value.ToLong() );
4157+ else if ( name == NAME_Voice_Enable )
4158+ player->userinfo.VoiceEnableChanged ( value.ToLong() );
41474159 else
41484160 {
41494161 FBaseCVar **cvarPointer = player->userinfo.CheckKey( name );
@@ -4562,6 +4574,9 @@
45624574 // Zero out all the player information.
45634575 PLAYER_ResetPlayerData( player );
45644576
4577+ // [AK] Delete this player's VoIP channel if it exists.
4578+ VOIPController::GetInstance( ).RemoveVoIPChannel( playerIndex );
4579+
45654580 // Refresh the HUD because this affects the number of players in the game.
45664581 HUD_ShouldRefreshBeforeRendering( );
45674582 }
@@ -5898,6 +5913,10 @@
58985913 Value.Int = pByteStream->ReadByte();
58995914 sv_allowprivatechat.ForceSet( Value, CVAR_Int );
59005915
5916+ // [AK] Read in, and set the value for sv_allowvoicechat.
5917+ Value.Int = pByteStream->ReadByte();
5918+ sv_allowvoicechat.ForceSet( Value, CVAR_Int );
5919+
59015920 // [AK] Read in, and set the value for sv_respawndelaytime.
59025921 Value.Float = pByteStream->ReadFloat();
59035922 sv_respawndelaytime.ForceSet( Value, CVAR_Float );
@@ -9185,6 +9204,20 @@
91859204
91869205 //*****************************************************************************
91879206 //
9207+static void client_PlayerVoIPAudioPacket( BYTESTREAM_s *byteStream )
9208+{
9209+ const unsigned int player = byteStream->ReadByte( );
9210+ const unsigned int frame = byteStream->ReadLong( );
9211+ const unsigned int length = byteStream->ReadByte( );
9212+ unsigned char *data = new unsigned char[length];
9213+
9214+ byteStream->ReadBuffer( data, length );
9215+ VOIPController::GetInstance( ).ReceiveAudioPacket( player, frame, data, length );
9216+ delete[] data;
9217+}
9218+
9219+//*****************************************************************************
9220+//
91889221 static void client_DoPusher( BYTESTREAM_s *pByteStream )
91899222 {
91909223 const ULONG ulType = pByteStream->ReadByte();
diff -r e3c6f5883952 -r d44f57100df3 src/d_netinfo.cpp
--- a/src/d_netinfo.cpp Sun Sep 03 12:19:24 2023 -0400
+++ b/src/d_netinfo.cpp Sun Sep 03 12:33:20 2023 -0400
@@ -66,6 +66,7 @@
6666 #include "gamemode.h"
6767 #include "team.h"
6868 #include "menu/menu.h"
69+#include "voicechat.h"
6970
7071 static FRandom pr_pickteam ("PickRandomTeam");
7172
@@ -102,6 +103,8 @@
102103 CVAR (Int, cl_connectiontype, 1, CVAR_USERINFO | CVAR_ARCHIVE);
103104 // [CK] Let the user control if they want clientside puffs or not.
104105 CVAR (Flag, cl_clientsidepuffs, cl_clientflags, CLIENTFLAGS_CLIENTSIDEPUFFS );
106+// [AK] Let the user decide whether voice chat is on/off and how to transmit audio.
107+CVAR (Int, voice_enable, VOICEMODE_PUSHTOTALK, CVAR_ARCHIVE | CVAR_NOSETBYACS | CVAR_USERINFO);
105108
106109 // [TP] Userinfo changes yet to be sent.
107110 static UserInfoChanges PendingUserinfoChanges;
@@ -616,6 +619,7 @@
616619 case NAME_CL_TicsPerUpdate: coninfo->TicsPerUpdateChanged(cl_ticsperupdate); break;
617620 case NAME_CL_ConnectionType: coninfo->ConnectionTypeChanged(cl_connectiontype); break;
618621 case NAME_CL_ClientFlags: coninfo->ClientFlagsChanged(cl_clientflags); break;
622+ case NAME_Voice_Enable: coninfo->VoiceEnableChanged(voice_enable); break;
619623
620624 // The rest do.
621625 default:
@@ -826,6 +830,18 @@
826830 return flags;
827831 }
828832
833+// [AK]
834+int userinfo_t::VoiceEnableChanged(int voiceenable)
835+{
836+ if ( (*this)[NAME_Voice_Enable] == nullptr )
837+ {
838+ Printf( "Error: No Voice_Enable key found!\n" );
839+ return 0;
840+ }
841+ *static_cast<FIntCVar *>((*this)[NAME_Voice_Enable]) = voiceenable;
842+ return voiceenable;
843+}
844+
829845 void D_UserInfoChanged (FBaseCVar *cvar)
830846 {
831847 UCVarValue val;
@@ -934,6 +950,19 @@
934950 return;
935951 }
936952 }
953+ // [AK]
954+ else if ( cvar == &voice_enable )
955+ {
956+ val = cvar->GetGenericRep( CVAR_Int );
957+ const int clampedValue = clamp<int>( val.Int, VOICEMODE_OFF, VOICEMODE_VOICEACTIVITY );
958+
959+ if ( val.Int != clampedValue )
960+ {
961+ val.Int = clampedValue;
962+ cvar->SetGenericRep( val, CVAR_Int );
963+ return;
964+ }
965+ }
937966
938967 val = cvar->GetGenericRep (CVAR_String);
939968 escaped_val = D_EscapeUserInfo(val.String);
@@ -1417,6 +1446,11 @@
14171446 info->ClientFlagsChanged ( atoi( value ) );
14181447 break;
14191448
1449+ // [AK]
1450+ case NAME_Voice_Enable:
1451+ info->VoiceEnableChanged ( atoi( value ) );
1452+ break;
1453+
14201454 default:
14211455 cvar_ptr = info->CheckKey(keyname);
14221456 if (cvar_ptr != NULL)
diff -r e3c6f5883952 -r d44f57100df3 src/d_player.h
--- a/src/d_player.h Sun Sep 03 12:19:24 2023 -0400
+++ b/src/d_player.h Sun Sep 03 12:33:20 2023 -0400
@@ -430,6 +430,8 @@
430430 int TicsPerUpdateChanged(int ticsperupdate);
431431 int ConnectionTypeChanged(int connectiontype);
432432 int ClientFlagsChanged(int flags);
433+ int VoiceEnableChanged(int voiceenable);
434+
433435 int GetRailColor() const
434436 {
435437 if ( CheckKey(NAME_RailColor) != NULL )
@@ -475,6 +477,15 @@
475477 return 0;
476478 }
477479 }
480+ int GetVoiceEnable() const
481+ {
482+ if ( CheckKey(NAME_Voice_Enable) != nullptr )
483+ return *static_cast<FIntCVar *>(*CheckKey(NAME_Voice_Enable));
484+ else {
485+ Printf( "Error: No Voice_Enable key found!\n" );
486+ return 0;
487+ }
488+ }
478489 };
479490
480491 void ReadUserInfo(FArchive &arc, userinfo_t &info, FString &skin);
diff -r e3c6f5883952 -r d44f57100df3 src/g_game.cpp
--- a/src/g_game.cpp Sun Sep 03 12:19:24 2023 -0400
+++ b/src/g_game.cpp Sun Sep 03 12:33:20 2023 -0400
@@ -112,6 +112,7 @@
112112 #include "p_3dmidtex.h"
113113 #include "a_lightning.h"
114114 #include "po_man.h"
115+#include "voicechat.h"
115116
116117 #include <zlib.h>
117118
@@ -1994,6 +1995,9 @@
19941995 break;
19951996 }
19961997
1998+ // [AK] Tick the VoIP controller.
1999+ VOIPController::GetInstance( ).Tick( );
2000+
19972001 // [BC] If any data has accumulated in our packet, send it out now.
19982002 if ( NETWORK_GetState( ) == NETSTATE_CLIENT )
19992003 CLIENT_EndTick( );
diff -r e3c6f5883952 -r d44f57100df3 src/namedef.h
--- a/src/namedef.h Sun Sep 03 12:19:24 2023 -0400
+++ b/src/namedef.h Sun Sep 03 12:33:20 2023 -0400
@@ -606,6 +606,8 @@
606606 xx(CL_ConnectionType)
607607 // [CK] Client flags for various booleans masked in a bitfield.
608608 xx(CL_ClientFlags)
609+// [AK] Let the user decide whether voice chat is on/off and how to transmit audio.
610+xx(Voice_Enable)
609611 // [BB] For the bot skill menu
610612 xx(BotSkillMenu)
611613 xx(ChooseBotSkill)
diff -r e3c6f5883952 -r d44f57100df3 src/network_enums.h
--- a/src/network_enums.h Sun Sep 03 12:19:24 2023 -0400
+++ b/src/network_enums.h Sun Sep 03 12:33:20 2023 -0400
@@ -121,6 +121,7 @@
121121 ENUM_ELEMENT ( SVC_RESETALLPLAYERSFRAGCOUNT ),
122122 ENUM_ELEMENT ( SVC_PLAYERISSPECTATOR ),
123123 ENUM_ELEMENT ( SVC_PLAYERSAY ),
124+ ENUM_ELEMENT ( SVC_PLAYERVOIPAUDIOPACKET ),
124125 ENUM_ELEMENT ( SVC_PLAYERTAUNT ),
125126 ENUM_ELEMENT ( SVC_PLAYERRESPAWNINVULNERABILITY ),
126127 ENUM_ELEMENT ( SVC_PLAYERUSEINVENTORY ),
@@ -446,6 +447,7 @@
446447 ENUM_ELEMENT( CLC_SETWANTHIDEACCOUNT ),
447448 ENUM_ELEMENT( CLC_SETVIDEORESOLUTION ),
448449 ENUM_ELEMENT( CLC_RCONSETCVAR ),
450+ ENUM_ELEMENT( CLC_VOIPAUDIOPACKET ),
449451
450452 ENUM_ELEMENT( NUM_CLIENT_COMMANDS )
451453 }
diff -r e3c6f5883952 -r d44f57100df3 src/sound/fmodsound.cpp
--- a/src/sound/fmodsound.cpp Sun Sep 03 12:19:24 2023 -0400
+++ b/src/sound/fmodsound.cpp Sun Sep 03 12:33:20 2023 -0400
@@ -65,6 +65,8 @@
6565 #include "v_palette.h"
6666 #include "cmdlib.h"
6767 #include "s_sound.h"
68+// [AK] New #includes.
69+#include "voicechat.h"
6870
6971 #if FMOD_VERSION > 0x42899 && FMOD_VERSION < 0x43600
7072 #error You are trying to compile with an unsupported version of FMOD.
@@ -1194,6 +1196,10 @@
11941196 Sys->set3DSettings(0.5f, 96.f, 1.f);
11951197 Sys->set3DRolloffCallback(RolloffCallback);
11961198 snd_sfxvolume.Callback ();
1199+
1200+ // [AK] Initialize the VoIP controller after initializing FMOD.
1201+ VOIPController::GetInstance( ).Init( Sys );
1202+
11971203 return true;
11981204 }
11991205
@@ -1238,6 +1244,9 @@
12381244 SfxReverbPlaceholder = NULL;
12391245 }
12401246
1247+ // [AK] Shut down the VoIP controller.
1248+ VOIPController::GetInstance( ).Shutdown( );
1249+
12411250 Sys->close();
12421251 if (OutputPlugin != 0)
12431252 {
@@ -2233,6 +2242,7 @@
22332242 FMOD_VECTOR pos, vel;
22342243 FMOD_VECTOR forward;
22352244 FMOD_VECTOR up;
2245+ float sfxPitch = 0.0f; // [AK]
22362246
22372247 if (!listener->valid)
22382248 {
@@ -2333,6 +2343,10 @@
23332343 }
23342344 }
23352345 }
2346+
2347+ // [AK] Update the pitch of the VoIP controller to match that of the SFX.
2348+ if ( PausableSfx->getPitch( &sfxPitch ) == FMOD_OK )
2349+ VOIPController::GetInstance( ).SetPitch( sfxPitch );
23362350 }
23372351
23382352 //==========================================================================
diff -r e3c6f5883952 -r d44f57100df3 src/sv_commands.cpp
--- a/src/sv_commands.cpp Sun Sep 03 12:19:24 2023 -0400
+++ b/src/sv_commands.cpp Sun Sep 03 12:33:20 2023 -0400
@@ -83,6 +83,8 @@
8383 #include "network/netcommand.h"
8484 #include "network/servercommands.h"
8585 #include "maprotation.h"
86+#include "voicechat.h"
87+#include "d_netinf.h"
8688
8789 CVAR (Bool, sv_showwarnings, false, CVAR_GLOBALCONFIG|CVAR_ARCHIVE)
8890
@@ -1213,6 +1215,47 @@
12131215
12141216 //*****************************************************************************
12151217 //
1218+void SERVERCOMMANDS_PlayerVoIPAudioPacket( ULONG player, unsigned int frame, unsigned char *data, unsigned int length, ULONG playerExtra, ServerCommandFlags flags )
1219+{
1220+ if (( sv_allowvoicechat == VOICECHAT_OFF ) || ( PLAYER_IsValidPlayer( player ) == false ) || ( data == nullptr ) || ( length == 0 ))
1221+ return;
1222+
1223+ // [AK] Potentially prevent spectators from talking to active players during LMS games.
1224+ const bool forbidVoiceChatToPlayers = GAMEMODE_IsClientForbiddenToChatToPlayers( player );
1225+
1226+ NetCommand command( SVC_PLAYERVOIPAUDIOPACKET );
1227+ command.addByte( player );
1228+ command.addLong( frame );
1229+ command.addByte( length );
1230+ command.addBuffer( data, length );
1231+
1232+ // [AK] We shouldn't care if a VoIP packet doesn't get received by the clients.
1233+ command.setUnreliable( true );
1234+
1235+ for ( ClientIterator it( playerExtra, flags ); it.notAtEnd( ); ++it )
1236+ {
1237+ // [AK] Don't broadcast to the same player that sent the VoIP packet,
1238+ // or any players that don't want to receive VoIP packets.
1239+ if (( *it == player ) || ( players[*it].userinfo.GetVoiceEnable( ) == VOICEMODE_OFF ))
1240+ continue;
1241+
1242+ // [AK] Don't broadcast to any live players if they're forbidden.
1243+ if (( forbidVoiceChatToPlayers ) && ( players[*it].bSpectating == false ))
1244+ continue;
1245+
1246+ // [AK] Don't broadcast to any player that aren't teammates if required.
1247+ if (( sv_allowvoicechat == VOICECHAT_TEAMMATESONLY ) && ( GAMEMODE_GetCurrentFlags( ) & GMF_PLAYERSONTEAMS ))
1248+ {
1249+ if ( PlayersAreTeammates( player, *it ) == false )
1250+ continue;
1251+ }
1252+
1253+ command.sendCommandToClients( *it, SVCF_ONLYTHISCLIENT );
1254+ }
1255+}
1256+
1257+//*****************************************************************************
1258+//
12161259 void SERVERCOMMANDS_PlayerTaunt( ULONG ulPlayer, ULONG ulPlayerExtra, ServerCommandFlags flags )
12171260 {
12181261 if ( PLAYER_IsValidPlayer( ulPlayer ) == false )
@@ -2400,6 +2443,8 @@
24002443 command.addByte( sv_limitcommands );
24012444 // [AK] Send sv_allowprivatechat.
24022445 command.addByte( sv_allowprivatechat );
2446+ // [AK] Send sv_allowvoicechat.
2447+ command.addByte( sv_allowvoicechat );
24032448 // [AK] Send sv_respawndelaytime.
24042449 command.addFloat( sv_respawndelaytime );
24052450 command.sendCommandToClients( ulPlayerExtra, flags );
diff -r e3c6f5883952 -r d44f57100df3 src/sv_commands.h
--- a/src/sv_commands.h Sun Sep 03 12:19:24 2023 -0400
+++ b/src/sv_commands.h Sun Sep 03 12:33:20 2023 -0400
@@ -134,6 +134,7 @@
134134 void SERVERCOMMANDS_PlayerIsSpectator( ULONG ulPlayer, ULONG ulPlayerExtra = MAXPLAYERS, ServerCommandFlags flags = 0 );
135135 void SERVERCOMMANDS_PlayerSay( ULONG ulPlayer, const char *pszString, ULONG ulMode, bool bForbidChatToPlayers, ULONG ulPlayerExtra = MAXPLAYERS, ServerCommandFlags flags = 0 );
136136 void SERVERCOMMANDS_PrivateSay( ULONG ulSender, ULONG ulReceiver, const char *pszString, bool bForbidChatToPlayers, ULONG ulPlayerExtra = MAXPLAYERS, ServerCommandFlags flags = 0 );
137+void SERVERCOMMANDS_PlayerVoIPAudioPacket( ULONG player, unsigned int frame, unsigned char *data, unsigned int length, ULONG playerExtra = MAXPLAYERS, ServerCommandFlags flags = 0 );
137138 void SERVERCOMMANDS_PlayerTaunt( ULONG ulPlayer, ULONG ulPlayerExtra = MAXPLAYERS, ServerCommandFlags flags = 0 );
138139 void SERVERCOMMANDS_PlayerRespawnInvulnerability( ULONG ulPlayer, ULONG ulPlayerExtra = MAXPLAYERS, ServerCommandFlags flags = 0 );
139140 void SERVERCOMMANDS_PlayerUseInventory( ULONG ulPlayer, AInventory *pItem, ULONG ulPlayerExtra = MAXPLAYERS, ServerCommandFlags flags = 0 );
diff -r e3c6f5883952 -r d44f57100df3 src/sv_main.cpp
--- a/src/sv_main.cpp Sun Sep 03 12:19:24 2023 -0400
+++ b/src/sv_main.cpp Sun Sep 03 12:33:20 2023 -0400
@@ -2256,6 +2256,9 @@
22562256 // [CK] We use a bitfield now.
22572257 else if ( name == NAME_CL_ClientFlags )
22582258 pPlayer->userinfo.ClientFlagsChanged ( value.ToLong() );
2259+ // [AK]
2260+ else if ( name == NAME_Voice_Enable )
2261+ pPlayer->userinfo.VoiceEnableChanged ( value.ToLong() );
22592262 // If this is a Hexen game, read in the player's class.
22602263 else if ( name == NAME_PlayerClass )
22612264 {
@@ -2300,7 +2303,8 @@
23002303 static const std::set<FName> required = {
23012304 NAME_Name, NAME_Autoaim, NAME_Gender, NAME_Skin, NAME_RailColor,
23022305 NAME_CL_ConnectionType, NAME_CL_ClientFlags,
2303- NAME_Handicap, NAME_CL_TicsPerUpdate, NAME_Color, NAME_ColorSet
2306+ NAME_Handicap, NAME_CL_TicsPerUpdate, NAME_Color, NAME_ColorSet,
2307+ NAME_Voice_Enable
23042308 };
23052309 std::set<FName> missing;
23062310 std::set_difference( required.begin(), required.end(), names.begin(), names.end(),
@@ -5150,6 +5154,19 @@
51505154 }
51515155 }
51525156 break;
5157+
5158+ case CLC_VOIPAUDIOPACKET:
5159+ {
5160+ const unsigned int frame = pByteStream->ReadLong( );
5161+ const unsigned int length = pByteStream->ReadByte( );
5162+ unsigned char *data = new unsigned char[length];
5163+
5164+ pByteStream->ReadBuffer( data, length );
5165+ SERVERCOMMANDS_PlayerVoIPAudioPacket( g_lCurrentClient, frame, data, length );
5166+ delete[] data;
5167+ }
5168+ break;
5169+
51535170 default:
51545171
51555172 Printf( PRINT_HIGH, "SERVER_ParseCommands: Unknown client message: %d\n", static_cast<int> (lCommand) );
diff -r e3c6f5883952 -r d44f57100df3 src/voicechat.cpp
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/voicechat.cpp Sun Sep 03 12:33:20 2023 -0400
@@ -0,0 +1,1283 @@
1+//-----------------------------------------------------------------------------
2+//
3+// Zandronum Source
4+// Copyright (C) 2023 Adam Kaminski
5+// Copyright (C) 2023 Zandronum Development Team
6+// All rights reserved.
7+//
8+// Redistribution and use in source and binary forms, with or without
9+// modification, are permitted provided that the following conditions are met:
10+//
11+// 1. Redistributions of source code must retain the above copyright notice,
12+// this list of conditions and the following disclaimer.
13+// 2. Redistributions in binary form must reproduce the above copyright notice,
14+// this list of conditions and the following disclaimer in the documentation
15+// and/or other materials provided with the distribution.
16+// 3. Neither the name of the Zandronum Development Team nor the names of its
17+// contributors may be used to endorse or promote products derived from this
18+// software without specific prior written permission.
19+// 4. Redistributions in any form must be accompanied by information on how to
20+// obtain complete source code for the software and any accompanying
21+// software that uses the software. The source code must either be included
22+// in the distribution or be available for no more than the cost of
23+// distribution plus a nominal fee, and must be freely redistributable
24+// under reasonable conditions. For an executable file, complete source
25+// code means the source code for all modules it contains. It does not
26+// include source code for modules or files that typically accompany the
27+// major components of the operating system on which the executable file
28+// runs.
29+//
30+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
31+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
32+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
34+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
35+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
36+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
37+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
38+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
39+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
40+// POSSIBILITY OF SUCH DAMAGE.
41+//
42+//
43+//
44+// Filename: voicechat.cpp
45+//
46+//-----------------------------------------------------------------------------
47+
48+#include "voicechat.h"
49+#include "c_dispatch.h"
50+#include "cl_commands.h"
51+#include "cl_demo.h"
52+#include "d_netinf.h"
53+#include "network.h"
54+#include "v_text.h"
55+#include "stats.h"
56+
57+//*****************************************************************************
58+// CONSOLE VARIABLES
59+
60+// [AK] Which input device to use when recording audio.
61+CVAR( Int, voice_recorddriver, 0, CVAR_ARCHIVE | CVAR_NOSETBYACS | CVAR_GLOBALCONFIG )
62+
63+// [AK] Enables noise suppression while transmitting audio.
64+CVAR( Bool, voice_suppressnoise, true, CVAR_ARCHIVE | CVAR_NOSETBYACS | CVAR_GLOBALCONFIG )
65+
66+// [AK] Allows the client to load a custom RNNoise model file.
67+CVAR( String, voice_noisemodelfile, "", CVAR_ARCHIVE | CVAR_NOSETBYACS | CVAR_GLOBALCONFIG )
68+
69+// [AK] How sensitive voice activity detection is, in decibels.
70+CUSTOM_CVAR( Float, voice_recordsensitivity, -50.0f, CVAR_ARCHIVE | CVAR_NOSETBYACS | CVAR_GLOBALCONFIG )
71+{
72+ const float clampedValue = clamp<float>( self, -100.0f, 0.0f );
73+
74+ if ( self != clampedValue )
75+ self = clampedValue;
76+}
77+
78+// [AK] Controls the volume of everyone's voices on the client's end.
79+CUSTOM_CVAR( Float, voice_outputvolume, 1.0f, CVAR_ARCHIVE | CVAR_NOSETBYACS | CVAR_GLOBALCONFIG )
80+{
81+ const float clampedValue = clamp<float>( self, 0.0f, 2.0f );
82+
83+ if ( self != clampedValue )
84+ {
85+ self = clampedValue;
86+ return;
87+ }
88+
89+ VOIPController::GetInstance( ).SetVolume( self );
90+}
91+
92+// [AK] How the voice chat is used on the server (0 = never, 1 = always, 2 = teammates only).
93+CUSTOM_CVAR( Int, sv_allowvoicechat, VOICECHAT_EVERYONE, CVAR_NOSETBYACS | CVAR_SERVERINFO )
94+{
95+ const int clampedValue = clamp<int>( self, VOICECHAT_OFF, VOICECHAT_TEAMMATESONLY );
96+
97+ if ( self != clampedValue )
98+ {
99+ self = clampedValue;
100+ return;
101+ }
102+
103+ // [AK] Notify the clients about the change.
104+ SERVER_SettingChanged( self, false );
105+}
106+
107+//*****************************************************************************
108+// CONSOLE COMMANDS
109+
110+// [AK] Everything past this point only compiles if compiling with sound.
111+#ifndef NO_SOUND
112+
113+// [AK] Lists all recording devices that are currently connected.
114+CCMD( voice_listrecorddrivers )
115+{
116+ VOIPController::GetInstance( ).ListRecordDrivers( );
117+}
118+
119+//*****************************************************************************
120+// FUNCTIONS
121+
122+//*****************************************************************************
123+//
124+// [AK] VOIPController::VOIPController
125+//
126+// Initializes all members of VOIPController to their default values, and resets
127+// the state of the "voicerecord" button.
128+//
129+//*****************************************************************************
130+
131+VOIPController::VOIPController( void ) :
132+ VoIPChannels{ nullptr },
133+ system( nullptr ),
134+ recordSound( nullptr ),
135+ VoIPChannelGroup( nullptr ),
136+ encoder( nullptr ),
137+ denoiseModel( nullptr ),
138+ denoiseState( nullptr ),
139+ recordDriverID( 0 ),
140+ framesSent( 0 ),
141+ lastRecordPosition( 0 ),
142+ isInitialized( false ),
143+ isActive( false ),
144+ isRecordButtonPressed( false ),
145+ transmissionType( TRANSMISSIONTYPE_OFF )
146+{
147+ Button_VoiceRecord.Reset( );
148+}
149+
150+//*****************************************************************************
151+//
152+// [AK] VOIPController::Init
153+//
154+// Initializes the VoIP controller.
155+//
156+//*****************************************************************************
157+
158+void VOIPController::Init( FMOD::System *mainSystem )
159+{
160+ int opusErrorCode = OPUS_OK;
161+
162+ // [AK] The server never initializes the voice recorder.
163+ if ( NETWORK_GetState( ) == NETSTATE_SERVER )
164+ return;
165+
166+ system = mainSystem;
167+
168+ // [AK] Abort if the FMOD system is invalid. This should never happen.
169+ if ( system == nullptr )
170+ {
171+ Printf( TEXTCOLOR_ORANGE "Invalid FMOD::System pointer used to initialize VoIP controller.\n" );
172+ return;
173+ }
174+
175+ FMOD_CREATESOUNDEXINFO exinfo = CreateSoundExInfo( RECORD_SAMPLE_RATE, RECORD_SOUND_LENGTH );
176+
177+ // [AK] Abort if creating the sound to record into failed.
178+ if ( system->createSound( nullptr, FMOD_LOOP_NORMAL | FMOD_2D | FMOD_OPENUSER, &exinfo, &recordSound ) != FMOD_OK )
179+ {
180+ Printf( TEXTCOLOR_ORANGE "Failed to create sound for recording.\n" );
181+ return;
182+ }
183+
184+ // [AK] Create the player VoIP channel group.
185+ if ( system->createChannelGroup( "VoIP", &VoIPChannelGroup ) != FMOD_OK )
186+ {
187+ Printf( TEXTCOLOR_ORANGE "Failed to create VoIP channel group for playback.\n" );
188+ return;
189+ }
190+
191+ encoder = opus_encoder_create( PLAYBACK_SAMPLE_RATE, 1, OPUS_APPLICATION_VOIP, &opusErrorCode );
192+
193+ // [AK] Stop here if the Opus encoder wasn't created successfully.
194+ if ( opusErrorCode != OPUS_OK )
195+ {
196+ Printf( TEXTCOLOR_ORANGE "Failed to create Opus encoder: %s.\n", opus_strerror( opusErrorCode ));
197+ return;
198+ }
199+
200+ opus_encoder_ctl( encoder, OPUS_SET_FORCE_CHANNELS( 1 ));
201+ opus_encoder_ctl( encoder, OPUS_SET_SIGNAL( OPUS_SIGNAL_VOICE ));
202+
203+ // [AK] Load a custom RNNoise model file if we can. Otherwise, use the built-in model.
204+ if ( strlen( voice_noisemodelfile ) > 0 )
205+ {
206+ const char *fileName = voice_noisemodelfile.GetGenericRep( CVAR_String ).String;
207+ FILE *modelFile = fopen( fileName, "r" );
208+
209+ if ( modelFile != nullptr )
210+ {
211+ denoiseModel = rnnoise_model_from_file( modelFile );
212+
213+ if ( denoiseModel == nullptr )
214+ Printf( TEXTCOLOR_ORANGE "Failed to load RNNoise model \"%s\". Using built-in model instead.\n", fileName );
215+ }
216+ else
217+ {
218+ Printf( TEXTCOLOR_YELLOW "Couldn't find RNNoise model \"%s\". Using built-in model instead.\n", fileName );
219+ }
220+ }
221+
222+ // [AK] Initialize the denoise state, used for noise suppression.
223+ denoiseState = rnnoise_create( denoiseModel );
224+
225+ isInitialized = true;
226+ Printf( "VoIP controller initialized successfully.\n" );
227+
228+ // [AK] Set the output volume after initialization.
229+ SetVolume( voice_outputvolume );
230+}
231+
232+//*****************************************************************************
233+//
234+// [AK] VOIPController::Shutdown
235+//
236+// Stops recording from the input device (if we were doing that), releases all
237+// memory used by the FMOD system, and shuts down the VoIP controller.
238+//
239+//*****************************************************************************
240+
241+void VOIPController::Shutdown( void )
242+{
243+ Deactivate( );
244+
245+ if ( encoder != nullptr )
246+ {
247+ opus_encoder_destroy( encoder );
248+ encoder = nullptr;
249+ }
250+
251+ if ( recordSound != nullptr )
252+ {
253+ recordSound->release( );
254+ recordSound = nullptr;
255+ }
256+
257+ if ( VoIPChannelGroup != nullptr )
258+ {
259+ VoIPChannelGroup->release( );
260+ VoIPChannelGroup = nullptr;
261+ }
262+
263+ if ( denoiseModel != nullptr )
264+ {
265+ rnnoise_model_free( denoiseModel );
266+ denoiseModel = nullptr;
267+ }
268+
269+ if ( denoiseState != nullptr )
270+ {
271+ rnnoise_destroy( denoiseState );
272+ denoiseState = nullptr;
273+ }
274+
275+ isInitialized = false;
276+ isRecordButtonPressed = false;
277+ Printf( "VoIP controller shutting down.\n" );
278+}
279+
280+//*****************************************************************************
281+//
282+// [AK] VOIPController::Activate
283+//
284+// Starts recording from the selected record driver.
285+//
286+//*****************************************************************************
287+
288+void VOIPController::Activate( void )
289+{
290+ int numRecordDrivers = 0;
291+
292+ if (( isInitialized == false ) || ( isActive ) || ( CLIENTDEMO_IsPlaying( )))
293+ return;
294+
295+ // [AK] Try to start recording from the selected record driver.
296+ if ( system->getRecordNumDrivers( &numRecordDrivers ) == FMOD_OK )
297+ {
298+ if ( numRecordDrivers > 0 )
299+ {
300+ if ( voice_recorddriver >= numRecordDrivers )
301+ {
302+ Printf( "Record driver %d doesn't exist. Using 0 instead.\n", *voice_recorddriver );
303+ recordDriverID = 0;
304+ }
305+ else
306+ {
307+ recordDriverID = voice_recorddriver;
308+ }
309+
310+ if ( system->recordStart( recordDriverID, recordSound, true ) != FMOD_OK )
311+ Printf( TEXTCOLOR_ORANGE "Failed to start VoIP recording.\n" );
312+ }
313+ else
314+ {
315+ Printf( TEXTCOLOR_ORANGE "Failed to find any connected record drivers.\n" );
316+ }
317+ }
318+ else
319+ {
320+ Printf( TEXTCOLOR_ORANGE "Failed to retrieve number of record drivers.\n" );
321+ }
322+
323+ isActive = true;
324+}
325+
326+
327+//*****************************************************************************
328+//
329+// [AK] VOIPController::Deactivate
330+//
331+// Stops recording from the VoIP controller.
332+//
333+//*****************************************************************************
334+
335+void VOIPController::Deactivate( void )
336+{
337+ if (( isInitialized == false ) || ( isActive == false ))
338+ return;
339+
340+ // [AK] Clear all of the VoIP channels.
341+ for ( unsigned int i = 0; i < MAXPLAYERS; i++ )
342+ RemoveVoIPChannel( i );
343+
344+ // [AK] If we're in the middle of a transmission, stop that too.
345+ StopTransmission( );
346+
347+ if ( system->recordStop( recordDriverID ) != FMOD_OK )
348+ {
349+ Printf( TEXTCOLOR_ORANGE "Failed to stop voice recording.\n" );
350+ return;
351+ }
352+
353+ framesSent = 0;
354+ isActive = false;
355+}
356+
357+//*****************************************************************************
358+//
359+// [AK] VOIPController::Tick
360+//
361+// Executes any routines that the VoIP controller must do every tick.
362+//
363+//*****************************************************************************
364+
365+template <typename T>
366+static void voicechat_ReadSoundBuffer( T *object, FMOD::Sound *sound, unsigned int &offset, const unsigned int length, void ( T::*callback )( unsigned char *, unsigned int ))
367+{
368+ void *ptr1, *ptr2;
369+ unsigned int len1, len2;
370+
371+ if (( object == nullptr ) || ( sound == nullptr ) || ( callback == nullptr ) || ( length == 0 ))
372+ return;
373+
374+ const unsigned int bufferSize = length * VOIPController::SAMPLE_SIZE;
375+ unsigned int soundLength = 0;
376+
377+ // [AK] Lock the portion of the sound buffer that we want to read.
378+ if ( sound->lock( offset * VOIPController::SAMPLE_SIZE, bufferSize, &ptr1, &ptr2, &len1, &len2 ) == FMOD_OK )
379+ {
380+ if (( ptr1 != nullptr ) && ( len1 > 0 ))
381+ {
382+ // [AK] Combine the ptr1 and ptr2 buffers into a single buffer.
383+ if (( ptr2 != nullptr ) && ( len2 > 0 ))
384+ {
385+ unsigned char *combinedBuffer = new unsigned char[bufferSize];
386+
387+ memcpy( combinedBuffer, ptr1, len1 );
388+ memcpy( combinedBuffer + len1, ptr2, len2 );
389+
390+ ( object->*callback )( combinedBuffer, bufferSize );
391+
392+ memcpy( ptr1, combinedBuffer, len1 );
393+ memcpy( ptr2, combinedBuffer + len1, len2 );
394+
395+ delete[] combinedBuffer;
396+ }
397+ else
398+ {
399+ ( object->*callback )( reinterpret_cast<unsigned char *>( ptr1 ), len1 );
400+ }
401+ }
402+
403+ // [AK] After everything's finished, unlock the sound buffer.
404+ sound->unlock( ptr1, ptr2, len1, len2 );
405+ }
406+
407+ // [AK] Increment the offset.
408+ offset += length;
409+
410+ if ( sound->getLength( &soundLength, FMOD_TIMEUNIT_PCM ) == FMOD_OK )
411+ offset = offset % soundLength;
412+}
413+
414+//*****************************************************************************
415+//
416+void VOIPController::Tick( void )
417+{
418+ // [AK] Don't tick while the VoIP controller is uninitialized.
419+ if ( isInitialized == false )
420+ return;
421+
422+ if ( IsVoiceChatAllowed( ))
423+ {
424+ if ( isActive == false )
425+ Activate( );
426+ }
427+ else if ( isActive )
428+ {
429+ Deactivate( );
430+ }
431+
432+ // [AK] Check the status of the "voicerecord" button. If the button's been
433+ // pressed, start transmitting, or it's been released stop transmitting.
434+ if ( Button_VoiceRecord.bDown == false )
435+ {
436+ if ( isRecordButtonPressed )
437+ {
438+ isRecordButtonPressed = false;
439+
440+ if ( transmissionType == TRANSMISSIONTYPE_BUTTON )
441+ StopTransmission( );
442+ }
443+ }
444+ else if ( isRecordButtonPressed == false )
445+ {
446+ isRecordButtonPressed = true;
447+
448+ if ( players[consoleplayer].userinfo.GetVoiceEnable( ) == VOICEMODE_PUSHTOTALK )
449+ {
450+ if ( IsVoiceChatAllowed( ))
451+ StartTransmission( TRANSMISSIONTYPE_BUTTON, true );
452+ // [AK] We can't transmit if we're watching a demo.
453+ else if ( CLIENTDEMO_IsPlaying( ))
454+ Printf( "Voice chat can't be used during demo playback.\n" );
455+ // ...or if we're in an offline game.
456+ else if (( NETWORK_GetState( ) == NETSTATE_SINGLE ) || ( NETWORK_GetState( ) == NETSTATE_SINGLE_MULTIPLAYER ))
457+ Printf( "Voice chat can't be used in a singleplayer game.\n" );
458+ // ...or if the server has disabled voice chatting.
459+ else if ( sv_allowvoicechat == VOICECHAT_OFF )
460+ Printf( "Voice chat has been disabled by the server.\n" );
461+ }
462+ }
463+
464+ if ( isActive == false )
465+ return;
466+
467+ // [AK] Are we're transmitting audio by pressing the "voicerecord" button right
468+ // now, or using voice activity detection? We'll check if we have enough new
469+ // samples recorded to fill an audio frame that can be encoded and sent out.
470+ if (( transmissionType != TRANSMISSIONTYPE_OFF ) || ( players[consoleplayer].userinfo.GetVoiceEnable( ) == VOICEMODE_VOICEACTIVITY ))
471+ {
472+ unsigned int recordPosition = 0;
473+
474+ if (( system->getRecordPosition( recordDriverID, &recordPosition ) == FMOD_OK ) && ( recordPosition != lastRecordPosition ))
475+ {
476+ unsigned int recordDelta = recordPosition >= lastRecordPosition ? recordPosition - lastRecordPosition : recordPosition + RECORD_SOUND_LENGTH - lastRecordPosition;
477+
478+ // [AK] We may need to send out multiple audio frames in a single tic.
479+ for ( unsigned int frame = 0; frame < recordDelta / RECORD_SAMPLES_PER_FRAME; frame++ )
480+ voicechat_ReadSoundBuffer( this, recordSound, lastRecordPosition, RECORD_SAMPLES_PER_FRAME, &VOIPController::ReadRecordSamples );
481+ }
482+ }
483+
484+ // [AK] Tick through all VoIP channels for each player.
485+ for ( unsigned int i = 0; i < MAXPLAYERS; i++ )
486+ {
487+ if ( VoIPChannels[i] == nullptr )
488+ continue;
489+
490+ // [AK] Delete this channel if this player's no longer valid.
491+ if ( PLAYER_IsValidPlayer( i ) == false )
492+ {
493+ RemoveVoIPChannel( i );
494+ continue;
495+ }
496+
497+ // [AK] If it's been long enough since we first received audio frames from
498+ // this player, start playing this channel. By now, the jitter buffer should
499+ // have enough samples for clean playback.
500+ if (( VoIPChannels[i]->sound != nullptr ) && ( VoIPChannels[i]->channel == nullptr ))
501+ {
502+ if (( VoIPChannels[i]->jitterBuffer.Size( ) == 0 ) || ( VoIPChannels[i]->playbackTick > gametic ))
503+ continue;
504+
505+ VoIPChannels[i]->StartPlaying( );
506+ }
507+
508+ // [AK] Keep updating the playback and reading more samples, such that there's
509+ // always enough gap between the number of samples read and played.
510+ if ( VoIPChannels[i]->channel != nullptr )
511+ {
512+ VoIPChannels[i]->UpdatePlayback( );
513+ const int sampleDiff = static_cast<int>( VoIPChannels[i]->samplesRead ) - static_cast<int>( VoIPChannels[i]->samplesPlayed );
514+
515+ if ( sampleDiff < READ_BUFFER_SIZE )
516+ {
517+ const unsigned int samplesToRead = MIN( VoIPChannels[i]->GetUnreadSamples( ), READ_BUFFER_SIZE - sampleDiff );
518+ voicechat_ReadSoundBuffer( VoIPChannels[i], VoIPChannels[i]->sound, VoIPChannels[i]->lastReadPosition, samplesToRead, &VOIPChannel::ReadSamples );
519+ }
520+ }
521+ }
522+}
523+
524+//*****************************************************************************
525+//
526+// [AK] VOIPController::ReadRecordSamples
527+//
528+// Reads samples from the recording sound's buffer into a single audio frame.
529+//
530+//*****************************************************************************
531+
532+void VOIPController::ReadRecordSamples( unsigned char *soundBuffer, unsigned int length )
533+{
534+ float uncompressedBuffer[RECORD_SAMPLES_PER_FRAME];
535+ float downsizedBuffer[PLAYBACK_SAMPLES_PER_FRAME];
536+ float rms = 0.0f;
537+
538+ for ( unsigned int i = 0; i < RECORD_SAMPLES_PER_FRAME; i++ )
539+ {
540+ const unsigned int indexBase = i * SAMPLE_SIZE;
541+ union { DWORD l; float f; } dataUnion;
542+
543+ dataUnion.l = 0;
544+
545+ // [AK] Convert from a byte array to a float in little-endian.
546+ for ( unsigned int byte = 0; byte < SAMPLE_SIZE; byte++ )
547+ dataUnion.l |= soundBuffer[indexBase + byte] << 8 * byte;
548+
549+ uncompressedBuffer[i] = dataUnion.f;
550+ }
551+
552+ // [AK] Denoise the audio frame.
553+ if (( voice_suppressnoise ) && ( denoiseState != nullptr ))
554+ {
555+ for ( unsigned int i = 0; i < RECORD_SAMPLES_PER_FRAME; i++ )
556+ uncompressedBuffer[i] *= SHRT_MAX;
557+
558+ rnnoise_process_frame( denoiseState, uncompressedBuffer, uncompressedBuffer );
559+
560+ for ( unsigned int i = 0; i < RECORD_SAMPLES_PER_FRAME; i++ )
561+ uncompressedBuffer[i] /= SHRT_MAX;
562+ }
563+
564+ // [AK] If using voice activity detection, calculate the RMS. This must be
565+ // done after denoising the audio frame.
566+ if ( transmissionType != TRANSMISSIONTYPE_BUTTON )
567+ {
568+ for ( unsigned int i = 0; i < RECORD_SAMPLES_PER_FRAME; i++ )
569+ rms += powf( uncompressedBuffer[i], 2 );
570+
571+ rms = sqrtf( rms / RECORD_SAMPLES_PER_FRAME );
572+ }
573+
574+ // [AK] Check if the audio frame should actually be sent. This is always the
575+ // case while pressing the "voicerecord" button, or if the sound intensity
576+ // exceeds the minimum threshold.
577+ if (( transmissionType == TRANSMISSIONTYPE_BUTTON ) || ( 20 * log10( rms ) >= voice_recordsensitivity ))
578+ {
579+ // [AK] If we're using voice activity, and not transmitting audio already,
580+ // then start transmitting now.
581+ if ( transmissionType == TRANSMISSIONTYPE_OFF )
582+ StartTransmission( TRANSMISSIONTYPE_VOICEACTIVITY, false );
583+
584+ // [AK] Downsize the input audio frame from 48 kHz to 24 kHz.
585+ for ( unsigned int i = 0; i < PLAYBACK_SAMPLES_PER_FRAME; i++ )
586+ downsizedBuffer[i] = ( uncompressedBuffer[2 * i] + uncompressedBuffer[2 * i + 1] ) / 2.0f;
587+
588+ unsigned char compressedBuffer[MAX_PACKET_SIZE];
589+ int numBytesEncoded = EncodeOpusFrame( downsizedBuffer, PLAYBACK_SAMPLES_PER_FRAME, compressedBuffer, MAX_PACKET_SIZE );
590+
591+ if ( numBytesEncoded > 0 )
592+ CLIENTCOMMANDS_VoIPAudioPacket( framesSent++, compressedBuffer, numBytesEncoded );
593+ }
594+ else
595+ {
596+ StopTransmission( );
597+ }
598+}
599+
600+//*****************************************************************************
601+//
602+// [AK] VOIPController::StartTransmission
603+//
604+// Prepares the VoIP controller to start transmitting audio to the server.
605+//
606+//*****************************************************************************
607+
608+void VOIPController::StartTransmission( const TRANSMISSIONTYPE_e type, const bool getRecordPosition )
609+{
610+ if (( isInitialized == false ) || ( isActive == false ) || ( transmissionType != TRANSMISSIONTYPE_OFF ))
611+ return;
612+
613+ if (( getRecordPosition ) && ( system->getRecordPosition( recordDriverID, &lastRecordPosition ) != FMOD_OK ))
614+ {
615+ Printf( TEXTCOLOR_ORANGE "Failed to get position of voice recording.\n" );
616+ return;
617+ }
618+
619+ transmissionType = type;
620+}
621+
622+//*****************************************************************************
623+//
624+// [AK] VOIPController::StopTransmission
625+//
626+// Stops transmitting audio to the server.
627+//
628+//*****************************************************************************
629+
630+void VOIPController::StopTransmission( void )
631+{
632+ transmissionType = TRANSMISSIONTYPE_OFF;
633+}
634+
635+//*****************************************************************************
636+//
637+// [AK] VOIPController::IsVoiceChatAllowed
638+//
639+// Checks if voice chat can be used right now.
640+//
641+//*****************************************************************************
642+
643+bool VOIPController::IsVoiceChatAllowed( void ) const
644+{
645+ // [AK] Voice chat can only be used in online games.
646+ if ( NETWORK_GetState( ) != NETSTATE_CLIENT )
647+ return false;
648+
649+ // [AK] Voice chat can only be used when it's enabled.
650+ if (( sv_allowvoicechat == VOICECHAT_OFF ) || ( players[consoleplayer].userinfo.GetVoiceEnable( ) == VOICEMODE_OFF ))
651+ return false;
652+
653+ // [AK] Voice chat can only be used while in the level or intermission screen.
654+ if (( gamestate != GS_LEVEL ) && ( gamestate != GS_INTERMISSION ))
655+ return false;
656+
657+ return true;
658+}
659+
660+//*****************************************************************************
661+//
662+// [AK] VOIPController::IsPlayerTalking
663+//
664+// Checks if the specified player is talking right now. If the player is the
665+// same as the local player, then they're talking while transmitting audio.
666+// Otherwise, they're talking if their channel is playing.
667+//
668+//*****************************************************************************
669+
670+bool VOIPController::IsPlayerTalking( const unsigned int player ) const
671+{
672+ if (( player == static_cast<unsigned>( consoleplayer )) && ( transmissionType != TRANSMISSIONTYPE_OFF ))
673+ return true;
674+
675+ if (( PLAYER_IsValidPlayer( player )) && ( VoIPChannels[player] != nullptr ) && ( VoIPChannels[player]->channel != nullptr ))
676+ return true;
677+
678+ return false;
679+}
680+
681+//*****************************************************************************
682+//
683+// [AK] VOIPController::SetVolume
684+//
685+// Adjusts the volume of all VoIP channels.
686+//
687+//*****************************************************************************
688+
689+void VOIPController::SetVolume( float volume )
690+{
691+ if ( isInitialized == false )
692+ return;
693+
694+ if (( VoIPChannelGroup == nullptr ) || ( VoIPChannelGroup->setVolume( volume ) != FMOD_OK ))
695+ Printf( TEXTCOLOR_ORANGE "Couldn't change the volume of the VoIP channel group.\n" );
696+}
697+
698+//*****************************************************************************
699+//
700+// [AK] VOIPController::SetPitch
701+//
702+// Adjusts the pitch of all VoIP channels.
703+//
704+//*****************************************************************************
705+
706+void VOIPController::SetPitch( float pitch )
707+{
708+ if ( isInitialized == false )
709+ return;
710+
711+ float oldPitch = 1.0f;
712+
713+ if (( VoIPChannelGroup == nullptr ) || ( VoIPChannelGroup->getPitch( &oldPitch ) != FMOD_OK ))
714+ {
715+ Printf( TEXTCOLOR_ORANGE "Couldn't get the pitch of the VoIP channel group.\n" );
716+ return;
717+ }
718+
719+ // [AK] Stop if the pitch is already the same.
720+ if ( pitch == oldPitch )
721+ return;
722+
723+ if ( VoIPChannelGroup->setPitch( pitch ) != FMOD_OK )
724+ {
725+ Printf( TEXTCOLOR_ORANGE "Couldn't change the pitch of the VoIP channel group.\n" );
726+ return;
727+ }
728+
729+ // [AK] When the pitch is changed, every VoIP channel's end delay time must
730+ // be updated to account for the new pitch. The epoch must also be reset.
731+ for ( unsigned int i = 0; i < MAXPLAYERS; i++ )
732+ {
733+ if (( VoIPChannels[i] != nullptr ) && ( VoIPChannels[i]->channel != nullptr ))
734+ VoIPChannels[i]->UpdateEndDelay( true );
735+ }
736+}
737+
738+//*****************************************************************************
739+//
740+// [AK] VOIPController::ListRecordDrivers
741+//
742+// Prints a list of all record drivers that are connected in the same format
743+// as FMODSoundRenderer::PrintDriversList.
744+//
745+//*****************************************************************************
746+
747+void VOIPController::ListRecordDrivers( void ) const
748+{
749+ int numDrivers = 0;
750+ char name[256];
751+
752+ if (( system != nullptr ) && ( system->getRecordNumDrivers( &numDrivers ) == FMOD_OK ))
753+ {
754+ for ( int i = 0; i < numDrivers; i++ )
755+ {
756+ if ( system->getRecordDriverInfo( i, name, sizeof( name ), nullptr ) == FMOD_OK )
757+ Printf( "%d. %s\n", i, name );
758+ }
759+ }
760+}
761+
762+//*****************************************************************************
763+//
764+// [AK] VOIPController::GrabStats
765+//
766+// Returns a string showing the VoIP controller's status, which VoIP channels
767+// are currently playing, and how many samples have been read and played.
768+//
769+//*****************************************************************************
770+
771+FString VOIPController::GrabStats( void ) const
772+{
773+ FString out;
774+
775+ out.Format( "VoIP controller status: %s\n", transmissionType != TRANSMISSIONTYPE_OFF ? "transmitting" : ( isActive ? "activated" : "deactivated" ));
776+
777+ for ( unsigned int i = 0; i < MAXPLAYERS; i++ )
778+ {
779+ if ( VoIPChannels[i] == nullptr )
780+ continue;
781+
782+ out.AppendFormat( "VoIP channel %u (%s): ", i, players[i].userinfo.GetName( ));
783+
784+ if ( IsPlayerTalking( i ))
785+ {
786+ out.AppendFormat( "samples read/played = %u/%u", VoIPChannels[i]->samplesRead, VoIPChannels[i]->samplesPlayed );
787+
788+ if ( VoIPChannels[i]->samplesRead >= VoIPChannels[i]->samplesPlayed )
789+ out.AppendFormat( " (diff = %u)", VoIPChannels[i]->samplesRead - VoIPChannels[i]->samplesPlayed );
790+ }
791+ else
792+ {
793+ out += "not talking";
794+ }
795+
796+ out += '\n';
797+ }
798+
799+ return out;
800+}
801+
802+//*****************************************************************************
803+//
804+// [AK] VOIPController::ReceiveAudioPacket
805+//
806+// This is called when the client receives an audio packet from the server,
807+// previously sent by another client. The packet is decoded and saved into the
808+// jitter buffer belonging to that client's channel, where it will be played.
809+//
810+//*****************************************************************************
811+
812+void VOIPController::ReceiveAudioPacket( const unsigned int player, const unsigned int frame, const unsigned char *data, const unsigned int length )
813+{
814+ if (( isActive == false ) || ( PLAYER_IsValidPlayer( player ) == false ) || ( data == nullptr ) || ( length == 0 ))
815+ return;
816+
817+ // [AK] If this player's channel doesn't exist yet, create a new one.
818+ if ( VoIPChannels[player] == nullptr )
819+ VoIPChannels[player] = new VOIPChannel( player );
820+
821+ // [AK] Don't accept any frames that arrived too late.
822+ if ( frame < VoIPChannels[player]->lastFrameRead )
823+ return;
824+
825+ VOIPChannel::AudioFrame newAudioFrame;
826+ newAudioFrame.frame = frame;
827+
828+ if ( VoIPChannels[player]->DecodeOpusFrame( data, length, newAudioFrame.samples, PLAYBACK_SAMPLES_PER_FRAME ) > 0 )
829+ {
830+ // [AK] Insert the new audio frame in the jitter buffer. The frames must
831+ // be ordered correctly so that the audio isn't distorted.
832+ for ( unsigned int i = 0; i < VoIPChannels[player]->jitterBuffer.Size( ); i++ )
833+ {
834+ if ( newAudioFrame.frame < VoIPChannels[player]->jitterBuffer[i].frame )
835+ {
836+ VoIPChannels[player]->jitterBuffer.Insert( i, newAudioFrame );
837+ return;
838+ }
839+ }
840+
841+ // [AK] Wait five tics before playing this VoIP channel.
842+ if (( VoIPChannels[player]->jitterBuffer.Size( ) == 0 ) && ( VoIPChannels[player]->channel == nullptr ))
843+ VoIPChannels[player]->playbackTick = gametic + 5;
844+
845+ VoIPChannels[player]->jitterBuffer.Push( newAudioFrame );
846+ }
847+}
848+
849+//*****************************************************************************
850+//
851+// [AK] VOIPController::RemoveVoIPChannel
852+//
853+// Deletes a channel from the VOIP controller.
854+//
855+//*****************************************************************************
856+
857+void VOIPController::RemoveVoIPChannel( const unsigned int player )
858+{
859+ if (( player < MAXPLAYERS ) && ( VoIPChannels[player] != nullptr ))
860+ {
861+ delete VoIPChannels[player];
862+ VoIPChannels[player] = nullptr;
863+ }
864+}
865+
866+//*****************************************************************************
867+//
868+// [AK] VOIPController::EncodeOpusFrame
869+//
870+// Encodes a single audio frame using the Opus audio codec, and returns the
871+// number of bytes encoded. If encoding fails, an error message is printed.
872+//
873+//*****************************************************************************
874+
875+int VOIPController::EncodeOpusFrame( const float *inBuffer, const unsigned int inLength, unsigned char *outBuffer, const unsigned int outLength )
876+{
877+ if (( inBuffer == nullptr ) || ( outBuffer == nullptr ))
878+ return 0;
879+
880+ int numBytesEncoded = opus_encode_float( encoder, inBuffer, inLength, outBuffer, outLength );
881+
882+ // [AK] Print the error message if encoding failed.
883+ if ( numBytesEncoded <= 0 )
884+ {
885+ Printf( TEXTCOLOR_ORANGE "Failed to encode Opus audio frame: %s.\n", opus_strerror( numBytesEncoded ));
886+ return 0;
887+ }
888+
889+ return numBytesEncoded;
890+}
891+
892+//*****************************************************************************
893+//
894+// [AK] VOIPController::CreateSoundExInfo
895+//
896+// Returns an FMOD_CREATESOUNDEXINFO struct with the settings needed to create
897+// new FMOD sounds used by the VoIP controller. The sample rate and file length
898+// (in PCM samples) can be adjusted as required.
899+//
900+//*****************************************************************************
901+
902+FMOD_CREATESOUNDEXINFO VOIPController::CreateSoundExInfo( const unsigned int sampleRate, const unsigned int fileLength )
903+{
904+ FMOD_CREATESOUNDEXINFO exinfo;
905+
906+ memset( &exinfo, 0, sizeof( FMOD_CREATESOUNDEXINFO ));
907+ exinfo.cbsize = sizeof( FMOD_CREATESOUNDEXINFO );
908+ exinfo.numchannels = 1;
909+ exinfo.format = FMOD_SOUND_FORMAT_PCMFLOAT;
910+ exinfo.defaultfrequency = sampleRate;
911+ exinfo.length = fileLength * SAMPLE_SIZE;
912+
913+ return exinfo;
914+}
915+
916+//*****************************************************************************
917+//
918+// [AK] VOIPController::ChannelCallback
919+//
920+// Static callback function that executes when a VoIP channel stops playing.
921+//
922+//*****************************************************************************
923+
924+FMOD_RESULT F_CALLBACK VOIPController::ChannelCallback( FMOD_CHANNEL *channel, FMOD_CHANNEL_CALLBACKTYPE type, void *commanddata1, void *commanddata2 )
925+{
926+ if ( type == FMOD_CHANNEL_CALLBACKTYPE_END )
927+ {
928+ FMOD::Channel *castedChannel = reinterpret_cast<FMOD::Channel *>( channel );
929+
930+ // [AK] Find which VoIP channel this object belongs to.
931+ for ( unsigned int i = 0; i < MAXPLAYERS; i++ )
932+ {
933+ VOIPChannel *VoIPChannel = GetInstance( ).VoIPChannels[i];
934+
935+ if (( VoIPChannel != nullptr ) && ( castedChannel == VoIPChannel->channel ))
936+ {
937+ // [AK] Reset the read and playback positions.
938+ VoIPChannel->channel = nullptr;
939+ VoIPChannel->lastReadPosition = 0;
940+ VoIPChannel->lastPlaybackPosition = 0;
941+
942+ // [AK] Check if this VoIP channel still has any samples that
943+ // haven't been read into the sound's buffer yet. If there are,
944+ // read them and play the channel again.
945+ if ( VoIPChannel->GetUnreadSamples( ) > 0 )
946+ {
947+ VoIPChannel->samplesPlayed = VoIPChannel->samplesRead;
948+ VoIPChannel->StartPlaying( );
949+ }
950+ else
951+ {
952+ VoIPChannel->lastFrameRead = 0;
953+ VoIPChannel->samplesRead = 0;
954+ VoIPChannel->samplesPlayed = 0;
955+ }
956+
957+ break;
958+ }
959+ }
960+ }
961+
962+ return FMOD_OK;
963+}
964+
965+//*****************************************************************************
966+//
967+// [AK] VOIPController::VOIPChannel::VOIPChannel
968+//
969+// Creates the channel's decoder and FMOD sound, and sets all members to their
970+// default values.
971+//
972+//*****************************************************************************
973+
974+VOIPController::VOIPChannel::VOIPChannel( const unsigned int player ) :
975+ player( player ),
976+ sound( nullptr ),
977+ channel( nullptr ),
978+ decoder( nullptr ),
979+ playbackTick( 0 ),
980+ lastReadPosition( 0 ),
981+ lastPlaybackPosition( 0 ),
982+ lastFrameRead( 0 ),
983+ samplesRead( 0 ),
984+ samplesPlayed( 0 ),
985+ dspEpochHi( 0 ),
986+ dspEpochLo( 0 ),
987+ endDelaySamples( 0 )
988+{
989+ int opusErrorCode = OPUS_OK;
990+ decoder = opus_decoder_create( PLAYBACK_SAMPLE_RATE, 1, &opusErrorCode );
991+
992+ // [AK] Print an error message if the Opus decoder wasn't created successfully.
993+ if ( opusErrorCode != OPUS_OK )
994+ Printf( TEXTCOLOR_ORANGE "Failed to create Opus decoder for VoIP channel %u: %s.\n", player, opus_strerror( opusErrorCode ));
995+
996+ FMOD_CREATESOUNDEXINFO exinfo = CreateSoundExInfo( PLAYBACK_SAMPLE_RATE, PLAYBACK_SOUND_LENGTH );
997+ FMOD_MODE mode = FMOD_2D | FMOD_OPENUSER | FMOD_LOOP_NORMAL | FMOD_SOFTWARE;
998+
999+ if (( VOIPController::GetInstance( ).system == nullptr ) || ( VOIPController::GetInstance( ).system->createSound( nullptr, mode, &exinfo, &sound ) != FMOD_OK ))
1000+ Printf( TEXTCOLOR_ORANGE "Failed to create sound for VoIP channel %u.\n", player );
1001+}
1002+
1003+//*****************************************************************************
1004+//
1005+// [AK] VOIPController::VOIPChannel::~VOIPChannel
1006+//
1007+// Destroys the decoder and FMOD sound/channel.
1008+//
1009+//*****************************************************************************
1010+
1011+VOIPController::VOIPChannel::~VOIPChannel( void )
1012+{
1013+ if ( channel != nullptr )
1014+ {
1015+ channel->stop( );
1016+ channel = nullptr;
1017+ }
1018+
1019+ if ( sound != nullptr )
1020+ {
1021+ sound->release( );
1022+ sound = nullptr;
1023+ }
1024+
1025+ if ( decoder != nullptr )
1026+ {
1027+ opus_decoder_destroy( decoder );
1028+ decoder = nullptr;
1029+ }
1030+}
1031+
1032+//*****************************************************************************
1033+//
1034+// [AK] VOIPController::VOIPChannel::GetUnreadSamples
1035+//
1036+// Returns the number of samples that haven't been read into the VoIP channel's
1037+// sound buffer yet. This includes the total samples still in the jitter buffer
1038+// and "extra" samples from previous VOIPChannel::ReadSamples calls.
1039+//
1040+//*****************************************************************************
1041+
1042+int VOIPController::VOIPChannel::GetUnreadSamples( void ) const
1043+{
1044+ return jitterBuffer.Size( ) * PLAYBACK_SAMPLES_PER_FRAME + extraSamples.Size( );
1045+}
1046+
1047+//*****************************************************************************
1048+//
1049+// [AK] VOIPController::VOIPChannel::DecodeOpusFrame
1050+//
1051+// Decodes a single audio frame using the Opus audio codec, and returns the
1052+// number of bytes decoded. If decoding fails, an error message is printed.
1053+//
1054+//*****************************************************************************
1055+
1056+int VOIPController::VOIPChannel::DecodeOpusFrame( const unsigned char *inBuffer, const unsigned int inLength, float *outBuffer, const unsigned int outLength )
1057+{
1058+ if (( decoder == nullptr ) || ( inBuffer == nullptr ) || ( outBuffer == nullptr ))
1059+ return 0;
1060+
1061+ int numBytesDecoded = opus_decode_float( decoder, inBuffer, inLength, outBuffer, outLength, 0 );
1062+
1063+ // [AK] Print the error message if decoding failed.
1064+ if ( numBytesDecoded <= 0 )
1065+ {
1066+ Printf( TEXTCOLOR_ORANGE "Failed to decode Opus audio frame: %s.\n", opus_strerror( numBytesDecoded ));
1067+ return 0;
1068+ }
1069+
1070+ return numBytesDecoded;
1071+}
1072+
1073+//*****************************************************************************
1074+//
1075+// [AK] VOIPController::VOIPChannel::StartPlaying
1076+//
1077+// Starts playing the VoIP channel.
1078+//
1079+//*****************************************************************************
1080+
1081+void VOIPController::VOIPChannel::StartPlaying( void )
1082+{
1083+ if ( channel != nullptr )
1084+ return;
1085+
1086+ if ( VOIPController::GetInstance( ).system->playSound( FMOD_CHANNEL_FREE, sound, true, &channel ) != FMOD_OK )
1087+ {
1088+ Printf( TEXTCOLOR_ORANGE "Failed to start playing VoIP channel %u.\n", player );
1089+ return;
1090+ }
1091+
1092+ channel->setCallback( VOIPController::ChannelCallback );
1093+
1094+ // [AK] Give the VoIP channels more priority than other sounds.
1095+ channel->setPriority( 0 );
1096+
1097+ // [AK] Reset the channel's end delay epoch before playing.
1098+ UpdateEndDelay( true );
1099+
1100+ voicechat_ReadSoundBuffer( this, sound, lastReadPosition, MIN( GetUnreadSamples( ), READ_BUFFER_SIZE ), &VOIPChannel::ReadSamples );
1101+
1102+ channel->setChannelGroup( VOIPController::GetInstance( ).VoIPChannelGroup );
1103+ channel->setPaused( false );
1104+}
1105+
1106+//*****************************************************************************
1107+//
1108+// [AK] VOIPController::VOIPChannel::ReadSamples
1109+//
1110+// Stops playing the voice recording, clears the jitter buffer, and releases
1111+// any memory the sound and/or channel was using.
1112+//
1113+//*****************************************************************************
1114+
1115+static void voicechat_FloatToByteArray( const float value, unsigned char *bytes )
1116+{
1117+ if ( bytes == nullptr )
1118+ return;
1119+
1120+ union { DWORD l; float f; } dataUnion;
1121+ dataUnion.f = value;
1122+
1123+ for ( unsigned int i = 0; i < 4; i++ )
1124+ bytes[i] = ( dataUnion.l >> 8 * i ) & 0xFF;
1125+}
1126+
1127+//*****************************************************************************
1128+//
1129+void VOIPController::VOIPChannel::ReadSamples( unsigned char *soundBuffer, const unsigned int length )
1130+{
1131+ const unsigned int samplesInBuffer = length / SAMPLE_SIZE;
1132+ unsigned int samplesReadIntoBuffer = 0;
1133+
1134+ // [AK] Read the extra samples into the sound buffer first. Make sure to
1135+ // only read as many samples as what can fit in the sound buffer.
1136+ if ( extraSamples.Size( ) > 0 )
1137+ {
1138+ const unsigned int maxExtraSamples = MIN<unsigned int>( extraSamples.Size( ), samplesInBuffer );
1139+
1140+ for ( unsigned int i = 0; i < maxExtraSamples; i++ )
1141+ {
1142+ voicechat_FloatToByteArray( extraSamples[0], soundBuffer + i * SAMPLE_SIZE );
1143+ extraSamples.Delete( 0 );
1144+ }
1145+
1146+ samplesReadIntoBuffer += maxExtraSamples;
1147+ }
1148+
1149+ // [AK] If there's still room left to read more samples, then start reading
1150+ // frames from the jitter buffer. First, find how many frames are needed in
1151+ // the sound buffer with respect to how many samples have already been read,
1152+ // then determine how many frames can actually be read. It's possible that
1153+ // there's less frames in the jitter buffer than what's required.
1154+ if ( samplesReadIntoBuffer < samplesInBuffer )
1155+ {
1156+ const unsigned int framesRequired = static_cast<unsigned int>( ceil( static_cast<float>( samplesInBuffer - samplesReadIntoBuffer ) / PLAYBACK_SAMPLES_PER_FRAME ));
1157+ const unsigned int framesToRead = MIN<unsigned int>( framesRequired, jitterBuffer.Size( ));
1158+
1159+ for ( unsigned int frame = 0; frame < framesToRead; frame++ )
1160+ {
1161+ for ( unsigned int i = 0; i < PLAYBACK_SAMPLES_PER_FRAME; i++ )
1162+ {
1163+ if ( samplesReadIntoBuffer < samplesInBuffer )
1164+ {
1165+ voicechat_FloatToByteArray( jitterBuffer[0].samples[i], soundBuffer + samplesReadIntoBuffer * SAMPLE_SIZE );
1166+ samplesReadIntoBuffer++;
1167+ }
1168+ else
1169+ {
1170+ extraSamples.Push( jitterBuffer[0].samples[i] );
1171+ }
1172+ }
1173+
1174+ lastFrameRead = jitterBuffer[0].frame;
1175+ jitterBuffer.Delete( 0 );
1176+ }
1177+ }
1178+
1179+ samplesRead += samplesReadIntoBuffer;
1180+ UpdateEndDelay( false );
1181+}
1182+
1183+//*****************************************************************************
1184+//
1185+// [AK] VOIPController::VOIPChannel::UpdatePlayback
1186+//
1187+// Updates the playback position and the number of samples played.
1188+//
1189+//*****************************************************************************
1190+
1191+void VOIPController::VOIPChannel::UpdatePlayback( void )
1192+{
1193+ unsigned int playbackPosition = 0;
1194+
1195+ // [AK] Check how many new samples have been played since the last call.
1196+ if ( channel->getPosition( &playbackPosition, FMOD_TIMEUNIT_PCM ) == FMOD_OK )
1197+ {
1198+ unsigned int playbackDelta = 0;
1199+
1200+ if ( playbackPosition >= lastPlaybackPosition )
1201+ playbackDelta = playbackPosition - lastPlaybackPosition;
1202+ else
1203+ playbackDelta = playbackPosition + PLAYBACK_SOUND_LENGTH - lastPlaybackPosition;
1204+
1205+ samplesPlayed += playbackDelta;
1206+ lastPlaybackPosition = playbackPosition;
1207+ }
1208+}
1209+
1210+//*****************************************************************************
1211+//
1212+// [AK] VOIPController::VOIPChannel::UpdateEndDelay
1213+//
1214+// Determines precisely when a VoIP channel needs to stop, with respect to the
1215+// FMOD system's DSP clock and sample rate. This is a sample-accurate way of
1216+// knowing how long a channel should play without any "spilling" (i.e. playing
1217+// more samples than read).
1218+//
1219+//*****************************************************************************
1220+
1221+void VOIPController::VOIPChannel::UpdateEndDelay( const bool resetEpoch )
1222+{
1223+ if (( channel == nullptr ) || ( VOIPController::GetInstance( ).system == nullptr ))
1224+ return;
1225+
1226+ // [AK] Resetting the epoch means that we get the current DSP clock time of
1227+ // the system and the current number of samples played. The latter becomes
1228+ // the new base which we subtract the number of read samples by.
1229+ if ( resetEpoch )
1230+ {
1231+ VOIPController::GetInstance( ).system->getDSPClock( &dspEpochHi, &dspEpochLo );
1232+ UpdatePlayback( );
1233+
1234+ endDelaySamples = samplesPlayed;
1235+ }
1236+
1237+ // [AK] The channel should stop immediately if the number of samples read is
1238+ // less than or equal to the "end delay" samples.
1239+ if ( samplesRead <= endDelaySamples )
1240+ {
1241+ channel->setDelay( FMOD_DELAYTYPE_DSPCLOCK_END, dspEpochHi, dspEpochLo );
1242+ return;
1243+ }
1244+
1245+ int sysSampleRate = 0;
1246+ unsigned int newDSPHi = dspEpochHi;
1247+ unsigned int newDSPLo = dspEpochLo;
1248+ FMOD::ChannelGroup *channelGroup = nullptr;
1249+
1250+ // [AK] It's important to consider that the system and channel might not
1251+ // be playing at the same sample rates. Therefore, we must convert the
1252+ // number of samples with respect to the system's sample rate.
1253+ VOIPController::GetInstance( ).system->getSoftwareFormat( &sysSampleRate, nullptr, nullptr, nullptr, nullptr, nullptr );
1254+ float scalar = static_cast<float>( sysSampleRate ) / PLAYBACK_SAMPLE_RATE;
1255+
1256+ // [AK] The channel's pitch might've changed (e.g. listening underwater).
1257+ // This also affects the end delay time; lower pitches extend the time and
1258+ // higher pitches shorten it.
1259+ if (( channel->getChannelGroup( &channelGroup ) == FMOD_OK ) && ( channelGroup != nullptr ))
1260+ {
1261+ float channelGroupPitch = 1.0f;
1262+
1263+ channelGroup->getPitch( &channelGroupPitch );
1264+ scalar /= channelGroupPitch;
1265+ }
1266+
1267+ FMOD_64BIT_ADD( newDSPHi, newDSPLo, 0, static_cast<unsigned int>(( samplesRead - endDelaySamples ) * scalar ));
1268+ channel->setDelay( FMOD_DELAYTYPE_DSPCLOCK_END, newDSPHi, newDSPLo );
1269+}
1270+
1271+#endif // NO_SOUND
1272+
1273+//*****************************************************************************
1274+// STATISTICS
1275+
1276+#ifndef NO_SOUND
1277+
1278+ADD_STAT( voice )
1279+{
1280+ return VOIPController::GetInstance( ).GrabStats( );
1281+}
1282+
1283+#endif // NO_SOUND
diff -r e3c6f5883952 -r d44f57100df3 src/voicechat.h
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/voicechat.h Sun Sep 03 12:33:20 2023 -0400
@@ -0,0 +1,232 @@
1+//-----------------------------------------------------------------------------
2+//
3+// Zandronum Source
4+// Copyright (C) 2023 Adam Kaminski
5+// Copyright (C) 2023 Zandronum Development Team
6+// All rights reserved.
7+//
8+// Redistribution and use in source and binary forms, with or without
9+// modification, are permitted provided that the following conditions are met:
10+//
11+// 1. Redistributions of source code must retain the above copyright notice,
12+// this list of conditions and the following disclaimer.
13+// 2. Redistributions in binary form must reproduce the above copyright notice,
14+// this list of conditions and the following disclaimer in the documentation
15+// and/or other materials provided with the distribution.
16+// 3. Neither the name of the Zandronum Development Team nor the names of its
17+// contributors may be used to endorse or promote products derived from this
18+// software without specific prior written permission.
19+// 4. Redistributions in any form must be accompanied by information on how to
20+// obtain complete source code for the software and any accompanying
21+// software that uses the software. The source code must either be included
22+// in the distribution or be available for no more than the cost of
23+// distribution plus a nominal fee, and must be freely redistributable
24+// under reasonable conditions. For an executable file, complete source
25+// code means the source code for all modules it contains. It does not
26+// include source code for modules or files that typically accompany the
27+// major components of the operating system on which the executable file
28+// runs.
29+//
30+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
31+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
32+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
34+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
35+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
36+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
37+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
38+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
39+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
40+// POSSIBILITY OF SUCH DAMAGE.
41+//
42+//
43+//
44+// Filename: voicechat.h
45+//
46+//-----------------------------------------------------------------------------
47+
48+#ifndef __VOICECHAT_H__
49+#define __VOICECHAT_H__
50+
51+#include "doomdef.h"
52+#include "c_cvars.h"
53+#include "networkshared.h"
54+
55+// [AK] Only include FMOD, Opus, and RNNoise files if compiling with sound.
56+#ifndef NO_SOUND
57+#include "fmod_wrap.h"
58+#include "opus.h"
59+#include "rnnoise.h"
60+#endif
61+
62+//*****************************************************************************
63+// DEFINES
64+
65+enum VOICECHAT_e
66+{
67+ // Voice chatting is disabled by the server.
68+ VOICECHAT_OFF,
69+
70+ // Everyone can chat with each other.
71+ VOICECHAT_EVERYONE,
72+
73+ // Players can only use voice chat amongst their teammates.
74+ VOICECHAT_TEAMMATESONLY,
75+};
76+
77+//*****************************************************************************
78+enum VOICEMODE_e
79+{
80+ // Voice chatting is disabled by the client.
81+ VOICEMODE_OFF,
82+
83+ // The player transmits audio by pressing down +voicerecord.
84+ VOICEMODE_PUSHTOTALK,
85+
86+ // The player transmits audio based on voice activity.
87+ VOICEMODE_VOICEACTIVITY,
88+};
89+
90+//*****************************************************************************
91+enum TRANSMISSIONTYPE_e
92+{
93+ // Not transmitting audio right now.
94+ TRANSMISSIONTYPE_OFF,
95+
96+ // Transmitting audio by pressing a button (i.e. "voicerecord").
97+ TRANSMISSIONTYPE_BUTTON,
98+
99+ // Transmitting audio based on voice activity.
100+ TRANSMISSIONTYPE_VOICEACTIVITY,
101+};
102+
103+//*****************************************************************************
104+// CLASSES
105+
106+class VOIPController
107+{
108+public:
109+ static VOIPController &GetInstance( void ) { static VOIPController instance; return instance; }
110+
111+// [AK] Some of these functions only exist as stubs if compiling without sound.
112+#ifdef NO_SOUND
113+
114+ void Tick( void ) { }
115+ void StartTransmission( const TRANSMISSIONTYPE_e type, const bool getRecordPosition ) { }
116+ void StopTransmission( void ) { }
117+ bool IsVoiceChatAllowed( void ) const { return false; }
118+ bool IsPlayerTalking( const unsigned int player ) const { return false; }
119+ void SetVolume( float volume ) { }
120+ void SetPitch( float pitch ) { }
121+ void ListRecordDrivers( void ) const { }
122+ FString GrabStats( void ) const { return ""; }
123+ void ReceiveAudioPacket( const unsigned int player, const unsigned int frame, const unsigned char *data, const unsigned int length ) { }
124+ void RemoveVoIPChannel( const unsigned int player ) { }
125+
126+private:
127+ VOIPController( void ) { }
128+ ~VOIPController( void ) { }
129+
130+#else
131+
132+ void Init( FMOD::System *mainSystem );
133+ void Shutdown( void );
134+ void Activate( void );
135+ void Deactivate( void );
136+ void Tick( void );
137+ void StartTransmission( const TRANSMISSIONTYPE_e type, const bool getRecordPosition );
138+ void StopTransmission( void );
139+ bool IsVoiceChatAllowed( void ) const;
140+ bool IsPlayerTalking( const unsigned int player ) const;
141+ void SetVolume( float volume );
142+ void SetPitch( float pitch );
143+ void ListRecordDrivers( void ) const;
144+ FString GrabStats( void ) const;
145+ void ReceiveAudioPacket( const unsigned int player, const unsigned int frame, const unsigned char *data, const unsigned int length );
146+ void RemoveVoIPChannel( const unsigned int player );
147+
148+ // [AK] Static constants of the audio's properties.
149+ static const int RECORD_SAMPLE_RATE = 48000; // 48 kHz.
150+ static const int PLAYBACK_SAMPLE_RATE = 24000; // 24 kHz.
151+ static const int SAMPLE_SIZE = sizeof( float ); // 32-bit floating point, mono-channel.
152+ static const int RECORD_SOUND_LENGTH = RECORD_SAMPLE_RATE; // 1 second.
153+ static const int PLAYBACK_SOUND_LENGTH = PLAYBACK_SAMPLE_RATE; // 1 second.
154+
155+ static const int READ_BUFFER_SIZE = 2048;
156+
157+ // [AK] Static constants for encoding or decoding frames via Opus.
158+ static const int FRAME_SIZE = 10; // 10 ms.
159+ static const int RECORD_SAMPLES_PER_FRAME = ( RECORD_SAMPLE_RATE * FRAME_SIZE ) / 1000;
160+ static const int PLAYBACK_SAMPLES_PER_FRAME = ( PLAYBACK_SAMPLE_RATE * FRAME_SIZE ) / 1000;
161+ static const int MAX_PACKET_SIZE = 1276; // Recommended max packet size by Opus.
162+
163+private:
164+ struct VOIPChannel
165+ {
166+ struct AudioFrame
167+ {
168+ unsigned int frame;
169+ float samples[PLAYBACK_SAMPLES_PER_FRAME];
170+ };
171+
172+ const unsigned int player;
173+ TArray<AudioFrame> jitterBuffer;
174+ TArray<float> extraSamples;
175+ FMOD::Sound *sound;
176+ FMOD::Channel *channel;
177+ OpusDecoder *decoder;
178+ int playbackTick;
179+ unsigned int lastReadPosition;
180+ unsigned int lastPlaybackPosition;
181+ unsigned int lastFrameRead;
182+ unsigned int samplesRead;
183+ unsigned int samplesPlayed;
184+ unsigned int dspEpochHi;
185+ unsigned int dspEpochLo;
186+ unsigned int endDelaySamples;
187+
188+ VOIPChannel( const unsigned int player );
189+ ~VOIPChannel( void );
190+
191+ int GetUnreadSamples( void ) const;
192+ int DecodeOpusFrame( const unsigned char *inBuffer, const unsigned int inLength, float *outBuffer, const unsigned int outLength );
193+ void StartPlaying( void );
194+ void ReadSamples( unsigned char *soundBuffer, const unsigned int length );
195+ void UpdatePlayback( void );
196+ void UpdateEndDelay( const bool resetEpoch );
197+ };
198+
199+ VOIPController( void );
200+ ~VOIPController( void ) { Shutdown( ); }
201+
202+ void ReadRecordSamples( unsigned char *soundBuffer, unsigned int length );
203+ int EncodeOpusFrame( const float *inBuffer, const unsigned int inLength, unsigned char *outBuffer, const unsigned int outLength );
204+
205+ static FMOD_CREATESOUNDEXINFO CreateSoundExInfo( const unsigned int sampleRate, const unsigned int fileLength );
206+ static FMOD_RESULT F_CALLBACK ChannelCallback( FMOD_CHANNEL *channel, FMOD_CHANNEL_CALLBACKTYPE type, void *commanddata1, void *commanddata2 );
207+
208+ VOIPChannel *VoIPChannels[MAXPLAYERS];
209+ FMOD::System *system;
210+ FMOD::Sound *recordSound;
211+ FMOD::ChannelGroup *VoIPChannelGroup;
212+ OpusEncoder *encoder;
213+ RNNModel *denoiseModel;
214+ DenoiseState *denoiseState;
215+ int recordDriverID;
216+ unsigned int framesSent;
217+ unsigned int lastRecordPosition;
218+ bool isInitialized;
219+ bool isActive;
220+ bool isRecordButtonPressed;
221+ TRANSMISSIONTYPE_e transmissionType;
222+
223+#endif // NO_SOUND
224+
225+};
226+
227+//*****************************************************************************
228+// EXTERNAL CONSOLE VARIABLES
229+
230+EXTERN_CVAR( Int, sv_allowvoicechat )
231+
232+#endif // __VOICECHAT_H__
diff -r e3c6f5883952 -r d44f57100df3 wadsrc/static/menudef.txt
--- a/wadsrc/static/menudef.txt Sun Sep 03 12:19:24 2023 -0400
+++ b/wadsrc/static/menudef.txt Sun Sep 03 12:33:20 2023 -0400
@@ -482,6 +482,7 @@
482482 Control "Say", "messagemode"
483483 Control "Team say", "messagemode2"
484484 Control "Private say", "messagemode3" // [AK]
485+ Control "Voice chat", "+voicerecord" // [AK]
485486 StaticText ""
486487 StaticText "Weapons", 1
487488 Control "Next weapon", "weapnext"
@@ -1657,6 +1658,7 @@
16571658 StaticText " "
16581659 Submenu "Advanced options", "AdvSoundOptions"
16591660 Submenu "Module replayer options", "ModReplayerOptions"
1661+ Submenu "Voice chat options", "ZA_VoiceChatOptions" // [AK]
16601662 }
16611663
16621664 /*=======================================
diff -r e3c6f5883952 -r d44f57100df3 wadsrc/static/menudef.za
--- a/wadsrc/static/menudef.za Sun Sep 03 12:19:24 2023 -0400
+++ b/wadsrc/static/menudef.za Sun Sep 03 12:33:20 2023 -0400
@@ -138,6 +138,31 @@
138138
139139 // =================================================================================================
140140 //
141+// VOICE CHAT OPTIONS
142+//
143+// =================================================================================================
144+
145+OptionValue ZA_VoiceMode
146+{
147+ 0, "Off"
148+ 1, "Push-to-talk"
149+ 2, "Voice activity"
150+}
151+
152+OptionMenu "ZA_VoiceChatOptions"
153+{
154+ Title "VOICE CHAT OPTIONS"
155+
156+ Option "Enable voice chat", "voice_enable", "ZA_VoiceMode"
157+ Slider "Output volume", "voice_outputvolume", 0.0, 2.0, 0.1
158+ Slider "Voice sensitivity (dB)", "voice_recordsensitivity", -100, 0, 5
159+ StaticText " "
160+ Option "Use noise suppression", "voice_suppressnoise", "YesNo"
161+ TextField "RNNoise model file", "voice_noisemodelfile", "voice_suppressnoise"
162+}
163+
164+// =================================================================================================
165+//
141166 // OFFLINE SKIRMISH
142167 //
143168 // =================================================================================================
@@ -594,7 +619,7 @@
594619 //
595620 // =================================================================================================
596621
597-OptionValue ZA_AllowPrivateChat
622+OptionValue ZA_AllowChat
598623 {
599624 0, "Never"
600625 1, "Everyone"
@@ -630,7 +655,8 @@
630655 TextField "Join password", "sv_joinpassword", "sv_forcejoinpassword"
631656 Option "Require login to join", "sv_forcelogintojoin", "YesNo"
632657 StaticText " "
633- Option "Allow private chat", "sv_allowprivatechat", "ZA_AllowPrivateChat"
658+ Option "Allow private chat", "sv_allowprivatechat", "ZA_AllowChat"
659+ Option "Allow voice chat", "sv_allowvoicechat", "ZA_AllowChat"
634660 // Option "Smooth lagging players", "sv_smoothplayers", "ZA_ExtrapolateLimit"
635661 NumberField "Max connected clients", "sv_maxclients", 0, 64
636662 NumberField "Max players", "sv_maxplayers", 0, 64