From 50a8f23b4305c1180ead1f396931b59934c5a5a2 Mon Sep 17 00:00:00 2001 From: Sergey Degtyar Date: Thu, 26 Jun 2025 06:20:42 +0300 Subject: [PATCH 1/6] engine: sdl: always reset device descriptors on shutdown --- engine/platform/sdl2/s_sdl2.c | 1 + 1 file changed, 1 insertion(+) diff --git a/engine/platform/sdl2/s_sdl2.c b/engine/platform/sdl2/s_sdl2.c index 3041aff1..5197c41f 100644 --- a/engine/platform/sdl2/s_sdl2.c +++ b/engine/platform/sdl2/s_sdl2.c @@ -342,6 +342,7 @@ void VoiceCapture_Shutdown( void ) return; SDL_CloseAudioDevice( in_dev ); + in_dev = 0; } #endif // XASH_SOUND == SOUND_SDL From f37bbec78eb9041046b919064856bd35c55a3bc2 Mon Sep 17 00:00:00 2001 From: Sergey Degtyar Date: Thu, 26 Jun 2025 06:47:57 +0300 Subject: [PATCH 2/6] engine: sound: voice buffer isolation and correct MP3 volume handling --- engine/client/s_mix.c | 24 +++++++++++++++++++++--- engine/client/s_stream.c | 3 ++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/engine/client/s_mix.c b/engine/client/s_mix.c index 8379831e..fe28f6df 100644 --- a/engine/client/s_mix.c +++ b/engine/client/s_mix.c @@ -22,6 +22,7 @@ enum IPAINTBUFFER = 0, IROOMBUFFER, ISTREAMBUFFER, + IVOICEBUFFER, CPAINTBUFFERS, }; @@ -63,6 +64,7 @@ static portable_samplepair_t *g_curpaintbuffer; static portable_samplepair_t streambuffer[(PAINTBUFFER_SIZE+1)]; static portable_samplepair_t paintbuffer[(PAINTBUFFER_SIZE+1)]; static portable_samplepair_t roombuffer[(PAINTBUFFER_SIZE+1)]; +static portable_samplepair_t voicebuffer[(PAINTBUFFER_SIZE+1)]; static portable_samplepair_t temppaintbuffer[(PAINTBUFFER_SIZE+1)]; static paintbuffer_t paintbuffers[CPAINTBUFFERS]; @@ -216,6 +218,7 @@ void MIX_InitAllPaintbuffers( void ) paintbuffers[IPAINTBUFFER].pbuf = paintbuffer; paintbuffers[IROOMBUFFER].pbuf = roombuffer; paintbuffers[ISTREAMBUFFER].pbuf = streambuffer; + paintbuffers[IVOICEBUFFER].pbuf = voicebuffer; MIX_SetCurrentPaintbuffer( IPAINTBUFFER ); } @@ -921,11 +924,12 @@ static void S_MixUpsample( int sampleCount, int filtertype ) static void MIX_MixRawSamplesBuffer( int end ) { - portable_samplepair_t *pbuf, *roombuf, *streambuf; + portable_samplepair_t *pbuf, *roombuf, *streambuf, *voicebuf; uint i, j, stop; roombuf = MIX_GetPFrontFromIPaint( IROOMBUFFER ); streambuf = MIX_GetPFrontFromIPaint( ISTREAMBUFFER ); + voicebuf = MIX_GetPFrontFromIPaint( IVOICEBUFFER ); if( s_listener.paused ) return; @@ -935,6 +939,7 @@ static void MIX_MixRawSamplesBuffer( int end ) // copy from the streaming sound source rawchan_t *ch = raw_channels[i]; qboolean stream; + qboolean is_voice; if( !ch ) continue; @@ -943,8 +948,18 @@ static void MIX_MixRawSamplesBuffer( int end ) if( !ch->leftvol && !ch->rightvol ) continue; + is_voice = (ch->entnum > 0 && ch->entnum <= MAX_CLIENTS) || + (ch->entnum == VOICE_LOOPBACK_INDEX) || + (ch->entnum == VOICE_LOCALCLIENT_INDEX); + stream = ch->entnum == S_RAW_SOUND_BACKGROUNDTRACK || CL_IsPlayerIndex( ch->entnum ); - pbuf = stream ? streambuf : roombuf; + + if( is_voice ) + pbuf = voicebuf; + else if( stream ) + pbuf = streambuf; + else + pbuf = roombuf; stop = (end < ch->s_rawend) ? end : ch->s_rawend; @@ -1045,7 +1060,10 @@ void MIX_PaintChannels( int endtime ) MIX_MixPaintbuffers( IPAINTBUFFER, IROOMBUFFER, IPAINTBUFFER, count, S_GetMasterVolume() ); // add music or soundtrack from movie (no dsp) - MIX_MixPaintbuffers( IPAINTBUFFER, ISTREAMBUFFER, IPAINTBUFFER, count, S_GetMusicVolume() ); + MIX_MixPaintbuffers( IPAINTBUFFER, ISTREAMBUFFER, IPAINTBUFFER, count, 1.0f ); + + // voice chat + MIX_MixPaintbuffers( IPAINTBUFFER, IVOICEBUFFER, IPAINTBUFFER, count, 1.0f ); // clip all values > 16 bit down to 16 bit MIX_CompressPaintbuffer( IPAINTBUFFER, count ); diff --git a/engine/client/s_stream.c b/engine/client/s_stream.c index 36d9ae72..fbc24efa 100644 --- a/engine/client/s_stream.c +++ b/engine/client/s_stream.c @@ -242,7 +242,8 @@ void S_StreamBackgroundTrack( void ) if( r > 0 ) { // add to raw buffer - S_RawEntSamples( S_RAW_SOUND_BACKGROUNDTRACK, fileSamples, info->rate, info->width, info->channels, raw, 255 ); + int music_vol = (int)(255.0f * S_GetMusicVolume()); + S_RawEntSamples( S_RAW_SOUND_BACKGROUNDTRACK, fileSamples, info->rate, info->width, info->channels, raw, music_vol ); } else { From 6bb7bcd378c6fc67d13422bd646eca8fa05ac3dd Mon Sep 17 00:00:00 2001 From: Sergey Degtyar Date: Thu, 26 Jun 2025 08:56:48 +0300 Subject: [PATCH 3/6] engine: client: GoldSrc voice chat support - Added support for GoldSrc voice mode, including initialization and shutdown functions. - Implemented data size verification for the GoldSrc voice package. - Creation and processing of voice packets, CRC verification of data integrity. - Added a Steam ID field to the client_static_t structure - Automatic gain control has been introduced for voice samples. - The voice status structure has been updated. - Refactored voice data handling for better type consistency and maintainability. - Fixed ISO C90 compliance issues for better portability. - Fixed decoder index usage. --- engine/client/cl_main.c | 4 + engine/client/cl_parse.c | 49 +-- engine/client/client.h | 1 + engine/client/voice.c | 889 +++++++++++++++++++++++++++++++-------- engine/client/voice.h | 46 +- 5 files changed, 773 insertions(+), 216 deletions(-) diff --git a/engine/client/cl_main.c b/engine/client/cl_main.c index cc8dcd3b..7796d23d 100644 --- a/engine/client/cl_main.c +++ b/engine/client/cl_main.c @@ -1029,6 +1029,10 @@ static void CL_WriteSteamTicket( sizebuf_t *send ) crc = CRC32_Final( crc ); i = GenerateRevEmu2013( buf, s, crc ); MSG_WriteBytes( send, buf, i ); + + // RevEmu2013: pTicket[1] = revHash (low), pTicket[5] = 0x01100001 (high) + *(uint32_t*)cls.steamid = LittleLong( ((uint32_t*)buf)[1] ); + *(uint32_t*)(cls.steamid + 4) = LittleLong( ((uint32_t*)buf)[5] ); } /* diff --git a/engine/client/cl_parse.c b/engine/client/cl_parse.c index 75b43f0e..554c12f3 100644 --- a/engine/client/cl_parse.c +++ b/engine/client/cl_parse.c @@ -2010,44 +2010,41 @@ CL_ParseVoiceData */ void CL_ParseVoiceData( sizebuf_t *msg, connprotocol_t proto ) { - int size, idx, frames; + int size, idx, frames = 0; byte received[8192]; idx = MSG_ReadByte( msg ) + 1; - if( proto == PROTO_GOLDSRC ) - { - size = MSG_ReadShort( msg ); - MSG_SeekToBit( msg, size << 3, SEEK_CUR ); // skip the entire buf, not supported yet - -#if 0 // shall we notify client.dll if nothing can be heard? - // must notify through as both local player and normal client - if( idx == cl.playernum + 1 ) - Voice_StatusAck( &voice.local, VOICE_LOOPBACK_INDEX ); - - Voice_StatusAck( &voice.players_status[idx], idx ); -#endif - return; - } - - frames = MSG_ReadByte( msg ); - size = MSG_ReadShort( msg ); - size = Q_min( size, sizeof( received )); - - MSG_ReadBytes( msg, received, size ); - if ( idx <= 0 || idx > cl.maxclients ) return; - // must notify through as both local player and normal client - if( idx == cl.playernum + 1 ) - Voice_StatusAck( &voice.local, VOICE_LOOPBACK_INDEX ); + if( proto == PROTO_GOLDSRC ) + { + size = MSG_ReadShort( msg ); + if ( size > 4096 ) + { + Con_Printf( S_ERROR "Voice data size is too large: %d bytes (max: %d)\n", size, VOICE_MAX_GS_DATA_SIZE ); + return; + } + } + else + { + frames = MSG_ReadByte( msg ); + size = MSG_ReadShort( msg ); + if (size > 8192 ) + { + Con_Printf( S_ERROR "Voice data size is too large: %d bytes (max: %d)\n", size, VOICE_MAX_DATA_SIZE ); + return; + } + } - Voice_StatusAck( &voice.players_status[idx], idx ); + size = Q_min( size, sizeof( received )); if ( !size ) return; + MSG_ReadBytes( msg, received, size ); + Voice_AddIncomingData( idx, received, size, frames ); } diff --git a/engine/client/client.h b/engine/client/client.h index c1766e37..951d8f44 100644 --- a/engine/client/client.h +++ b/engine/client/client.h @@ -638,6 +638,7 @@ typedef struct // server's build number (might be zero) int build_num; + uint8_t steamid[8]; } client_static_t; #ifdef __cplusplus diff --git a/engine/client/voice.c b/engine/client/voice.c index 32eda583..7fcc3739 100644 --- a/engine/client/voice.c +++ b/engine/client/voice.c @@ -16,9 +16,11 @@ GNU General Public License for more details. #define CUSTOM_MODES 1 // required to correctly link with Opus Custom #include +#include #include "common.h" #include "client.h" #include "voice.h" +#include "crclib.h" voice_state_t voice = { 0 }; @@ -31,11 +33,84 @@ CVAR_DEFINE_AUTO( voice_maxgain, "5.0", FCVAR_PRIVILEGED|FCVAR_ARCHIVE, "automat CVAR_DEFINE_AUTO( voice_inputfromfile, "0", FCVAR_PRIVILEGED, "input voice from voice_input.wav" ); static void Voice_ApplyGainAdjust( int16_t *samples, int count, float scale ); +static qboolean Voice_InitGoldSrcMode( int quality ); +static qboolean Voice_InitOpusCustomMode( int quality ); +static void Voice_ShutdownGoldSrcMode( void ); +static void Voice_ShutdownOpusCustomMode( void ); +static uint Voice_GetOpusCompressedData( byte *out, uint maxsize, uint *frames ); +static uint Voice_GetGSCompressedData( byte *out, uint maxsize, uint *frames ); +static int Voice_ProcessGSData( int ent, const uint8_t *data, uint32_t size ); +static uint Voice_CreateGSVoicePacket( byte *out, const byte *voice_data, uint voice_size ); +static void Voice_StartChannel( uint samples, byte *data, int entnum ); +static void Voice_Status( int entindex, qboolean bTalking ); +static void Voice_StatusTimeout( voice_status_t *status, int entindex, double frametime ); +static void Voice_Shutdown( void ); -// in case user enabled voice after connection -// can't keep in `voice` struct because it gets zeroed on shutdown -static string voice_codec_init; -static int voice_quality_init; +/* +=============================================================================== + + UTILITY FUNCTIONS + +=============================================================================== +*/ + +/* +========================= +Voice_IsGoldSrcMode + +Check if codec is GoldSrc mode +========================= +*/ +qboolean Voice_IsGoldSrcMode( const char *codec ) +{ + return ( COM_CheckString( codec ) == 0 || Q_strstr( codec, "voice_speex" ) != NULL ); +} + +/* +========================= +Voice_IsOpusCustomMode + +Check if codec is Opus Custom mode +========================= +*/ +qboolean Voice_IsOpusCustomMode( const char *codec ) +{ + return Q_strcmp( codec, VOICE_OPUS_CUSTOM_CODEC ) == 0; +} + +/* +========================= +Voice_GetBitrateForQuality + +Get bitrate for given quality level +========================= +*/ +int Voice_GetBitrateForQuality( int quality, qboolean goldsrc ) +{ + if( goldsrc ) + { + switch( quality ) + { + case 1: return 6000; // 6 kbps + case 2: return 12000; // 12 kbps + case 3: return 24000; // 24 kbps + case 4: return 36000; // 36 kbps + case 5: return 48000; // 48 kbps + default: return 36000; // default + } + } + else + { + switch( quality ) + { + case 1: return 6000; // 6 kbps + case 2: return 12000; // 12 kbps + case 4: return 64000; // 64 kbps + case 5: return 96000; // 96 kbps + default: return 36000; // default + } + } +} /* =============================================================================== @@ -45,11 +120,18 @@ static int voice_quality_init; =============================================================================== */ +/* +========================= +Voice_InitCustomMode + +Initialize Opus Custom mode +========================= +*/ static qboolean Voice_InitCustomMode( void ) { int err = 0; - voice.width = sizeof( opus_int16 ); + voice.width = sizeof( int16_t ); voice.samplerate = VOICE_OPUS_CUSTOM_SAMPLERATE; voice.frame_size = VOICE_OPUS_CUSTOM_FRAME_SIZE; @@ -68,6 +150,7 @@ static qboolean Voice_InitCustomMode( void ) ========================= Voice_InitOpusDecoder +Initialize Opus decoders for clients ========================= */ static qboolean Voice_InitOpusDecoder( void ) @@ -76,12 +159,23 @@ static qboolean Voice_InitOpusDecoder( void ) for( int i = 0; i < cl.maxclients; i++ ) { - voice.decoders[i] = opus_custom_decoder_create( voice.custom_mode, VOICE_PCM_CHANNELS, &err ); - - if( !voice.decoders[i] ) + if( voice.goldsrc ) { - Con_Printf( S_ERROR "Can't create Opus decoder for %i: %s\n", i, opus_strerror( err )); - return false; + voice.gs_decoders[i] = opus_decoder_create( GS_DEFAULT_SAMPLE_RATE, VOICE_PCM_CHANNELS, &err ); + if( !voice.gs_decoders[i] ) + { + Con_Printf( S_ERROR "Can't create GoldSrc Opus decoder for %i: %s\n", i, opus_strerror( err )); + return false; + } + } + else + { + voice.decoders[i] = opus_custom_decoder_create( voice.custom_mode, VOICE_PCM_CHANNELS, &err ); + if( !voice.decoders[i] ) + { + Con_Printf( S_ERROR "Can't create Custom Opus decoder for %i: %s\n", i, opus_strerror( err )); + return false; + } } } @@ -92,36 +186,34 @@ static qboolean Voice_InitOpusDecoder( void ) ========================= Voice_InitOpusEncoder +Initialize Opus encoder with quality settings ========================= */ static qboolean Voice_InitOpusEncoder( int quality ) { int err = 0; + int bitrate = Voice_GetBitrateForQuality( quality, voice.goldsrc ); - voice.encoder = opus_custom_encoder_create( voice.custom_mode, VOICE_PCM_CHANNELS, &err ); - if( !voice.encoder ) + if (voice.goldsrc) { - Con_Printf( S_ERROR "Can't create Opus encoder: %s\n", opus_strerror( err )); - return false; + voice.gs_encoder = opus_encoder_create( GS_DEFAULT_SAMPLE_RATE, VOICE_PCM_CHANNELS, OPUS_APPLICATION_VOIP, &err ); + if( !voice.gs_encoder ) + { + Con_Printf( S_ERROR "Can't create GoldSrc Opus encoder: %s\n", opus_strerror( err )); + return false; + } + opus_encoder_ctl( voice.gs_encoder, OPUS_SET_DTX( 1 )); + opus_encoder_ctl( voice.gs_encoder, OPUS_SET_BITRATE( bitrate )); } - - switch( quality ) + else { - case 1: // 6 kbps - opus_custom_encoder_ctl( voice.encoder, OPUS_SET_BITRATE( 6000 )); - break; - case 2: // 12 kbps - opus_custom_encoder_ctl( voice.encoder, OPUS_SET_BITRATE( 12000 )); - break; - case 4: // 64 kbps - opus_custom_encoder_ctl( voice.encoder, OPUS_SET_BITRATE( 64000 )); - break; - case 5: // 96 kbps - opus_custom_encoder_ctl( voice.encoder, OPUS_SET_BITRATE( 96000 )); - break; - default: // 36 kbps - opus_custom_encoder_ctl( voice.encoder, OPUS_SET_BITRATE( 36000 )); - break; + voice.encoder = opus_custom_encoder_create( voice.custom_mode, VOICE_PCM_CHANNELS, &err ); + if( !voice.encoder ) + { + Con_Printf( S_ERROR "Can't create Opus encoder: %s\n", opus_strerror( err )); + return false; + } + opus_custom_encoder_ctl( voice.encoder, OPUS_SET_BITRATE( bitrate )); } return true; @@ -131,17 +223,29 @@ static qboolean Voice_InitOpusEncoder( int quality ) ========================= Voice_ShutdownOpusDecoder +Cleanup Opus decoders ========================= */ static void Voice_ShutdownOpusDecoder( void ) { for( int i = 0; i < MAX_CLIENTS; i++ ) { - if( !voice.decoders[i] ) - continue; - - opus_custom_decoder_destroy( voice.decoders[i] ); - voice.decoders[i] = NULL; + if( voice.goldsrc ) + { + if( voice.gs_decoders[i] ) + { + opus_decoder_destroy( voice.gs_decoders[i] ); + voice.gs_decoders[i] = NULL; + } + } + else + { + if( voice.decoders[i] ) + { + opus_custom_decoder_destroy( voice.decoders[i] ); + voice.decoders[i] = NULL; + } + } } } @@ -149,17 +253,36 @@ static void Voice_ShutdownOpusDecoder( void ) ========================= Voice_ShutdownOpusEncoder +Cleanup Opus encoder ========================= */ static void Voice_ShutdownOpusEncoder( void ) { - if( voice.encoder ) + if( voice.goldsrc ) { - opus_custom_encoder_destroy( voice.encoder ); - voice.encoder = NULL; + if( voice.gs_encoder ) + { + opus_encoder_destroy( voice.gs_encoder ); + voice.gs_encoder = NULL; + } + } + else + { + if( voice.encoder ) + { + opus_custom_encoder_destroy( voice.encoder ); + voice.encoder = NULL; + } } } +/* +========================= +Voice_ShutdownCustomMode + +Cleanup Opus Custom mode +========================= +*/ static void Voice_ShutdownCustomMode( void ) { if( voice.custom_mode ) @@ -169,10 +292,155 @@ static void Voice_ShutdownCustomMode( void ) } } +/* +========================= +Voice_InitGoldSrcMode + +Initialize GoldSrc voice mode +========================= +*/ +static qboolean Voice_InitGoldSrcMode( int quality ) +{ + voice.goldsrc = true; + voice.autogain.block_size = 128; + voice.width = sizeof( int16_t ); + voice.samplerate = GS_DEFAULT_SAMPLE_RATE; + voice.frame_size = GS_DEFAULT_FRAME_SIZE; + + if( !Voice_InitOpusDecoder() ) + { + Con_Printf( S_ERROR "Can't create GoldSrc decoders, voice chat is disabled.\n" ); + return false; + } + + if( !Voice_InitOpusEncoder( quality ) ) + { + Con_Printf( S_WARN "Other players will not be able to hear you.\n" ); + return false; + } + + return true; +} + +/* +========================= +Voice_InitOpusCustomMode + +Initialize Opus Custom voice mode +========================= +*/ +static qboolean Voice_InitOpusCustomMode( int quality ) +{ + voice.goldsrc = false; + voice.autogain.block_size = 128; + voice.samplerate = VOICE_OPUS_CUSTOM_SAMPLERATE; + voice.frame_size = VOICE_OPUS_CUSTOM_FRAME_SIZE; + voice.width = sizeof( int16_t ); + + if( !Voice_InitCustomMode() || !Voice_InitOpusDecoder() ) + { + Con_Printf( S_ERROR "Can't create Opus Custom decoders, voice chat is disabled.\n" ); + return false; + } + + if( !Voice_InitOpusEncoder( quality ) ) + { + Con_Printf( S_WARN "Other players will not be able to hear you.\n" ); + return false; + } + + return true; +} + +/* +========================= +Voice_ShutdownGoldSrcMode + +Cleanup GoldSrc mode +========================= +*/ +static void Voice_ShutdownGoldSrcMode( void ) +{ + Voice_ShutdownOpusDecoder(); + Voice_ShutdownOpusEncoder(); +} + +/* +========================= +Voice_ShutdownOpusCustomMode + +Cleanup Opus Custom mode +========================= +*/ +static void Voice_ShutdownOpusCustomMode( void ) +{ + Voice_ShutdownOpusDecoder(); + Voice_ShutdownOpusEncoder(); + Voice_ShutdownCustomMode(); +} + +/* +=============================================================================== + + VOICE PROCESSING + +=============================================================================== +*/ + +/* +========================= +Voice_ApplyGainAdjust + +Apply automatic gain control to voice samples +========================= +*/ +static void Voice_ApplyGainAdjust( int16_t *samples, int count, float scale ) +{ + float gain, modifiedMax; + int average, blockOffset = 0; + + for( ;; ) + { + int i, localMax = 0, localSum = 0; + int blockSize = Q_min(count - blockOffset, voice.autogain.block_size); + + if( blockSize < 1 ) + break; + + for( i = 0; i < blockSize; ++i ) + { + int sample = samples[blockOffset + i]; + int absSample = abs( sample ); + + if( absSample > localMax ) + localMax = absSample; + + localSum += absSample; + gain = voice.autogain.current_gain + i * voice.autogain.gain_multiplier; + samples[blockOffset + i] = bound( SHRT_MIN, (int)( sample * gain ), SHRT_MAX ); + } + + if( blockOffset % voice.autogain.block_size == 0 ) + { + average = localSum / blockSize; + modifiedMax = average + ( localMax - average ) * voice_avggain.value; + + voice.autogain.current_gain = voice.autogain.next_gain * scale; + voice.autogain.next_gain = Q_min((float)SHRT_MAX / (modifiedMax > 1 ? modifiedMax : 1), voice_maxgain.value) * scale; + if (blockSize > 1) + voice.autogain.gain_multiplier = (voice.autogain.next_gain - voice.autogain.current_gain) / (blockSize - 1); + else + voice.autogain.gain_multiplier = 0.0f; + } + blockOffset += blockSize; + } +} + /* ========================= Voice_GetOpusCompressedData +Get compressed voice data for Opus Custom mode ========================= */ static uint Voice_GetOpusCompressedData( byte *out, uint maxsize, uint *frames ) @@ -204,21 +472,16 @@ static uint Voice_GetOpusCompressedData( byte *out, uint maxsize, uint *frames ) { int bytes; -#if 1 if( !voice.input_file ) - { - // adjust gain before encoding, but only for input from voice - Voice_ApplyGainAdjust((opus_int16*)(voice.input_buffer + ofs), voice.frame_size, voice_transmit_scale.value); - } -#endif + Voice_ApplyGainAdjust((int16_t*)(voice.input_buffer + ofs), voice.frame_size, voice_transmit_scale.value); - bytes = opus_custom_encode( voice.encoder, (const opus_int16 *)( voice.input_buffer + ofs ), + bytes = opus_custom_encode( voice.encoder, (const int16_t *)( voice.input_buffer + ofs ), voice.frame_size, out + size + sizeof( uint16_t ), maxsize ); if( bytes > 0 ) { // write compressed frame size - *((uint16_t *)&out[size]) = bytes; + *((uint16_t *)&out[size]) = LittleShort(bytes); size += bytes + sizeof( uint16_t ); maxsize -= bytes + sizeof( uint16_t ); @@ -235,10 +498,8 @@ static uint Voice_GetOpusCompressedData( byte *out, uint maxsize, uint *frames ) if( ofs ) { fs_offset_t remaining = voice.input_buffer_pos - ofs; - // move remaining samples to the beginning of buffer memmove( voice.input_buffer, voice.input_buffer + ofs, remaining ); - voice.input_buffer_pos = remaining; } @@ -248,6 +509,262 @@ static uint Voice_GetOpusCompressedData( byte *out, uint maxsize, uint *frames ) return size; } +/* +========================= +Voice_GetGSCompressedData + +Get compressed voice data for GoldSrc mode +========================= +*/ +static uint Voice_GetGSCompressedData( byte *out, uint maxsize, uint *frames ) +{ + uint ofs = 0, size = 0; + const uint frame_size_samples = GS_DEFAULT_FRAME_SIZE; + const uint frame_size_bytes = GS_DEFAULT_FRAME_SIZE * voice.width; + static uint16_t sequence = 0; + + if( voice.input_file ) + { + uint numbytes; + double updateInterval, curtime = Sys_DoubleTime(); + + updateInterval = curtime - voice.start_time; + voice.start_time = curtime; + + numbytes = updateInterval * voice.samplerate * voice.width * VOICE_PCM_CHANNELS; + numbytes = Q_min( numbytes, voice.input_file->size - voice.input_file_pos ); + numbytes = Q_min( numbytes, sizeof( voice.input_buffer ) - voice.input_buffer_pos ); + + memcpy( voice.input_buffer + voice.input_buffer_pos, voice.input_file->buffer + voice.input_file_pos, numbytes ); + voice.input_buffer_pos += numbytes; + voice.input_file_pos += numbytes; + } + + if( !voice.input_file ) + VoiceCapture_Lock( true ); + + *frames = 0; + + while (voice.input_buffer_pos - ofs >= frame_size_bytes) + { + int bytes; + + int is_silence = 1; + int16_t *samples = (int16_t *)(voice.input_buffer + ofs); + for (uint i = 0; i < frame_size_samples; ++i) { + if (samples[i] != 0) { + is_silence = 0; + break; + } + } + if (is_silence) { + ofs += frame_size_bytes; + continue; + } + + if ( !voice.input_file ) + Voice_ApplyGainAdjust(samples, frame_size_samples, voice_transmit_scale.value); + + bytes = opus_encode( voice.gs_encoder, samples, + frame_size_samples, out + size + 4, maxsize - 4 ); + + if( bytes > 0 ) + { + *(uint16_t*)(out + size) = LittleShort(bytes); + *(uint16_t*)(out + size + 2) = LittleShort(sequence++); + + size += bytes + 4; + maxsize -= bytes + 4; + + (*frames)++; + } + else + { + Con_Printf( S_ERROR "%s: failed to encode frame: %s\n", __func__, opus_strerror( bytes )); + } + + ofs += frame_size_bytes; + } + + if( ofs ) + { + fs_offset_t remaining = voice.input_buffer_pos - ofs; + memmove( voice.input_buffer, voice.input_buffer + ofs, remaining ); + voice.input_buffer_pos = remaining; + } + + if( !voice.input_file ) + VoiceCapture_Lock( false ); + + return size; +} + +/* +========================= +Voice_ProcessGSData + +Process GoldSrc voice data and return number of samples +========================= +*/ +static int Voice_ProcessGSData( int ent, const uint8_t *data, uint32_t size ) +{ + uint32_t crc_in_packet; + uint32_t crc; + size_t offset; + int16_t pcm[GS_MAX_DECOMPRESSED_SAMPLES]; + size_t output_samples; + uint16_t sample_rate; + uint8_t vpc_type; + uint16_t data_len; + OpusDecoder *decoder; + size_t opus_offset; + int decoded; + size_t silence_samples; + uint16_t frame_size; + + if (!data || size < 18 || size < 4 || ent <= 0 || ent > cl.maxclients) + return 0; + + crc_in_packet = LittleLong(*(uint32_t*)(data + size - 4)); + crc = CRC32_INIT_VALUE; + CRC32_ProcessBuffer(&crc, data, size - 4); + crc = CRC32_Final(crc); + + if (crc != crc_in_packet) { + Con_Printf( S_WARN "Voice packet CRC32 mismatch\n" ); + return 0; + } + + offset = 8; + output_samples = 0; + + if (offset >= size - 4 || data[offset] != GS_VPC_SETSAMPLERATE) { + Con_Printf( S_WARN "Invalid voice packet type: %d\n", data[offset] ); + return 0; + } + offset++; + + if (offset + 4 > size - 4) + return 0; + + sample_rate = LittleShort(*(uint16_t*)(data + offset)); + offset += 2; + + vpc_type = data[offset++]; + data_len = LittleShort(*(uint16_t*)(data + offset)); + offset += 2; + + if (offset + data_len > size - 4) { + Con_Printf( S_WARN "Voice packet data_len out of bounds\n" ); + return 0; + } + + if (vpc_type == GS_VPC_VDATA_OPUS_PLC) { + decoder = voice.gs_decoders[ent]; + if (!decoder) { + Con_Printf( S_WARN "No decoder available for entity %d\n", ent ); + return 0; + } + opus_offset = 0; + while (opus_offset + 4 <= data_len) { + frame_size = LittleShort(*(uint16_t*)(data + offset + opus_offset)); + opus_offset += 4; + + if (frame_size == 0) { + if (output_samples + 160 > GS_MAX_DECOMPRESSED_SAMPLES) { + Con_Printf( S_WARN "Voice buffer overflow\n" ); + return 0; + } + memset(pcm + output_samples, 0, 160 * sizeof(int16_t)); + output_samples += 160; + continue; + } + if (frame_size == 0xFFFF) { + opus_decoder_ctl(decoder, OPUS_RESET_STATE); + break; + } + if (opus_offset + frame_size > data_len) { + Con_Printf( S_WARN "Opus frame size exceeds data length\n" ); + return 0; + } + decoded = opus_decode(decoder, data + offset + opus_offset, frame_size, + pcm + output_samples, GS_DEFAULT_FRAME_SIZE, 0); + if (decoded < 0) { + Con_Printf( S_WARN "Opus decode error: %s\n", opus_strerror(decoded) ); + return 0; + } + output_samples += decoded; + opus_offset += frame_size; + } + } else if (vpc_type == GS_VPC_VDATA_SILENCE) { + silence_samples = data_len / 2; + if (silence_samples > GS_MAX_DECOMPRESSED_SAMPLES) { + Con_Printf( S_WARN "Silence data too large\n" ); + return 0; + } + memset(pcm, 0, silence_samples * sizeof(int16_t)); + output_samples = silence_samples; + } else { + Con_Printf( S_WARN "Unsupported voice data type: %d\n", vpc_type ); + return 0; + } + + if (output_samples > 0) + Voice_StartChannel(output_samples, (byte*)pcm, ent); + + return output_samples; +} + +/* +========================= +Voice_CreateGSVoicePacket + +Create GoldSrc voice packet +========================= +*/ +static uint Voice_CreateGSVoicePacket( byte *out, const byte *voice_data, uint voice_size ) +{ + uint offset = 0; + uint32_t crc; + + if (!out || !voice_data || voice_size == 0) + { + Con_Printf( S_WARN "Invalid voice data for packet creation\n" ); + return 0; + } + + if ( cls.steamid ) { + memcpy(out + offset, cls.steamid, 8); + } else { + memset(out + offset, 0, 8); // fallback: 0 + } + + offset += 8; + + out[offset] = GS_VPC_SETSAMPLERATE; + offset += 1; + + *(uint16_t*)(out + offset) = LittleShort(GS_DEFAULT_SAMPLE_RATE); + offset += 2; + + out[offset] = GS_VPC_VDATA_OPUS_PLC; + offset += 1; + + *(uint16_t*)(out + offset) = LittleShort(voice_size); + offset += 2; + + memcpy( out + offset, voice_data, voice_size ); + offset += voice_size; + + crc = CRC32_INIT_VALUE; + CRC32_ProcessBuffer( &crc, out, offset ); + crc = CRC32_Final( crc ); + *(uint32_t*)(out + offset) = LittleLong(crc); + offset += 4; + + return offset; +} + /* =============================================================================== @@ -256,56 +773,11 @@ static uint Voice_GetOpusCompressedData( byte *out, uint maxsize, uint *frames ) =============================================================================== */ -/* -========================= -Voice_ApplyGainAdjust - -========================= -*/ -static void Voice_ApplyGainAdjust( int16_t *samples, int count, float scale ) -{ - float gain, modifiedMax; - int average, blockOffset = 0; - - for( ;; ) - { - int i, localMax = 0, localSum = 0; - int blockSize = Q_min( count - ( blockOffset + voice.autogain.block_size ), voice.autogain.block_size ); - - if( blockSize < 1 ) - break; - - for( i = 0; i < blockSize; ++i ) - { - int sample = samples[blockOffset + i]; - int absSample = abs( sample ); - - if( absSample > localMax ) - localMax = absSample; - - localSum += absSample; - gain = voice.autogain.current_gain + i * voice.autogain.gain_multiplier; - samples[blockOffset + i] = bound( SHRT_MIN, (int)( sample * gain ), SHRT_MAX ); - } - - if( blockOffset % voice.autogain.block_size == 0 ) - { - average = localSum / blockSize; - modifiedMax = average + ( localMax - average ) * voice_avggain.value; - - voice.autogain.current_gain = voice.autogain.next_gain * scale; - voice.autogain.next_gain = Q_min( (float)SHRT_MAX / modifiedMax, voice_maxgain.value ) * scale; - voice.autogain.gain_multiplier = ( voice.autogain.next_gain - voice.autogain.current_gain ) / ( blockSize - 1 ); - } - blockOffset += blockSize; - } -} - /* ========================= Voice_Status -Notify user dll aboit voice transmission +Notify user dll about voice transmission ========================= */ static void Voice_Status( int entindex, qboolean bTalking ) @@ -356,6 +828,7 @@ void Voice_StatusAck( voice_status_t *status, int playerIndex ) ========================= Voice_IsRecording +Check if voice is currently recording ========================= */ qboolean Voice_IsRecording( void ) @@ -367,6 +840,7 @@ qboolean Voice_IsRecording( void ) ========================= Voice_RecordStop +Stop voice recording ========================= */ void Voice_RecordStop( void ) @@ -390,6 +864,7 @@ void Voice_RecordStop( void ) ========================= Voice_RecordStart +Start voice recording ========================= */ void Voice_RecordStart( void ) @@ -456,6 +931,9 @@ void Voice_Disconnect( void ) VoiceCapture_Shutdown(); voice.device_opened = false; + + + Voice_ShutdownOpusDecoder(); } /* @@ -484,35 +962,53 @@ void Voice_AddIncomingData( int ent, const byte *data, uint size, uint frames ) int samples = 0; int ofs = 0; - if( playernum < 0 || playernum >= cl.maxclients || !voice.decoders[playernum] ) + if( !voice.initialized || !voice_enable.value ) return; - // decode frame by frame - for( ;; ) + // must notify through as both local player and normal client + if( ent + 1 == cl.playernum + 1 ) + Voice_StatusAck( &voice.local, VOICE_LOOPBACK_INDEX ); + + Voice_StatusAck( &voice.players_status[ent], ent ); + + if( voice.goldsrc ) { - int frame_samples; - uint16_t compressed_size; - - // no compressed size mark - if( ofs + sizeof( uint16_t ) > size ) - break; - - compressed_size = *(const uint16_t *)(data + ofs); - ofs += sizeof( uint16_t ); - - // no frame data - if( ofs + compressed_size > size ) - break; - - frame_samples = opus_custom_decode( voice.decoders[playernum], data + ofs, compressed_size, - (opus_int16*)voice.decompress_buffer + samples, voice.frame_size ); - - ofs += compressed_size; - samples += frame_samples; + // Voice_ProcessGSData handles Voice_StartChannel internally + Voice_ProcessGSData( ent, (const uint8_t *)data, size ); } + else + { + // Validate player index and decoder + if( playernum < 0 || playernum >= cl.maxclients || !voice.decoders[playernum] ) + return; - if( samples > 0 ) - Voice_StartChannel( samples, voice.decompress_buffer, ent ); + // decode frame by frame + for( ;; ) + { + int frame_samples; + uint16_t compressed_size; + + // no compressed size mark + if( ofs + sizeof( uint16_t ) > size ) + break; + + compressed_size = *(const uint16_t *)(data + ofs); + ofs += sizeof( uint16_t ); + + // no frame data + if( ofs + compressed_size > size ) + break; + + frame_samples = opus_custom_decode( voice.decoders[playernum], data + ofs, compressed_size, + (int16_t*)voice.decompress_buffer + samples, voice.frame_size ); + + ofs += compressed_size; + samples += frame_samples; + } + + if( samples > 0 ) + Voice_StartChannel( samples, voice.decompress_buffer, ent ); + } } /* @@ -524,13 +1020,37 @@ Encode our voice data and send it to server */ void CL_AddVoiceToDatagram( void ) { - uint size, frames = 0; + byte buffer[VOICE_MAX_DATA_SIZE]; + uint size, frames; - if( cls.state != ca_active || !Voice_IsRecording() || !voice.encoder ) + if( cls.state != ca_active || !voice.device_opened || !Voice_IsRecording() ) + return; + + if( voice.goldsrc ) + { + if( !voice.gs_encoder ) + return; + + size = Voice_GetGSCompressedData( buffer, sizeof( buffer ), &frames ); + if( size > 0 && MSG_GetNumBytesLeft( &cls.datagram ) >= size + 32 ) + { + uint packet_size = Voice_CreateGSVoicePacket( voice.compress_buffer, buffer, size ); + MSG_BeginClientCmd( &cls.datagram, clc_voicedata ); + MSG_WriteShort( &cls.datagram, packet_size ); + MSG_WriteBytes( &cls.datagram, voice.compress_buffer, packet_size ); + + if ( voice_loopback.value && packet_size > 0 && frames > 0 ) + { + Voice_AddIncomingData( cl.playernum + 1, voice.compress_buffer, packet_size, frames ); + } + } + return; + } + + if( !voice.encoder ) return; size = Voice_GetOpusCompressedData( voice.compress_buffer, sizeof( voice.compress_buffer ), &frames ); - if( size > 0 && MSG_GetNumBytesLeft( &cls.datagram ) >= size + 32 ) { MSG_BeginClientCmd( &cls.datagram, clc_voicedata ); @@ -571,9 +1091,12 @@ static void Voice_Shutdown( void ) int i; Voice_RecordStop(); - Voice_ShutdownOpusDecoder(); - Voice_ShutdownOpusEncoder(); - Voice_ShutdownCustomMode(); + + if( voice.goldsrc ) + Voice_ShutdownGoldSrcMode(); + else + Voice_ShutdownOpusCustomMode(); + VoiceCapture_Shutdown(); if( voice.local.talking_ack ) @@ -585,14 +1108,26 @@ static void Voice_Shutdown( void ) Voice_Status( i, false ); } - memset( &voice, 0, sizeof( voice )); + voice.initialized = false; + voice.is_recording = false; + voice.device_opened = false; + voice.start_time = 0.0; + + voice.input_buffer_pos = 0; + voice.input_file_pos = 0; + memset( voice.input_buffer, 0, sizeof( voice.input_buffer )); + memset( voice.compress_buffer, 0, sizeof( voice.compress_buffer )); + memset( voice.decompress_buffer, 0, sizeof( voice.decompress_buffer )); + memset( &voice.local, 0, sizeof( voice.local )); + memset( voice.players_status, 0, sizeof( voice.players_status )); + memset( &voice.autogain, 0, sizeof( voice.autogain )); } /* ========================= Voice_Idle -Run timeout for all clients +Run timeout for clients ========================= */ void Voice_Idle( double frametime ) @@ -605,15 +1140,19 @@ void Voice_Idle( double frametime ) if( voice_enable.value ) { - if( cls.state == ca_active && COM_CheckString( voice_codec_init ) && voice_quality_init != 0 ) - Voice_Init( voice_codec_init, voice_quality_init, false ); + // if( cls.state == ca_active && COM_CheckString( voice.codec ) && voice.quality != 0 ) + if( cls.state == ca_active ) + Voice_Init( voice.codec, voice.quality, false ); } else Voice_Shutdown(); } // update local player status first - Voice_StatusTimeout( &voice.local, VOICE_LOOPBACK_INDEX, frametime ); + if ( voice.goldsrc ) + Voice_StatusTimeout( &voice.local, cl.playernum + 1, frametime ); + else + Voice_StatusTimeout( &voice.local, VOICE_LOOPBACK_INDEX, frametime ); for( i = 0; i < MAX_CLIENTS; i++ ) Voice_StatusTimeout( &voice.players_status[i], i, frametime ); @@ -628,67 +1167,53 @@ Initialize the voice subsystem */ qboolean Voice_Init( const char *pszCodecName, int quality, qboolean preinit ) { - if( Q_strcmp( pszCodecName, VOICE_OPUS_CUSTOM_CODEC )) - { - if( COM_CheckStringEmpty( pszCodecName )) - Con_Printf( S_ERROR "Server requested unsupported codec: %s\n", pszCodecName ); - - // reset saved codec name, we won't enable voice for this connection - voice_codec_init[0] = 0; - voice_quality_init = 0; - return false; - } - - Q_strncpy( voice_codec_init, pszCodecName, sizeof( voice_codec_init )); - voice_quality_init = quality; + Q_strncpy( voice.codec, pszCodecName, sizeof( voice.codec )); + voice.quality = quality; if( !voice_enable.value ) return false; - // reinitialize only if codec parameters are different - if( Q_strcmp( pszCodecName, voice.codec ) || voice.quality != quality ) + if( preinit ) + return true; + + if( Voice_IsGoldSrcMode( pszCodecName ) ) { + if( !Voice_InitGoldSrcMode( quality ) ) + { + Voice_Shutdown(); + return false; + } + } + else if( Voice_IsOpusCustomMode( pszCodecName ) ) + { + if( !Voice_InitOpusCustomMode( quality ) ) + { + Voice_Shutdown(); + return false; + } + } + else + { + // unsupported codec Voice_Shutdown(); - voice.autogain.block_size = 128; - - if( !Voice_InitCustomMode( )) - { - // no reason to init encoder and open audio device - // if we can't hear other players - Voice_Shutdown(); - return false; - } - - // we can hear others players, so it's fine to fail now - voice.initialized = true; - Q_strncpy( voice.codec, pszCodecName, sizeof( voice.codec )); - - if( !Voice_InitOpusEncoder( quality )) - { - Con_Printf( S_WARN "Other players will not be able to hear you.\n" ); - return false; - } - - voice.quality = quality; + voice.goldsrc = false; + voice.codec[0] = 0; + voice.quality = 0; + voice.samplerate = 0; + voice.frame_size = 0; + voice.width = 0; + voice.initialized = false; + return false; } - if( !preinit ) - { - Voice_ShutdownOpusDecoder(); - if( !Voice_InitOpusDecoder()) - { - // no reason to init encoder and open audio device - // if we can't hear other players - Con_Printf( S_ERROR "Can't create decoders, voice chat is disabled.\n" ); - Voice_Shutdown(); - return false; - } + Q_strncpy( voice.codec, pszCodecName, sizeof( voice.codec )); + voice.quality = quality; - voice.device_opened = VoiceCapture_Init(); - - if( !voice.device_opened ) - Con_Printf( S_WARN "No microphone is available.\n" ); - } + voice.device_opened = VoiceCapture_Init(); + if( !voice.device_opened ) + Con_Printf( S_WARN "No microphone is available.\n" ); + + voice.initialized = true; return true; } diff --git a/engine/client/voice.h b/engine/client/voice.h index b5f66b19..fba4c882 100644 --- a/engine/client/voice.h +++ b/engine/client/voice.h @@ -24,11 +24,15 @@ GNU General Public License for more details. typedef struct OpusCustomEncoder OpusCustomEncoder; typedef struct OpusCustomDecoder OpusCustomDecoder; typedef struct OpusCustomMode OpusCustomMode; +typedef struct OpusEncoder OpusEncoder; +typedef struct OpusDecoder OpusDecoder; #define VOICE_LOOPBACK_INDEX (-2) #define VOICE_LOCALCLIENT_INDEX (-1) #define VOICE_PCM_CHANNELS 1 // always mono +#define VOICE_MAX_DATA_SIZE 8192 +#define VOICE_MAX_GS_DATA_SIZE 4096 // never change these parameters when using opuscustom #define VOICE_OPUS_CUSTOM_SAMPLERATE 44100 @@ -40,16 +44,42 @@ typedef struct OpusCustomMode OpusCustomMode; // a1ba: do not change, we don't have any re-encoding support now #define VOICE_DEFAULT_CODEC VOICE_OPUS_CUSTOM_CODEC +// GoldSrc voice configuration +#define GS_MAX_DECOMPRESSED_SAMPLES 32768 +#define GS_DEFAULT_SAMPLE_RATE 24000 +#define GS_DEFAULT_FRAME_SIZE 480 + +// VPC (Voice Packet Control) types +enum gs_vpc_type { + GS_VPC_VDATA_SILENCE = 0, + GS_VPC_VDATA_MILES = 1, + GS_VPC_VDATA_SPEEX = 2, + GS_VPC_VDATA_RAW = 3, + GS_VPC_VDATA_SILK = 4, + GS_VPC_VDATA_OPUS_PLC = 6, + GS_VPC_SETSAMPLERATE = 11, + GS_VPC_UNKNOWN = 10 +}; + typedef struct voice_status_s { qboolean talking_ack; double talking_timeout; } voice_status_t; +typedef struct voice_autogain_s +{ + int block_size; + float current_gain; + float next_gain; + float gain_multiplier; +} voice_autogain_t; + typedef struct voice_state_s { string codec; int quality; + qboolean goldsrc; qboolean initialized; qboolean is_recording; @@ -64,6 +94,9 @@ typedef struct voice_state_s OpusCustomEncoder *encoder; OpusCustomDecoder *decoders[MAX_CLIENTS]; + OpusEncoder *gs_encoder; + OpusDecoder *gs_decoders[MAX_CLIENTS]; + // audio info uint width; uint samplerate; @@ -79,19 +112,12 @@ typedef struct voice_state_s wavdata_t *input_file; fs_offset_t input_file_pos; // in bytes - // automatic gain control - struct { - int block_size; - float current_gain; - float next_gain; - float gain_multiplier; - } autogain; + voice_autogain_t autogain; } voice_state_t; extern voice_state_t voice; void CL_AddVoiceToDatagram( void ); - void Voice_RegisterCvars( void ); qboolean Voice_Init( const char *pszCodecName, int quality, qboolean preinit ); void Voice_Idle( double frametime ); @@ -102,4 +128,8 @@ void Voice_Disconnect( void ); void Voice_AddIncomingData( int ent, const byte *data, uint size, uint frames ); void Voice_StatusAck( voice_status_t *status, int playerIndex ); +qboolean Voice_IsGoldSrcMode( const char *codec ); +qboolean Voice_IsOpusCustomMode( const char *codec ); +int Voice_GetBitrateForQuality( int quality, qboolean goldsrc ); + #endif // VOICE_H From fddf0200db87a883a0c869e4b9cfdf3ff444e877 Mon Sep 17 00:00:00 2001 From: Sergey Degtyar Date: Thu, 3 Jul 2025 13:36:25 +0300 Subject: [PATCH 4/6] engine: client: replaced hardcoded buffer sizes with defined constants for voice data --- engine/client/cl_parse.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/engine/client/cl_parse.c b/engine/client/cl_parse.c index 554c12f3..ac31fb2f 100644 --- a/engine/client/cl_parse.c +++ b/engine/client/cl_parse.c @@ -2011,7 +2011,7 @@ CL_ParseVoiceData void CL_ParseVoiceData( sizebuf_t *msg, connprotocol_t proto ) { int size, idx, frames = 0; - byte received[8192]; + byte received[VOICE_MAX_DATA_SIZE]; idx = MSG_ReadByte( msg ) + 1; @@ -2021,7 +2021,7 @@ void CL_ParseVoiceData( sizebuf_t *msg, connprotocol_t proto ) if( proto == PROTO_GOLDSRC ) { size = MSG_ReadShort( msg ); - if ( size > 4096 ) + if ( size > VOICE_MAX_GS_DATA_SIZE ) { Con_Printf( S_ERROR "Voice data size is too large: %d bytes (max: %d)\n", size, VOICE_MAX_GS_DATA_SIZE ); return; @@ -2031,7 +2031,7 @@ void CL_ParseVoiceData( sizebuf_t *msg, connprotocol_t proto ) { frames = MSG_ReadByte( msg ); size = MSG_ReadShort( msg ); - if (size > 8192 ) + if (size > VOICE_MAX_DATA_SIZE ) { Con_Printf( S_ERROR "Voice data size is too large: %d bytes (max: %d)\n", size, VOICE_MAX_DATA_SIZE ); return; From cca1732245a6d90a13a14b05859d4132d3fe60ab Mon Sep 17 00:00:00 2001 From: Sergey Degtyar Date: Thu, 3 Jul 2025 13:44:40 +0300 Subject: [PATCH 5/6] engine: client: refactored voice.c - reformatted code. - changed function declarations to static. - ensured consistent use of defined constants for buffer sizes. --- engine/client/voice.c | 483 +++++++++++++++++++++--------------------- engine/client/voice.h | 53 +++-- 2 files changed, 271 insertions(+), 265 deletions(-) diff --git a/engine/client/voice.c b/engine/client/voice.c index 7fcc3739..a78bcc6b 100644 --- a/engine/client/voice.c +++ b/engine/client/voice.c @@ -24,28 +24,14 @@ GNU General Public License for more details. voice_state_t voice = { 0 }; -CVAR_DEFINE_AUTO( voice_enable, "1", FCVAR_PRIVILEGED|FCVAR_ARCHIVE, "enable voice chat" ); +CVAR_DEFINE_AUTO( voice_enable, "1", FCVAR_PRIVILEGED | FCVAR_ARCHIVE, "enable voice chat" ); CVAR_DEFINE_AUTO( voice_loopback, "0", FCVAR_PRIVILEGED, "loopback voice back to the speaker" ); -CVAR_DEFINE_AUTO( voice_scale, "1.0", FCVAR_PRIVILEGED|FCVAR_ARCHIVE, "incoming voice volume scale" ); -CVAR_DEFINE_AUTO( voice_transmit_scale, "1.0", FCVAR_PRIVILEGED|FCVAR_ARCHIVE, "outcoming voice volume scale" ); -CVAR_DEFINE_AUTO( voice_avggain, "0.5", FCVAR_PRIVILEGED|FCVAR_ARCHIVE, "automatic voice gain control (average)" ); -CVAR_DEFINE_AUTO( voice_maxgain, "5.0", FCVAR_PRIVILEGED|FCVAR_ARCHIVE, "automatic voice gain control (maximum)" ); +CVAR_DEFINE_AUTO( voice_scale, "1.0", FCVAR_PRIVILEGED | FCVAR_ARCHIVE, "incoming voice volume scale" ); +CVAR_DEFINE_AUTO( voice_transmit_scale, "1.0", FCVAR_PRIVILEGED | FCVAR_ARCHIVE, "outcoming voice volume scale" ); +CVAR_DEFINE_AUTO( voice_avggain, "0.5", FCVAR_PRIVILEGED | FCVAR_ARCHIVE, "automatic voice gain control (average)" ); +CVAR_DEFINE_AUTO( voice_maxgain, "5.0", FCVAR_PRIVILEGED | FCVAR_ARCHIVE, "automatic voice gain control (maximum)" ); CVAR_DEFINE_AUTO( voice_inputfromfile, "0", FCVAR_PRIVILEGED, "input voice from voice_input.wav" ); -static void Voice_ApplyGainAdjust( int16_t *samples, int count, float scale ); -static qboolean Voice_InitGoldSrcMode( int quality ); -static qboolean Voice_InitOpusCustomMode( int quality ); -static void Voice_ShutdownGoldSrcMode( void ); -static void Voice_ShutdownOpusCustomMode( void ); -static uint Voice_GetOpusCompressedData( byte *out, uint maxsize, uint *frames ); -static uint Voice_GetGSCompressedData( byte *out, uint maxsize, uint *frames ); -static int Voice_ProcessGSData( int ent, const uint8_t *data, uint32_t size ); -static uint Voice_CreateGSVoicePacket( byte *out, const byte *voice_data, uint voice_size ); -static void Voice_StartChannel( uint samples, byte *data, int entnum ); -static void Voice_Status( int entindex, qboolean bTalking ); -static void Voice_StatusTimeout( voice_status_t *status, int entindex, double frametime ); -static void Voice_Shutdown( void ); - /* =============================================================================== @@ -61,9 +47,14 @@ Voice_IsGoldSrcMode Check if codec is GoldSrc mode ========================= */ -qboolean Voice_IsGoldSrcMode( const char *codec ) +static qboolean Voice_IsGoldSrcMode( const char *codec ) { - return ( COM_CheckString( codec ) == 0 || Q_strstr( codec, "voice_speex" ) != NULL ); + // check for an empty codec string + // if the server does not use ReVoice or VTC + // it will send an empty codec + // however, decoding is still possible + // if the voice data contains Opus + return( COM_CheckString( codec ) == 0 || Q_strstr( codec, "voice_speex" ) != NULL ); } /* @@ -73,7 +64,7 @@ Voice_IsOpusCustomMode Check if codec is Opus Custom mode ========================= */ -qboolean Voice_IsOpusCustomMode( const char *codec ) +static qboolean Voice_IsOpusCustomMode( const char *codec ) { return Q_strcmp( codec, VOICE_OPUS_CUSTOM_CODEC ) == 0; } @@ -85,7 +76,7 @@ Voice_GetBitrateForQuality Get bitrate for given quality level ========================= */ -int Voice_GetBitrateForQuality( int quality, qboolean goldsrc ) +static int Voice_GetBitrateForQuality( int quality, qboolean goldsrc ) { if( goldsrc ) { @@ -194,7 +185,7 @@ static qboolean Voice_InitOpusEncoder( int quality ) int err = 0; int bitrate = Voice_GetBitrateForQuality( quality, voice.goldsrc ); - if (voice.goldsrc) + if( voice.goldsrc ) { voice.gs_encoder = opus_encoder_create( GS_DEFAULT_SAMPLE_RATE, VOICE_PCM_CHANNELS, OPUS_APPLICATION_VOIP, &err ); if( !voice.gs_encoder ) @@ -234,16 +225,16 @@ static void Voice_ShutdownOpusDecoder( void ) { if( voice.gs_decoders[i] ) { - opus_decoder_destroy( voice.gs_decoders[i] ); - voice.gs_decoders[i] = NULL; + opus_decoder_destroy( voice.gs_decoders[i] ); + voice.gs_decoders[i] = NULL; } } else { if( voice.decoders[i] ) { - opus_custom_decoder_destroy( voice.decoders[i] ); - voice.decoders[i] = NULL; + opus_custom_decoder_destroy( voice.decoders[i] ); + voice.decoders[i] = NULL; } } } @@ -307,13 +298,13 @@ static qboolean Voice_InitGoldSrcMode( int quality ) voice.samplerate = GS_DEFAULT_SAMPLE_RATE; voice.frame_size = GS_DEFAULT_FRAME_SIZE; - if( !Voice_InitOpusDecoder() ) + if( !Voice_InitOpusDecoder()) { Con_Printf( S_ERROR "Can't create GoldSrc decoders, voice chat is disabled.\n" ); return false; } - if( !Voice_InitOpusEncoder( quality ) ) + if( !Voice_InitOpusEncoder( quality )) { Con_Printf( S_WARN "Other players will not be able to hear you.\n" ); return false; @@ -337,13 +328,13 @@ static qboolean Voice_InitOpusCustomMode( int quality ) voice.frame_size = VOICE_OPUS_CUSTOM_FRAME_SIZE; voice.width = sizeof( int16_t ); - if( !Voice_InitCustomMode() || !Voice_InitOpusDecoder() ) + if( !Voice_InitCustomMode() || !Voice_InitOpusDecoder()) { Con_Printf( S_ERROR "Can't create Opus Custom decoders, voice chat is disabled.\n" ); return false; } - if( !Voice_InitOpusEncoder( quality ) ) + if( !Voice_InitOpusEncoder( quality )) { Con_Printf( S_WARN "Other players will not be able to hear you.\n" ); return false; @@ -397,12 +388,12 @@ Apply automatic gain control to voice samples static void Voice_ApplyGainAdjust( int16_t *samples, int count, float scale ) { float gain, modifiedMax; - int average, blockOffset = 0; + int average, blockOffset = 0; for( ;; ) { int i, localMax = 0, localSum = 0; - int blockSize = Q_min(count - blockOffset, voice.autogain.block_size); + int blockSize = Q_min( count - blockOffset, voice.autogain.block_size ); if( blockSize < 1 ) break; @@ -426,9 +417,9 @@ static void Voice_ApplyGainAdjust( int16_t *samples, int count, float scale ) modifiedMax = average + ( localMax - average ) * voice_avggain.value; voice.autogain.current_gain = voice.autogain.next_gain * scale; - voice.autogain.next_gain = Q_min((float)SHRT_MAX / (modifiedMax > 1 ? modifiedMax : 1), voice_maxgain.value) * scale; - if (blockSize > 1) - voice.autogain.gain_multiplier = (voice.autogain.next_gain - voice.autogain.current_gain) / (blockSize - 1); + voice.autogain.next_gain = Q_min((float)SHRT_MAX / ( modifiedMax > 1 ? modifiedMax : 1 ), voice_maxgain.value ) * scale; + if( blockSize > 1 ) + voice.autogain.gain_multiplier = ( voice.autogain.next_gain - voice.autogain.current_gain ) / ( blockSize - 1 ); else voice.autogain.gain_multiplier = 0.0f; } @@ -450,7 +441,7 @@ static uint Voice_GetOpusCompressedData( byte *out, uint maxsize, uint *frames ) if( voice.input_file ) { - uint numbytes; + uint numbytes; double updateInterval, curtime = Sys_DoubleTime(); updateInterval = curtime - voice.start_time; @@ -473,20 +464,20 @@ static uint Voice_GetOpusCompressedData( byte *out, uint maxsize, uint *frames ) int bytes; if( !voice.input_file ) - Voice_ApplyGainAdjust((int16_t*)(voice.input_buffer + ofs), voice.frame_size, voice_transmit_scale.value); + Voice_ApplyGainAdjust((int16_t *)( voice.input_buffer + ofs ), voice.frame_size, voice_transmit_scale.value ); bytes = opus_custom_encode( voice.encoder, (const int16_t *)( voice.input_buffer + ofs ), - voice.frame_size, out + size + sizeof( uint16_t ), maxsize ); + voice.frame_size, out + size + sizeof( uint16_t ), maxsize ); if( bytes > 0 ) { // write compressed frame size - *((uint16_t *)&out[size]) = LittleShort(bytes); + *((uint16_t *)&out[size] ) = LittleShort( bytes ); size += bytes + sizeof( uint16_t ); maxsize -= bytes + sizeof( uint16_t ); - (*frames)++; + ( *frames )++; } else { @@ -518,85 +509,88 @@ Get compressed voice data for GoldSrc mode */ static uint Voice_GetGSCompressedData( byte *out, uint maxsize, uint *frames ) { - uint ofs = 0, size = 0; - const uint frame_size_samples = GS_DEFAULT_FRAME_SIZE; - const uint frame_size_bytes = GS_DEFAULT_FRAME_SIZE * voice.width; - static uint16_t sequence = 0; + uint ofs, size = 0; + const uint frame_size_samples = GS_DEFAULT_FRAME_SIZE; + const uint frame_size_bytes = GS_DEFAULT_FRAME_SIZE * voice.width; + static uint16_t sequence = 0; - if( voice.input_file ) - { - uint numbytes; - double updateInterval, curtime = Sys_DoubleTime(); + if( voice.input_file ) + { + uint numbytes; + double updateInterval, curtime = Sys_DoubleTime(); - updateInterval = curtime - voice.start_time; - voice.start_time = curtime; + updateInterval = curtime - voice.start_time; + voice.start_time = curtime; - numbytes = updateInterval * voice.samplerate * voice.width * VOICE_PCM_CHANNELS; - numbytes = Q_min( numbytes, voice.input_file->size - voice.input_file_pos ); - numbytes = Q_min( numbytes, sizeof( voice.input_buffer ) - voice.input_buffer_pos ); + numbytes = updateInterval * voice.samplerate * voice.width * VOICE_PCM_CHANNELS; + numbytes = Q_min( numbytes, voice.input_file->size - voice.input_file_pos ); + numbytes = Q_min( numbytes, sizeof( voice.input_buffer ) - voice.input_buffer_pos ); - memcpy( voice.input_buffer + voice.input_buffer_pos, voice.input_file->buffer + voice.input_file_pos, numbytes ); - voice.input_buffer_pos += numbytes; - voice.input_file_pos += numbytes; - } + memcpy( voice.input_buffer + voice.input_buffer_pos, voice.input_file->buffer + voice.input_file_pos, numbytes ); + voice.input_buffer_pos += numbytes; + voice.input_file_pos += numbytes; + } - if( !voice.input_file ) - VoiceCapture_Lock( true ); + if( !voice.input_file ) + VoiceCapture_Lock( true ); - *frames = 0; + *frames = 0; - while (voice.input_buffer_pos - ofs >= frame_size_bytes) - { - int bytes; + while( voice.input_buffer_pos - ofs >= frame_size_bytes ) + { + int bytes; + int is_silence = 1; + int16_t *samples = (int16_t *)( voice.input_buffer + ofs ); - int is_silence = 1; - int16_t *samples = (int16_t *)(voice.input_buffer + ofs); - for (uint i = 0; i < frame_size_samples; ++i) { - if (samples[i] != 0) { - is_silence = 0; - break; - } - } - if (is_silence) { - ofs += frame_size_bytes; - continue; - } + for( uint i = 0; i < frame_size_samples; ++i ) + { + if( samples[i] != 0 ) + { + is_silence = 0; + break; + } + } + if( is_silence ) + { + ofs += frame_size_bytes; + continue; + } - if ( !voice.input_file ) - Voice_ApplyGainAdjust(samples, frame_size_samples, voice_transmit_scale.value); + if( !voice.input_file ) + Voice_ApplyGainAdjust( samples, frame_size_samples, voice_transmit_scale.value ); - bytes = opus_encode( voice.gs_encoder, samples, - frame_size_samples, out + size + 4, maxsize - 4 ); + bytes = opus_encode( voice.gs_encoder, samples, + frame_size_samples, out + size + 4, maxsize - 4 ); - if( bytes > 0 ) - { - *(uint16_t*)(out + size) = LittleShort(bytes); - *(uint16_t*)(out + size + 2) = LittleShort(sequence++); + if( bytes > 0 ) + { + *(uint16_t *)( out + size ) = LittleShort( bytes ); + *(uint16_t *)( out + size + 2 ) = LittleShort( sequence++ ); - size += bytes + 4; - maxsize -= bytes + 4; + size += bytes + sizeof( uint32_t ); + maxsize -= bytes + sizeof( uint32_t ); - (*frames)++; - } - else - { - Con_Printf( S_ERROR "%s: failed to encode frame: %s\n", __func__, opus_strerror( bytes )); - } + ( *frames )++; + } + else + { + Con_Printf( S_ERROR "%s: failed to encode frame: %s\n", __func__, opus_strerror( bytes )); + } - ofs += frame_size_bytes; - } + ofs += frame_size_bytes; + } - if( ofs ) - { - fs_offset_t remaining = voice.input_buffer_pos - ofs; - memmove( voice.input_buffer, voice.input_buffer + ofs, remaining ); - voice.input_buffer_pos = remaining; - } + if( ofs ) + { + fs_offset_t remaining = voice.input_buffer_pos - ofs; + memmove( voice.input_buffer, voice.input_buffer + ofs, remaining ); + voice.input_buffer_pos = remaining; + } - if( !voice.input_file ) - VoiceCapture_Lock( false ); + if( !voice.input_file ) + VoiceCapture_Lock( false ); - return size; + return size; } /* @@ -608,111 +602,127 @@ Process GoldSrc voice data and return number of samples */ static int Voice_ProcessGSData( int ent, const uint8_t *data, uint32_t size ) { - uint32_t crc_in_packet; - uint32_t crc; - size_t offset; - int16_t pcm[GS_MAX_DECOMPRESSED_SAMPLES]; - size_t output_samples; - uint16_t sample_rate; - uint8_t vpc_type; - uint16_t data_len; - OpusDecoder *decoder; - size_t opus_offset; - int decoded; - size_t silence_samples; - uint16_t frame_size; + uint32_t crc_in_packet; + uint32_t crc; + size_t offset; + int16_t pcm[GS_MAX_DECOMPRESSED_SAMPLES]; + size_t output_samples; + uint16_t sample_rate; + uint8_t vpc_type; + uint16_t data_len; + OpusDecoder *decoder; + size_t opus_offset; + int decoded; + size_t silence_samples; + uint16_t frame_size; - if (!data || size < 18 || size < 4 || ent <= 0 || ent > cl.maxclients) - return 0; + if( !data || ent <= 0 || ent > cl.maxclients ) + return 0; - crc_in_packet = LittleLong(*(uint32_t*)(data + size - 4)); - crc = CRC32_INIT_VALUE; - CRC32_ProcessBuffer(&crc, data, size - 4); - crc = CRC32_Final(crc); + crc_in_packet = LittleLong( *(uint32_t *)( data + size - sizeof( uint32_t ))); + crc = CRC32_INIT_VALUE; + CRC32_ProcessBuffer( &crc, data, size - sizeof( uint32_t )); + crc = CRC32_Final( crc ); - if (crc != crc_in_packet) { - Con_Printf( S_WARN "Voice packet CRC32 mismatch\n" ); - return 0; - } + if( crc != crc_in_packet ) + { + Con_Printf( S_WARN "Voice packet CRC32 mismatch\n" ); + return 0; + } - offset = 8; - output_samples = 0; + offset = sizeof( uint64_t ); + output_samples = 0; - if (offset >= size - 4 || data[offset] != GS_VPC_SETSAMPLERATE) { - Con_Printf( S_WARN "Invalid voice packet type: %d\n", data[offset] ); - return 0; - } - offset++; + if( offset >= size - sizeof( uint32_t ) || data[offset] != GS_VPC_SETSAMPLERATE ) + { + Con_Printf( S_WARN "Invalid voice packet type: %d\n", data[offset] ); + return 0; + } + offset++; - if (offset + 4 > size - 4) - return 0; + if( offset + sizeof( uint32_t ) > size - sizeof( uint32_t )) + return 0; - sample_rate = LittleShort(*(uint16_t*)(data + offset)); - offset += 2; + sample_rate = LittleShort( *(uint16_t *)( data + offset )); + offset += sizeof( uint16_t ); - vpc_type = data[offset++]; - data_len = LittleShort(*(uint16_t*)(data + offset)); - offset += 2; + vpc_type = data[offset++]; + data_len = LittleShort( *(uint16_t *)( data + offset )); + offset += sizeof( uint16_t ); - if (offset + data_len > size - 4) { - Con_Printf( S_WARN "Voice packet data_len out of bounds\n" ); - return 0; - } + if( offset + data_len > size - sizeof( uint32_t )) + { + Con_Printf( S_WARN "Voice packet data_len out of bounds\n" ); + return 0; + } - if (vpc_type == GS_VPC_VDATA_OPUS_PLC) { - decoder = voice.gs_decoders[ent]; - if (!decoder) { - Con_Printf( S_WARN "No decoder available for entity %d\n", ent ); - return 0; - } - opus_offset = 0; - while (opus_offset + 4 <= data_len) { - frame_size = LittleShort(*(uint16_t*)(data + offset + opus_offset)); - opus_offset += 4; + if( vpc_type == GS_VPC_VDATA_OPUS_PLC ) + { + decoder = voice.gs_decoders[ent]; + if( !decoder ) + { + Con_Printf( S_WARN "No decoder available for entity %d\n", ent ); + return 0; + } + opus_offset = 0; + while( opus_offset + sizeof( uint32_t ) <= data_len ) + { + frame_size = LittleShort( *(uint16_t *)( data + offset + opus_offset )); + opus_offset += sizeof( uint32_t ); + // if frame size is 0, it means silence + if( frame_size == 0 ) + { + if( output_samples + VOICE_DEFAULT_SILENCE_FRAME_SIZE > GS_MAX_DECOMPRESSED_SAMPLES ) + { + Con_Printf( S_WARN "Voice buffer overflow\n" ); + return 0; + } + memset( pcm + output_samples, 0, VOICE_DEFAULT_SILENCE_FRAME_SIZE * sizeof( int16_t )); + output_samples += VOICE_DEFAULT_SILENCE_FRAME_SIZE; + continue; + } + if( frame_size == 0xFFFF ) + { + opus_decoder_ctl( decoder, OPUS_RESET_STATE ); + break; + } + if( opus_offset + frame_size > data_len ) + { + Con_Printf( S_WARN "Opus frame size exceeds data length\n" ); + return 0; + } + decoded = opus_decode( decoder, data + offset + opus_offset, frame_size, + pcm + output_samples, voice.frame_size, 0 ); + if( decoded < 0 ) + { + Con_Printf( S_WARN "Opus decode error: %s\n", opus_strerror( decoded )); + return 0; + } + output_samples += decoded; + opus_offset += frame_size; + } + } + else if( vpc_type == GS_VPC_VDATA_SILENCE ) + { + silence_samples = data_len / 2; + if( silence_samples > GS_MAX_DECOMPRESSED_SAMPLES ) + { + Con_Printf( S_WARN "Silence data too large\n" ); + return 0; + } + memset( pcm, 0, silence_samples * sizeof( int16_t )); + output_samples = silence_samples; + } + else + { + Con_Printf( S_WARN "Unsupported voice data type: %d\n", vpc_type ); + return 0; + } - if (frame_size == 0) { - if (output_samples + 160 > GS_MAX_DECOMPRESSED_SAMPLES) { - Con_Printf( S_WARN "Voice buffer overflow\n" ); - return 0; - } - memset(pcm + output_samples, 0, 160 * sizeof(int16_t)); - output_samples += 160; - continue; - } - if (frame_size == 0xFFFF) { - opus_decoder_ctl(decoder, OPUS_RESET_STATE); - break; - } - if (opus_offset + frame_size > data_len) { - Con_Printf( S_WARN "Opus frame size exceeds data length\n" ); - return 0; - } - decoded = opus_decode(decoder, data + offset + opus_offset, frame_size, - pcm + output_samples, GS_DEFAULT_FRAME_SIZE, 0); - if (decoded < 0) { - Con_Printf( S_WARN "Opus decode error: %s\n", opus_strerror(decoded) ); - return 0; - } - output_samples += decoded; - opus_offset += frame_size; - } - } else if (vpc_type == GS_VPC_VDATA_SILENCE) { - silence_samples = data_len / 2; - if (silence_samples > GS_MAX_DECOMPRESSED_SAMPLES) { - Con_Printf( S_WARN "Silence data too large\n" ); - return 0; - } - memset(pcm, 0, silence_samples * sizeof(int16_t)); - output_samples = silence_samples; - } else { - Con_Printf( S_WARN "Unsupported voice data type: %d\n", vpc_type ); - return 0; - } + if( output_samples > 0 ) + Voice_StartChannel( output_samples, (byte *)pcm, ent ); - if (output_samples > 0) - Voice_StartChannel(output_samples, (byte*)pcm, ent); - - return output_samples; + return output_samples; } /* @@ -724,34 +734,37 @@ Create GoldSrc voice packet */ static uint Voice_CreateGSVoicePacket( byte *out, const byte *voice_data, uint voice_size ) { - uint offset = 0; + uint offset = 0; uint32_t crc; - if (!out || !voice_data || voice_size == 0) + if( !out || !voice_data || voice_size == 0 ) { Con_Printf( S_WARN "Invalid voice data for packet creation\n" ); return 0; } - if ( cls.steamid ) { - memcpy(out + offset, cls.steamid, 8); - } else { - memset(out + offset, 0, 8); // fallback: 0 + if( cls.steamid ) + { + memcpy( out + offset, cls.steamid, 8 ); + } + else + { + memset( out + offset, 0, 8 ); // fallback: 0 } - offset += 8; + offset += sizeof( uint64_t ); out[offset] = GS_VPC_SETSAMPLERATE; - offset += 1; + offset++; - *(uint16_t*)(out + offset) = LittleShort(GS_DEFAULT_SAMPLE_RATE); - offset += 2; + *(uint16_t *)( out + offset ) = LittleShort( GS_DEFAULT_SAMPLE_RATE ); + offset += sizeof( uint16_t ); out[offset] = GS_VPC_VDATA_OPUS_PLC; - offset += 1; + offset++; - *(uint16_t*)(out + offset) = LittleShort(voice_size); - offset += 2; + *(uint16_t *)( out + offset ) = LittleShort( voice_size ); + offset += sizeof( uint16_t ); memcpy( out + offset, voice_data, voice_size ); offset += voice_size; @@ -759,8 +772,8 @@ static uint Voice_CreateGSVoicePacket( byte *out, const byte *voice_data, uint v crc = CRC32_INIT_VALUE; CRC32_ProcessBuffer( &crc, out, offset ); crc = CRC32_Final( crc ); - *(uint32_t*)(out + offset) = LittleLong(crc); - offset += 4; + *(uint32_t *)( out + offset ) = LittleLong( crc ); + offset += sizeof( uint32_t ); return offset; } @@ -896,7 +909,7 @@ void Voice_RecordStart( void ) if( !Voice_IsRecording( ) && voice.device_opened ) voice.is_recording = VoiceCapture_Activate( true ); - if( Voice_IsRecording() ) + if( Voice_IsRecording()) Voice_Status( VOICE_LOCALCLIENT_INDEX, true ); } @@ -931,7 +944,7 @@ void Voice_Disconnect( void ) VoiceCapture_Shutdown(); voice.device_opened = false; - + Voice_ShutdownOpusDecoder(); } @@ -943,7 +956,7 @@ Voice_StartChannel Feed the decoded data to engine sound subsystem ========================= */ -static void Voice_StartChannel( uint samples, byte *data, int entnum ) +void Voice_StartChannel( uint samples, byte *data, int entnum ) { SND_ForceInitMouth( entnum ); S_RawEntSamples( entnum, samples, voice.samplerate, voice.width, VOICE_PCM_CHANNELS, data, bound( 0, 255 * voice_scale.value, 255 )); @@ -966,7 +979,7 @@ void Voice_AddIncomingData( int ent, const byte *data, uint size, uint frames ) return; // must notify through as both local player and normal client - if( ent + 1 == cl.playernum + 1 ) + if( ent == cl.playernum ) Voice_StatusAck( &voice.local, VOICE_LOOPBACK_INDEX ); Voice_StatusAck( &voice.players_status[ent], ent ); @@ -985,14 +998,14 @@ void Voice_AddIncomingData( int ent, const byte *data, uint size, uint frames ) // decode frame by frame for( ;; ) { - int frame_samples; + int frame_samples; uint16_t compressed_size; // no compressed size mark if( ofs + sizeof( uint16_t ) > size ) break; - compressed_size = *(const uint16_t *)(data + ofs); + compressed_size = *(const uint16_t *)( data + ofs ); ofs += sizeof( uint16_t ); // no frame data @@ -1000,7 +1013,7 @@ void Voice_AddIncomingData( int ent, const byte *data, uint size, uint frames ) break; frame_samples = opus_custom_decode( voice.decoders[playernum], data + ofs, compressed_size, - (int16_t*)voice.decompress_buffer + samples, voice.frame_size ); + (int16_t *)voice.decompress_buffer + samples, voice.frame_size ); ofs += compressed_size; samples += frame_samples; @@ -1023,14 +1036,14 @@ void CL_AddVoiceToDatagram( void ) byte buffer[VOICE_MAX_DATA_SIZE]; uint size, frames; - if( cls.state != ca_active || !voice.device_opened || !Voice_IsRecording() ) + if( cls.state != ca_active || !voice.device_opened || !Voice_IsRecording()) return; if( voice.goldsrc ) { if( !voice.gs_encoder ) return; - + size = Voice_GetGSCompressedData( buffer, sizeof( buffer ), &frames ); if( size > 0 && MSG_GetNumBytesLeft( &cls.datagram ) >= size + 32 ) { @@ -1039,7 +1052,7 @@ void CL_AddVoiceToDatagram( void ) MSG_WriteShort( &cls.datagram, packet_size ); MSG_WriteBytes( &cls.datagram, voice.compress_buffer, packet_size ); - if ( voice_loopback.value && packet_size > 0 && frames > 0 ) + if( voice_loopback.value && packet_size > 0 && frames > 0 ) { Voice_AddIncomingData( cl.playernum + 1, voice.compress_buffer, packet_size, frames ); } @@ -1049,7 +1062,7 @@ void CL_AddVoiceToDatagram( void ) if( !voice.encoder ) return; - + size = Voice_GetOpusCompressedData( voice.compress_buffer, sizeof( voice.compress_buffer ), &frames ); if( size > 0 && MSG_GetNumBytesLeft( &cls.datagram ) >= size + 32 ) { @@ -1112,12 +1125,12 @@ static void Voice_Shutdown( void ) voice.is_recording = false; voice.device_opened = false; voice.start_time = 0.0; - + voice.input_buffer_pos = 0; voice.input_file_pos = 0; memset( voice.input_buffer, 0, sizeof( voice.input_buffer )); memset( voice.compress_buffer, 0, sizeof( voice.compress_buffer )); - memset( voice.decompress_buffer, 0, sizeof( voice.decompress_buffer )); + memset( voice.decompress_buffer, 0, sizeof( voice.decompress_buffer )); memset( &voice.local, 0, sizeof( voice.local )); memset( voice.players_status, 0, sizeof( voice.players_status )); memset( &voice.autogain, 0, sizeof( voice.autogain )); @@ -1140,7 +1153,6 @@ void Voice_Idle( double frametime ) if( voice_enable.value ) { - // if( cls.state == ca_active && COM_CheckString( voice.codec ) && voice.quality != 0 ) if( cls.state == ca_active ) Voice_Init( voice.codec, voice.quality, false ); } @@ -1149,10 +1161,7 @@ void Voice_Idle( double frametime ) } // update local player status first - if ( voice.goldsrc ) - Voice_StatusTimeout( &voice.local, cl.playernum + 1, frametime ); - else - Voice_StatusTimeout( &voice.local, VOICE_LOOPBACK_INDEX, frametime ); + Voice_StatusTimeout( &voice.local, VOICE_LOOPBACK_INDEX, frametime ); for( i = 0; i < MAX_CLIENTS; i++ ) Voice_StatusTimeout( &voice.players_status[i], i, frametime ); @@ -1176,17 +1185,17 @@ qboolean Voice_Init( const char *pszCodecName, int quality, qboolean preinit ) if( preinit ) return true; - if( Voice_IsGoldSrcMode( pszCodecName ) ) + if( Voice_IsGoldSrcMode( pszCodecName )) { - if( !Voice_InitGoldSrcMode( quality ) ) + if( !Voice_InitGoldSrcMode( quality )) { Voice_Shutdown(); return false; } } - else if( Voice_IsOpusCustomMode( pszCodecName ) ) + else if( Voice_IsOpusCustomMode( pszCodecName )) { - if( !Voice_InitOpusCustomMode( quality ) ) + if( !Voice_InitOpusCustomMode( quality )) { Voice_Shutdown(); return false; @@ -1195,6 +1204,7 @@ qboolean Voice_Init( const char *pszCodecName, int quality, qboolean preinit ) else { // unsupported codec + Con_Printf( S_WARN "Server requested unsupported voice codec: %s\n", pszCodecName ); Voice_Shutdown(); voice.goldsrc = false; voice.codec[0] = 0; @@ -1206,14 +1216,11 @@ qboolean Voice_Init( const char *pszCodecName, int quality, qboolean preinit ) return false; } - Q_strncpy( voice.codec, pszCodecName, sizeof( voice.codec )); - voice.quality = quality; - voice.device_opened = VoiceCapture_Init(); if( !voice.device_opened ) Con_Printf( S_WARN "No microphone is available.\n" ); - + voice.initialized = true; return true; } diff --git a/engine/client/voice.h b/engine/client/voice.h index fba4c882..aeb7ab18 100644 --- a/engine/client/voice.h +++ b/engine/client/voice.h @@ -27,11 +27,11 @@ typedef struct OpusCustomMode OpusCustomMode; typedef struct OpusEncoder OpusEncoder; typedef struct OpusDecoder OpusDecoder; -#define VOICE_LOOPBACK_INDEX (-2) -#define VOICE_LOCALCLIENT_INDEX (-1) +#define VOICE_LOOPBACK_INDEX ( -2 ) +#define VOICE_LOCALCLIENT_INDEX ( -1 ) -#define VOICE_PCM_CHANNELS 1 // always mono -#define VOICE_MAX_DATA_SIZE 8192 +#define VOICE_PCM_CHANNELS 1 // always mono +#define VOICE_MAX_DATA_SIZE 8192 #define VOICE_MAX_GS_DATA_SIZE 4096 // never change these parameters when using opuscustom @@ -39,32 +39,34 @@ typedef struct OpusDecoder OpusDecoder; // must follow opus custom requirements // also be divisible with MAX_RAW_SAMPLES #define VOICE_OPUS_CUSTOM_FRAME_SIZE 1024 -#define VOICE_OPUS_CUSTOM_CODEC "opus_custom_44k_512" +#define VOICE_OPUS_CUSTOM_CODEC "opus_custom_44k_512" // a1ba: do not change, we don't have any re-encoding support now -#define VOICE_DEFAULT_CODEC VOICE_OPUS_CUSTOM_CODEC +#define VOICE_DEFAULT_CODEC VOICE_OPUS_CUSTOM_CODEC +#define VOICE_DEFAULT_SILENCE_FRAME_SIZE 160 // GoldSrc voice configuration #define GS_MAX_DECOMPRESSED_SAMPLES 32768 -#define GS_DEFAULT_SAMPLE_RATE 24000 -#define GS_DEFAULT_FRAME_SIZE 480 +#define GS_DEFAULT_SAMPLE_RATE 24000 +#define GS_DEFAULT_FRAME_SIZE 480 // VPC (Voice Packet Control) types -enum gs_vpc_type { - GS_VPC_VDATA_SILENCE = 0, - GS_VPC_VDATA_MILES = 1, +enum gs_vpc_type +{ + GS_VPC_VDATA_SILENCE = 0, + GS_VPC_VDATA_MILES = 1, GS_VPC_VDATA_SPEEX = 2, GS_VPC_VDATA_RAW = 3, GS_VPC_VDATA_SILK = 4, - GS_VPC_VDATA_OPUS_PLC = 6, - GS_VPC_SETSAMPLERATE = 11, - GS_VPC_UNKNOWN = 10 + GS_VPC_VDATA_OPUS_PLC = 6, + GS_VPC_SETSAMPLERATE = 11, + GS_VPC_UNKNOWN = 10 }; typedef struct voice_status_s { qboolean talking_ack; - double talking_timeout; + double talking_timeout; } voice_status_t; typedef struct voice_autogain_s @@ -77,17 +79,17 @@ typedef struct voice_autogain_s typedef struct voice_state_s { - string codec; - int quality; + string codec; + int quality; qboolean goldsrc; qboolean initialized; qboolean is_recording; qboolean device_opened; - double start_time; + double start_time; - voice_status_t local; - voice_status_t players_status[MAX_CLIENTS]; + voice_status_t local; + voice_status_t players_status[MAX_CLIENTS]; // opus stuff OpusCustomMode *custom_mode; @@ -106,11 +108,11 @@ typedef struct voice_state_s byte input_buffer[MAX_RAW_SAMPLES]; byte compress_buffer[MAX_RAW_SAMPLES]; byte decompress_buffer[MAX_RAW_SAMPLES]; - fs_offset_t input_buffer_pos; // in bytes + fs_offset_t input_buffer_pos; // in bytes // input from file - wavdata_t *input_file; - fs_offset_t input_file_pos; // in bytes + wavdata_t *input_file; + fs_offset_t input_file_pos; // in bytes voice_autogain_t autogain; } voice_state_t; @@ -127,9 +129,6 @@ void Voice_RecordStart( void ); void Voice_Disconnect( void ); void Voice_AddIncomingData( int ent, const byte *data, uint size, uint frames ); void Voice_StatusAck( voice_status_t *status, int playerIndex ); - -qboolean Voice_IsGoldSrcMode( const char *codec ); -qboolean Voice_IsOpusCustomMode( const char *codec ); -int Voice_GetBitrateForQuality( int quality, qboolean goldsrc ); +void Voice_StartChannel( uint samples, byte *data, int entnum ); #endif // VOICE_H From 21b05167246de6265f409240cbddd4999b886876 Mon Sep 17 00:00:00 2001 From: Sergey Degtyar Date: Thu, 3 Jul 2025 16:21:41 +0300 Subject: [PATCH 6/6] engine: client: move voice reset to Voice_Shutdown --- engine/client/voice.c | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/engine/client/voice.c b/engine/client/voice.c index a78bcc6b..4b1a3287 100644 --- a/engine/client/voice.c +++ b/engine/client/voice.c @@ -509,7 +509,7 @@ Get compressed voice data for GoldSrc mode */ static uint Voice_GetGSCompressedData( byte *out, uint maxsize, uint *frames ) { - uint ofs, size = 0; + uint ofs = 0, size = 0; const uint frame_size_samples = GS_DEFAULT_FRAME_SIZE; const uint frame_size_bytes = GS_DEFAULT_FRAME_SIZE * voice.width; static uint16_t sequence = 0; @@ -1124,7 +1124,11 @@ static void Voice_Shutdown( void ) voice.initialized = false; voice.is_recording = false; voice.device_opened = false; + voice.goldsrc = false; voice.start_time = 0.0; + voice.samplerate = 0; + voice.frame_size = 0; + voice.width = 0; voice.input_buffer_pos = 0; voice.input_file_pos = 0; @@ -1206,13 +1210,6 @@ qboolean Voice_Init( const char *pszCodecName, int quality, qboolean preinit ) // unsupported codec Con_Printf( S_WARN "Server requested unsupported voice codec: %s\n", pszCodecName ); Voice_Shutdown(); - voice.goldsrc = false; - voice.codec[0] = 0; - voice.quality = 0; - voice.samplerate = 0; - voice.frame_size = 0; - voice.width = 0; - voice.initialized = false; return false; }