You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2090 lines
64 KiB
2090 lines
64 KiB
//========= Copyright � 1996-2005, Valve Corporation, All rights reserved. ============//
|
|
//
|
|
// Purpose:
|
|
//
|
|
// $NoKeywords: $
|
|
//
|
|
//=============================================================================//
|
|
|
|
#include "audio_pch.h"
|
|
#include "tier1/circularbuffer.h"
|
|
#include "voice.h"
|
|
#include "voice_wavefile.h"
|
|
#include "r_efx.h"
|
|
#include "cdll_int.h"
|
|
#include "voice_gain.h"
|
|
#include "voice_mixer_controls.h"
|
|
#include "snd_dma.h"
|
|
#include "ivoicerecord.h"
|
|
#include "ivoicecodec.h"
|
|
#include "filesystem.h"
|
|
#include "../../filesystem_engine.h"
|
|
#include "tier1/utlbuffer.h"
|
|
#include "../../cl_splitscreen.h"
|
|
#include "vgui_baseui_interface.h"
|
|
#include "demo.h"
|
|
|
|
extern IVEngineClient *engineClient;
|
|
|
|
#if defined( _X360 )
|
|
#include "xauddefs.h"
|
|
#endif
|
|
|
|
#include "steam/steam_api.h"
|
|
|
|
// memdbgon must be the last include file in a .cpp file!!!
|
|
#include "tier0/memdbgon.h"
|
|
|
|
static CSteamAPIContext g_SteamAPIContext;
|
|
static CSteamAPIContext *steamapicontext = NULL;
|
|
|
|
void Voice_EndChannel( int iChannel );
|
|
void VoiceTweak_EndVoiceTweakMode();
|
|
void EngineTool_OverrideSampleRate( int& rate );
|
|
|
|
// Special entity index used for tweak mode.
|
|
#define TWEAKMODE_ENTITYINDEX -500
|
|
|
|
// Special channel index passed to Voice_AddIncomingData when in tweak mode.
|
|
#define TWEAKMODE_CHANNELINDEX -100
|
|
|
|
|
|
// How long does the sign stay above someone's head when they talk?
|
|
#define SPARK_TIME 0.2
|
|
|
|
// How long a voice channel has to be inactive before we free it.
|
|
#define DIE_COUNTDOWN 0.5
|
|
|
|
// Size of the circular buffer. This should be BIGGER than the pad time,
|
|
// or else a little burst of data right as we fil the buffer will cause
|
|
// us to have nowhere to put the data and overflow the buffer!
|
|
#define VOICE_RECEIVE_BUFFER_SECONDS 2.0
|
|
|
|
// If you can figure out how to get OSX to just compute this value (and use it as a template argument)
|
|
// in the circular buffer below. Then go for it.
|
|
#define VOICE_RECEIVE_BUFFER_SIZE 88200
|
|
COMPILE_TIME_ASSERT( VOICE_RECEIVE_BUFFER_SIZE == VOICE_OUTPUT_SAMPLE_RATE * BYTES_PER_SAMPLE * VOICE_RECEIVE_BUFFER_SECONDS );
|
|
|
|
#define LOCALPLAYERTALKING_TIMEOUT 0.2f // How long it takes for the client to decide the server isn't sending acks
|
|
// of voice data back.
|
|
|
|
// true when using the speex codec
|
|
static bool g_bIsSpeex = false;
|
|
|
|
// If this is defined, then the data is converted to 8-bit and sent otherwise uncompressed.
|
|
// #define VOICE_SEND_RAW_TEST
|
|
|
|
// The format we sample voice in.
|
|
WAVEFORMATEX g_VoiceSampleFormat =
|
|
{
|
|
WAVE_FORMAT_PCM, // wFormatTag
|
|
1, // nChannels
|
|
VOICE_OUTPUT_SAMPLE_RATE, // nSamplesPerSec
|
|
VOICE_OUTPUT_SAMPLE_RATE*2, // nAvgBytesPerSec
|
|
2, // nBlockAlign
|
|
16, // wBitsPerSample
|
|
sizeof(WAVEFORMATEX) // cbSize
|
|
};
|
|
|
|
|
|
ConVar voice_loopback( "voice_loopback", "0", FCVAR_USERINFO );
|
|
ConVar voice_fadeouttime( "voice_fadeouttime", "0.0" ); // It fades to no sound at the tail end of your voice data when you release the key.
|
|
ConVar voice_threshold_delay( "voice_thresold_delay", "0.5" );
|
|
|
|
ConVar voice_record_steam( "voice_record_steam", "0", 0, "If true use Steam to record voice (not the engine codec)" );
|
|
|
|
ConVar voice_scale("voice_scale", "1.0", FCVAR_ARCHIVE | FCVAR_RELEASE, "Overall volume of voice over IP" );
|
|
ConVar voice_caster_scale( "voice_caster_scale", "1", FCVAR_ARCHIVE );
|
|
|
|
|
|
// Debugging cvars.
|
|
ConVar voice_profile( "voice_profile", "0" );
|
|
ConVar voice_showchannels( "voice_showchannels", "0" ); // 1 = list channels
|
|
// 2 = show timing info, etc
|
|
ConVar voice_showincoming( "voice_showincoming", "0" ); // show incoming voice data
|
|
|
|
int Voice_SamplesPerSec()
|
|
{
|
|
if ( voice_record_steam.GetBool() && steamapicontext && steamapicontext->SteamUser() )
|
|
return steamapicontext->SteamUser()->GetVoiceOptimalSampleRate();
|
|
|
|
int rate = ( g_bIsSpeex ? VOICE_OUTPUT_SAMPLE_RATE_SPEEX : VOICE_OUTPUT_SAMPLE_RATE ); //g_VoiceSampleFormat.nSamplesPerSec;
|
|
EngineTool_OverrideSampleRate( rate );
|
|
return rate;
|
|
}
|
|
|
|
int Voice_AvgBytesPerSec()
|
|
{
|
|
int rate = Voice_SamplesPerSec();
|
|
|
|
return ( rate * g_VoiceSampleFormat.wBitsPerSample ) >> 3;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Convar callback
|
|
//-----------------------------------------------------------------------------
|
|
void VoiceEnableCallback( IConVar *var, const char *pOldValue, float flOldValue )
|
|
{
|
|
if ( ((ConVar *)var)->GetBool() )
|
|
{
|
|
Voice_ForceInit();
|
|
}
|
|
}
|
|
|
|
ConVar voice_system_enable( "voice_system_enable", "1", FCVAR_ARCHIVE, "Toggle voice system.", VoiceEnableCallback ); // Globally enable or disable voice system.
|
|
ConVar voice_enable( "voice_enable", "1", FCVAR_ARCHIVE, "Toggle voice transmit and receive." );
|
|
ConVar voice_caster_enable( "voice_caster_enable", "0", FCVAR_ARCHIVE, "Toggle voice transmit and receive for casters. 0 = no caster, account number of caster to enable." );
|
|
ConVar voice_threshold( "voice_threshold", "4000", FCVAR_ARCHIVE | FCVAR_CLIENTDLL );
|
|
|
|
extern ConVar sv_voicecodec;
|
|
|
|
// Have it force your mixer control settings so waveIn comes from the microphone.
|
|
// CD rippers change your waveIn to come from the CD drive
|
|
ConVar voice_forcemicrecord( "voice_forcemicrecord", "1", FCVAR_ARCHIVE );
|
|
|
|
int g_nVoiceFadeSamples = 1; // Calculated each frame from the cvar.
|
|
float g_VoiceFadeMul = 1; // 1 / (g_nVoiceFadeSamples - 1).
|
|
|
|
// While in tweak mode, you can't hear anything anyone else is saying, and your own voice data
|
|
// goes directly to the speakers.
|
|
bool g_bInTweakMode = false;
|
|
int g_VoiceTweakSpeakingVolume = 0;
|
|
|
|
bool g_bVoiceAtLeastPartiallyInitted = false;
|
|
|
|
// Timing info for each frame.
|
|
static double g_CompressTime = 0;
|
|
static double g_DecompressTime = 0;
|
|
static double g_GainTime = 0;
|
|
static double g_UpsampleTime = 0;
|
|
|
|
class CVoiceTimer
|
|
{
|
|
public:
|
|
inline void Start()
|
|
{
|
|
if( voice_profile.GetInt() )
|
|
{
|
|
m_StartTime = Plat_FloatTime();
|
|
}
|
|
}
|
|
|
|
inline void End(double *out)
|
|
{
|
|
if( voice_profile.GetInt() )
|
|
{
|
|
*out += Plat_FloatTime() - m_StartTime;
|
|
}
|
|
}
|
|
|
|
double m_StartTime;
|
|
};
|
|
|
|
|
|
static float g_fLocalPlayerTalkingLastUpdateRealTime = 0.0f;
|
|
static bool g_bLocalPlayerTalkingAck[ MAX_SPLITSCREEN_CLIENTS ];
|
|
static float g_LocalPlayerTalkingTimeout[ MAX_SPLITSCREEN_CLIENTS ];
|
|
|
|
|
|
CSysModule *g_hVoiceCodecDLL = 0;
|
|
|
|
// Voice recorder. Can be waveIn, DSound, or whatever.
|
|
static IVoiceRecord *g_pVoiceRecord = NULL;
|
|
static IVoiceCodec *g_pEncodeCodec = NULL;
|
|
|
|
static bool g_bVoiceRecording = false; // Are we recording at the moment?
|
|
|
|
// A high precision client-local timestamp that is assumed to progress in approximate realtime
|
|
// (in lockstep with any transmitted audio). This is sent with voice packets, so that recipients
|
|
// can properly account for silence.
|
|
static uint32 s_nRecordingTimestamp_UncompressedSampleOffset;
|
|
|
|
/// Realtime time value corresponding to the above audio recoridng timestamp. This realtime value
|
|
/// is used so that we can advance the audio timestamp approximately if we don't get called for a while.
|
|
/// if there's a gigantic gap, it probably really doesn't matter. But for small gaps we'd like to
|
|
/// get the timing about right.
|
|
static double s_flRecordingTimestamp_PlatTime;
|
|
|
|
/// Make sure timestamnp system is ready to go.
|
|
static void VoiceRecord_CheckInitTimestamp()
|
|
{
|
|
if ( s_flRecordingTimestamp_PlatTime == 0.0 && s_nRecordingTimestamp_UncompressedSampleOffset == 0 )
|
|
{
|
|
s_nRecordingTimestamp_UncompressedSampleOffset = 1;
|
|
s_flRecordingTimestamp_PlatTime = Plat_FloatTime();
|
|
}
|
|
}
|
|
|
|
/// Advance audio timestamp using the platform timer
|
|
static void VoiceRecord_ForceAdvanceSampleOffsetUsingPlatTime()
|
|
{
|
|
VoiceRecord_CheckInitTimestamp();
|
|
|
|
// Advance the timestamp forward
|
|
double flNow = Plat_FloatTime();
|
|
int nSamplesElapsed = ( flNow - s_flRecordingTimestamp_PlatTime ) * ( g_bIsSpeex ? VOICE_OUTPUT_SAMPLE_RATE_SPEEX : VOICE_OUTPUT_SAMPLE_RATE );
|
|
if ( nSamplesElapsed > 0 )
|
|
s_nRecordingTimestamp_UncompressedSampleOffset += (uint32)nSamplesElapsed;
|
|
s_flRecordingTimestamp_PlatTime = flNow;
|
|
}
|
|
|
|
// Which "section" are we in? A section is basically a segment of non-silence data that we might want to transmit.
|
|
static uint8 s_nRecordingSection;
|
|
|
|
/// Byte offset of compressed data, within the current section. As per TCP-style sequence numbering conventions,
|
|
/// this matches the most recent sequence number we sent.
|
|
static uint32 s_nRecordingSectionCompressedByteOffset;
|
|
|
|
// Called when we know that we are currently in silence, or at the beginning or end
|
|
// of a non-silence section
|
|
static void VoiceRecord_MarkSectionBoundary()
|
|
{
|
|
// We allow this function to be called redundantly.
|
|
// Don't advance the section number unless we really need to
|
|
if ( s_nRecordingSectionCompressedByteOffset > 0 || s_nRecordingSection == 0 )
|
|
{
|
|
++s_nRecordingSection;
|
|
if ( s_nRecordingSection == 0 ) // never use section 0
|
|
s_nRecordingSection = 1;
|
|
}
|
|
|
|
// Always reset byte offset
|
|
s_nRecordingSectionCompressedByteOffset = 0;
|
|
|
|
// Reset encoder state for the next real section with data, whenever that may be
|
|
if ( g_pEncodeCodec )
|
|
g_pEncodeCodec->ResetState();
|
|
}
|
|
|
|
static bool VoiceRecord_Start()
|
|
{
|
|
// Update timestamp, so we can properly account for silence
|
|
VoiceRecord_ForceAdvanceSampleOffsetUsingPlatTime();
|
|
VoiceRecord_MarkSectionBoundary();
|
|
|
|
if ( voice_record_steam.GetBool() && steamapicontext && steamapicontext->SteamUser() )
|
|
{
|
|
steamapicontext->SteamUser()->StartVoiceRecording();
|
|
return true;
|
|
}
|
|
else if ( g_pVoiceRecord )
|
|
{
|
|
return g_pVoiceRecord->RecordStart();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void VoiceRecord_Stop()
|
|
{
|
|
// Update timestamp, so we can properly account for silence
|
|
VoiceRecord_ForceAdvanceSampleOffsetUsingPlatTime();
|
|
VoiceRecord_MarkSectionBoundary();
|
|
|
|
if ( voice_record_steam.GetBool() && steamapicontext && steamapicontext->SteamUser() )
|
|
{
|
|
steamapicontext->SteamUser()->StopVoiceRecording();
|
|
}
|
|
else if ( g_pVoiceRecord )
|
|
{
|
|
return g_pVoiceRecord->RecordStop();
|
|
}
|
|
}
|
|
|
|
// Hacked functions to create the inputs and codecs..
|
|
#ifdef _PS3
|
|
static IVoiceRecord* CreateVoiceRecord_DSound(int nSamplesPerSec) { return NULL; }
|
|
#else
|
|
extern IVoiceRecord* CreateVoiceRecord_DSound(int nSamplesPerSec);
|
|
#endif
|
|
|
|
ConVar voice_gain_rate( "voice_gain_rate", "1.0", FCVAR_NONE );
|
|
ConVar voice_gain_downward_multiplier( "voice_gain_downward_multiplier", "100.0", FCVAR_NONE ); // how quickly it will lower gain when it detects that the current gain value will cause clipping
|
|
ConVar voice_gain_target( "voice_gain_target", "32000", FCVAR_NONE );
|
|
ConVar voice_gain_max( "voice_gain_max", "35", FCVAR_NONE );
|
|
|
|
class CGainManager
|
|
{
|
|
public:
|
|
|
|
CGainManager( void );
|
|
|
|
void Apply( short *pBuffer, int buffer_size, bool bCaster );
|
|
|
|
private:
|
|
|
|
double m_fTargetGain;
|
|
double m_fCurrentGain;
|
|
};
|
|
|
|
CGainManager::CGainManager( void )
|
|
{
|
|
m_fTargetGain = 1.0f;
|
|
m_fCurrentGain = 1.0f;
|
|
}
|
|
|
|
void CGainManager::Apply( short *pSamples, int nSamples, bool bCaster )
|
|
{
|
|
if ( nSamples == 0 )
|
|
return;
|
|
|
|
// Scan for peak
|
|
int iPeak = 0;
|
|
for ( int i = 0; i < nSamples; i++ )
|
|
{
|
|
int iSample = abs( pSamples[i] );
|
|
iPeak = Max( iPeak, iSample );
|
|
}
|
|
|
|
if ( bCaster )
|
|
{
|
|
m_fTargetGain = ( voice_gain_target.GetFloat() * Clamp( voice_caster_scale.GetFloat(), 0.0f, 2.0f ) ) / (float)iPeak;
|
|
}
|
|
else
|
|
{
|
|
m_fTargetGain = ( voice_gain_target.GetFloat() * Clamp( voice_scale.GetFloat(), 0.0f, 2.0f ) ) / (float)iPeak;
|
|
}
|
|
|
|
double fMovementRate = voice_gain_rate.GetFloat();
|
|
double fMaxGain = voice_gain_max.GetFloat();
|
|
|
|
for ( int i = 0; i < nSamples; i++ )
|
|
{
|
|
int nSample = int( float( pSamples[i] ) * m_fCurrentGain );
|
|
pSamples[i] = (short)Clamp( nSample, -32768, 32767 );
|
|
|
|
// Adjust downward very very quickly to prevent clipping
|
|
m_fCurrentGain += ( m_fTargetGain - m_fCurrentGain ) * fMovementRate * 0.0001 * ( ( m_fTargetGain < m_fCurrentGain ) ? voice_gain_downward_multiplier.GetFloat() : 1.0f );
|
|
m_fCurrentGain = Clamp( m_fCurrentGain, 0.0, fMaxGain );
|
|
}
|
|
|
|
//Msg( "Peak: %d, Current Gain: %2.2f, TargetGain: %2.2f\n", iPeak, (float)m_fCurrentGain, (float)m_fTargetGain );
|
|
}
|
|
|
|
|
|
//
|
|
// Used for storing incoming voice data from an entity.
|
|
//
|
|
class CVoiceChannel
|
|
{
|
|
public:
|
|
CVoiceChannel();
|
|
|
|
// Called when someone speaks and a new voice channel is setup to hold the data.
|
|
void Init( int nEntity, float timePadding, bool bCaster = false );
|
|
|
|
public:
|
|
int m_iEntity; // Number of the entity speaking on this channel (index into cl_entities).
|
|
// This is -1 when the channel is unused.
|
|
|
|
|
|
CSizedCircularBuffer
|
|
<VOICE_RECEIVE_BUFFER_SIZE> m_Buffer; // Circular buffer containing the voice data.
|
|
|
|
bool m_bStarved; // Set to true when the channel runs out of data for the mixer.
|
|
// The channel is killed at that point.
|
|
bool m_bFirstPacket; // Have we received any packets yet?
|
|
|
|
float m_TimePad; // Set to TIME_PADDING when the first voice packet comes in from a sender.
|
|
// We add time padding (for frametime differences)
|
|
// by waiting a certain amount of time before starting to output the sound.
|
|
double m_flTimeFirstPacket;
|
|
double m_flTimeExpectedStart;
|
|
|
|
int m_nMinDesiredLeadSamples; // Healthy amount of buffering. This is simply the time padding value passed to init, times the expected sample rate
|
|
int m_nMaxDesiredLeadSamples; // Excessive amount of buffering. Too much more and we risk overflowing the buffer.
|
|
|
|
IVoiceCodec *m_pVoiceCodec; // Each channel gets is own IVoiceCodec instance so the codec can maintain state.
|
|
|
|
CGainManager m_GainManager;
|
|
|
|
CVoiceChannel *m_pNext;
|
|
|
|
bool m_bProximity;
|
|
int m_nViewEntityIndex;
|
|
int m_nSoundGuid;
|
|
|
|
uint8 m_nCurrentSection;
|
|
uint32 m_nExpectedCompressedByteOffset;
|
|
uint32 m_nExpectedUncompressedSampleOffset;
|
|
short m_nLastSample;
|
|
|
|
bool m_bCaster;
|
|
};
|
|
|
|
|
|
CVoiceChannel::CVoiceChannel()
|
|
{
|
|
m_iEntity = -1;
|
|
m_pVoiceCodec = NULL;
|
|
m_nViewEntityIndex = -1;
|
|
m_nSoundGuid = -1;
|
|
m_bCaster = false;
|
|
}
|
|
|
|
void CVoiceChannel::Init( int nEntity, float timePadding, bool bCaster )
|
|
{
|
|
m_iEntity = nEntity;
|
|
m_bStarved = false;
|
|
m_bFirstPacket = true;
|
|
m_nLastSample = 0;
|
|
m_Buffer.Flush();
|
|
m_bCaster = bCaster;
|
|
m_TimePad = timePadding;
|
|
if ( m_TimePad <= 0.0f )
|
|
{
|
|
m_TimePad = FLT_EPSILON; // Must have at least one update
|
|
}
|
|
|
|
// Don't aim to fill the buffer up too full, or we will overflow.
|
|
// this buffer class does not grow, so we really don't ever want
|
|
// it to get full.
|
|
const float kflMaxTimePad = VOICE_RECEIVE_BUFFER_SECONDS * 0.8f;
|
|
if ( m_TimePad > kflMaxTimePad )
|
|
{
|
|
Assert( m_TimePad < kflMaxTimePad );
|
|
m_TimePad = kflMaxTimePad;
|
|
}
|
|
|
|
if ( g_bIsSpeex )
|
|
{
|
|
m_nMaxDesiredLeadSamples = int( kflMaxTimePad * VOICE_OUTPUT_SAMPLE_RATE_SPEEX );
|
|
m_nMinDesiredLeadSamples = Max( 256, int( m_TimePad * VOICE_OUTPUT_SAMPLE_RATE_SPEEX ) );
|
|
}
|
|
else
|
|
{
|
|
m_nMaxDesiredLeadSamples = int( kflMaxTimePad * VOICE_OUTPUT_SAMPLE_RATE );
|
|
m_nMinDesiredLeadSamples = Max( 256, int( m_TimePad * VOICE_OUTPUT_SAMPLE_RATE ) );
|
|
}
|
|
|
|
m_flTimeFirstPacket = Plat_FloatTime();
|
|
m_flTimeExpectedStart = m_flTimeFirstPacket + m_TimePad;
|
|
|
|
m_nCurrentSection = 0;
|
|
m_nExpectedCompressedByteOffset = 0;
|
|
m_nExpectedUncompressedSampleOffset = 0;
|
|
|
|
if ( m_pVoiceCodec )
|
|
m_pVoiceCodec->ResetState();
|
|
}
|
|
|
|
|
|
|
|
// Incoming voice channels.
|
|
CVoiceChannel g_VoiceChannels[VOICE_NUM_CHANNELS];
|
|
|
|
|
|
// These are used for recording the wave data into files for debugging.
|
|
#define MAX_WAVEFILEDATA_LEN 1024*1024
|
|
char *g_pUncompressedFileData = NULL;
|
|
int g_nUncompressedDataBytes = 0;
|
|
const char *g_pUncompressedDataFilename = NULL;
|
|
|
|
char *g_pDecompressedFileData = NULL;
|
|
int g_nDecompressedDataBytes = 0;
|
|
const char *g_pDecompressedDataFilename = NULL;
|
|
|
|
char *g_pMicInputFileData = NULL;
|
|
int g_nMicInputFileBytes = 0;
|
|
int g_CurMicInputFileByte = 0;
|
|
double g_MicStartTime;
|
|
|
|
static ConVar voice_writevoices( "voice_writevoices", "0", 0, "Saves each speaker's voice data into separate .wav files\n" );
|
|
class CVoiceWriterData
|
|
{
|
|
public:
|
|
CVoiceWriterData() :
|
|
m_pChannel( NULL ),
|
|
m_nCount( 0 ),
|
|
m_Buffer()
|
|
{
|
|
}
|
|
|
|
CVoiceWriterData( const CVoiceWriterData &src )
|
|
{
|
|
m_pChannel = src.m_pChannel;
|
|
m_nCount = src.m_nCount;
|
|
m_Buffer.Clear();
|
|
m_Buffer.Put( src.m_Buffer.Base(), src.m_Buffer.TellPut() );
|
|
}
|
|
|
|
static bool Less( const CVoiceWriterData &lhs, const CVoiceWriterData &rhs )
|
|
{
|
|
return lhs.m_pChannel < rhs.m_pChannel;
|
|
}
|
|
|
|
CVoiceChannel *m_pChannel;
|
|
int m_nCount;
|
|
CUtlBuffer m_Buffer;
|
|
};
|
|
|
|
class CVoiceWriter
|
|
{
|
|
public:
|
|
CVoiceWriter() :
|
|
m_VoiceWriter( 0, 0, CVoiceWriterData::Less )
|
|
{
|
|
}
|
|
|
|
void Flush()
|
|
{
|
|
for ( int i = m_VoiceWriter.FirstInorder(); i != m_VoiceWriter.InvalidIndex(); i = m_VoiceWriter.NextInorder( i ) )
|
|
{
|
|
CVoiceWriterData *data = &m_VoiceWriter[ i ];
|
|
|
|
if ( data->m_Buffer.TellPut() <= 0 )
|
|
continue;
|
|
data->m_Buffer.Purge();
|
|
}
|
|
}
|
|
|
|
void Finish()
|
|
{
|
|
if ( !g_pSoundServices->IsConnected() )
|
|
{
|
|
Flush();
|
|
return;
|
|
}
|
|
|
|
for ( int i = m_VoiceWriter.FirstInorder(); i != m_VoiceWriter.InvalidIndex(); i = m_VoiceWriter.NextInorder( i ) )
|
|
{
|
|
CVoiceWriterData *data = &m_VoiceWriter[ i ];
|
|
|
|
if ( data->m_Buffer.TellPut() <= 0 )
|
|
continue;
|
|
|
|
int index = data->m_pChannel - g_VoiceChannels;
|
|
Assert( index >= 0 && index < ARRAYSIZE( g_VoiceChannels ) );
|
|
|
|
char path[ MAX_PATH ];
|
|
Q_snprintf( path, sizeof( path ), "%s/voice", g_pSoundServices->GetGameDir() );
|
|
g_pFileSystem->CreateDirHierarchy( path );
|
|
|
|
char fn[ MAX_PATH ];
|
|
Q_snprintf( fn, sizeof( fn ), "%s/pl%02d_slot%d-time%d.wav", path, index, data->m_nCount, (int)g_pSoundServices->GetClientTime() );
|
|
|
|
WriteWaveFile( fn, (const char *)data->m_Buffer.Base(), data->m_Buffer.TellPut(), g_VoiceSampleFormat.wBitsPerSample, g_VoiceSampleFormat.nChannels, Voice_SamplesPerSec() );
|
|
|
|
Msg( "Writing file %s\n", fn );
|
|
|
|
++data->m_nCount;
|
|
data->m_Buffer.Purge();
|
|
}
|
|
}
|
|
|
|
|
|
void AddDecompressedData( CVoiceChannel *ch, const byte *data, size_t datalen )
|
|
{
|
|
if ( !voice_writevoices.GetBool() )
|
|
return;
|
|
|
|
CVoiceWriterData search;
|
|
search.m_pChannel = ch;
|
|
int idx = m_VoiceWriter.Find( search );
|
|
if ( idx == m_VoiceWriter.InvalidIndex() )
|
|
{
|
|
idx = m_VoiceWriter.Insert( search );
|
|
}
|
|
|
|
CVoiceWriterData *slot = &m_VoiceWriter[ idx ];
|
|
slot->m_Buffer.Put( data, datalen );
|
|
}
|
|
private:
|
|
|
|
CUtlRBTree< CVoiceWriterData > m_VoiceWriter;
|
|
};
|
|
|
|
static CVoiceWriter g_VoiceWriter;
|
|
|
|
inline void ApplyFadeToSamples(short *pSamples, int nSamples, int fadeOffset, float fadeMul)
|
|
{
|
|
for(int i=0; i < nSamples; i++)
|
|
{
|
|
float percent = (i+fadeOffset) * fadeMul;
|
|
pSamples[i] = (short)(pSamples[i] * (1 - percent));
|
|
}
|
|
}
|
|
|
|
|
|
bool Voice_Enabled( void )
|
|
{
|
|
return voice_enable.GetBool();
|
|
}
|
|
|
|
bool Voice_CasterEnabled( uint32 uCasterAccountID )
|
|
{
|
|
return ( uCasterAccountID == ( uint32 )( voice_caster_enable.GetInt() ) );
|
|
}
|
|
|
|
void Voice_SetCaster( uint32 uCasterAccountID )
|
|
{
|
|
voice_caster_enable.SetValue( ( int )( uCasterAccountID ) );
|
|
}
|
|
|
|
bool Voice_SystemEnabled( void )
|
|
{
|
|
return voice_system_enable.GetBool();
|
|
}
|
|
|
|
ConVar voice_buffer_debug( "voice_buffer_debug", "0" );
|
|
|
|
int Voice_GetOutputData(
|
|
const int iChannel, //! The voice channel it wants samples from.
|
|
char *copyBufBytes, //! The buffer to copy the samples into.
|
|
const int copyBufSize, //! Maximum size of copyBuf.
|
|
const int samplePosition, //! Which sample to start at.
|
|
const int sampleCount //! How many samples to get.
|
|
)
|
|
{
|
|
CVoiceChannel *pChannel = &g_VoiceChannels[iChannel];
|
|
short *pCopyBuf = (short*)copyBufBytes;
|
|
|
|
|
|
int maxOutSamples = copyBufSize / BYTES_PER_SAMPLE;
|
|
|
|
// Find out how much we want and get it from the received data channel.
|
|
CCircularBuffer *pBuffer = &pChannel->m_Buffer;
|
|
int nReadAvail = pBuffer->GetReadAvailable();
|
|
int nBytesToRead = MIN(MIN(nReadAvail, (int)maxOutSamples), sampleCount * BYTES_PER_SAMPLE);
|
|
int nSamplesGotten = pBuffer->Read(pCopyBuf, nBytesToRead) / BYTES_PER_SAMPLE;
|
|
|
|
if ( voice_buffer_debug.GetBool() )
|
|
{
|
|
Msg( "%.2f: Voice_GetOutputData channel %d avail %d bytes, tried %d bytes, got %d samples\n", Plat_FloatTime(), iChannel, nReadAvail, nBytesToRead, nSamplesGotten );
|
|
}
|
|
|
|
// Are we at the end of the buffer's data? If so, fade data to silence so it doesn't clip.
|
|
int readSamplesAvail = pBuffer->GetReadAvailable() / BYTES_PER_SAMPLE;
|
|
if(readSamplesAvail < g_nVoiceFadeSamples)
|
|
{
|
|
if ( voice_buffer_debug.GetBool() )
|
|
{
|
|
Msg( "%.2f: Voice_GetOutputData channel %d applying fade\n", Plat_FloatTime(), iChannel );
|
|
}
|
|
|
|
int bufferFadeOffset = MAX((readSamplesAvail + nSamplesGotten) - g_nVoiceFadeSamples, 0);
|
|
int globalFadeOffset = MAX(g_nVoiceFadeSamples - (readSamplesAvail + nSamplesGotten), 0);
|
|
|
|
ApplyFadeToSamples(
|
|
&pCopyBuf[bufferFadeOffset],
|
|
nSamplesGotten - bufferFadeOffset,
|
|
globalFadeOffset,
|
|
g_VoiceFadeMul);
|
|
}
|
|
|
|
// If there weren't enough samples in the received data channel,
|
|
// pad it with a copy of the most recent data, and if there
|
|
// isn't any, then use zeros.
|
|
if ( nSamplesGotten < sampleCount )
|
|
{
|
|
if ( voice_buffer_debug.GetBool() )
|
|
{
|
|
Msg( "%.2f: Voice_GetOutputData channel %d padding!\n", Plat_FloatTime(), iChannel );
|
|
}
|
|
|
|
int wantedSampleCount = MIN( sampleCount, maxOutSamples );
|
|
int nAdditionalNeeded = (wantedSampleCount - nSamplesGotten);
|
|
if ( nSamplesGotten > 0 )
|
|
{
|
|
short *dest = (short *)&pCopyBuf[ nSamplesGotten ];
|
|
int nSamplesToDuplicate = MIN( nSamplesGotten, nAdditionalNeeded );
|
|
const short *src = (short *)&pCopyBuf[ nSamplesGotten - nSamplesToDuplicate ];
|
|
|
|
Q_memcpy( dest, src, nSamplesToDuplicate * BYTES_PER_SAMPLE );
|
|
|
|
if ( voice_buffer_debug.GetBool() )
|
|
{
|
|
Msg( "duplicating %d samples\n", nSamplesToDuplicate );
|
|
}
|
|
|
|
nAdditionalNeeded -= nSamplesToDuplicate;
|
|
if ( nAdditionalNeeded > 0 )
|
|
{
|
|
dest = (short *)&pCopyBuf[ nSamplesGotten + nSamplesToDuplicate ];
|
|
Q_memset(dest, 0, nAdditionalNeeded * BYTES_PER_SAMPLE);
|
|
|
|
if ( voice_buffer_debug.GetBool() )
|
|
{
|
|
Msg( "zeroing %d samples\n", nAdditionalNeeded );
|
|
}
|
|
|
|
Assert( ( nAdditionalNeeded + nSamplesGotten + nSamplesToDuplicate ) == wantedSampleCount );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Q_memset( &pCopyBuf[ nSamplesGotten ], 0, nAdditionalNeeded * BYTES_PER_SAMPLE );
|
|
|
|
if ( voice_buffer_debug.GetBool() )
|
|
{
|
|
Msg( "no buffer data, zeroing all %d samples\n", nAdditionalNeeded );
|
|
}
|
|
|
|
}
|
|
nSamplesGotten = wantedSampleCount;
|
|
}
|
|
|
|
// If the buffer is out of data, mark this channel to go away.
|
|
if(pBuffer->GetReadAvailable() == 0)
|
|
{
|
|
if ( voice_buffer_debug.GetBool() )
|
|
{
|
|
Msg( "%.2f: Voice_GetOutputData channel %d starved!\n", Plat_FloatTime(), iChannel );
|
|
}
|
|
pChannel->m_bStarved = true;
|
|
}
|
|
|
|
if(voice_showchannels.GetInt() >= 2)
|
|
{
|
|
Msg("Voice - mixed %d samples from channel %d\n", nSamplesGotten, iChannel);
|
|
}
|
|
|
|
VoiceSE_MoveMouth(pChannel->m_iEntity, (short*)copyBufBytes, nSamplesGotten);
|
|
return nSamplesGotten;
|
|
}
|
|
|
|
|
|
void Voice_OnAudioSourceShutdown( int iChannel )
|
|
{
|
|
Voice_EndChannel( iChannel );
|
|
}
|
|
|
|
|
|
// ------------------------------------------------------------------------ //
|
|
// Internal stuff.
|
|
// ------------------------------------------------------------------------ //
|
|
|
|
CVoiceChannel* GetVoiceChannel(int iChannel, bool bAssert=true)
|
|
{
|
|
if(iChannel < 0 || iChannel >= VOICE_NUM_CHANNELS)
|
|
{
|
|
if(bAssert)
|
|
{
|
|
Assert(false);
|
|
}
|
|
return NULL;
|
|
}
|
|
else
|
|
{
|
|
return &g_VoiceChannels[iChannel];
|
|
}
|
|
}
|
|
|
|
//char g_pszCurrentVoiceCodec[256];
|
|
//int g_iCurrentVoiceVersion = -1;
|
|
|
|
bool Voice_Init(const char *pCodecName, int iVersion )
|
|
{
|
|
if ( voice_system_enable.GetInt() == 0 )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// if ( V_strncmp( g_pszCurrentVoiceCodec, pCodecName, sizeof( g_pszCurrentVoiceCodec ) ) == 0 && g_iCurrentVoiceVersion == iVersion )
|
|
// return true;
|
|
|
|
EngineVGui()->UpdateProgressBar( PROGRESS_DEFAULT );
|
|
|
|
Voice_Deinit();
|
|
|
|
g_bVoiceAtLeastPartiallyInitted = true;
|
|
|
|
if(!VoiceSE_Init())
|
|
return false;
|
|
|
|
if ( V_strcmp( pCodecName, "vaudio_speex" ) == 0 )
|
|
{
|
|
g_bIsSpeex = true;
|
|
g_VoiceSampleFormat.nSamplesPerSec = VOICE_OUTPUT_SAMPLE_RATE_SPEEX;
|
|
g_VoiceSampleFormat.nAvgBytesPerSec = VOICE_OUTPUT_SAMPLE_RATE_SPEEX * 2;
|
|
}
|
|
else
|
|
{
|
|
g_bIsSpeex = false;
|
|
g_VoiceSampleFormat.nSamplesPerSec = VOICE_OUTPUT_SAMPLE_RATE;
|
|
g_VoiceSampleFormat.nAvgBytesPerSec = VOICE_OUTPUT_SAMPLE_RATE * 2;
|
|
}
|
|
|
|
EngineVGui()->UpdateProgressBar( PROGRESS_DEFAULT );
|
|
|
|
#ifdef OSX
|
|
IVoiceRecord* CreateVoiceRecord_AudioQueue(int sampleRate);
|
|
g_pVoiceRecord = CreateVoiceRecord_AudioQueue( Voice_SamplesPerSec() );
|
|
//g_pVoiceRecord = NULL;
|
|
if ( !g_pVoiceRecord )
|
|
#endif
|
|
// Get the voice input device.
|
|
g_pVoiceRecord = CreateVoiceRecord_DSound( Voice_SamplesPerSec() );
|
|
if( !g_pVoiceRecord )
|
|
{
|
|
Msg( "Unable to initialize DirectSoundCapture. You won't be able to speak to other players." );
|
|
}
|
|
|
|
if ( steamapicontext == NULL )
|
|
{
|
|
steamapicontext = &g_SteamAPIContext;
|
|
steamapicontext->Init();
|
|
}
|
|
|
|
EngineVGui()->UpdateProgressBar( PROGRESS_DEFAULT );
|
|
|
|
// Get the codec.
|
|
CreateInterfaceFn createCodecFn;
|
|
//
|
|
// We must explicitly check codec DLL strings against valid codecs
|
|
// to avoid remote code execution by loading a module supplied in server string
|
|
// See security issue disclosed 12-Jan-2016
|
|
//
|
|
if ( !V_strcmp( pCodecName, "vaudio_celt" )
|
|
|| !V_strcmp( pCodecName, "vaudio_speex" )
|
|
|| !V_strcmp( pCodecName, "vaudio_miles" ) )
|
|
{
|
|
g_hVoiceCodecDLL = FileSystem_LoadModule( pCodecName );
|
|
}
|
|
else
|
|
{
|
|
g_hVoiceCodecDLL = NULL;
|
|
}
|
|
|
|
EngineVGui()->UpdateProgressBar( PROGRESS_DEFAULT );
|
|
|
|
if ( !g_hVoiceCodecDLL || (createCodecFn = Sys_GetFactory(g_hVoiceCodecDLL)) == NULL ||
|
|
(g_pEncodeCodec = (IVoiceCodec*)createCodecFn(pCodecName, NULL)) == NULL || !g_pEncodeCodec->Init( iVersion ) )
|
|
{
|
|
Msg("Unable to load voice codec '%s'. Voice disabled.\n", pCodecName);
|
|
Voice_Deinit();
|
|
return false;
|
|
}
|
|
|
|
for(int i=0; i < VOICE_NUM_CHANNELS; i++)
|
|
{
|
|
CVoiceChannel *pChannel = &g_VoiceChannels[i];
|
|
|
|
EngineVGui()->UpdateProgressBar( PROGRESS_DEFAULT );
|
|
|
|
if((pChannel->m_pVoiceCodec = (IVoiceCodec*)createCodecFn(pCodecName, NULL)) == NULL || !pChannel->m_pVoiceCodec->Init( iVersion ))
|
|
{
|
|
Voice_Deinit();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
EngineVGui()->UpdateProgressBar( PROGRESS_DEFAULT );
|
|
|
|
InitMixerControls();
|
|
|
|
if( voice_forcemicrecord.GetInt() )
|
|
{
|
|
if( g_pMixerControls )
|
|
g_pMixerControls->SelectMicrophoneForWaveInput();
|
|
}
|
|
|
|
// V_strncpy( g_pszCurrentVoiceCodec, pCodecName, sizeof( g_pszCurrentVoiceCodec ) );
|
|
// g_iCurrentVoiceVersion = iVersion;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void Voice_EndChannel(int iChannel)
|
|
{
|
|
Assert(iChannel >= 0 && iChannel < VOICE_NUM_CHANNELS);
|
|
|
|
CVoiceChannel *pChannel = &g_VoiceChannels[iChannel];
|
|
|
|
if ( pChannel->m_iEntity != -1 )
|
|
{
|
|
int iEnt = pChannel->m_iEntity;
|
|
pChannel->m_iEntity = -1;
|
|
|
|
if ( pChannel->m_bProximity == true )
|
|
{
|
|
VoiceSE_EndChannel( iChannel, iEnt );
|
|
}
|
|
else
|
|
{
|
|
VoiceSE_EndChannel( iChannel, pChannel->m_nViewEntityIndex );
|
|
}
|
|
|
|
g_pSoundServices->OnChangeVoiceStatus( iEnt, -1, false );
|
|
VoiceSE_CloseMouth( iEnt );
|
|
|
|
pChannel->m_nViewEntityIndex = -1;
|
|
pChannel->m_nSoundGuid = -1;
|
|
|
|
// If the tweak mode channel is ending
|
|
}
|
|
}
|
|
|
|
|
|
void Voice_EndAllChannels()
|
|
{
|
|
for(int i=0; i < VOICE_NUM_CHANNELS; i++)
|
|
{
|
|
Voice_EndChannel(i);
|
|
}
|
|
}
|
|
|
|
bool EngineTool_SuppressDeInit();
|
|
|
|
void Voice_Deinit()
|
|
{
|
|
// This call tends to be expensive and when voice is not enabled it will continually
|
|
// call in here, so avoid the work if possible.
|
|
if( !g_bVoiceAtLeastPartiallyInitted )
|
|
return;
|
|
|
|
if ( EngineTool_SuppressDeInit() )
|
|
return;
|
|
|
|
// g_pszCurrentVoiceCodec[0] = 0;
|
|
// g_iCurrentVoiceVersion = -1;
|
|
|
|
Voice_EndAllChannels();
|
|
|
|
Voice_RecordStop();
|
|
|
|
for(int i=0; i < VOICE_NUM_CHANNELS; i++)
|
|
{
|
|
CVoiceChannel *pChannel = &g_VoiceChannels[i];
|
|
|
|
if(pChannel->m_pVoiceCodec)
|
|
{
|
|
pChannel->m_pVoiceCodec->Release();
|
|
pChannel->m_pVoiceCodec = NULL;
|
|
}
|
|
}
|
|
|
|
if(g_pEncodeCodec)
|
|
{
|
|
g_pEncodeCodec->Release();
|
|
g_pEncodeCodec = NULL;
|
|
}
|
|
|
|
if(g_hVoiceCodecDLL)
|
|
{
|
|
FileSystem_UnloadModule(g_hVoiceCodecDLL);
|
|
g_hVoiceCodecDLL = NULL;
|
|
}
|
|
|
|
if(g_pVoiceRecord)
|
|
{
|
|
g_pVoiceRecord->Release();
|
|
g_pVoiceRecord = NULL;
|
|
}
|
|
|
|
VoiceSE_Term();
|
|
|
|
g_bVoiceAtLeastPartiallyInitted = false;
|
|
}
|
|
|
|
bool Voice_GetLoopback()
|
|
{
|
|
return !!voice_loopback.GetInt();
|
|
}
|
|
|
|
|
|
void Voice_LocalPlayerTalkingAck( int iSsSlot )
|
|
{
|
|
iSsSlot = clamp( iSsSlot, 0, MAX_SPLITSCREEN_CLIENTS - 1 );
|
|
|
|
if( !g_bLocalPlayerTalkingAck[ iSsSlot ] )
|
|
{
|
|
// Tell the client DLL when this changes.
|
|
g_pSoundServices->OnChangeVoiceStatus( -2, iSsSlot, TRUE );
|
|
}
|
|
|
|
g_bLocalPlayerTalkingAck[ iSsSlot ] = true;
|
|
g_LocalPlayerTalkingTimeout[ iSsSlot ] = 0;
|
|
}
|
|
|
|
|
|
void Voice_UpdateVoiceTweakMode()
|
|
{
|
|
// Tweak mode just pulls data from the voice stream, and does nothing with it
|
|
if(!g_bInTweakMode || !g_pVoiceRecord)
|
|
return;
|
|
|
|
char uchVoiceData[16384];
|
|
bool bFinal = false;
|
|
VoiceFormat_t format;
|
|
Voice_GetCompressedData(uchVoiceData, sizeof(uchVoiceData), bFinal, &format );
|
|
}
|
|
|
|
|
|
bool Voice_Idle(float frametime)
|
|
{
|
|
if( voice_system_enable.GetInt() == 0 )
|
|
{
|
|
Voice_Deinit();
|
|
return false;
|
|
}
|
|
|
|
float fTimeDiff = Plat_FloatTime() - g_fLocalPlayerTalkingLastUpdateRealTime;
|
|
|
|
if ( fTimeDiff < frametime )
|
|
{
|
|
// Not enough time has passed... don't update
|
|
return false;
|
|
}
|
|
|
|
// Set how much time has passed since the last update
|
|
frametime = MIN( fTimeDiff, frametime * 2.0f ); // Cap how much time can pass at 2 tick sizes
|
|
|
|
// Remember when we last updated
|
|
g_fLocalPlayerTalkingLastUpdateRealTime = Plat_FloatTime();
|
|
|
|
for ( int k = 0; k < MAX_SPLITSCREEN_CLIENTS; ++ k )
|
|
{
|
|
if(g_bLocalPlayerTalkingAck[k])
|
|
{
|
|
g_LocalPlayerTalkingTimeout[k] += frametime;
|
|
if(g_LocalPlayerTalkingTimeout[k] > LOCALPLAYERTALKING_TIMEOUT)
|
|
{
|
|
g_bLocalPlayerTalkingAck[k] = false;
|
|
|
|
// Tell the client DLL.
|
|
g_pSoundServices->OnChangeVoiceStatus(-2, k, FALSE);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Precalculate these to speedup the voice fadeout.
|
|
g_nVoiceFadeSamples = MAX((int)(voice_fadeouttime.GetFloat() * ( g_bIsSpeex ? VOICE_OUTPUT_SAMPLE_RATE_SPEEX : VOICE_OUTPUT_SAMPLE_RATE ) ), 2);
|
|
g_VoiceFadeMul = 1.0f / (g_nVoiceFadeSamples - 1);
|
|
|
|
if(g_pVoiceRecord)
|
|
g_pVoiceRecord->Idle();
|
|
|
|
// If we're in voice tweak mode, feed our own data back to us.
|
|
Voice_UpdateVoiceTweakMode();
|
|
|
|
// Age the channels.
|
|
int nActive = 0;
|
|
for(int i=0; i < VOICE_NUM_CHANNELS; i++)
|
|
{
|
|
CVoiceChannel *pChannel = &g_VoiceChannels[i];
|
|
|
|
if(pChannel->m_iEntity != -1)
|
|
{
|
|
if(pChannel->m_bStarved)
|
|
{
|
|
// Kill the channel. It's done playing.
|
|
Voice_EndChannel(i);
|
|
pChannel->m_nSoundGuid = -1;
|
|
}
|
|
else if ( pChannel->m_nSoundGuid < 0 )
|
|
{
|
|
|
|
// Sound is not currently playing. Should it be?
|
|
// Start it if enough time has elapsed, or if we have
|
|
// enough buffered.
|
|
pChannel->m_TimePad -= frametime;
|
|
int nDesiredLeadBytes = pChannel->m_nMinDesiredLeadSamples*BYTES_PER_SAMPLE;
|
|
if( pChannel->m_TimePad <= 0 || pChannel->m_Buffer.GetReadAvailable() >= nDesiredLeadBytes )
|
|
{
|
|
|
|
double flNow = Plat_FloatTime();
|
|
float flEpasedSinceFirstPacket = flNow - pChannel->m_flTimeFirstPacket;
|
|
if ( voice_showincoming.GetBool() )
|
|
{
|
|
Msg( "%.2f: Starting channel %d. %d bytes buffered, %.0fms elapsed. (%d samples more than desired, %.0fms later than expected)\n",
|
|
flNow, i,
|
|
pChannel->m_Buffer.GetReadAvailable(), flEpasedSinceFirstPacket * 1000.0f,
|
|
pChannel->m_Buffer.GetReadAvailable() - nDesiredLeadBytes, ( flNow - pChannel->m_flTimeExpectedStart ) * 1000.0f );
|
|
}
|
|
|
|
// Start its audio.
|
|
FORCE_DEFAULT_SPLITSCREEN_PLAYER_GUARD;
|
|
|
|
pChannel->m_nViewEntityIndex = g_pSoundServices->GetViewEntity( 0 );
|
|
pChannel->m_nSoundGuid = VoiceSE_StartChannel( i, pChannel->m_iEntity, pChannel->m_bProximity, pChannel->m_nViewEntityIndex );
|
|
if ( pChannel->m_nSoundGuid <= 0 )
|
|
{
|
|
// couldn't allocate a sound channel for this voice data
|
|
Voice_EndChannel(i);
|
|
pChannel->m_nSoundGuid = -1;
|
|
}
|
|
else
|
|
{
|
|
g_pSoundServices->OnChangeVoiceStatus( pChannel->m_iEntity, -1, true );
|
|
|
|
VoiceSE_InitMouth(pChannel->m_iEntity);
|
|
}
|
|
}
|
|
|
|
++nActive;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(nActive == 0)
|
|
VoiceSE_EndOverdrive();
|
|
|
|
VoiceSE_Idle(frametime);
|
|
|
|
// voice_showchannels.
|
|
if( voice_showchannels.GetInt() >= 1 )
|
|
{
|
|
for(int i=0; i < VOICE_NUM_CHANNELS; i++)
|
|
{
|
|
CVoiceChannel *pChannel = &g_VoiceChannels[i];
|
|
|
|
if(pChannel->m_iEntity == -1)
|
|
continue;
|
|
|
|
Msg("Voice - chan %d, ent %d, bufsize: %d\n", i, pChannel->m_iEntity, pChannel->m_Buffer.GetReadAvailable());
|
|
}
|
|
}
|
|
|
|
// Show profiling data?
|
|
if( voice_profile.GetInt() )
|
|
{
|
|
Msg("Voice - compress: %7.2fu, decompress: %7.2fu, gain: %7.2fu, upsample: %7.2fu, total: %7.2fu\n",
|
|
g_CompressTime*1000000.0,
|
|
g_DecompressTime*1000000.0,
|
|
g_GainTime*1000000.0,
|
|
g_UpsampleTime*1000000.0,
|
|
(g_CompressTime+g_DecompressTime+g_GainTime+g_UpsampleTime)*1000000.0
|
|
);
|
|
|
|
g_CompressTime = g_DecompressTime = g_GainTime = g_UpsampleTime = 0;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool Voice_IsRecording()
|
|
{
|
|
return g_bVoiceRecording;
|
|
}
|
|
|
|
|
|
bool Voice_RecordStart(
|
|
const char *pUncompressedFile,
|
|
const char *pDecompressedFile,
|
|
const char *pMicInputFile)
|
|
{
|
|
if(!g_pEncodeCodec)
|
|
return false;
|
|
|
|
g_VoiceWriter.Flush();
|
|
|
|
Voice_RecordStop();
|
|
|
|
if(pMicInputFile)
|
|
{
|
|
int a, b, c;
|
|
ReadWaveFile(pMicInputFile, g_pMicInputFileData, g_nMicInputFileBytes, a, b, c);
|
|
g_CurMicInputFileByte = 0;
|
|
g_MicStartTime = Plat_FloatTime();
|
|
}
|
|
|
|
if(pUncompressedFile)
|
|
{
|
|
g_pUncompressedFileData = new char[MAX_WAVEFILEDATA_LEN];
|
|
g_nUncompressedDataBytes = 0;
|
|
g_pUncompressedDataFilename = pUncompressedFile;
|
|
}
|
|
|
|
if(pDecompressedFile)
|
|
{
|
|
g_pDecompressedFileData = new char[MAX_WAVEFILEDATA_LEN];
|
|
g_nDecompressedDataBytes = 0;
|
|
g_pDecompressedDataFilename = pDecompressedFile;
|
|
}
|
|
|
|
g_bVoiceRecording = false;
|
|
if(g_pVoiceRecord)
|
|
{
|
|
g_bVoiceRecording = VoiceRecord_Start();
|
|
if(g_bVoiceRecording)
|
|
{
|
|
g_pSoundServices->OnChangeVoiceStatus(-1, GET_ACTIVE_SPLITSCREEN_SLOT(), TRUE); // Tell the client DLL.
|
|
}
|
|
}
|
|
|
|
return g_bVoiceRecording;
|
|
}
|
|
|
|
|
|
bool Voice_RecordStop()
|
|
{
|
|
// Write the files out for debugging.
|
|
if(g_pMicInputFileData)
|
|
{
|
|
delete [] g_pMicInputFileData;
|
|
g_pMicInputFileData = NULL;
|
|
}
|
|
|
|
if(g_pUncompressedFileData)
|
|
{
|
|
WriteWaveFile(g_pUncompressedDataFilename, g_pUncompressedFileData, g_nUncompressedDataBytes, g_VoiceSampleFormat.wBitsPerSample, g_VoiceSampleFormat.nChannels, Voice_SamplesPerSec() );
|
|
delete [] g_pUncompressedFileData;
|
|
g_pUncompressedFileData = NULL;
|
|
}
|
|
|
|
if(g_pDecompressedFileData)
|
|
{
|
|
WriteWaveFile(g_pDecompressedDataFilename, g_pDecompressedFileData, g_nDecompressedDataBytes, g_VoiceSampleFormat.wBitsPerSample, g_VoiceSampleFormat.nChannels, Voice_SamplesPerSec() );
|
|
delete [] g_pDecompressedFileData;
|
|
g_pDecompressedFileData = NULL;
|
|
}
|
|
|
|
g_VoiceWriter.Finish();
|
|
|
|
VoiceRecord_Stop();
|
|
|
|
if(g_bVoiceRecording)
|
|
{
|
|
g_pSoundServices->OnChangeVoiceStatus(-1, GET_ACTIVE_SPLITSCREEN_SLOT(), FALSE); // Tell the client DLL.
|
|
}
|
|
|
|
g_bVoiceRecording = false;
|
|
return(true);
|
|
}
|
|
|
|
static float s_flThresholdDecayTime = 0.0f;
|
|
|
|
|
|
int Voice_GetCompressedData(char *pchDest, int nCount, bool bFinal, VoiceFormat_t *pOutFormat, uint8 *pnOutSectionNumber, uint32 *pnOutSectionSequenceNumber, uint32 *pnOutUncompressedSampleOffset )
|
|
{
|
|
double flNow = Plat_FloatTime();
|
|
|
|
// Make sure timestamp is initialized
|
|
VoiceRecord_CheckInitTimestamp();
|
|
|
|
// Here we protect again a weird client usage pattern where they don't call this function for a while.
|
|
// If that happens, advance the timestamp.
|
|
float flSecondsElapsed = flNow - s_flRecordingTimestamp_PlatTime;
|
|
if ( flSecondsElapsed > 2.0 )
|
|
{
|
|
Warning( "Voice_GetCompressedData not called for %.1fms; manually advancing uncompressed sample offset and starting a new section\n", flSecondsElapsed * 1000.0f );
|
|
VoiceRecord_ForceAdvanceSampleOffsetUsingPlatTime();
|
|
VoiceRecord_MarkSectionBoundary();
|
|
}
|
|
s_flRecordingTimestamp_PlatTime = flNow;
|
|
|
|
// Assume failure
|
|
if ( pnOutSectionNumber )
|
|
*pnOutSectionNumber = 0;
|
|
if ( pnOutSectionSequenceNumber )
|
|
*pnOutSectionSequenceNumber = 0;
|
|
if ( pnOutUncompressedSampleOffset )
|
|
*pnOutUncompressedSampleOffset = 0;
|
|
|
|
if ( voice_record_steam.GetBool() && steamapicontext && steamapicontext->SteamUser() )
|
|
{
|
|
uint32 cbCompressedWritten = 0;
|
|
uint32 cbCompressed = 0;
|
|
// uint32 cbUncompressed = 0;
|
|
EVoiceResult result = steamapicontext->SteamUser()->GetAvailableVoice( &cbCompressed, NULL, 0 );
|
|
if ( result == k_EVoiceResultOK )
|
|
{
|
|
result = steamapicontext->SteamUser()->GetVoice( true, pchDest, nCount, &cbCompressedWritten, false, NULL, 0, NULL, 0 );
|
|
|
|
g_pSoundServices->OnChangeVoiceStatus( -3, GET_ACTIVE_SPLITSCREEN_SLOT(), true );
|
|
}
|
|
else
|
|
{
|
|
g_pSoundServices->OnChangeVoiceStatus( -3, GET_ACTIVE_SPLITSCREEN_SLOT(), false );
|
|
}
|
|
if ( pOutFormat )
|
|
{
|
|
*pOutFormat = VoiceFormat_Steam;
|
|
}
|
|
|
|
if ( cbCompressedWritten > 0 )
|
|
{
|
|
s_nRecordingSectionCompressedByteOffset += cbCompressedWritten;
|
|
|
|
if ( pnOutSectionNumber )
|
|
*pnOutSectionNumber = s_nRecordingSection;
|
|
if ( pnOutSectionSequenceNumber )
|
|
*pnOutSectionSequenceNumber = s_nRecordingSectionCompressedByteOffset;
|
|
|
|
// !FIXME! Uncompressed sample offset doesn't work right now with the Steam codec.
|
|
// We'd have to get the uncompressed audio in order to advance it properly.
|
|
//if ( pnOutUncompressedSampleOffset )
|
|
// *pnOutUncompressedSampleOffset = s_nRecordingTimestamp_UncompressedSampleOffset;
|
|
// s_nRecordingTimestamp_UncompressedSampleOffset += xxxxx
|
|
}
|
|
|
|
return cbCompressedWritten;
|
|
}
|
|
|
|
IVoiceCodec *pCodec = g_pEncodeCodec;
|
|
if( g_pVoiceRecord && pCodec )
|
|
{
|
|
|
|
static ConVarRef voice_vox( "voice_vox" );
|
|
static ConVarRef voice_chat_bubble_show_volume( "voice_chat_bubble_show_volume" );
|
|
static ConVarRef voice_vox_current_peak( "voice_vox_current_peak" );
|
|
|
|
// Get uncompressed data from the recording device
|
|
short tempData[8192];
|
|
int samplesWanted = MIN(nCount/BYTES_PER_SAMPLE, (int)sizeof(tempData)/BYTES_PER_SAMPLE);
|
|
int gotten = g_pVoiceRecord->GetRecordedData(tempData, samplesWanted);
|
|
|
|
// If they want to get the data from a file instead of the mic, use that.
|
|
if(g_pMicInputFileData)
|
|
{
|
|
int nShouldGet = (flNow - g_MicStartTime) * Voice_SamplesPerSec();
|
|
gotten = MIN(sizeof(tempData)/BYTES_PER_SAMPLE, MIN(nShouldGet, (g_nMicInputFileBytes - g_CurMicInputFileByte) / BYTES_PER_SAMPLE));
|
|
memcpy(tempData, &g_pMicInputFileData[g_CurMicInputFileByte], gotten*BYTES_PER_SAMPLE);
|
|
g_CurMicInputFileByte += gotten * BYTES_PER_SAMPLE;
|
|
g_MicStartTime = flNow;
|
|
}
|
|
|
|
// Check for detecting levels
|
|
if ( !g_pMicInputFileData && gotten && ( voice_vox.GetBool() || g_VoiceTweakAPI.IsStillTweaking() || voice_chat_bubble_show_volume.GetBool() ) )
|
|
{
|
|
// TERROR: If the voice data is essentially silent, don't transmit
|
|
short *pData = tempData;
|
|
int averageData = 0;
|
|
int minData = 16384;
|
|
int maxData = -16384;
|
|
for ( int i=0; i<gotten; ++i )
|
|
{
|
|
short val = *pData;
|
|
averageData += val;
|
|
minData = MIN( val, minData );
|
|
maxData = MAX( val, maxData );
|
|
++pData;
|
|
}
|
|
averageData /= gotten;
|
|
int deltaData = maxData - minData;
|
|
|
|
voice_vox_current_peak.SetValue( deltaData );
|
|
|
|
if ( voice_vox.GetBool() || g_VoiceTweakAPI.IsStillTweaking() )
|
|
{
|
|
if ( deltaData < voice_threshold.GetFloat() )
|
|
{
|
|
if ( s_flThresholdDecayTime < flNow )
|
|
{
|
|
g_pSoundServices->OnChangeVoiceStatus( -1, GET_ACTIVE_SPLITSCREEN_SLOT(), false );
|
|
|
|
// End the current section, if any
|
|
VoiceRecord_MarkSectionBoundary();
|
|
s_nRecordingTimestamp_UncompressedSampleOffset += gotten;
|
|
return 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
g_pSoundServices->OnChangeVoiceStatus( -1, GET_ACTIVE_SPLITSCREEN_SLOT(), true );
|
|
|
|
// Pad out our threshold clipping so words aren't clipped together
|
|
s_flThresholdDecayTime = flNow + voice_threshold_delay.GetFloat();
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef VOICE_SEND_RAW_TEST
|
|
int nCompressedBytes = MIN( gotten, nCount );
|
|
for ( int i=0; i < nCompressedBytes; i++ )
|
|
{
|
|
pchDest[i] = (char)(tempData[i] >> 8);
|
|
}
|
|
#else
|
|
int nCompressedBytes = pCodec->Compress((char*)tempData, gotten, pchDest, nCount, !!bFinal);
|
|
#endif
|
|
|
|
// Write to our file buffers..
|
|
if(g_pUncompressedFileData)
|
|
{
|
|
int nToWrite = MIN(gotten*BYTES_PER_SAMPLE, MAX_WAVEFILEDATA_LEN - g_nUncompressedDataBytes);
|
|
memcpy(&g_pUncompressedFileData[g_nUncompressedDataBytes], tempData, nToWrite);
|
|
g_nUncompressedDataBytes += nToWrite;
|
|
}
|
|
|
|
// TERROR: -3 signals that we're talking
|
|
// !FIXME! @FD: I think this is wrong. it's possible for us to get some data, but just
|
|
// not have enough for the compressor to spit out a packet. But I'm afraid to make this
|
|
// change so close to TI, so I'm just making a note in case we revisit this. I'm
|
|
// not sure that it matters.
|
|
g_pSoundServices->OnChangeVoiceStatus( -3, GET_ACTIVE_SPLITSCREEN_SLOT(), (nCompressedBytes > 0) );
|
|
if ( pOutFormat )
|
|
{
|
|
*pOutFormat = VoiceFormat_Engine;
|
|
}
|
|
|
|
if ( nCompressedBytes > 0 )
|
|
{
|
|
s_nRecordingSectionCompressedByteOffset += nCompressedBytes;
|
|
|
|
if ( pnOutSectionNumber )
|
|
*pnOutSectionNumber = s_nRecordingSection;
|
|
if ( pnOutSectionSequenceNumber )
|
|
*pnOutSectionSequenceNumber = s_nRecordingSectionCompressedByteOffset;
|
|
if ( pnOutUncompressedSampleOffset )
|
|
*pnOutUncompressedSampleOffset = s_nRecordingTimestamp_UncompressedSampleOffset;
|
|
}
|
|
|
|
// Advance uncompressed sample number. Note that if we feed a small number of samples into the compressor,
|
|
// it might not actually return compressed data, until we hit a complete packet.
|
|
// !KLUDGE! Here we are assuming a specific compression properties!
|
|
if ( g_bIsSpeex )
|
|
{
|
|
// speex compresses 160 samples into 20 bytes with our settings (quality 4, which is quality 6 internally)
|
|
int nPackets = nCompressedBytes / 20;
|
|
Assert( nCompressedBytes == nPackets * 20 );
|
|
s_nRecordingTimestamp_UncompressedSampleOffset += nPackets*160;
|
|
}
|
|
else
|
|
{
|
|
// celt compresses 512 samples into 64 bytes with our settings
|
|
int nPackets = nCompressedBytes / 64;
|
|
Assert( nCompressedBytes == nPackets * 64 );
|
|
s_nRecordingTimestamp_UncompressedSampleOffset += nPackets*512;
|
|
}
|
|
|
|
// If they are telling us this is the last packet (and they are about to stop recording),
|
|
// then believe them
|
|
if ( bFinal )
|
|
VoiceRecord_MarkSectionBoundary();
|
|
|
|
return nCompressedBytes;
|
|
}
|
|
else
|
|
{
|
|
// TERROR: -3 signals that we're silent
|
|
g_pSoundServices->OnChangeVoiceStatus( -3, GET_ACTIVE_SPLITSCREEN_SLOT(), false );
|
|
VoiceRecord_MarkSectionBoundary();
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
|
|
//------------------ Copyright (c) 1999 Valve, LLC. ----------------------------
|
|
// Purpose: Assigns a channel to an entity by searching for either a channel
|
|
// already assigned to that entity or picking the least recently used
|
|
// channel. If the LRU channel is picked, it is flushed and all other
|
|
// channels are aged.
|
|
// Input : nEntity - entity number to assign to a channel.
|
|
// Output : A channel index to which the entity has been assigned.
|
|
//------------------------------------------------------------------------------
|
|
int Voice_AssignChannel(int nEntity, bool bProximity, bool bCaster, float timePadding )
|
|
{
|
|
// See if a channel already exists for this entity and if so, just return it.
|
|
int iFree = -1;
|
|
for(int i=0; i < VOICE_NUM_CHANNELS; i++)
|
|
{
|
|
CVoiceChannel *pChannel = &g_VoiceChannels[i];
|
|
|
|
if(pChannel->m_iEntity == nEntity)
|
|
{
|
|
return i;
|
|
}
|
|
else if(pChannel->m_iEntity == -1 && pChannel->m_pVoiceCodec)
|
|
{
|
|
pChannel->m_pVoiceCodec->ResetState();
|
|
iFree = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If they're all used, then don't allow them to make a new channel.
|
|
if(iFree == -1)
|
|
{
|
|
return VOICE_CHANNEL_ERROR;
|
|
}
|
|
|
|
CVoiceChannel *pChannel = &g_VoiceChannels[iFree];
|
|
pChannel->Init( nEntity, timePadding, bCaster );
|
|
pChannel->m_bProximity = bProximity;
|
|
VoiceSE_StartOverdrive();
|
|
|
|
return iFree;
|
|
}
|
|
|
|
|
|
//------------------ Copyright (c) 1999 Valve, LLC. ----------------------------
|
|
// Purpose: Determines which channel has been assigened to a given entity.
|
|
// Input : nEntity - entity number.
|
|
// Output : The index of the channel assigned to the entity, VOICE_CHANNEL_ERROR
|
|
// if no channel is currently assigned to the given entity.
|
|
//------------------------------------------------------------------------------
|
|
int Voice_GetChannel(int nEntity)
|
|
{
|
|
for(int i=0; i < VOICE_NUM_CHANNELS; i++)
|
|
if(g_VoiceChannels[i].m_iEntity == nEntity)
|
|
return i;
|
|
|
|
return VOICE_CHANNEL_ERROR;
|
|
}
|
|
|
|
|
|
static void UpsampleIntoBuffer(
|
|
const short *pSrc,
|
|
int nSrcSamples,
|
|
CCircularBuffer *pBuffer,
|
|
int nDestSamples)
|
|
{
|
|
if ( nDestSamples == nSrcSamples )
|
|
{
|
|
// !FIXME! This function should accept a const pointer!
|
|
pBuffer->Write( const_cast<short*>( pSrc ), nDestSamples*sizeof(short) );
|
|
}
|
|
else
|
|
{
|
|
for ( int i = 0 ; i < nDestSamples ; ++i )
|
|
{
|
|
double flSrc = (double)nSrcSamples * i / nDestSamples;
|
|
int iSample = (int)flSrc;
|
|
double frac = flSrc - floor(flSrc);
|
|
int iSampleNext = Min( iSample + 1, nSrcSamples - 1 );
|
|
|
|
double val1 = pSrc[iSample];
|
|
double val2 = pSrc[iSampleNext];
|
|
short newSample = (short)(val1 + (val2 - val1) * frac);
|
|
pBuffer->Write(&newSample, sizeof(newSample));
|
|
}
|
|
}
|
|
}
|
|
|
|
//------------------ Copyright (c) 1999 Valve, LLC. ----------------------------
|
|
// Purpose: Adds received voice data to
|
|
// Input :
|
|
// Output :
|
|
//------------------------------------------------------------------------------
|
|
|
|
void Voice_AddIncomingData(
|
|
int nChannel,
|
|
const char *pchData,
|
|
int nCount,
|
|
uint8 nSectionNumber,
|
|
uint32 nSectionSequenceNumber,
|
|
uint32 nUncompressedSampleOffset,
|
|
VoiceFormat_t format
|
|
) {
|
|
CVoiceChannel *pChannel;
|
|
|
|
if((pChannel = GetVoiceChannel(nChannel)) == NULL || !pChannel->m_pVoiceCodec)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( voice_showincoming.GetBool() )
|
|
{
|
|
Msg( "%.2f: Received voice channel=%2d: section=%4d seq=%8d time=%8d bytes=%4d, buffered=%5d\n",
|
|
Plat_FloatTime(), nChannel, nSectionNumber, nSectionSequenceNumber, nUncompressedSampleOffset, nCount, pChannel->m_Buffer.GetReadAvailable() );
|
|
}
|
|
|
|
// Get byte offset at the *start* of the packet.
|
|
uint32 nPacketByteOffsetWithinSection = nSectionSequenceNumber - (uint32)nCount;
|
|
|
|
// If we have previously been starved, but now are adding more data,
|
|
// then we need to reset the buffer back to a good state. Don't try
|
|
// to fill it up now. What should ordinarily happen when the buffer
|
|
// gets starved is that we should kill the channel, and any new data that
|
|
// comes in gets assigned a new channel. But if this channel is marked
|
|
// as having gotten starved out, and we are adding new data to it, then
|
|
// we have not yet killed it. So just insert some silence.
|
|
bool bFillWithSilenceToCatchUp = false;
|
|
if ( pChannel->m_bStarved )
|
|
{
|
|
if ( voice_showincoming.GetBool() )
|
|
{
|
|
Warning( "%.2f: Received voice channel=%2d: section=%4d seq=%8d time=%8d bytes=%4d reusing buffer after starvation. Padding with silence to reset buffering.\n",
|
|
Plat_FloatTime(), nChannel, nSectionNumber, nSectionSequenceNumber, nUncompressedSampleOffset, nCount );
|
|
}
|
|
|
|
bFillWithSilenceToCatchUp = true;
|
|
}
|
|
|
|
// Check section and sequence numbers, see if there was a dropped packet or maybe a gap of silence that was not transmitted
|
|
int nSampleOffsetGap = 0; // NOTE: measured in uncompressed rate (samplespersec below), NOT the data rate we send to the mixer, which is VOICE_OUTPUT_SAMPLE_RATE
|
|
int nLostBytes = 0;
|
|
if ( nSectionNumber != 0 ) // new format message? (This will be zero on matches before around 7/11/2014)
|
|
{
|
|
if ( nSectionNumber != pChannel->m_nCurrentSection )
|
|
{
|
|
pChannel->m_nExpectedCompressedByteOffset = 0;
|
|
pChannel->m_pVoiceCodec->ResetState();
|
|
}
|
|
|
|
// Check if the sample pointer is not the exact next thing we expected, then we might need to insert some silence.
|
|
// We'll handle the fact that the gap might have been due to a lost packet and not silence, and other degenerate
|
|
// cases, later
|
|
nSampleOffsetGap = nUncompressedSampleOffset - pChannel->m_nExpectedUncompressedSampleOffset;
|
|
}
|
|
else
|
|
{
|
|
Assert( nUncompressedSampleOffset == 0 ); // section number and uncompressed sample offset were added in the same protocol change. How could we have one without the other?
|
|
}
|
|
|
|
// If this is the first packet, or we were starved and getting rebooted, then
|
|
// force a reset. Otherwise, check if we lost a packet
|
|
if ( pChannel->m_bStarved || pChannel->m_bFirstPacket )
|
|
{
|
|
pChannel->m_pVoiceCodec->ResetState();
|
|
nLostBytes = 0;
|
|
nSampleOffsetGap = 0;
|
|
}
|
|
else if ( pChannel->m_nExpectedCompressedByteOffset != nPacketByteOffsetWithinSection )
|
|
{
|
|
if ( nSectionSequenceNumber != 0 ) // old voice packets don't have sequence numbers
|
|
nLostBytes = nPacketByteOffsetWithinSection - pChannel->m_nExpectedCompressedByteOffset;
|
|
|
|
// Check if the sequence number is significantly out of whack, then something went
|
|
// pretty badly wrong, or we have a bug. Don't try to handle this gracefully,
|
|
// just insert a little silence, and reset
|
|
if ( nLostBytes < 0 || nLostBytes > nCount*4 + 1024 )
|
|
{
|
|
Warning( "%.2f: Received voice channel=%2d: section=%4d seq=%8d time=%8d bytes=%4d LOST %d bytes? (Offset %d, expected %d)\n",
|
|
Plat_FloatTime(), nChannel, nSectionNumber, nSectionSequenceNumber, nUncompressedSampleOffset, nCount,
|
|
nLostBytes, pChannel->m_nExpectedCompressedByteOffset, nPacketByteOffsetWithinSection );
|
|
nLostBytes = 0;
|
|
pChannel->m_pVoiceCodec->ResetState();
|
|
bFillWithSilenceToCatchUp = true;
|
|
}
|
|
else
|
|
{
|
|
// Sequence number skipped by a reasonable amount, indicating a small amount of lost data,
|
|
// which is totally normal. Only spew if we're debugging this.
|
|
if ( voice_showincoming.GetBool() )
|
|
{
|
|
Warning( " LOST %d bytes. (Expected %u, got %u)\n", nLostBytes, pChannel->m_nExpectedCompressedByteOffset, nPacketByteOffsetWithinSection );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Decompress.
|
|
short decompressedBuffer[11500];
|
|
COMPILE_TIME_ASSERT( BYTES_PER_SAMPLE == sizeof(decompressedBuffer[0]) );
|
|
int nDecompressedSamplesForDroppedPacket = 0;
|
|
int nDecompressedSamplesForThisPacket = 0;
|
|
|
|
#ifdef VOICE_SEND_RAW_TEST
|
|
for ( int i=0; i < nCount; i++ )
|
|
decompressedBuffer[i] = pchData[i] << 8;
|
|
nDecompressedSamplesForThisPacket = nCount
|
|
|
|
#else
|
|
|
|
const int nDesiredSampleRate = g_bIsSpeex ? VOICE_OUTPUT_SAMPLE_RATE_SPEEX : VOICE_OUTPUT_SAMPLE_RATE;
|
|
|
|
int samplesPerSec;
|
|
|
|
if ( format == VoiceFormat_Steam )
|
|
{
|
|
uint32 nBytesWritten = 0;
|
|
EVoiceResult result = steamapicontext->SteamUser()->DecompressVoice( pchData, nCount, decompressedBuffer, sizeof(decompressedBuffer), &nBytesWritten, nDesiredSampleRate );
|
|
|
|
if ( result == k_EVoiceResultOK )
|
|
{
|
|
nDecompressedSamplesForThisPacket = nBytesWritten / BYTES_PER_SAMPLE;
|
|
}
|
|
else
|
|
{
|
|
Warning( "%.2f: Voice_AddIncomingData channel %d Size %d failed to decompress steam data result %d\n", Plat_FloatTime(), nChannel, nCount, result );
|
|
}
|
|
|
|
samplesPerSec = nDesiredSampleRate;
|
|
}
|
|
else
|
|
{
|
|
|
|
char *decompressedDest = (char*)decompressedBuffer;
|
|
int nDecompressBytesRemaining = sizeof(decompressedBuffer);
|
|
|
|
// First, if we lost some data, let the codec know.
|
|
if ( nLostBytes > 0 )
|
|
{
|
|
nDecompressedSamplesForDroppedPacket = pChannel->m_pVoiceCodec->Decompress( NULL, nLostBytes, decompressedDest, nDecompressBytesRemaining );
|
|
int nDecompressedBytesForDroppedPacket = nDecompressedSamplesForDroppedPacket * BYTES_PER_SAMPLE;
|
|
decompressedDest += nDecompressedBytesForDroppedPacket;
|
|
nDecompressBytesRemaining -= nDecompressedBytesForDroppedPacket;
|
|
}
|
|
|
|
// Now decompress the actual data
|
|
nDecompressedSamplesForThisPacket = pChannel->m_pVoiceCodec->Decompress( pchData, nCount, decompressedDest, nDecompressBytesRemaining );
|
|
if ( nDecompressedSamplesForThisPacket <= 0 )
|
|
{
|
|
Warning( "%.2f: Voice_AddIncomingData channel %d Size %d engine failed to decompress\n", Plat_FloatTime(), nChannel, nCount );
|
|
nDecompressedSamplesForThisPacket = 0;
|
|
}
|
|
samplesPerSec = g_VoiceSampleFormat.nSamplesPerSec;
|
|
EngineTool_OverrideSampleRate( samplesPerSec );
|
|
}
|
|
|
|
#endif
|
|
|
|
int nDecompressedSamplesTotal = nDecompressedSamplesForDroppedPacket + nDecompressedSamplesForThisPacket;
|
|
int nDecompressedBytesTotal = nDecompressedSamplesTotal * BYTES_PER_SAMPLE;
|
|
|
|
pChannel->m_GainManager.Apply( decompressedBuffer, nDecompressedSamplesTotal, pChannel->m_bCaster );
|
|
|
|
// We might need to fill with some silence. Calculate the number of samples we need to fill.
|
|
// Note that here we need to be careful not to confuse the network transmission reference
|
|
// rate with the rate of data sent to the mixer. (At the time I write this, they are the same,
|
|
// but that might change in the future.)
|
|
int nSamplesOfSilenceToInsertToMixer = 0; // mixer rate
|
|
if ( nSampleOffsetGap != 0 )
|
|
{
|
|
|
|
//
|
|
// Check for some things going way off the rails
|
|
//
|
|
|
|
// If it's already negative, then something went haywire.
|
|
if ( nSampleOffsetGap < 0 )
|
|
{
|
|
// This is weird. The sample number moved backwards.
|
|
Warning( "%.2f: Received voice channel=%2d: section=%4d seq=%8d time=%8d bytes=%4d, timestamp moved backwards (%d). Expected %u, received %u.\n",
|
|
Plat_FloatTime(), nChannel, nSectionNumber, nSectionSequenceNumber, nUncompressedSampleOffset, nCount,
|
|
nSampleOffsetGap, pChannel->m_nExpectedCompressedByteOffset, nUncompressedSampleOffset );
|
|
}
|
|
else
|
|
{
|
|
// If we dropped a packet, this would totally explain the gap.
|
|
nSampleOffsetGap -= nDecompressedSamplesForDroppedPacket;
|
|
if ( nSampleOffsetGap < 0 )
|
|
{
|
|
Warning( "%.2f: Received voice channel=%2d: section=%4d seq=%8d time=%8d bytes=%4d, timestamp moved backwards (%d) after synthesizing dropped packet. Expected %u+%u = %u, received %u.\n",
|
|
Plat_FloatTime(), nChannel, nSectionNumber, nSectionSequenceNumber, nUncompressedSampleOffset, nCount,
|
|
nSampleOffsetGap,
|
|
pChannel->m_nExpectedCompressedByteOffset,
|
|
nDecompressedSamplesForDroppedPacket,
|
|
pChannel->m_nExpectedCompressedByteOffset + nDecompressedSamplesForDroppedPacket,
|
|
nUncompressedSampleOffset );
|
|
}
|
|
}
|
|
|
|
// Is the gap massively larger than we should reasonably expect?
|
|
// this probably indicates something is wrong or we have a bug.
|
|
if ( nSampleOffsetGap > VOICE_RECEIVE_BUFFER_SECONDS * samplesPerSec )
|
|
{
|
|
Warning( "%.2f: Received voice channel=%2d: section=%4d seq=%8d time=%8d bytes=%4d, timestamp moved backwards (%d) after synthesizing dropped packet. Expected %u+%u = %u, received %u.\n",
|
|
Plat_FloatTime(), nChannel, nSectionNumber, nSectionSequenceNumber, nUncompressedSampleOffset, nCount,
|
|
nSampleOffsetGap,
|
|
pChannel->m_nExpectedCompressedByteOffset,
|
|
nDecompressedSamplesForDroppedPacket,
|
|
pChannel->m_nExpectedCompressedByteOffset + nDecompressedSamplesForDroppedPacket,
|
|
nUncompressedSampleOffset );
|
|
}
|
|
else if ( nSampleOffsetGap > 0 )
|
|
{
|
|
// A relatively small positive gap, which means we actually want to insert silence.
|
|
// This is the normal situation.
|
|
|
|
// Convert from the network reference rate to the mixer rate
|
|
nSamplesOfSilenceToInsertToMixer = nSampleOffsetGap * samplesPerSec / nDesiredSampleRate;
|
|
|
|
// Only spew about this if we're logging
|
|
if ( voice_showincoming.GetBool() )
|
|
{
|
|
Msg( " Timestamp gap of %d (%u -> %u). Will insert %d samples of silence\n", nSampleOffsetGap, pChannel->m_nExpectedUncompressedSampleOffset, nUncompressedSampleOffset, nSamplesOfSilenceToInsertToMixer );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert from voice decompression rate to the rate we send to the mixer.
|
|
int nDecompressedSamplesAtMixerRate = nDecompressedSamplesTotal * samplesPerSec / nDesiredSampleRate;
|
|
|
|
// Check current buffer state do some calculations on how much we could fit, and how
|
|
// much would get us to our ideal amount
|
|
int nBytesBuffered = pChannel->m_Buffer.GetReadAvailable();
|
|
int nSamplesBuffered = nBytesBuffered / BYTES_PER_SAMPLE;
|
|
int nMaxBytesToWrite = pChannel->m_Buffer.GetWriteAvailable();
|
|
int nMaxSamplesToWrite = nMaxBytesToWrite / BYTES_PER_SAMPLE;
|
|
int nSamplesNeededToReachMinDesiredLeadTime = Max( pChannel->m_nMinDesiredLeadSamples - nSamplesBuffered, 0 );
|
|
int nSamplesNeededToReachMaxDesiredLeadTime = Max( pChannel->m_nMaxDesiredLeadSamples - nSamplesBuffered, 0 );
|
|
int nSamplesOfSilenceMax = Max( 0, nMaxSamplesToWrite - nDecompressedSamplesAtMixerRate );
|
|
int nSamplesOfSilenceToReachMinDesiredLeadTime = Clamp( nSamplesNeededToReachMinDesiredLeadTime - nDecompressedSamplesAtMixerRate, 0, nSamplesOfSilenceMax );
|
|
int nSamplesOfSilenceToReachMaxDesiredLeadTime = Clamp( nSamplesNeededToReachMaxDesiredLeadTime - nDecompressedSamplesAtMixerRate, 0, nSamplesOfSilenceMax );
|
|
Assert( nSamplesOfSilenceToReachMinDesiredLeadTime <= nSamplesOfSilenceToReachMaxDesiredLeadTime );
|
|
Assert( nSamplesOfSilenceToReachMaxDesiredLeadTime <= nSamplesOfSilenceMax );
|
|
|
|
// Check if something went wrong with a previous batch of audio in this buffer,
|
|
// and we should just try to reset the buffering to a healthy position by
|
|
// filling with silence.
|
|
if ( bFillWithSilenceToCatchUp && nSamplesOfSilenceToReachMinDesiredLeadTime > nSamplesOfSilenceToInsertToMixer )
|
|
nSamplesOfSilenceToInsertToMixer = nSamplesOfSilenceToReachMinDesiredLeadTime;
|
|
|
|
// Limit silence samples
|
|
if ( nSamplesOfSilenceToInsertToMixer > nSamplesOfSilenceMax )
|
|
nSamplesOfSilenceToInsertToMixer = nSamplesOfSilenceMax;
|
|
|
|
// Insert silence, if necessary
|
|
if ( nSamplesOfSilenceToInsertToMixer > 0 )
|
|
{
|
|
// Check if out buffer lead time is not where we want it to be, then silence
|
|
// is a great opportunity to stretch things a bit and get us back where we'd like.
|
|
// This does change the timing slightly, but that is a far preferable change than
|
|
// later the buffer draining and us outputting distorted audio.
|
|
float kMaxStretch = 1.2f;
|
|
if ( nSamplesOfSilenceToInsertToMixer < nSamplesOfSilenceToReachMinDesiredLeadTime )
|
|
{
|
|
nSamplesOfSilenceToInsertToMixer = Min( int( nSamplesOfSilenceToInsertToMixer * kMaxStretch ), nSamplesOfSilenceToReachMinDesiredLeadTime );
|
|
}
|
|
else if ( nSamplesOfSilenceToInsertToMixer > nSamplesOfSilenceToReachMaxDesiredLeadTime )
|
|
{
|
|
float kMinStretch = 1.0 / kMaxStretch;
|
|
nSamplesOfSilenceToInsertToMixer = Max( int( nSamplesOfSilenceToInsertToMixer * kMinStretch ), nSamplesOfSilenceToReachMaxDesiredLeadTime );
|
|
}
|
|
|
|
if ( voice_showincoming.GetBool() )
|
|
{
|
|
Msg( " Actually inserting %d samples of silence\n", nSamplesOfSilenceToInsertToMixer );
|
|
}
|
|
|
|
// OK, we know how much silence we're going to insert. Before we insert silence,
|
|
// we're going to try to make a nice transition back down to zero, in case
|
|
// the last data didn't end near zero. (Highly likely if we dropped a packet.)
|
|
// This prevents a pop.
|
|
|
|
int nDesiredSamplesToRamp = nDesiredSampleRate / 500; // 2ms
|
|
int nSamplesToRamp = Min( nDesiredSamplesToRamp, nSamplesOfSilenceToInsertToMixer );
|
|
for ( int i = 1 ; i <= nSamplesToRamp ; ++i ) // No need to duplicate the previous sample. But make sure we end at zero
|
|
{
|
|
// Compute interpolation parameter
|
|
float t = float(i) / float(nSamplesToRamp);
|
|
|
|
// Smoothstep
|
|
t = 3.0f * t*t - 2.0 * t*t*t;
|
|
|
|
short val = short( pChannel->m_nLastSample * ( 1.0f - t ) );
|
|
pChannel->m_Buffer.Write( &val, sizeof(val) );
|
|
}
|
|
|
|
// Fill with silence
|
|
int nSilenceSamplesRemaining = nSamplesOfSilenceToInsertToMixer - nSamplesToRamp;
|
|
short zero = 0;
|
|
while ( nSilenceSamplesRemaining > 0 )
|
|
{
|
|
pChannel->m_Buffer.Write( &zero, sizeof(zero) );
|
|
--nSilenceSamplesRemaining;
|
|
}
|
|
|
|
pChannel->m_nLastSample = 0;
|
|
|
|
nSamplesNeededToReachMinDesiredLeadTime -= nSamplesOfSilenceToInsertToMixer;
|
|
nSamplesNeededToReachMaxDesiredLeadTime -= nSamplesOfSilenceToInsertToMixer;
|
|
}
|
|
|
|
if ( nDecompressedSamplesTotal > 0 )
|
|
{
|
|
|
|
// Upsample the actual voice data into the dest buffer. We could do this in a mixer but it complicates the mixer.
|
|
UpsampleIntoBuffer(
|
|
decompressedBuffer,
|
|
nDecompressedSamplesTotal,
|
|
&pChannel->m_Buffer,
|
|
nDecompressedSamplesAtMixerRate );
|
|
|
|
// Save off the value of the last sample, in case the next bit of data is missing and we need to transition out.
|
|
pChannel->m_nLastSample = decompressedBuffer[nDecompressedSamplesTotal-1];
|
|
|
|
// Write to our file buffer..
|
|
if(g_pDecompressedFileData)
|
|
{
|
|
int nToWrite = MIN(nDecompressedSamplesTotal*BYTES_PER_SAMPLE, MAX_WAVEFILEDATA_LEN - g_nDecompressedDataBytes);
|
|
memcpy(&g_pDecompressedFileData[g_nDecompressedDataBytes], decompressedBuffer, nToWrite);
|
|
g_nDecompressedDataBytes += nToWrite;
|
|
}
|
|
|
|
g_VoiceWriter.AddDecompressedData( pChannel, (const byte *)decompressedBuffer, nDecompressedBytesTotal );
|
|
}
|
|
|
|
// Check if our circular buffer is totally full, then that's bad.
|
|
// The circular buffer is a fixed size, and overflow is not
|
|
// graceful. This really should never happen, except when skipping a lot of frames in a demo.
|
|
if ( pChannel->m_Buffer.GetWriteAvailable() <= 0 )
|
|
{
|
|
if ( demoplayer && demoplayer->IsPlayingBack() )
|
|
{
|
|
// well, this is normal: demo is being played back and large chunks of it may be skipped at a time
|
|
}
|
|
else
|
|
{
|
|
Warning( "Voice channel %d circular buffer overflow!\n", nChannel );
|
|
}
|
|
}
|
|
|
|
// Save state for next time
|
|
pChannel->m_nCurrentSection = nSectionNumber;
|
|
pChannel->m_nExpectedCompressedByteOffset = nSectionSequenceNumber;
|
|
pChannel->m_nExpectedUncompressedSampleOffset = nUncompressedSampleOffset + nDecompressedSamplesForThisPacket;
|
|
pChannel->m_bFirstPacket = false;
|
|
pChannel->m_bStarved = false; // This only really matters if you call Voice_AddIncomingData between the time the mixer
|
|
// asks for data and Voice_Idle is called.
|
|
}
|
|
|
|
|
|
|
|
#if DEAD
|
|
//------------------ Copyright (c) 1999 Valve, LLC. ----------------------------
|
|
// Purpose: Flushes a given receive channel.
|
|
// Input : nChannel - index of channel to flush.
|
|
//------------------------------------------------------------------------------
|
|
void Voice_FlushChannel(int nChannel)
|
|
{
|
|
if ((nChannel < 0) || (nChannel >= VOICE_NUM_CHANNELS))
|
|
{
|
|
Assert(false);
|
|
return;
|
|
}
|
|
|
|
g_VoiceChannels[nChannel].m_Buffer.Flush();
|
|
}
|
|
#endif
|
|
|
|
|
|
//------------------------------------------------------------------------------
|
|
// IVoiceTweak implementation.
|
|
//------------------------------------------------------------------------------
|
|
|
|
int VoiceTweak_StartVoiceTweakMode()
|
|
{
|
|
// If we're already in voice tweak mode, return an error.
|
|
if ( g_bInTweakMode )
|
|
{
|
|
Assert(!"VoiceTweak_StartVoiceTweakMode called while already in tweak mode.");
|
|
return 0;
|
|
}
|
|
|
|
if ( g_pEncodeCodec == NULL )
|
|
{
|
|
Voice_Init( sv_voicecodec.GetString(), VOICE_CURRENT_VERSION );
|
|
}
|
|
|
|
g_bInTweakMode = true;
|
|
Voice_RecordStart(NULL, NULL, NULL);
|
|
|
|
return 1;
|
|
}
|
|
|
|
void VoiceTweak_EndVoiceTweakMode()
|
|
{
|
|
if(!g_bInTweakMode)
|
|
{
|
|
Assert(!"VoiceTweak_EndVoiceTweakMode called when not in tweak mode.");
|
|
return;
|
|
}
|
|
|
|
static ConVarRef voice_vox( "voice_vox" );
|
|
|
|
if ( !voice_vox.GetBool() )
|
|
{
|
|
Voice_RecordStop();
|
|
}
|
|
|
|
g_bInTweakMode = false;
|
|
}
|
|
|
|
void VoiceTweak_SetControlFloat(VoiceTweakControl iControl, float flValue)
|
|
{
|
|
if(!g_pMixerControls)
|
|
return;
|
|
|
|
if(iControl == MicrophoneVolume)
|
|
{
|
|
g_pMixerControls->SetValue_Float(IMixerControls::MicVolume, flValue);
|
|
}
|
|
else if ( iControl == MicBoost )
|
|
{
|
|
g_pMixerControls->SetValue_Float( IMixerControls::MicBoost, flValue );
|
|
}
|
|
else if(iControl == OtherSpeakerScale)
|
|
{
|
|
voice_scale.SetValue( flValue );
|
|
|
|
// this forces all voice channels to use the new voice_scale value instead of waiting for the next network update
|
|
for(int i=0; i < VOICE_NUM_CHANNELS; i++)
|
|
{
|
|
CVoiceChannel *pChannel = &g_VoiceChannels[i];
|
|
if ( pChannel && pChannel->m_iEntity > -1 )
|
|
{
|
|
pChannel->Init( pChannel->m_iEntity, pChannel->m_TimePad );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Voice_ForceInit()
|
|
{
|
|
if ( voice_system_enable.GetBool())
|
|
{
|
|
Voice_Init( sv_voicecodec.GetString(), VOICE_CURRENT_VERSION );
|
|
}
|
|
}
|
|
|
|
float VoiceTweak_GetControlFloat(VoiceTweakControl iControl)
|
|
{
|
|
if (!g_pMixerControls && voice_system_enable.GetBool())
|
|
{
|
|
Voice_Init( sv_voicecodec.GetString(), VOICE_CURRENT_VERSION );
|
|
}
|
|
|
|
if(!g_pMixerControls)
|
|
return 0;
|
|
|
|
if(iControl == MicrophoneVolume)
|
|
{
|
|
float value = 1;
|
|
g_pMixerControls->GetValue_Float(IMixerControls::MicVolume, value);
|
|
return value;
|
|
}
|
|
else if(iControl == OtherSpeakerScale)
|
|
{
|
|
return voice_scale.GetFloat();
|
|
}
|
|
else if(iControl == SpeakingVolume)
|
|
{
|
|
return g_VoiceTweakSpeakingVolume * 1.0f / 32768;
|
|
}
|
|
else if ( iControl == MicBoost )
|
|
{
|
|
float flValue = 1;
|
|
g_pMixerControls->GetValue_Float( IMixerControls::MicBoost, flValue );
|
|
return flValue;
|
|
}
|
|
else
|
|
{
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
bool VoiceTweak_IsStillTweaking()
|
|
{
|
|
return g_bInTweakMode;
|
|
}
|
|
|
|
bool VoiceTweak_IsControlFound(VoiceTweakControl iControl)
|
|
{
|
|
if (!g_pMixerControls && voice_system_enable.GetBool())
|
|
{
|
|
Voice_Init( sv_voicecodec.GetString(), VOICE_CURRENT_VERSION );
|
|
}
|
|
|
|
if(!g_pMixerControls)
|
|
return false;
|
|
|
|
if(iControl == MicrophoneVolume)
|
|
{
|
|
float fDummy;
|
|
return g_pMixerControls->GetValue_Float(IMixerControls::MicVolume,fDummy);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void Voice_Spatialize( channel_t *channel )
|
|
{
|
|
// do nothing now
|
|
}
|
|
|
|
IVoiceTweak g_VoiceTweakAPI =
|
|
{
|
|
VoiceTweak_StartVoiceTweakMode,
|
|
VoiceTweak_EndVoiceTweakMode,
|
|
VoiceTweak_SetControlFloat,
|
|
VoiceTweak_GetControlFloat,
|
|
VoiceTweak_IsStillTweaking,
|
|
VoiceTweak_IsControlFound,
|
|
};
|
|
|
|
|