This is a fork of Zandronum used on servers hosted by The Sentinels Playground (TSPG), Euroboros (EB), and Down Under Doomers (DUD).
修订版 | d44f57100df3e673f6364879cfe53190aa92cf0d (tree) |
---|---|
时间 | 2023-09-04 01:33:20 |
作者 | Adam Kaminski <kaminskiadam9@gmai...> |
Commiter | Adam Kaminski |
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.
@@ -251,6 +251,11 @@ | ||
251 | 251 | add_subdirectory( wadsrc_st ) |
252 | 252 | add_subdirectory( src ) |
253 | 253 | |
254 | +# [AK] Library for noise suppression for VoIP. | |
255 | +if ( NOT NO_SOUND ) | |
256 | + add_subdirectory( rnnoise ) | |
257 | +endif ( NOT NO_SOUND ) | |
258 | + | |
254 | 259 | if( NOT WIN32 AND NOT APPLE ) |
255 | 260 | # [BB] We don't need output_sdl (only used for sound), if we are just building the server. |
256 | 261 | if ( NOT SERVERONLY ) |
@@ -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 ) |
@@ -18,6 +18,7 @@ | ||
18 | 18 | *+ - Added the new AUTHINFO lump, allowing modders to define their own list of lumps to be authenticated. [Kaminsky] |
19 | 19 | *+ - Revamped the scoreboard, and added the new SCORINFO lump that allows full customization of the scoreboard. [Kaminsky] |
20 | 20 | *+ - 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] | |
21 | 22 | + - Added the +NOMORPHLIMITATIONS flag, allowing morphs to switch weapons, play sounds, and be affected by speed powerups. [geNia/Binary] |
22 | 23 | + - Added CVars: "con_interpolate" and "con_speed" which interpolates and controls how fast the console moves. Based on featues from ZCC. [Kaminsky] |
23 | 24 | + - Added ACS functions: "GetCurrentMapPosition", "GetEventResult", "GetActorSectorLocation", and "ChangeTeamScore". [Kaminsky] |
@@ -1201,6 +1201,10 @@ | ||
1201 | 1201 | > |
1202 | 1202 | </File> |
1203 | 1203 | <File |
1204 | + RelativePath=".\src\voicechat.cpp" | |
1205 | + > | |
1206 | + </File> | |
1207 | + <File | |
1204 | 1208 | RelativePath=".\src\w_wad.cpp" |
1205 | 1209 | > |
1206 | 1210 | </File> |
@@ -1850,6 +1854,10 @@ | ||
1850 | 1854 | > |
1851 | 1855 | </File> |
1852 | 1856 | <File |
1857 | + RelativePath=".\src\voicechat.h" | |
1858 | + > | |
1859 | + </File> | |
1860 | + <File | |
1853 | 1861 | RelativePath=".\src\w_wad.h" |
1854 | 1862 | > |
1855 | 1863 | </File> |
@@ -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 ) |
@@ -671,6 +671,13 @@ | ||
671 | 671 | set ( ZDOOM_LIBS ${ZDOOM_LIBS} crypt32 ) |
672 | 672 | endif ( WIN32 ) |
673 | 673 | |
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 | + | |
674 | 681 | if( NOT DYN_FLUIDSYNTH) |
675 | 682 | if( FLUIDSYNTH_FOUND ) |
676 | 683 | set( ZDOOM_LIBS ${ZDOOM_LIBS} "${FLUIDSYNTH_LIBRARIES}" ) |
@@ -1245,6 +1252,7 @@ | ||
1245 | 1252 | v_pfx.cpp |
1246 | 1253 | v_text.cpp |
1247 | 1254 | v_video.cpp |
1255 | + voicechat.cpp #ZA | |
1248 | 1256 | w_wad.cpp |
1249 | 1257 | wi_stuff.cpp |
1250 | 1258 | za_database.cpp #ZA |
@@ -1464,6 +1472,7 @@ | ||
1464 | 1472 | xlat |
1465 | 1473 | ../gdtoa |
1466 | 1474 | ../dumb/include |
1475 | + ../rnnoise/ #ZA | |
1467 | 1476 | ../sqlite/ #ZA |
1468 | 1477 | ../upnpnat/ #ST |
1469 | 1478 | ${CMAKE_BINARY_DIR}/gdtoa |
@@ -143,7 +143,8 @@ | ||
143 | 143 | Button_User1, Button_User2, Button_User3, Button_User4, |
144 | 144 | Button_AM_PanLeft, Button_AM_PanRight, Button_AM_PanDown, Button_AM_PanUp, |
145 | 145 | 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. | |
147 | 148 | |
148 | 149 | |
149 | 150 | bool ParsingKeyConf, UnsafeExecutionContext; |
@@ -173,7 +174,8 @@ | ||
173 | 174 | |
174 | 175 | FActionMap ActionMaps[] = |
175 | 176 | { |
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. | |
177 | 179 | { &Button_AM_PanLeft, 0x0d52d67b, "am_panleft"}, |
178 | 180 | { &Button_User2, 0x125f5226, "user2" }, |
179 | 181 | { &Button_Jump, 0x1eefa611, "jump" }, |
@@ -203,7 +203,8 @@ | ||
203 | 203 | Button_User1, Button_User2, Button_User3, Button_User4, |
204 | 204 | Button_AM_PanLeft, Button_AM_PanRight, Button_AM_PanDown, Button_AM_PanUp, |
205 | 205 | 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. | |
207 | 208 | extern bool ParsingKeyConf, UnsafeExecutionContext; |
208 | 209 | |
209 | 210 | void ResetButtonTriggers (); // Call ResetTriggers for all buttons |
@@ -896,3 +896,16 @@ | ||
896 | 896 | CLIENT_GetLocalBuffer( )->ByteStream.WriteString( cvarName ); |
897 | 897 | CLIENT_GetLocalBuffer( )->ByteStream.WriteString( cvarValue ); |
898 | 898 | } |
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 | +} |
@@ -115,5 +115,6 @@ | ||
115 | 115 | void CLIENTCOMMANDS_SetWantHideAccount( bool wantHideCountry ); |
116 | 116 | void CLIENTCOMMANDS_SetVideoResolution(); |
117 | 117 | 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 ); | |
118 | 119 | |
119 | 120 | #endif // __CL_COMMANDS_H__ |
@@ -130,6 +130,7 @@ | ||
130 | 130 | #include "v_text.h" |
131 | 131 | #include "maprotation.h" |
132 | 132 | #include "st_hud.h" |
133 | +#include "voicechat.h" | |
133 | 134 | |
134 | 135 | //***************************************************************************** |
135 | 136 | // MISC CRAP THAT SHOULDN'T BE HERE BUT HAS TO BE BECAUSE OF SLOPPY CODING |
@@ -199,6 +200,7 @@ | ||
199 | 200 | // [BB] Does not work with the latest ZDoom changes. Check if it's still necessary. |
200 | 201 | //static void client_SetPlayerPieces( BYTESTREAM_s *pByteStream ); |
201 | 202 | static void client_IgnorePlayer( BYTESTREAM_s *pByteStream ); |
203 | +static void client_PlayerVoIPAudioPacket( BYTESTREAM_s *byteStream ); | |
202 | 204 | |
203 | 205 | // Game commands. |
204 | 206 | static void client_SetGameMode( BYTESTREAM_s *pByteStream ); |
@@ -429,6 +431,9 @@ | ||
429 | 431 | |
430 | 432 | // [AK] Clear out saved chat messages from the players. |
431 | 433 | CHAT_ClearChatMessages( ulIdx ); |
434 | + | |
435 | + // [AK] Delete this player's VoIP channel if it exists. | |
436 | + VOIPController::GetInstance( ).RemoveVoIPChannel( ulIdx ); | |
432 | 437 | } |
433 | 438 | |
434 | 439 | // [AK] Also clear out saved chat messages from the server. |
@@ -1908,6 +1913,11 @@ | ||
1908 | 1913 | client_IgnorePlayer( pByteStream ); |
1909 | 1914 | break; |
1910 | 1915 | |
1916 | + case SVC_PLAYERVOIPAUDIOPACKET: | |
1917 | + | |
1918 | + client_PlayerVoIPAudioPacket( pByteStream ); | |
1919 | + break; | |
1920 | + | |
1911 | 1921 | case SVC_EXTENDEDCOMMAND: |
1912 | 1922 | { |
1913 | 1923 | const LONG lExtCommand = pByteStream->ReadByte(); |
@@ -4144,6 +4154,8 @@ | ||
4144 | 4154 | // [CK] We do compressed bitfields now. |
4145 | 4155 | else if ( name == NAME_CL_ClientFlags ) |
4146 | 4156 | player->userinfo.ClientFlagsChanged ( value.ToLong() ); |
4157 | + else if ( name == NAME_Voice_Enable ) | |
4158 | + player->userinfo.VoiceEnableChanged ( value.ToLong() ); | |
4147 | 4159 | else |
4148 | 4160 | { |
4149 | 4161 | FBaseCVar **cvarPointer = player->userinfo.CheckKey( name ); |
@@ -4562,6 +4574,9 @@ | ||
4562 | 4574 | // Zero out all the player information. |
4563 | 4575 | PLAYER_ResetPlayerData( player ); |
4564 | 4576 | |
4577 | + // [AK] Delete this player's VoIP channel if it exists. | |
4578 | + VOIPController::GetInstance( ).RemoveVoIPChannel( playerIndex ); | |
4579 | + | |
4565 | 4580 | // Refresh the HUD because this affects the number of players in the game. |
4566 | 4581 | HUD_ShouldRefreshBeforeRendering( ); |
4567 | 4582 | } |
@@ -5898,6 +5913,10 @@ | ||
5898 | 5913 | Value.Int = pByteStream->ReadByte(); |
5899 | 5914 | sv_allowprivatechat.ForceSet( Value, CVAR_Int ); |
5900 | 5915 | |
5916 | + // [AK] Read in, and set the value for sv_allowvoicechat. | |
5917 | + Value.Int = pByteStream->ReadByte(); | |
5918 | + sv_allowvoicechat.ForceSet( Value, CVAR_Int ); | |
5919 | + | |
5901 | 5920 | // [AK] Read in, and set the value for sv_respawndelaytime. |
5902 | 5921 | Value.Float = pByteStream->ReadFloat(); |
5903 | 5922 | sv_respawndelaytime.ForceSet( Value, CVAR_Float ); |
@@ -9185,6 +9204,20 @@ | ||
9185 | 9204 | |
9186 | 9205 | //***************************************************************************** |
9187 | 9206 | // |
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 | +// | |
9188 | 9221 | static void client_DoPusher( BYTESTREAM_s *pByteStream ) |
9189 | 9222 | { |
9190 | 9223 | const ULONG ulType = pByteStream->ReadByte(); |
@@ -66,6 +66,7 @@ | ||
66 | 66 | #include "gamemode.h" |
67 | 67 | #include "team.h" |
68 | 68 | #include "menu/menu.h" |
69 | +#include "voicechat.h" | |
69 | 70 | |
70 | 71 | static FRandom pr_pickteam ("PickRandomTeam"); |
71 | 72 |
@@ -102,6 +103,8 @@ | ||
102 | 103 | CVAR (Int, cl_connectiontype, 1, CVAR_USERINFO | CVAR_ARCHIVE); |
103 | 104 | // [CK] Let the user control if they want clientside puffs or not. |
104 | 105 | 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); | |
105 | 108 | |
106 | 109 | // [TP] Userinfo changes yet to be sent. |
107 | 110 | static UserInfoChanges PendingUserinfoChanges; |
@@ -616,6 +619,7 @@ | ||
616 | 619 | case NAME_CL_TicsPerUpdate: coninfo->TicsPerUpdateChanged(cl_ticsperupdate); break; |
617 | 620 | case NAME_CL_ConnectionType: coninfo->ConnectionTypeChanged(cl_connectiontype); break; |
618 | 621 | case NAME_CL_ClientFlags: coninfo->ClientFlagsChanged(cl_clientflags); break; |
622 | + case NAME_Voice_Enable: coninfo->VoiceEnableChanged(voice_enable); break; | |
619 | 623 | |
620 | 624 | // The rest do. |
621 | 625 | default: |
@@ -826,6 +830,18 @@ | ||
826 | 830 | return flags; |
827 | 831 | } |
828 | 832 | |
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 | + | |
829 | 845 | void D_UserInfoChanged (FBaseCVar *cvar) |
830 | 846 | { |
831 | 847 | UCVarValue val; |
@@ -934,6 +950,19 @@ | ||
934 | 950 | return; |
935 | 951 | } |
936 | 952 | } |
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 | + } | |
937 | 966 | |
938 | 967 | val = cvar->GetGenericRep (CVAR_String); |
939 | 968 | escaped_val = D_EscapeUserInfo(val.String); |
@@ -1417,6 +1446,11 @@ | ||
1417 | 1446 | info->ClientFlagsChanged ( atoi( value ) ); |
1418 | 1447 | break; |
1419 | 1448 | |
1449 | + // [AK] | |
1450 | + case NAME_Voice_Enable: | |
1451 | + info->VoiceEnableChanged ( atoi( value ) ); | |
1452 | + break; | |
1453 | + | |
1420 | 1454 | default: |
1421 | 1455 | cvar_ptr = info->CheckKey(keyname); |
1422 | 1456 | if (cvar_ptr != NULL) |
@@ -430,6 +430,8 @@ | ||
430 | 430 | int TicsPerUpdateChanged(int ticsperupdate); |
431 | 431 | int ConnectionTypeChanged(int connectiontype); |
432 | 432 | int ClientFlagsChanged(int flags); |
433 | + int VoiceEnableChanged(int voiceenable); | |
434 | + | |
433 | 435 | int GetRailColor() const |
434 | 436 | { |
435 | 437 | if ( CheckKey(NAME_RailColor) != NULL ) |
@@ -475,6 +477,15 @@ | ||
475 | 477 | return 0; |
476 | 478 | } |
477 | 479 | } |
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 | + } | |
478 | 489 | }; |
479 | 490 | |
480 | 491 | void ReadUserInfo(FArchive &arc, userinfo_t &info, FString &skin); |
@@ -112,6 +112,7 @@ | ||
112 | 112 | #include "p_3dmidtex.h" |
113 | 113 | #include "a_lightning.h" |
114 | 114 | #include "po_man.h" |
115 | +#include "voicechat.h" | |
115 | 116 | |
116 | 117 | #include <zlib.h> |
117 | 118 |
@@ -1994,6 +1995,9 @@ | ||
1994 | 1995 | break; |
1995 | 1996 | } |
1996 | 1997 | |
1998 | + // [AK] Tick the VoIP controller. | |
1999 | + VOIPController::GetInstance( ).Tick( ); | |
2000 | + | |
1997 | 2001 | // [BC] If any data has accumulated in our packet, send it out now. |
1998 | 2002 | if ( NETWORK_GetState( ) == NETSTATE_CLIENT ) |
1999 | 2003 | CLIENT_EndTick( ); |
@@ -606,6 +606,8 @@ | ||
606 | 606 | xx(CL_ConnectionType) |
607 | 607 | // [CK] Client flags for various booleans masked in a bitfield. |
608 | 608 | xx(CL_ClientFlags) |
609 | +// [AK] Let the user decide whether voice chat is on/off and how to transmit audio. | |
610 | +xx(Voice_Enable) | |
609 | 611 | // [BB] For the bot skill menu |
610 | 612 | xx(BotSkillMenu) |
611 | 613 | xx(ChooseBotSkill) |
@@ -121,6 +121,7 @@ | ||
121 | 121 | ENUM_ELEMENT ( SVC_RESETALLPLAYERSFRAGCOUNT ), |
122 | 122 | ENUM_ELEMENT ( SVC_PLAYERISSPECTATOR ), |
123 | 123 | ENUM_ELEMENT ( SVC_PLAYERSAY ), |
124 | + ENUM_ELEMENT ( SVC_PLAYERVOIPAUDIOPACKET ), | |
124 | 125 | ENUM_ELEMENT ( SVC_PLAYERTAUNT ), |
125 | 126 | ENUM_ELEMENT ( SVC_PLAYERRESPAWNINVULNERABILITY ), |
126 | 127 | ENUM_ELEMENT ( SVC_PLAYERUSEINVENTORY ), |
@@ -446,6 +447,7 @@ | ||
446 | 447 | ENUM_ELEMENT( CLC_SETWANTHIDEACCOUNT ), |
447 | 448 | ENUM_ELEMENT( CLC_SETVIDEORESOLUTION ), |
448 | 449 | ENUM_ELEMENT( CLC_RCONSETCVAR ), |
450 | + ENUM_ELEMENT( CLC_VOIPAUDIOPACKET ), | |
449 | 451 | |
450 | 452 | ENUM_ELEMENT( NUM_CLIENT_COMMANDS ) |
451 | 453 | } |
@@ -65,6 +65,8 @@ | ||
65 | 65 | #include "v_palette.h" |
66 | 66 | #include "cmdlib.h" |
67 | 67 | #include "s_sound.h" |
68 | +// [AK] New #includes. | |
69 | +#include "voicechat.h" | |
68 | 70 | |
69 | 71 | #if FMOD_VERSION > 0x42899 && FMOD_VERSION < 0x43600 |
70 | 72 | #error You are trying to compile with an unsupported version of FMOD. |
@@ -1194,6 +1196,10 @@ | ||
1194 | 1196 | Sys->set3DSettings(0.5f, 96.f, 1.f); |
1195 | 1197 | Sys->set3DRolloffCallback(RolloffCallback); |
1196 | 1198 | snd_sfxvolume.Callback (); |
1199 | + | |
1200 | + // [AK] Initialize the VoIP controller after initializing FMOD. | |
1201 | + VOIPController::GetInstance( ).Init( Sys ); | |
1202 | + | |
1197 | 1203 | return true; |
1198 | 1204 | } |
1199 | 1205 |
@@ -1238,6 +1244,9 @@ | ||
1238 | 1244 | SfxReverbPlaceholder = NULL; |
1239 | 1245 | } |
1240 | 1246 | |
1247 | + // [AK] Shut down the VoIP controller. | |
1248 | + VOIPController::GetInstance( ).Shutdown( ); | |
1249 | + | |
1241 | 1250 | Sys->close(); |
1242 | 1251 | if (OutputPlugin != 0) |
1243 | 1252 | { |
@@ -2233,6 +2242,7 @@ | ||
2233 | 2242 | FMOD_VECTOR pos, vel; |
2234 | 2243 | FMOD_VECTOR forward; |
2235 | 2244 | FMOD_VECTOR up; |
2245 | + float sfxPitch = 0.0f; // [AK] | |
2236 | 2246 | |
2237 | 2247 | if (!listener->valid) |
2238 | 2248 | { |
@@ -2333,6 +2343,10 @@ | ||
2333 | 2343 | } |
2334 | 2344 | } |
2335 | 2345 | } |
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 ); | |
2336 | 2350 | } |
2337 | 2351 | |
2338 | 2352 | //========================================================================== |
@@ -83,6 +83,8 @@ | ||
83 | 83 | #include "network/netcommand.h" |
84 | 84 | #include "network/servercommands.h" |
85 | 85 | #include "maprotation.h" |
86 | +#include "voicechat.h" | |
87 | +#include "d_netinf.h" | |
86 | 88 | |
87 | 89 | CVAR (Bool, sv_showwarnings, false, CVAR_GLOBALCONFIG|CVAR_ARCHIVE) |
88 | 90 |
@@ -1213,6 +1215,47 @@ | ||
1213 | 1215 | |
1214 | 1216 | //***************************************************************************** |
1215 | 1217 | // |
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 | +// | |
1216 | 1259 | void SERVERCOMMANDS_PlayerTaunt( ULONG ulPlayer, ULONG ulPlayerExtra, ServerCommandFlags flags ) |
1217 | 1260 | { |
1218 | 1261 | if ( PLAYER_IsValidPlayer( ulPlayer ) == false ) |
@@ -2400,6 +2443,8 @@ | ||
2400 | 2443 | command.addByte( sv_limitcommands ); |
2401 | 2444 | // [AK] Send sv_allowprivatechat. |
2402 | 2445 | command.addByte( sv_allowprivatechat ); |
2446 | + // [AK] Send sv_allowvoicechat. | |
2447 | + command.addByte( sv_allowvoicechat ); | |
2403 | 2448 | // [AK] Send sv_respawndelaytime. |
2404 | 2449 | command.addFloat( sv_respawndelaytime ); |
2405 | 2450 | command.sendCommandToClients( ulPlayerExtra, flags ); |
@@ -134,6 +134,7 @@ | ||
134 | 134 | void SERVERCOMMANDS_PlayerIsSpectator( ULONG ulPlayer, ULONG ulPlayerExtra = MAXPLAYERS, ServerCommandFlags flags = 0 ); |
135 | 135 | void SERVERCOMMANDS_PlayerSay( ULONG ulPlayer, const char *pszString, ULONG ulMode, bool bForbidChatToPlayers, ULONG ulPlayerExtra = MAXPLAYERS, ServerCommandFlags flags = 0 ); |
136 | 136 | 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 ); | |
137 | 138 | void SERVERCOMMANDS_PlayerTaunt( ULONG ulPlayer, ULONG ulPlayerExtra = MAXPLAYERS, ServerCommandFlags flags = 0 ); |
138 | 139 | void SERVERCOMMANDS_PlayerRespawnInvulnerability( ULONG ulPlayer, ULONG ulPlayerExtra = MAXPLAYERS, ServerCommandFlags flags = 0 ); |
139 | 140 | void SERVERCOMMANDS_PlayerUseInventory( ULONG ulPlayer, AInventory *pItem, ULONG ulPlayerExtra = MAXPLAYERS, ServerCommandFlags flags = 0 ); |
@@ -2256,6 +2256,9 @@ | ||
2256 | 2256 | // [CK] We use a bitfield now. |
2257 | 2257 | else if ( name == NAME_CL_ClientFlags ) |
2258 | 2258 | pPlayer->userinfo.ClientFlagsChanged ( value.ToLong() ); |
2259 | + // [AK] | |
2260 | + else if ( name == NAME_Voice_Enable ) | |
2261 | + pPlayer->userinfo.VoiceEnableChanged ( value.ToLong() ); | |
2259 | 2262 | // If this is a Hexen game, read in the player's class. |
2260 | 2263 | else if ( name == NAME_PlayerClass ) |
2261 | 2264 | { |
@@ -2300,7 +2303,8 @@ | ||
2300 | 2303 | static const std::set<FName> required = { |
2301 | 2304 | NAME_Name, NAME_Autoaim, NAME_Gender, NAME_Skin, NAME_RailColor, |
2302 | 2305 | 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 | |
2304 | 2308 | }; |
2305 | 2309 | std::set<FName> missing; |
2306 | 2310 | std::set_difference( required.begin(), required.end(), names.begin(), names.end(), |
@@ -5150,6 +5154,19 @@ | ||
5150 | 5154 | } |
5151 | 5155 | } |
5152 | 5156 | 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 | + | |
5153 | 5170 | default: |
5154 | 5171 | |
5155 | 5172 | Printf( PRINT_HIGH, "SERVER_ParseCommands: Unknown client message: %d\n", static_cast<int> (lCommand) ); |
@@ -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 |
@@ -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__ |
@@ -482,6 +482,7 @@ | ||
482 | 482 | Control "Say", "messagemode" |
483 | 483 | Control "Team say", "messagemode2" |
484 | 484 | Control "Private say", "messagemode3" // [AK] |
485 | + Control "Voice chat", "+voicerecord" // [AK] | |
485 | 486 | StaticText "" |
486 | 487 | StaticText "Weapons", 1 |
487 | 488 | Control "Next weapon", "weapnext" |
@@ -1657,6 +1658,7 @@ | ||
1657 | 1658 | StaticText " " |
1658 | 1659 | Submenu "Advanced options", "AdvSoundOptions" |
1659 | 1660 | Submenu "Module replayer options", "ModReplayerOptions" |
1661 | + Submenu "Voice chat options", "ZA_VoiceChatOptions" // [AK] | |
1660 | 1662 | } |
1661 | 1663 | |
1662 | 1664 | /*======================================= |
@@ -138,6 +138,31 @@ | ||
138 | 138 | |
139 | 139 | // ================================================================================================= |
140 | 140 | // |
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 | +// | |
141 | 166 | // OFFLINE SKIRMISH |
142 | 167 | // |
143 | 168 | // ================================================================================================= |
@@ -594,7 +619,7 @@ | ||
594 | 619 | // |
595 | 620 | // ================================================================================================= |
596 | 621 | |
597 | -OptionValue ZA_AllowPrivateChat | |
622 | +OptionValue ZA_AllowChat | |
598 | 623 | { |
599 | 624 | 0, "Never" |
600 | 625 | 1, "Everyone" |
@@ -630,7 +655,8 @@ | ||
630 | 655 | TextField "Join password", "sv_joinpassword", "sv_forcejoinpassword" |
631 | 656 | Option "Require login to join", "sv_forcelogintojoin", "YesNo" |
632 | 657 | 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" | |
634 | 660 | // Option "Smooth lagging players", "sv_smoothplayers", "ZA_ExtrapolateLimit" |
635 | 661 | NumberField "Max connected clients", "sv_maxclients", 0, 64 |
636 | 662 | NumberField "Max players", "sv_maxplayers", 0, 64 |