From 2b9e050f57616ee84335224997e7fc34c9566e0a Mon Sep 17 00:00:00 2001 From: Velaron Date: Sun, 9 May 2021 16:32:53 +0300 Subject: [PATCH] engine: voice support --- .gitmodules | 3 + 3rdparty/opus | 1 + engine/client/cl_main.c | 10 +- engine/client/cl_parse.c | 30 +++- engine/client/s_main.c | 36 +++- engine/client/s_mix.c | 3 + engine/client/s_mouth.c | 65 +++++++ engine/client/sound.h | 7 +- engine/client/voice.c | 234 +++++++++++++++++++++++++ engine/client/voice.h | 56 ++++++ engine/platform/android/snd_opensles.c | 15 ++ engine/platform/platform.h | 3 + engine/platform/sdl/s_sdl.c | 71 ++++++++ engine/platform/stub/s_stub.c | 15 ++ engine/server/sv_client.c | 55 ++++++ engine/server/sv_init.c | 14 ++ engine/server/sv_main.c | 4 + engine/wscript | 6 +- scripts/waifulib/opus.py | 42 +++++ wscript | 5 +- 20 files changed, 664 insertions(+), 11 deletions(-) create mode 160000 3rdparty/opus create mode 100644 engine/client/voice.c create mode 100644 engine/client/voice.h create mode 100644 scripts/waifulib/opus.py diff --git a/.gitmodules b/.gitmodules index d16a4037..dc4d389a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "vgui_support"] path = vgui_support url = https://github.com/FWGS/vgui_support +[submodule "opus"] + path = 3rdparty/opus + url = https://github.com/xiph/opus diff --git a/3rdparty/opus b/3rdparty/opus new file mode 160000 index 00000000..dfd6c88a --- /dev/null +++ b/3rdparty/opus @@ -0,0 +1 @@ +Subproject commit dfd6c88aaa54a03a61434c413e30c217eb98f1d5 diff --git a/engine/client/cl_main.c b/engine/client/cl_main.c index b49bb695..172a724b 100644 --- a/engine/client/cl_main.c +++ b/engine/client/cl_main.c @@ -864,6 +864,9 @@ void CL_WritePacket( void ) cl.commands[cls.netchan.outgoing_sequence & CL_UPDATE_MASK].sendsize = MSG_GetNumBytesWritten( &buf ); cl.commands[cls.netchan.outgoing_sequence & CL_UPDATE_MASK].heldback = false; + // send voice data to the server + CL_AddVoiceToDatagram(); + // composite the rest of the datagram.. if( MSG_GetNumBitsWritten( &cls.datagram ) <= MSG_GetNumBitsLeft( &buf )) MSG_WriteBits( &buf, MSG_GetData( &cls.datagram ), MSG_GetNumBitsWritten( &cls.datagram )); @@ -2823,6 +2826,8 @@ void CL_InitLocal( void ) Cvar_RegisterVariable( &cl_logocolor ); Cvar_RegisterVariable( &cl_test_bandwidth ); + Voice_RegisterCvars(); + // register our variables cl_crosshair = Cvar_Get( "crosshair", "1", FCVAR_ARCHIVE, "show weapon chrosshair" ); cl_nodelta = Cvar_Get ("cl_nodelta", "0", 0, "disable delta-compression for server messages" ); @@ -3024,8 +3029,8 @@ void Host_ClientFrame( void ) // a new portion updates from server CL_RedoPrediction (); - // TODO: implement -// Voice_Idle( host.frametime ); + // update voice + Voice_Idle( host.frametime ); // emit visible entities CL_EmitEntities (); @@ -3079,6 +3084,7 @@ void CL_Init( void ) VID_Init(); // init video S_Init(); // init sound + Voice_Init( "opus", 0 ); // init voice // unreliable buffer. unsed for unreliable commands and voice stream MSG_Init( &cls.datagram, "cls.datagram", cls.datagram_buf, sizeof( cls.datagram_buf )); diff --git a/engine/client/cl_parse.c b/engine/client/cl_parse.c index 30f40da7..311656d0 100644 --- a/engine/client/cl_parse.c +++ b/engine/client/cl_parse.c @@ -1685,7 +1685,10 @@ CL_ParseVoiceInit */ void CL_ParseVoiceInit( sizebuf_t *msg ) { - // TODO: ??? + char *pszCodec = MSG_ReadString( msg ); + int quality = MSG_ReadByte( msg ); + + Voice_Init( pszCodec, quality ); } /* @@ -1696,7 +1699,28 @@ CL_ParseVoiceData */ void CL_ParseVoiceData( sizebuf_t *msg ) { - // TODO: ??? + int size, idx, frames; + unsigned char received[8192]; + + idx = MSG_ReadByte( msg ) + 1; + + frames = MSG_ReadByte( msg ); + + size = MSG_ReadShort( msg ); + size = Q_min( size, 8192 ); + + MSG_ReadBytes( msg, received, size ); + + if ( idx <= 0 || idx > cl.maxclients ) + return; + + if ( idx == cl.playernum + 1 ) + Voice_LocalPlayerTalkingAck(); + + if ( !size ) + return; + + Voice_AddIncomingData( idx, received, size, frames ); } /* @@ -2340,6 +2364,7 @@ void CL_ParseServerMessage( sizebuf_t *msg, qboolean normal_message ) break; case svc_voicedata: CL_ParseVoiceData( msg ); + cl.frames[cl.parsecountmod].graphdata.voicebytes += MSG_GetNumBytesRead( msg ) - bufStart; break; case svc_resourcelocation: CL_ParseResLocation( msg ); @@ -3127,6 +3152,7 @@ void CL_ParseLegacyServerMessage( sizebuf_t *msg, qboolean normal_message ) break; case svc_voicedata: CL_ParseVoiceData( msg ); + cl.frames[cl.parsecountmod].graphdata.voicebytes += MSG_GetNumBytesRead( msg ) - bufStart; break; case svc_resourcelocation: CL_ParseResLocation( msg ); diff --git a/engine/client/s_main.c b/engine/client/s_main.c index 332c2beb..272502f9 100644 --- a/engine/client/s_main.c +++ b/engine/client/s_main.c @@ -1127,7 +1127,7 @@ static uint S_RawSamplesStereo( portable_samplepair_t *rawsamples, uint rawend, S_RawEntSamples =================== */ -static void S_RawEntSamples( int entnum, uint samples, uint rate, word width, word channels, const byte *data, int snd_vol ) +void S_RawEntSamples( int entnum, uint samples, uint rate, word width, word channels, const byte *data, int snd_vol ) { rawchan_t *ch; @@ -1286,6 +1286,9 @@ static void S_FreeIdleRawChannels( void ) if( ch->s_rawend >= paintedtime ) continue; + + if ( ch->entnum > 0 ) + SND_ForceCloseMouth( ch->entnum ); if(( paintedtime - ch->s_rawend ) / SOUND_DMA_SPEED >= S_RAW_SOUND_IDLE_SEC ) { @@ -1858,6 +1861,33 @@ void S_SoundInfo_f( void ) S_PrintBackgroundTrackState (); } +/* +================= +S_VoiceRecordStart_f +================= +*/ +void S_VoiceRecordStart_f( void ) +{ + if( cls.state != ca_active ) + return; + + Voice_RecordStart(); +} + +/* +================= +S_VoiceRecordStop_f +================= +*/ +void S_VoiceRecordStop_f( void ) +{ + if( cls.state != ca_active || !Voice_IsRecording() ) + return; + + CL_AddVoiceToDatagram(); + Voice_RecordStop(); +} + /* ================ S_Init @@ -1892,8 +1922,8 @@ qboolean S_Init( void ) Cmd_AddCommand( "soundlist", S_SoundList_f, "display loaded sounds" ); Cmd_AddCommand( "s_info", S_SoundInfo_f, "print sound system information" ); Cmd_AddCommand( "s_fade", S_SoundFade_f, "fade all sounds then stop all" ); - Cmd_AddCommand( "+voicerecord", Cmd_Null_f, "start voice recording (non-implemented)" ); - Cmd_AddCommand( "-voicerecord", Cmd_Null_f, "stop voice recording (non-implemented)" ); + Cmd_AddCommand( "+voicerecord", S_VoiceRecordStart_f, "start voice recording" ); + Cmd_AddCommand( "-voicerecord", S_VoiceRecordStop_f, "stop voice recording" ); Cmd_AddCommand( "spk", S_SayReliable_f, "reliable play a specified sententce" ); Cmd_AddCommand( "speak", S_Say_f, "playing a specified sententce" ); diff --git a/engine/client/s_mix.c b/engine/client/s_mix.c index bcc8304c..721f05fa 100644 --- a/engine/client/s_mix.c +++ b/engine/client/s_mix.c @@ -958,6 +958,9 @@ void MIX_MixRawSamplesBuffer( int end ) pbuf[j-paintedtime].left += ( ch->rawsamples[j & ( ch->max_samples - 1 )].left * ch->leftvol ) >> 8; pbuf[j-paintedtime].right += ( ch->rawsamples[j & ( ch->max_samples - 1 )].right * ch->rightvol ) >> 8; } + + if ( ch->entnum > 0 ) + SND_MoveMouthRaw( ch, &ch->rawsamples[paintedtime & ( ch->max_samples - 1 )], stop - paintedtime ); } } diff --git a/engine/client/s_mouth.c b/engine/client/s_mouth.c index 42acd578..3b93c8c8 100644 --- a/engine/client/s_mouth.c +++ b/engine/client/s_mouth.c @@ -150,3 +150,68 @@ void SND_MoveMouth16( channel_t *ch, wavdata_t *pSource, int count ) pMouth->sndcount = 0; } } + +void SND_ForceInitMouth( int entnum ) +{ + cl_entity_t *clientEntity; + + clientEntity = CL_GetEntityByIndex( entnum ); + + if ( clientEntity ) + { + clientEntity->mouth.mouthopen = 0; + clientEntity->mouth.sndavg = 0; + clientEntity->mouth.sndcount = 0; + } +} + +void SND_ForceCloseMouth( int entnum ) +{ + cl_entity_t *clientEntity; + + clientEntity = CL_GetEntityByIndex( entnum ); + + if ( clientEntity ) + clientEntity->mouth.mouthopen = 0; +} + +void SND_MoveMouthRaw( rawchan_t *ch, portable_samplepair_t *pData, int count ) +{ + cl_entity_t *clientEntity; + mouth_t *pMouth = NULL; + int savg, data; + int scount = 0; + uint i; + + clientEntity = CL_GetEntityByIndex( ch->entnum ); + if( !clientEntity ) return; + + pMouth = &clientEntity->mouth; + + if( pData == NULL ) + return; + + i = 0; + scount = pMouth->sndcount; + savg = 0; + + while ( i < count && scount < CAVGSAMPLES ) + { + data = pData[i].left; // mono sound anyway + data = ( bound( -32767, data, 0x7ffe ) >> 8 ); + savg += abs( data ); + + i += 80 + ( (byte)data & 0x1F ); + scount++; + } + + pMouth->sndavg += savg; + pMouth->sndcount = (byte)scount; + + if ( pMouth->sndcount >= CAVGSAMPLES ) + { + pMouth->mouthopen = pMouth->sndavg / CAVGSAMPLES; + pMouth->sndavg = 0; + pMouth->sndcount = 0; + } +} \ No newline at end of file diff --git a/engine/client/sound.h b/engine/client/sound.h index 930d114d..326f842b 100644 --- a/engine/client/sound.h +++ b/engine/client/sound.h @@ -27,6 +27,7 @@ extern poolhandle_t sndpool; #define SOUND_22k 22050 // 22khz sample rate #define SOUND_32k 32000 // 32khz sample rate #define SOUND_44k 44100 // 44khz sample rate +#define SOUND_48k 48000 // 48khz sample rate #define DMA_MSEC_PER_SAMPLE ((float)(1000.0 / SOUND_DMA_SPEED)) // fixed point stuff for real-time resampling @@ -202,7 +203,7 @@ typedef struct #define MAX_DYNAMIC_CHANNELS (60 + NUM_AMBIENTS) #define MAX_CHANNELS (256 + MAX_DYNAMIC_CHANNELS) // Scourge Of Armagon has too many static sounds on hip2m4.bsp -#define MAX_RAW_CHANNELS 16 +#define MAX_RAW_CHANNELS 48 #define MAX_RAW_SAMPLES 8192 extern sound_t ambient_sfx[NUM_AMBIENTS]; @@ -271,6 +272,7 @@ int S_GetCurrentStaticSounds( soundlist_t *pout, int size ); int S_GetCurrentDynamicSounds( soundlist_t *pout, int size ); sfx_t *S_GetSfxByHandle( sound_t handle ); rawchan_t *S_FindRawChannel( int entnum, qboolean create ); +void S_RawEntSamples( int entnum, uint samples, uint rate, word width, word channels, const byte *data, int snd_vol ); void S_RawSamples( uint samples, uint rate, word width, word channels, const byte *data, int entnum ); void S_StopSound( int entnum, int channel, const char *soundname ); void S_UpdateFrame( struct ref_viewpass_s *rvp ); @@ -283,9 +285,12 @@ void S_FreeSounds( void ); // s_mouth.c // void SND_InitMouth( int entnum, int entchannel ); +void SND_ForceInitMouth( int entnum ); void SND_MoveMouth8( channel_t *ch, wavdata_t *pSource, int count ); void SND_MoveMouth16( channel_t *ch, wavdata_t *pSource, int count ); +void SND_MoveMouthRaw( rawchan_t *ch, portable_samplepair_t *pData, int count ); void SND_CloseMouth( channel_t *ch ); +void SND_ForceCloseMouth( int entnum ); // // s_stream.c diff --git a/engine/client/voice.c b/engine/client/voice.c new file mode 100644 index 00000000..c0644b2c --- /dev/null +++ b/engine/client/voice.c @@ -0,0 +1,234 @@ +#include "voice.h" + +wavdata_t *input_file; +fs_offset_t input_pos; + +voice_state_t voice; + +CVAR_DEFINE_AUTO( voice_enable, "1", FCVAR_ARCHIVE, "enable voice chat" ); +CVAR_DEFINE_AUTO( voice_loopback, "0", 0, "loopback voice back to the speaker" ); +CVAR_DEFINE_AUTO( voice_scale, "1.0", FCVAR_ARCHIVE, "incoming voice volume scale" ); +CVAR_DEFINE_AUTO( voice_inputfromfile, "0", 0, "input voice from voice_input.wav" ); + +void Voice_RegisterCvars( void ) +{ + Cvar_RegisterVariable( &voice_enable ); + Cvar_RegisterVariable( &voice_loopback ); + Cvar_RegisterVariable( &voice_scale ); + Cvar_RegisterVariable( &voice_inputfromfile ); +} + +static void Voice_Status( int entindex, qboolean bTalking ) +{ + clgame.dllFuncs.pfnVoiceStatus( entindex, bTalking ); +} + +// parameters currently unused +qboolean Voice_Init( const char *pszCodecName, int quality ) +{ + int err; + + if ( !voice_enable.value ) + return false; + + Voice_DeInit(); + + voice.was_init = true; + + voice.channels = 1; + voice.width = 2; + voice.samplerate = SOUND_48k; + voice.frame_size = voice.channels * ( (float)voice.samplerate / ( 1000.0f / 20.0f ) ) * voice.width; + + if ( !VoiceCapture_Init() ) + { + Voice_DeInit(); + return false; + } + + voice.encoder = opus_encoder_create( voice.samplerate, voice.channels, OPUS_APPLICATION_VOIP, &err ); + voice.decoder = opus_decoder_create( voice.samplerate, voice.channels, &err ); + + return true; +} + +void Voice_DeInit( void ) +{ + if ( !voice.was_init ) + return; + + Voice_RecordStop(); + + opus_encoder_destroy( voice.encoder ); + opus_decoder_destroy( voice.decoder ); + + voice.was_init = false; +} + +uint Voice_GetCompressedData( byte *out, uint maxsize, uint *frames ) +{ + uint ofs, size = 0; + + if ( input_file ) + { + uint numbytes; + double time; + + time = Sys_DoubleTime(); + + numbytes = ( time - voice.start_time ) * voice.samplerate; + numbytes = Q_min( numbytes, input_file->size - input_pos ); + numbytes = Q_min( numbytes, sizeof( voice.buffer ) - voice.buffer_pos ); + + memcpy( voice.buffer + voice.buffer_pos, input_file->buffer + input_pos, numbytes ); + voice.buffer_pos += numbytes; + input_pos += numbytes; + + voice.start_time = time; + } + + for ( ofs = 0; voice.buffer_pos - ofs >= voice.frame_size && ofs <= voice.buffer_pos; ofs += voice.frame_size ) + { + int bytes; + + bytes = opus_encode( voice.encoder, (const opus_int16*)(voice.buffer + ofs), voice.frame_size / voice.width, out + size, maxsize ); + memmove( voice.buffer, voice.buffer + voice.frame_size, sizeof( voice.buffer ) - voice.frame_size ); + voice.buffer_pos -= voice.frame_size; + + if ( bytes > 0 ) + { + size += bytes; + (*frames)++; + } + } + + return size; +} + +void Voice_Idle( float frametime ) +{ + if ( !voice_enable.value ) + { + Voice_DeInit(); + return; + } + + if ( voice.talking_ack ) + { + voice.talking_timeout += frametime; + + if ( voice.talking_timeout > 0.2f ) + { + voice.talking_ack = false; + Voice_Status( -2, false ); + } + } +} + +qboolean Voice_IsRecording( void ) +{ + return voice.is_recording; +} + +void Voice_RecordStop( void ) +{ + if ( input_file ) + { + FS_FreeSound( input_file ); + input_file = NULL; + } + + voice.buffer_pos = 0; + memset( voice.buffer, 0, sizeof( voice.buffer ) ); + + if ( Voice_IsRecording() ) + Voice_Status( -1, false ); + + VoiceCapture_RecordStop(); + + voice.is_recording = false; +} + +void Voice_RecordStart( void ) +{ + Voice_RecordStop(); + + if ( voice_inputfromfile.value ) + { + input_file = FS_LoadSound( "voice_input.wav", NULL, 0 ); + + if ( input_file ) + { + Sound_Process( &input_file, voice.samplerate, voice.width, SOUND_RESAMPLE ); + input_pos = 0; + + voice.start_time = Sys_DoubleTime(); + voice.is_recording = true; + } + else + { + FS_FreeSound( input_file ); + input_file = NULL; + } + } + + if ( !Voice_IsRecording() ) + voice.is_recording = VoiceCapture_RecordStart(); + + if ( Voice_IsRecording() ) + Voice_Status( -1, true ); +} + +void Voice_AddIncomingData( int ent, byte *data, uint size, uint frames ) +{ + byte decompressed[MAX_RAW_SAMPLES]; + int samples; + + samples = opus_decode( voice.decoder, (const byte*)data, size, (short *)decompressed, voice.frame_size / voice.width * frames, false ); + + if ( samples > 0 ) + Voice_StartChannel( samples, decompressed, ent ); +} + +void CL_AddVoiceToDatagram( void ) +{ + uint size, frames = 0; + byte data[MAX_RAW_SAMPLES]; + + if ( cls.state != ca_active || !Voice_IsRecording() ) + return; + + size = Voice_GetCompressedData( data, sizeof( data ), &frames ); + + if ( size > 0 && MSG_GetNumBytesLeft( &cls.datagram ) >= size + 32 ) + { + MSG_BeginClientCmd( &cls.datagram, clc_voicedata ); + MSG_WriteByte( &cls.datagram, Voice_GetLoopback() ); + MSG_WriteByte( &cls.datagram, frames ); + MSG_WriteShort( &cls.datagram, size ); + MSG_WriteBytes( &cls.datagram, data, size ); + } +} + +qboolean Voice_GetLoopback( void ) +{ + return voice_loopback.value; +} + +void Voice_LocalPlayerTalkingAck( void ) +{ + if ( !voice.talking_ack ) + { + Voice_Status( -2, true ); + } + + voice.talking_ack = true; + voice.talking_timeout = 0.0f; +} + +void Voice_StartChannel( uint samples, byte *data, int entnum ) +{ + SND_ForceInitMouth( entnum ); + Voice_Status( entnum, true ); + S_RawEntSamples( entnum, samples, voice.samplerate, voice.width, voice.channels, data, 128.0f * voice_scale.value ); +} \ No newline at end of file diff --git a/engine/client/voice.h b/engine/client/voice.h new file mode 100644 index 00000000..441d9c9d --- /dev/null +++ b/engine/client/voice.h @@ -0,0 +1,56 @@ +#ifndef VOICE_H +#define VOICE_H + +#include + +#include "common.h" +#include "client.h" +#include "sound.h" +#include "soundlib/soundlib.h" +#include "library.h" + +#define SAMPLES_PER_SEC ( SOUND_48k / BYTES_PER_SAMPLE ) + +extern convar_t voice_scale; + +typedef struct voice_state_s +{ + qboolean was_init; + qboolean is_recording; + float start_time; + qboolean talking_ack; + float talking_timeout; + + // opus stuff + OpusEncoder *encoder; + OpusDecoder *decoder; + + // audio info + uint channels; + uint width; + uint samplerate; + uint frame_size; + + // input buffer + byte buffer[MAX_RAW_SAMPLES]; + fs_offset_t buffer_pos; +} voice_state_t; + +extern voice_state_t voice; + +void CL_AddVoiceToDatagram( void ); + +void Voice_RegisterCvars( void ); +qboolean Voice_Init( const char *pszCodecName, int quality ); +void Voice_DeInit( void ); +uint Voice_GetCompressedData( byte *out, uint maxsize, uint *frames ); +void Voice_Idle( float frametime ); +qboolean Voice_IsRecording( void ); +void Voice_RecordStop( void ); +void Voice_RecordStart( void ); +void Voice_AddIncomingData( int ent, byte *data, uint size, uint frames ); +qboolean Voice_GetLoopback( void ); +void Voice_LocalPlayerTalkingAck( void ); +void Voice_StartChannel( uint samples, byte *data, int entnum ); + +#endif // VOICE_H \ No newline at end of file diff --git a/engine/platform/android/snd_opensles.c b/engine/platform/android/snd_opensles.c index 012d9313..056ebd62 100644 --- a/engine/platform/android/snd_opensles.c +++ b/engine/platform/android/snd_opensles.c @@ -254,4 +254,19 @@ void SNDDMA_BeginPainting( void ) { pthread_mutex_lock( &snddma_android_mutex ); } + +qboolean VoiceCapture_Init( void ) +{ + return false; +} + +qboolean VoiceCapture_RecordStart( void ) +{ + return false; +} + +void VoiceCapture_RecordStop( void ) +{ + return 0; +} #endif diff --git a/engine/platform/platform.h b/engine/platform/platform.h index e764421a..51c5c3fd 100644 --- a/engine/platform/platform.h +++ b/engine/platform/platform.h @@ -161,5 +161,8 @@ void SNDDMA_Activate( qboolean active ); // pause audio // void SNDDMA_PrintDeviceName( void ); // unused // void SNDDMA_LockSound( void ); // unused // void SNDDMA_UnlockSound( void ); // unused +qboolean VoiceCapture_Init( void ); +qboolean VoiceCapture_RecordStart( void ); +void VoiceCapture_RecordStop( void ); #endif // PLATFORM_H diff --git a/engine/platform/sdl/s_sdl.c b/engine/platform/sdl/s_sdl.c index 8763969c..95edfcc1 100644 --- a/engine/platform/sdl/s_sdl.c +++ b/engine/platform/sdl/s_sdl.c @@ -18,6 +18,7 @@ GNU General Public License for more details. #if XASH_SOUND == SOUND_SDL #include "sound.h" +#include "voice.h" #include @@ -43,6 +44,8 @@ so it can unlock and free the data block after it has been played. ======================================================================= */ static int sdl_dev; +static SDL_AudioDeviceID in_dev; +static SDL_AudioFormat sdl_format; //static qboolean snd_firsttime = true; //static qboolean primary_format_set; @@ -133,6 +136,8 @@ qboolean SNDDMA_Init( void ) dma.buffer = Z_Calloc( dma.samples * 2 ); dma.samplepos = 0; + sdl_format = obtained.format; + Con_Printf( "Using SDL audio driver: %s @ %d Hz\n", SDL_GetCurrentAudioDriver( ), obtained.freq ); dma.initialized = true; @@ -220,4 +225,70 @@ void SNDDMA_Activate( qboolean active ) SDL_PauseAudioDevice( sdl_dev, !active ); } + +/* +=========== +SDL_SoundInputCallback +=========== +*/ +void SDL_SoundInputCallback( void *userdata, Uint8 *stream, int len ) +{ + int size; + + size = Q_min( len, sizeof( voice.buffer ) - voice.buffer_pos ); + SDL_memset( voice.buffer + voice.buffer_pos, 0, size ); + SDL_MixAudioFormat( voice.buffer + voice.buffer_pos, stream, sdl_format, size, SDL_MIX_MAXVOLUME ); + voice.buffer_pos += size; +} + +/* +=========== +VoiceCapture_Init +=========== +*/ +qboolean VoiceCapture_Init( void ) +{ + SDL_AudioSpec wanted, spec; + + SDL_zero( wanted ); + wanted.freq = voice.samplerate; + wanted.format = AUDIO_S16LSB; + wanted.channels = voice.channels; + wanted.samples = voice.frame_size / voice.width; + wanted.callback = SDL_SoundInputCallback; + + in_dev = SDL_OpenAudioDevice( NULL, SDL_TRUE, &wanted, &spec, 0 ); + + if( SDLash_IsAudioError( in_dev ) ) + { + Con_Printf( "VoiceCapture_Init: error creating capture device (%s)\n", SDL_GetError() ); + return false; + } + + Con_Printf( S_NOTE "VoiceCapture_Init: capture device creation success (%i: %s)\n", in_dev, SDL_GetAudioDeviceName( in_dev, SDL_TRUE ) ); + return true; +} + +/* +=========== +VoiceCapture_RecordStart +=========== +*/ +qboolean VoiceCapture_RecordStart( void ) +{ + SDL_PauseAudioDevice( in_dev, SDL_FALSE ); + + return true; +} + +/* +=========== +VoiceCapture_RecordStop +=========== +*/ +void VoiceCapture_RecordStop( void ) +{ + SDL_PauseAudioDevice( in_dev, SDL_TRUE ); +} + #endif // XASH_SOUND == SOUND_SDL diff --git a/engine/platform/stub/s_stub.c b/engine/platform/stub/s_stub.c index 509ae9b7..c3855dd6 100644 --- a/engine/platform/stub/s_stub.c +++ b/engine/platform/stub/s_stub.c @@ -94,5 +94,20 @@ void SNDDMA_Shutdown( void ) } } +qboolean VoiceCapture_Init( void ) +{ + return false; +} + +qboolean VoiceCapture_RecordStart( void ) +{ + return false; +} + +void VoiceCapture_RecordStop( void ) +{ + return 0; +} + #endif #endif diff --git a/engine/server/sv_client.c b/engine/server/sv_client.c index 9cdc9f92..3ab3c04c 100644 --- a/engine/server/sv_client.c +++ b/engine/server/sv_client.c @@ -2561,6 +2561,58 @@ void SV_ParseCvarValue2( sv_client_t *cl, sizebuf_t *msg ) Con_Reportf( "Cvar query response: name:%s, request ID %d, cvar:%s, value:%s\n", cl->name, requestID, name, value ); } +/* +=================== +SV_ParseVoiceData +=================== +*/ +void SV_ParseVoiceData( sv_client_t *cl, sizebuf_t *msg ) +{ + char received[4096]; + sv_client_t *cur; + int i, client; + uint length, size, frames; + + cl->m_bLoopback = MSG_ReadByte( msg ); + + frames = MSG_ReadByte( msg ); + + size = MSG_ReadShort( msg ); + client = cl - svs.clients; + + if ( size > sizeof( received ) ) + { + Con_DPrintf( "SV_ParseVoiceData: invalid incoming packet.\n" ); + SV_DropClient( cl, false ); + return; + } + + if ( !Cvar_VariableInteger( "sv_voiceenable" ) ) + return; + + MSG_ReadBytes( msg, received, size ); + + for( i = 0, cur = svs.clients; i < svs.maxclients; i++, cur++ ) + { + if ( cur->state < cs_connected && cl != cur ) + continue; + + length = size; + + if ( MSG_GetNumBytesLeft( &cur->datagram ) < length + 6 ) + continue; + + if ( cl == cur && !cur->m_bLoopback ) + length = 0; + + MSG_BeginServerCmd( &cur->datagram, svc_voicedata ); + MSG_WriteByte( &cur->datagram, client ); + MSG_WriteByte( &cur->datagram, frames ); + MSG_WriteShort( &cur->datagram, length ); + MSG_WriteBytes( &cur->datagram, received, length ); + } +} + /* =================== SV_ExecuteClientMessage @@ -2631,6 +2683,9 @@ void SV_ExecuteClientMessage( sv_client_t *cl, sizebuf_t *msg ) case clc_fileconsistency: SV_ParseConsistencyResponse( cl, msg ); break; + case clc_voicedata: + SV_ParseVoiceData( cl, msg ); + break; case clc_requestcvarvalue: SV_ParseCvarValue( cl, msg ); break; diff --git a/engine/server/sv_init.c b/engine/server/sv_init.c index 8f55a25e..d77f21a1 100644 --- a/engine/server/sv_init.c +++ b/engine/server/sv_init.c @@ -386,6 +386,18 @@ void SV_CreateResourceList( void ) } } +/* +================ +SV_WriteVoiceCodec +================ +*/ +void SV_WriteVoiceCodec( sizebuf_t *msg ) +{ + MSG_BeginServerCmd( msg, svc_voiceinit ); + MSG_WriteString( msg, "opus" ); + MSG_WriteByte( msg, 0 ); +} + /* ================ SV_CreateBaseline @@ -404,6 +416,8 @@ void SV_CreateBaseline( void ) int delta_type; int entnum; + SV_WriteVoiceCodec( &sv.signon ); + if( FBitSet( host.features, ENGINE_QUAKE_COMPATIBLE )) playermodel = SV_ModelIndex( DEFAULT_PLAYER_PATH_QUAKE ); else playermodel = SV_ModelIndex( DEFAULT_PLAYER_PATH_HALFLIFE ); diff --git a/engine/server/sv_main.c b/engine/server/sv_main.c index 7f017abe..bae1f522 100644 --- a/engine/server/sv_main.c +++ b/engine/server/sv_main.c @@ -112,6 +112,9 @@ CVAR_DEFINE_AUTO( violence_ablood, "1", 0, "draw alien blood" ); CVAR_DEFINE_AUTO( violence_hgibs, "1", 0, "show human gib entities" ); CVAR_DEFINE_AUTO( violence_agibs, "1", 0, "show alien gib entities" ); +// voice chat +CVAR_DEFINE_AUTO( sv_voiceenable, "1", FCVAR_ARCHIVE|FCVAR_SERVER, "enable voice support" ); + convar_t *sv_novis; // disable server culling entities by vis convar_t *sv_pausable; convar_t *timeout; // seconds without any message @@ -974,6 +977,7 @@ void SV_Init( void ) Cvar_RegisterVariable( &listipcfgfile ); Cvar_RegisterVariable( &mapchangecfgfile ); + Cvar_RegisterVariable( &sv_voiceenable ); Cvar_RegisterVariable( &sv_trace_messages ); sv_allow_joystick = Cvar_Get( "sv_allow_joystick", "1", FCVAR_ARCHIVE, "allow connect with joystick enabled" ); diff --git a/engine/wscript b/engine/wscript index 972d8804..23ea25bb 100644 --- a/engine/wscript +++ b/engine/wscript @@ -35,7 +35,7 @@ def options(opt): grp.add_option('--enable-engine-fuzz', action = 'store_true', dest = 'ENGINE_FUZZ', default = False, help = 'add LLVM libFuzzer [default: %default]' ) - opt.load('sdl2') + opt.load('sdl2 opus') def configure(conf): # check for dedicated server build @@ -65,7 +65,7 @@ def configure(conf): else: conf.load('sdl2') if not conf.env.HAVE_SDL2: - conf.fatal('SDL2 not availiable! If you want to build dedicated server, specify --dedicated') + conf.fatal('SDL2 not available! If you want to build dedicated server, specify --dedicated') conf.define('XASH_SDL', 2) if conf.env.DEST_OS == 'haiku': @@ -165,6 +165,8 @@ def build(bld): 'client/*.c', 'client/vgui/*.c', 'client/avi/*.c']) + + libs.append('OPUS') includes = ['common', 'server', 'client', 'client/vgui', 'tests', '.', '../public', '../common', '../filesystem', '../pm_shared' ] diff --git a/scripts/waifulib/opus.py b/scripts/waifulib/opus.py new file mode 100644 index 00000000..d1ef2a07 --- /dev/null +++ b/scripts/waifulib/opus.py @@ -0,0 +1,42 @@ +# encoding: utf-8 + +import os + +def options(opt): + pass + +def configure(conf): + path = conf.path.find_dir('3rdparty/opus') + conf.env.LIB_OPUS = ['opus'] + conf.env.INCLUDES_OPUS = [path.find_dir('include/').abspath()] + +def build(bld): + path = bld.path.find_dir('3rdparty/opus') + + sources = path.ant_glob([ + 'src/*.c', + 'celt/*.c', + 'silk/*.c', + 'silk/float/*.c']) + + includes = [ + path.find_dir('include/'), + path.find_dir('celt/'), + path.find_dir('silk/'), + path.find_dir('silk/float/') + ] + + defines = [ + 'USE_ALLOCA', + 'OPUS_BUILD', + 'PACKAGE_VERSION="1.3.1"' + ] + + bld.stlib( + source = sources, + target = 'opus', + features = 'c', + includes = includes, + defines = defines, + subsystem = bld.env.MSVC_SUBSYSTEM + ) \ No newline at end of file diff --git a/wscript b/wscript index 07e49635..2516c722 100644 --- a/wscript +++ b/wscript @@ -353,9 +353,12 @@ int main(int argc, char **argv) { strcasestr(argv[1], argv[2]); return 0; }''' continue conf.add_subproject(i.name) + + conf.load('opus') def build(bld): - bld.load('xshlib') + bld.load('opus xshlib') + for i in SUBDIRS: if not i.is_enabled(bld): continue