From 8f355fe1888cd1f54f4135e31f6ef9b73e3b9e05 Mon Sep 17 00:00:00 2001 From: smallmodel <15067410+smallmodel@users.noreply.github.com> Date: Sun, 22 Sep 2024 16:50:27 +0200 Subject: [PATCH] Add Voip code from ioq3 --- code/client/cl_cgame.cpp | 36 +++++++ code/client/cl_main.cpp | 22 ++--- code/client/cl_parse.cpp | 196 ++++++++++++++++++++++++++++++++++++++- code/client/cl_ui.cpp | 2 +- code/client/client.h | 70 +++++++++++++- code/client/snd_public.h | 17 ++++ code/null/null_snddma.c | 23 +++++ code/qcommon/qcommon.h | 6 +- code/server/sv_main.c | 7 +- 9 files changed, 358 insertions(+), 21 deletions(-) diff --git a/code/client/cl_cgame.cpp b/code/client/cl_cgame.cpp index 13ee3756..ee6ce6cd 100644 --- a/code/client/cl_cgame.cpp +++ b/code/client/cl_cgame.cpp @@ -1111,6 +1111,42 @@ void CL_FirstSnapshot( void ) { Cbuf_AddText( cl_activeAction->string ); Cvar_Set( "activeAction", "" ); } + +#ifdef USE_MUMBLE + if ((cl_useMumble->integer) && !mumble_islinked()) { + int ret = mumble_link(CLIENT_WINDOW_TITLE); + Com_Printf("Mumble: Linking to Mumble application %s\n", ret==0?"ok":"failed"); + } +#endif + +#ifdef USE_VOIP + if (!clc.voipCodecInitialized) { + int i; + int error; + + clc.opusEncoder = opus_encoder_create(48000, 1, OPUS_APPLICATION_VOIP, &error); + + if ( error ) { + Com_DPrintf("VoIP: Error opus_encoder_create %d\n", error); + return; + } + + for (i = 0; i < MAX_CLIENTS; i++) { + clc.opusDecoder[i] = opus_decoder_create(48000, 1, &error); + if ( error ) { + Com_DPrintf("VoIP: Error opus_decoder_create(%d) %d\n", i, error); + return; + } + clc.voipIgnore[i] = qfalse; + clc.voipGain[i] = 1.0f; + } + clc.voipCodecInitialized = qtrue; + clc.voipMuteAll = qfalse; + Cmd_AddCommand ("voip", CL_Voip_f); + Cvar_Set("cl_voipSendTarget", "spatial"); + Com_Memset(clc.voipTargets, ~0, sizeof(clc.voipTargets)); + } +#endif } static int lastSnapFlags; diff --git a/code/client/cl_main.cpp b/code/client/cl_main.cpp index 9b05deaa..482a0885 100644 --- a/code/client/cl_main.cpp +++ b/code/client/cl_main.cpp @@ -704,7 +704,7 @@ void CL_PlayDemo_f( void ) { clc.state = CA_CONNECTED; clc.demoplaying = qtrue; - Q_strncpyz( cls.servername, Cmd_Argv(1), sizeof( cls.servername ) ); + Q_strncpyz( clc.servername, Cmd_Argv(1), sizeof( clc.servername ) ); // read demo messages until connected while ( clc.state >= CA_CONNECTED && clc.state < CA_PRIMED ) { @@ -830,7 +830,7 @@ void CL_MapLoading( qboolean flush, const char *pszMapName ) { if (pszMapName) { // if we are already connected to the local host, stay connected - if (clc.state >= CA_CONNECTED && !Q_stricmp(cls.servername, "localhost")) { + if (clc.state >= CA_CONNECTED && !Q_stricmp(clc.servername, "localhost")) { clc.state = CA_CONNECTED; // so the connect screen is drawn Com_Memset(cls.updateInfoString, 0, sizeof(cls.updateInfoString)); Com_Memset(clc.serverMessage, 0, sizeof(clc.serverMessage)); @@ -841,11 +841,11 @@ void CL_MapLoading( qboolean flush, const char *pszMapName ) { // clear nextmap so the cinematic shutdown doesn't execute it Cvar_Set("nextmap", ""); CL_Disconnect(); - Q_strncpyz(cls.servername, "localhost", sizeof(cls.servername)); + Q_strncpyz(clc.servername, "localhost", sizeof(clc.servername)); clc.state = CA_CHALLENGING; // so the connect screen is drawn clc.connectStartTime = cls.realtime; clc.connectTime = -RETRANSMIT_TIMEOUT; - NET_StringToAdr(cls.servername, &clc.serverAddress, NA_UNSPEC); + NET_StringToAdr(clc.servername, &clc.serverAddress, NA_UNSPEC); // we don't need a challenge on the localhost CL_CheckForResend(); @@ -1232,12 +1232,12 @@ CL_Reconnect_f ================ */ void CL_Reconnect_f( void ) { - if ( !strlen( cls.servername ) || !strcmp( cls.servername, "localhost" ) ) { + if ( !strlen( clc.servername ) || !strcmp( clc.servername, "localhost" ) ) { Com_Printf( "Can't reconnect to localhost.\n" ); return; } Cvar_Set("ui_singlePlayerActive", "0"); - Cbuf_AddText( va("connect %s\n", cls.servername ) ); + Cbuf_AddText( va("connect %s\n", clc.servername ) ); } /* @@ -1273,9 +1273,9 @@ void CL_Connect( const char *server, netadrtype_t family ) { CL_FlushMemory( ); */ - Q_strncpyz( cls.servername, server, sizeof( cls.servername ) ); + Q_strncpyz( clc.servername, server, sizeof( clc.servername ) ); - if( !NET_StringToAdr( cls.servername, &clc.serverAddress, family ) ) { + if( !NET_StringToAdr( clc.servername, &clc.serverAddress, family ) ) { Com_Printf( "Bad server address\n" ); clc.state = CA_DISCONNECTED; UI_PushMenu("badserveraddy"); @@ -1286,7 +1286,7 @@ void CL_Connect( const char *server, netadrtype_t family ) { } serverString = NET_AdrToStringwPort(clc.serverAddress); - Com_Printf( "%s resolved to %s\n", cls.servername, serverString ); + Com_Printf( "%s resolved to %s\n", clc.servername, serverString ); if (cl_guidServerUniq->integer) CL_UpdateGUID(serverString, strlen(serverString)); @@ -1705,7 +1705,7 @@ CL_Clientinfo_f void CL_Clientinfo_f( void ) { Com_Printf( "--------- Client Information ---------\n" ); Com_Printf( "state: %i\n", clc.state ); - Com_Printf( "Server: %s\n", cls.servername ); + Com_Printf( "Server: %s\n", clc.servername ); Com_Printf ("User info settings:\n"); Info_Print( Cvar_InfoString( CVAR_USERINFO ) ); Com_Printf( "--------------------------------------\n" ); @@ -2618,7 +2618,7 @@ void CL_Frame ( int msec ) { now.tm_min, now.tm_sec ); - Q_strncpyz( serverName, cls.servername, MAX_OSPATH ); + Q_strncpyz( serverName, clc.servername, MAX_OSPATH ); // Replace the ":" in the address as it is not a valid // file name character p = strstr( serverName, ":" ); diff --git a/code/client/cl_parse.cpp b/code/client/cl_parse.cpp index 12e8602a..77037a36 100644 --- a/code/client/cl_parse.cpp +++ b/code/client/cl_parse.cpp @@ -429,6 +429,18 @@ void CL_SystemInfoChanged( void ) { // in some cases, outdated cp commands might get sent with this news serverId cl.serverId = atoi( Info_ValueForKey( systemInfo, "sv_serverid" ) ); +#ifdef USE_VOIP +#ifdef LEGACY_PROTOCOL + if(clc.compat) + clc.voipEnabled = qfalse; + else +#endif + { + s = Info_ValueForKey( systemInfo, "sv_voipProtocol" ); + clc.voipEnabled = !Q_stricmp(s, "opus"); + } +#endif + // don't set any vars when playing a demo if ( clc.demoplaying ) { return; @@ -784,6 +796,178 @@ void CL_ParseDownload ( msg_t *msg ) { } } +#ifdef USE_VOIP +static +qboolean CL_ShouldIgnoreVoipSender(int sender) +{ + if (!cl_voip->integer) + return qtrue; // VoIP is disabled. + else if ((sender == clc.clientNum) && (!clc.demoplaying)) + return qtrue; // ignore own voice (unless playing back a demo). + else if (clc.voipMuteAll) + return qtrue; // all channels are muted with extreme prejudice. + else if (clc.voipIgnore[sender]) + return qtrue; // just ignoring this guy. + else if (clc.voipGain[sender] == 0.0f) + return qtrue; // too quiet to play. + + return qfalse; +} + +/* +===================== +CL_PlayVoip + +Play raw data +===================== +*/ + +static void CL_PlayVoip(int sender, int samplecnt, const byte *data, int flags) +{ + if(flags & VOIP_DIRECT) + { + S_RawSamples(sender + 1, samplecnt, 48000, 2, 1, + data, clc.voipGain[sender], -1); + } + + if(flags & VOIP_SPATIAL) + { + S_RawSamples(sender + MAX_CLIENTS + 1, samplecnt, 48000, 2, 1, + data, 1.0f, sender); + } +} + +/* +===================== +CL_ParseVoip + +A VoIP message has been received from the server +===================== +*/ +static +void CL_ParseVoip ( msg_t *msg, qboolean ignoreData ) { + static short decoded[VOIP_MAX_PACKET_SAMPLES*4]; // !!! FIXME: don't hard code + + const int sender = MSG_ReadShort(msg); + const int generation = MSG_ReadByte(msg); + const int sequence = MSG_ReadLong(msg); + const int frames = MSG_ReadByte(msg); + const int packetsize = MSG_ReadShort(msg); + const int flags = MSG_ReadBits(msg, VOIP_FLAGCNT); + unsigned char encoded[4000]; + int numSamples; + int seqdiff; + int written = 0; + int i; + + Com_DPrintf("VoIP: %d-byte packet from client %d\n", packetsize, sender); + + if (sender < 0) + return; // short/invalid packet, bail. + else if (generation < 0) + return; // short/invalid packet, bail. + else if (sequence < 0) + return; // short/invalid packet, bail. + else if (frames < 0) + return; // short/invalid packet, bail. + else if (packetsize < 0) + return; // short/invalid packet, bail. + + if (packetsize > sizeof (encoded)) { // overlarge packet? + int bytesleft = packetsize; + while (bytesleft) { + int br = bytesleft; + if (br > sizeof (encoded)) + br = sizeof (encoded); + MSG_ReadData(msg, encoded, br); + bytesleft -= br; + } + return; // overlarge packet, bail. + } + + MSG_ReadData(msg, encoded, packetsize); + + if (ignoreData) { + return; // just ignore legacy speex voip data + } else if (!clc.voipCodecInitialized) { + return; // can't handle VoIP without libopus! + } else if (sender >= MAX_CLIENTS) { + return; // bogus sender. + } else if (CL_ShouldIgnoreVoipSender(sender)) { + return; // Channel is muted, bail. + } + + // !!! FIXME: make sure data is narrowband? Does decoder handle this? + + Com_DPrintf("VoIP: packet accepted!\n"); + + seqdiff = sequence - clc.voipIncomingSequence[sender]; + + // This is a new "generation" ... a new recording started, reset the bits. + if (generation != clc.voipIncomingGeneration[sender]) { + Com_DPrintf("VoIP: new generation %d!\n", generation); + opus_decoder_ctl(clc.opusDecoder[sender], OPUS_RESET_STATE); + clc.voipIncomingGeneration[sender] = generation; + seqdiff = 0; + } else if (seqdiff < 0) { // we're ahead of the sequence?! + // This shouldn't happen unless the packet is corrupted or something. + Com_DPrintf("VoIP: misordered sequence! %d < %d!\n", + sequence, clc.voipIncomingSequence[sender]); + // reset the decoder just in case. + opus_decoder_ctl(clc.opusDecoder[sender], OPUS_RESET_STATE); + seqdiff = 0; + } else if (seqdiff * VOIP_MAX_PACKET_SAMPLES*2 >= sizeof (decoded)) { // dropped more than we can handle? + // just start over. + Com_DPrintf("VoIP: Dropped way too many (%d) frames from client #%d\n", + seqdiff, sender); + opus_decoder_ctl(clc.opusDecoder[sender], OPUS_RESET_STATE); + seqdiff = 0; + } + + if (seqdiff != 0) { + Com_DPrintf("VoIP: Dropped %d frames from client #%d\n", + seqdiff, sender); + // tell opus that we're missing frames... + for (i = 0; i < seqdiff; i++) { + assert((written + VOIP_MAX_PACKET_SAMPLES) * 2 < sizeof (decoded)); + numSamples = opus_decode(clc.opusDecoder[sender], NULL, 0, decoded + written, VOIP_MAX_PACKET_SAMPLES, 0); + if ( numSamples <= 0 ) { + Com_DPrintf("VoIP: Error decoding frame %d from client #%d\n", i, sender); + continue; + } + written += numSamples; + } + } + + numSamples = opus_decode(clc.opusDecoder[sender], encoded, packetsize, decoded + written, ARRAY_LEN(decoded) - written, 0); + + if ( numSamples <= 0 ) { + Com_DPrintf("VoIP: Error decoding voip data from client #%d\n", sender); + numSamples = 0; + } + + #if 0 + static FILE *encio = NULL; + if (encio == NULL) encio = fopen("voip-incoming-encoded.bin", "wb"); + if (encio != NULL) { fwrite(encoded, packetsize, 1, encio); fflush(encio); } + static FILE *decio = NULL; + if (decio == NULL) decio = fopen("voip-incoming-decoded.bin", "wb"); + if (decio != NULL) { fwrite(decoded+written, numSamples*2, 1, decio); fflush(decio); } + #endif + + written += numSamples; + + Com_DPrintf("VoIP: playback %d bytes, %d samples, %d frames\n", + written * 2, written, frames); + + if(written > 0) + CL_PlayVoip(sender, written, (const byte *) decoded, flags); + + clc.voipIncomingSequence[sender] = sequence + frames; +} +#endif + + /* ===================== CL_ParseCommandString @@ -932,8 +1116,16 @@ void CL_ParseServerMessage( msg_t *msg ) { } CL_ParseCGMessage( msg ); break; + case svc_voipSpeex: +#ifdef USE_VOIP + CL_ParseVoip( msg, qtrue ); +#endif + break; + case svc_voipOpus: +#ifdef USE_VOIP + CL_ParseVoip( msg, !clc.voipEnabled ); +#endif + break; } } } - - diff --git a/code/client/cl_ui.cpp b/code/client/cl_ui.cpp index 02304e07..f74bb5be 100644 --- a/code/client/cl_ui.cpp +++ b/code/client/cl_ui.cpp @@ -265,7 +265,7 @@ static void GetClientState(uiClientState_t *state) { state->connectPacketCount = clc.connectPacketCount; state->connState = clc.state; - Q_strncpyz(state->servername, cls.servername, sizeof(state->servername)); + Q_strncpyz(state->servername, clc.servername, sizeof(state->servername)); Q_strncpyz(state->updateInfoString, cls.updateInfoString, sizeof(state->updateInfoString)); Q_strncpyz(state->messageString, clc.serverMessage, sizeof(state->messageString)); state->clientNum = cl.snap.ps.clientNum; diff --git a/code/client/client.h b/code/client/client.h index c739d4fb..b33b123a 100644 --- a/code/client/client.h +++ b/code/client/client.h @@ -36,6 +36,10 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #include "cl_curl.h" #endif /* USE_CURL */ +#ifdef USE_VOIP +#include +#endif + // file full of random crap that gets used to create cl_guid #define QKEY_FILE "qkey" #define QKEY_SIZE 2048 @@ -174,6 +178,7 @@ typedef struct { int lastPacketSentTime; // for retransmits during connection int lastPacketTime; // for timeouts + char servername[MAX_OSPATH]; // name of server from original connect (used by reconnect) netadr_t serverAddress; int connectTime; // for connection retransmits int connectStartTime; @@ -239,15 +244,42 @@ typedef struct { int timeDemoMaxDuration; // maximum frame duration unsigned char timeDemoDurations[ MAX_TIMEDEMO_DURATIONS ]; // log of frame durations + float aviVideoFrameRemainder; + float aviSoundFrameRemainder; + +#ifdef USE_VOIP + qboolean voipEnabled; + qboolean voipCodecInitialized; + + // incoming data... + // !!! FIXME: convert from parallel arrays to array of a struct. + OpusDecoder *opusDecoder[MAX_CLIENTS]; + byte voipIncomingGeneration[MAX_CLIENTS]; + int voipIncomingSequence[MAX_CLIENTS]; + float voipGain[MAX_CLIENTS]; + qboolean voipIgnore[MAX_CLIENTS]; + qboolean voipMuteAll; + + // outgoing data... + // if voipTargets[i / 8] & (1 << (i % 8)), + // then we are sending to clientnum i. + uint8_t voipTargets[(MAX_CLIENTS + 7) / 8]; + uint8_t voipFlags; + OpusEncoder *opusEncoder; + int voipOutgoingDataSize; + int voipOutgoingDataFrames; + int voipOutgoingSequence; + byte voipOutgoingGeneration; + byte voipOutgoingData[1024]; + float voipPower; +#endif + #ifdef LEGACY_PROTOCOL qboolean compat; #endif // big stuff at end of structure so most offsets are 15 bits or less netchan_t netchan; - - float aviVideoFrameRemainder; - float aviSoundFrameRemainder; } clientConnection_t; extern clientConnection_t clc; @@ -297,8 +329,6 @@ typedef struct { qboolean cddialog; // bring up the cd needed dialog next frame qboolean no_menus; - char servername[MAX_OSPATH]; // name of server from original connect (used by reconnect) - // when the server clears the hunk, all of these must be restarted qboolean rendererRegistered; qboolean cgameStarted; @@ -445,6 +475,32 @@ extern cvar_t *cl_r_fullscreen; extern cvar_t *cl_consoleKeys; +#ifdef USE_MUMBLE +extern cvar_t *cl_useMumble; +extern cvar_t *cl_mumbleScale; +#endif + +#ifdef USE_VOIP +// cl_voipSendTarget is a string: "all" to broadcast to everyone, "none" to +// send to no one, or a comma-separated list of client numbers: +// "0,7,2,23" ... an empty string is treated like "all". +extern cvar_t *cl_voipUseVAD; +extern cvar_t *cl_voipVADThreshold; +extern cvar_t *cl_voipSend; +extern cvar_t *cl_voipSendTarget; +extern cvar_t *cl_voipGainDuringCapture; +extern cvar_t *cl_voipCaptureMult; +extern cvar_t *cl_voipShowMeter; +extern cvar_t *cl_voip; + +// 20ms at 48k +#define VOIP_MAX_FRAME_SAMPLES ( 20 * 48 ) + +// 3 frame is 60ms of audio, the max opus will encode at once +#define VOIP_MAX_PACKET_FRAMES 3 +#define VOIP_MAX_PACKET_SAMPLES ( VOIP_MAX_FRAME_SAMPLES * VOIP_MAX_PACKET_FRAMES ) +#endif + extern cvar_t *cg_gametype; extern cvar_t* j_pitch; @@ -546,6 +602,10 @@ extern int cl_connectedToPureServer; extern int cl_connectedToCheatServer; extern msg_t *cl_currentMSG; +#ifdef USE_VOIP +void CL_Voip_f( void ); +#endif + void CL_SystemInfoChanged( void ); void CL_ParseServerMessage( msg_t *msg ); diff --git a/code/client/snd_public.h b/code/client/snd_public.h index 596e066f..1ad7476d 100644 --- a/code/client/snd_public.h +++ b/code/client/snd_public.h @@ -166,3 +166,20 @@ unsigned int S_GetMusicOffset(); #endif #endif + + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef USE_VOIP +void S_StartCapture( void ); +int S_AvailableCaptureSamples( void ); +void S_Capture( int samples, byte *data ); +void S_StopCapture( void ); +void S_MasterGain( float gain ); +#endif + +#ifdef __cplusplus +} +#endif diff --git a/code/null/null_snddma.c b/code/null/null_snddma.c index 8bd0c65b..9b782fa7 100644 --- a/code/null/null_snddma.c +++ b/code/null/null_snddma.c @@ -47,6 +47,29 @@ void SNDDMA_Submit(void) { } +#ifdef USE_VOIP +void SNDDMA_StartCapture(void) +{ +} + +int SNDDMA_AvailableCaptureSamples(void) +{ + return 0; +} + +void SNDDMA_Capture(int samples, byte *data) +{ +} + +void SNDDMA_StopCapture(void) +{ +} + +void SNDDMA_MasterGain( float val ) +{ +} +#endif + void SNDDMA_Activate(void) { } diff --git a/code/qcommon/qcommon.h b/code/qcommon/qcommon.h index 17fa85be..58c7bc4d 100644 --- a/code/qcommon/qcommon.h +++ b/code/qcommon/qcommon.h @@ -362,7 +362,11 @@ enum svc_ops_e { svc_centerprint, svc_locprint, svc_cgameMessage, - svc_EOF + svc_EOF, + +// new commands, supported only by ioquake3 protocol but not legacy + svc_voipSpeex, // not wrapped in USE_VOIP, so this value is reserved. + svc_voipOpus, // }; // diff --git a/code/server/sv_main.c b/code/server/sv_main.c index 5e0b1fea..1be02ad4 100644 --- a/code/server/sv_main.c +++ b/code/server/sv_main.c @@ -24,7 +24,11 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #include "../gamespy/sv_gamespy.h" #include "../gamespy/sv_gqueryreporting.h" -cvar_t *sv_mapname; +#ifdef USE_VOIP +cvar_t *sv_voip; +cvar_t *sv_voipProtocol; +#endif + serverStatic_t svs; // persistant server info server_t sv; // local server //vm_t *gvm = NULL; // game virtual machine @@ -45,6 +49,7 @@ cvar_t *sv_reconnectlimit; // minimum seconds between connect messages cvar_t *sv_showloss; // report when usercmds are lost cvar_t *sv_padPackets; // add nop bytes to messages cvar_t *sv_killserver; // menu system can set to 1 to shut server down +cvar_t *sv_mapname; cvar_t *sv_mapChecksum; cvar_t *sv_serverid; cvar_t *sv_minRate;