summaryrefslogtreecommitdiff
path: root/engine/audio/private/voice.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'engine/audio/private/voice.cpp')
-rw-r--r--engine/audio/private/voice.cpp1566
1 files changed, 1566 insertions, 0 deletions
diff --git a/engine/audio/private/voice.cpp b/engine/audio/private/voice.cpp
new file mode 100644
index 0000000..ce506d1
--- /dev/null
+++ b/engine/audio/private/voice.cpp
@@ -0,0 +1,1566 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+// Purpose:
+//
+// $NoKeywords: $
+//
+//=============================================================================//
+
+#include "audio_pch.h"
+#include "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 "ivoicerecord.h"
+#include "ivoicecodec.h"
+#include "filesystem.h"
+#include "../../filesystem_engine.h"
+#include "tier1/utlbuffer.h"
+#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 );
+
+// A fallback codec that should be the most likely to work for local/offline use
+#define VOICE_FALLBACK_CODEC "vaudio_celt"
+
+// 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
+
+#define VOICE_RECEIVE_BUFFER_SIZE (VOICE_OUTPUT_SAMPLE_RATE_MAX * BYTES_PER_SAMPLE)
+
+#define LOCALPLAYERTALKING_TIMEOUT 0.2f // How long it takes for the client to decide the server isn't sending acks
+ // of voice data back.
+
+// 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
+ // These two can be dynamically changed by voice_init
+ VOICE_OUTPUT_SAMPLE_RATE_LOW, // nSamplesPerSec
+ VOICE_OUTPUT_SAMPLE_RATE_LOW*2, // nAvgBytesPerSec
+ 2, // nBlockAlign
+ 16, // wBitsPerSample
+ sizeof(WAVEFORMATEX) // cbSize
+};
+
+static bool Voice_SetSampleRate( DWORD rate )
+{
+ if ( g_VoiceSampleFormat.nSamplesPerSec != rate ||
+ g_VoiceSampleFormat.nAvgBytesPerSec != rate * 2 )
+ {
+ g_VoiceSampleFormat.nSamplesPerSec = rate;
+ g_VoiceSampleFormat.nAvgBytesPerSec = rate * 2;
+ return true;
+ }
+
+ return false;
+}
+
+int Voice_SamplesPerSec()
+{
+ int rate = g_VoiceSampleFormat.nSamplesPerSec;
+ EngineTool_OverrideSampleRate( rate );
+ return rate;
+}
+
+int Voice_AvgBytesPerSec()
+{
+ int rate = g_VoiceSampleFormat.nSamplesPerSec;
+ EngineTool_OverrideSampleRate( rate );
+ return ( rate * g_VoiceSampleFormat.wBitsPerSample ) >> 3;
+}
+
+ConVar voice_avggain( "voice_avggain", "0.5" );
+ConVar voice_maxgain( "voice_maxgain", "10" );
+ConVar voice_scale( "voice_scale", "1", FCVAR_ARCHIVE );
+
+ConVar voice_loopback( "voice_loopback", "0", FCVAR_USERINFO );
+ConVar voice_fadeouttime( "voice_fadeouttime", "0.1" ); // It fades to no sound at the tail end of your voice data when you release the key.
+
+// 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
+
+ConVar voice_enable( "voice_enable", "1", FCVAR_ARCHIVE ); // Globally enable or disable voice.
+#ifdef VOICE_VOX_ENABLE
+ConVar voice_threshold( "voice_threshold", "2000", FCVAR_ARCHIVE );
+#endif // VOICE_VOX_ENABLE
+
+// 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 );
+
+// This should not be lower than the maximum difference between clients' frame durations (due to cmdrate/updaterate),
+// plus some jitter allowance.
+ConVar voice_buffer_ms( "voice_buffer_ms", "100", FCVAR_INTERNAL_USE,
+ "How many milliseconds of voice to buffer to avoid dropouts due to jitter and frame time differences." );
+
+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;
+
+// The codec and sample rate passed to Voice_Init. "" and -1 if voice is not initialized
+char g_szVoiceCodec[_MAX_PATH] = { 0 };
+int g_nVoiceRequestedSampleRate = -1;
+
+const char *Voice_ConfiguredCodec() { return g_szVoiceCodec; }
+int Voice_ConfiguredSampleRate() { return g_nVoiceRequestedSampleRate; }
+
+// 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 bool g_bLocalPlayerTalkingAck = false;
+static float g_LocalPlayerTalkingTimeout = 0;
+
+
+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?
+static bool g_bVoiceRecordStopping = false; // Are we waiting to stop?
+bool g_bUsingSteamVoice = false;
+
+#ifdef WIN32
+extern IVoiceRecord* CreateVoiceRecord_DSound(int nSamplesPerSec);
+#elif defined( OSX )
+extern IVoiceRecord* CreateVoiceRecord_AudioQueue(int sampleRate);
+#endif
+
+#ifdef POSIX
+extern IVoiceRecord* CreateVoiceRecord_OpenAL(int sampleRate);
+#endif
+
+
+static bool VoiceRecord_Start()
+{
+ if ( g_bUsingSteamVoice )
+ {
+ if ( steamapicontext && steamapicontext->SteamUser() )
+ {
+ steamapicontext->SteamUser()->StartVoiceRecording();
+ return true;
+ }
+ }
+ else if ( g_pVoiceRecord )
+ {
+ return g_pVoiceRecord->RecordStart();
+ }
+ return false;
+}
+
+static void VoiceRecord_Stop()
+{
+ if ( g_bUsingSteamVoice )
+ {
+ if ( steamapicontext && steamapicontext->SteamUser() )
+ {
+ steamapicontext->SteamUser()->StopVoiceRecording();
+ }
+ }
+ else if ( g_pVoiceRecord )
+ {
+ return g_pVoiceRecord->RecordStop();
+ }
+}
+
+//
+// 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);
+
+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.
+
+ // Used for upsampling..
+ double m_LastFraction; // Fraction between m_LastSample and the next sample.
+ short m_LastSample;
+
+ bool m_bStarved; // Set to true when the channel runs out of data for the mixer.
+ // The channel is killed at that point.
+
+ 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.
+
+ IVoiceCodec *m_pVoiceCodec; // Each channel gets is own IVoiceCodec instance so the codec can maintain state.
+
+ CAutoGain m_AutoGain; // Gain we're applying to this channel
+
+ CVoiceChannel *m_pNext;
+
+ bool m_bProximity;
+ int m_nViewEntityIndex;
+ int m_nSoundGuid;
+};
+
+
+CVoiceChannel::CVoiceChannel()
+{
+ m_iEntity = -1;
+ m_pVoiceCodec = NULL;
+ m_nViewEntityIndex = -1;
+ m_nSoundGuid = -1;
+}
+
+void CVoiceChannel::Init(int nEntity)
+{
+ m_iEntity = nEntity;
+ m_bStarved = false;
+ m_Buffer.Flush();
+ m_TimePad = Clamp( voice_buffer_ms.GetFloat(), 1.f, 5000.f ) / 1000.f;
+ m_LastSample = 0;
+ m_LastFraction = 0.999;
+
+ m_AutoGain.Reset( 128, voice_maxgain.GetFloat(), voice_avggain.GetFloat(), voice_scale.GetFloat() );
+}
+
+
+
+// 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()
+ {
+ }
+
+ // Copy ctor is needed to insert into CVoiceWriter's CUtlRBTree.
+ CVoiceWriterData(const CVoiceWriterData& other) :
+ m_pChannel( other.m_pChannel ),
+ m_nCount( other.m_nCount ),
+ m_Buffer( )
+ {
+ m_Buffer.CopyBuffer( other.m_Buffer );
+ }
+
+ 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;
+
+private:
+ CVoiceWriterData& operator=(const CVoiceWriterData&);
+};
+
+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 < (int)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();
+}
+
+
+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 nBytesToRead = pBuffer->GetReadAvailable();
+ nBytesToRead = min(min(nBytesToRead, (int)maxOutSamples), sampleCount * BYTES_PER_SAMPLE);
+ int nSamplesGotten = pBuffer->Read(pCopyBuf, nBytesToRead) / BYTES_PER_SAMPLE;
+
+ // 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)
+ {
+ 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 )
+ {
+ 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 );
+
+ //Msg( "duplicating %d samples\n", nSamplesToDuplicate );
+
+ nAdditionalNeeded -= nSamplesToDuplicate;
+ if ( nAdditionalNeeded > 0 )
+ {
+ dest = (short *)&pCopyBuf[ nSamplesGotten + nSamplesToDuplicate ];
+ Q_memset(dest, 0, nAdditionalNeeded * BYTES_PER_SAMPLE);
+
+ // Msg( "zeroing %d samples\n", nAdditionalNeeded );
+
+ Assert( ( nAdditionalNeeded + nSamplesGotten + nSamplesToDuplicate ) == wantedSampleCount );
+ }
+ }
+ else
+ {
+ Q_memset( &pCopyBuf[ nSamplesGotten ], 0, nAdditionalNeeded * BYTES_PER_SAMPLE );
+ }
+ nSamplesGotten = wantedSampleCount;
+ }
+
+ // If the buffer is out of data, mark this channel to go away.
+ if(pBuffer->GetReadAvailable() == 0)
+ {
+ 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];
+ }
+}
+
+// Helper for doing a default-init with some codec if we weren't passed specific parameters
+bool Voice_InitWithDefault( const char *pCodecName )
+{
+ if ( !pCodecName || !*pCodecName )
+ {
+ return false;
+ }
+
+ int nRate = Voice_GetDefaultSampleRate( pCodecName );
+ if ( nRate < 0 )
+ {
+ Msg( "Voice_InitWithDefault: Unable to determine defaults for codec \"%s\"\n", pCodecName );
+ return false;
+ }
+
+ return Voice_Init( pCodecName, Voice_GetDefaultSampleRate( pCodecName ) );
+}
+
+bool Voice_Init( const char *pCodecName, int nSampleRate )
+{
+ if ( voice_enable.GetInt() == 0 )
+ {
+ return false;
+ }
+
+ if ( !pCodecName || !pCodecName[0] )
+ {
+ return false;
+ }
+
+ bool bSpeex = Q_stricmp( pCodecName, "vaudio_speex" ) == 0;
+ bool bCelt = Q_stricmp( pCodecName, "vaudio_celt" ) == 0;
+ bool bSteam = Q_stricmp( pCodecName, "steam" ) == 0;
+ // Miles has not been in use for voice in a long long time. Not worth the surface to support ancient demos that may
+ // use it (and probably do not work for other reasons)
+ // "vaudio_miles"
+
+ if ( !( bSpeex || bCelt || bSteam ) )
+ {
+ Msg( "Voice_Init Failed: invalid voice codec %s.\n", pCodecName );
+ return false;
+ }
+
+ Voice_Deinit();
+
+ g_bVoiceAtLeastPartiallyInitted = true;
+ V_strncpy( g_szVoiceCodec, pCodecName, sizeof(g_szVoiceCodec) );
+ g_nVoiceRequestedSampleRate = nSampleRate;
+
+ g_bUsingSteamVoice = bSteam;
+
+ if ( !steamapicontext )
+ {
+ steamapicontext = &g_SteamAPIContext;
+ steamapicontext->Init();
+ }
+
+ if ( g_bUsingSteamVoice )
+ {
+ if ( !steamapicontext->SteamFriends() || !steamapicontext->SteamUser() )
+ {
+ Msg( "Voice_Init: Requested Steam voice, but cannot access API. Voice will not function\n" );
+ return false;
+ }
+ }
+
+ // For steam, nSampleRate 0 means "use optimal steam sample rate".
+ if ( bSteam && nSampleRate == 0 )
+ {
+ Msg( "Voice_Init: Using Steam voice optimal sample rate %d\n",
+ steamapicontext->SteamUser()->GetVoiceOptimalSampleRate() );
+ // Steam's sample rate may change and not be supported by our rather unflexible sound engine. However, steam
+ // will resample as necessary in DecompressVoice, so we can pretend we're outputting at native rates.
+ //
+ // Behind the scenes, we'll request steam give us the encoded stream at its "optimal" rate, then we'll try to
+ // decompress the output at this rate, making it transparent to us that the encoded stream is not at our output
+ // rate.
+ Voice_SetSampleRate( SOUND_DMA_SPEED );
+ }
+ else
+ {
+ Voice_SetSampleRate( nSampleRate );
+ }
+
+ if(!VoiceSE_Init())
+ return false;
+
+ // Get the voice input device.
+#ifdef OSX
+ g_pVoiceRecord = CreateVoiceRecord_AudioQueue( Voice_SamplesPerSec() );
+ if ( !g_pVoiceRecord )
+ {
+ // Fall back to OpenAL
+ g_pVoiceRecord = CreateVoiceRecord_OpenAL( Voice_SamplesPerSec() );
+ }
+#elif defined( WIN32 )
+ g_pVoiceRecord = CreateVoiceRecord_DSound( Voice_SamplesPerSec() );
+#else
+ g_pVoiceRecord = CreateVoiceRecord_OpenAL( Voice_SamplesPerSec() );
+#endif
+
+ if( !g_pVoiceRecord )
+ {
+ Msg( "Unable to initialize sound capture. You won't be able to speak to other players." );
+ }
+
+ // Init codec DLL for non-steam
+ if ( !bSteam )
+ {
+ // CELT's qualities are 0-3, we historically just passed 4 to the other two even though they don't really map to the
+ // same thing.
+ //
+ // Changing the quality level we use here will require either extending SVC_VoiceInit to pass down which quality is
+ // in use or using a different codec name (vaudio_celtHD!) for backwards compatibility
+ int quality = bCelt ? 3 : 4;
+
+ // Get the codec.
+ CreateInterfaceFn createCodecFn = NULL;
+ g_hVoiceCodecDLL = FileSystem_LoadModule(pCodecName);
+
+ if ( !g_hVoiceCodecDLL || (createCodecFn = Sys_GetFactory(g_hVoiceCodecDLL)) == NULL ||
+ (g_pEncodeCodec = (IVoiceCodec*)createCodecFn(pCodecName, NULL)) == NULL || !g_pEncodeCodec->Init( quality ) )
+ {
+ Msg("Unable to load voice codec '%s'. Voice disabled. (module %i, iface %i, codec %i)\n",
+ pCodecName, !!g_hVoiceCodecDLL, !!createCodecFn, !!g_pEncodeCodec);
+ Voice_Deinit();
+ return false;
+ }
+
+ for (int i=0; i < VOICE_NUM_CHANNELS; i++)
+ {
+ CVoiceChannel *pChannel = &g_VoiceChannels[i];
+
+ if ((pChannel->m_pVoiceCodec = (IVoiceCodec*)createCodecFn(pCodecName, NULL)) == NULL || !pChannel->m_pVoiceCodec->Init( quality ))
+ {
+ Voice_Deinit();
+ return false;
+ }
+ }
+ }
+
+ // XXX(JohnS): These don't do much in Steam codec mode, but code below uses their presence to mean 'voice fully
+ // initialized' and other things assume they will succeed.
+ InitMixerControls();
+
+ // Steam mode uses steam for raw input so this isn't meaningful and could have side-effects
+ if( voice_forcemicrecord.GetInt() && !bSteam )
+ {
+ if( g_pMixerControls )
+ g_pMixerControls->SelectMicrophoneForWaveInput();
+ }
+
+ 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, false );
+ VoiceSE_CloseMouth( iEnt );
+
+ pChannel->m_nViewEntityIndex = -1;
+ pChannel->m_nSoundGuid = -1;
+
+ // If the tweak mode channel is ending
+ if ( iChannel == 0 &&
+ g_bInTweakMode )
+ {
+ VoiceTweak_EndVoiceTweakMode();
+ }
+ }
+}
+
+
+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;
+
+ 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;
+ g_szVoiceCodec[0] = '\0';
+ g_nVoiceRequestedSampleRate = -1;
+ g_bUsingSteamVoice = false;
+}
+
+bool Voice_GetLoopback()
+{
+ return !!voice_loopback.GetInt();
+}
+
+
+void Voice_LocalPlayerTalkingAck()
+{
+ if(!g_bLocalPlayerTalkingAck)
+ {
+ // Tell the client DLL when this changes.
+ g_pSoundServices->OnChangeVoiceStatus(-2, TRUE);
+ }
+
+ g_bLocalPlayerTalkingAck = true;
+ g_LocalPlayerTalkingTimeout = 0;
+}
+
+
+void Voice_UpdateVoiceTweakMode()
+{
+ if(!g_bInTweakMode || !g_pVoiceRecord)
+ return;
+
+ CVoiceChannel *pTweakChannel = GetVoiceChannel( 0 );
+
+ if ( pTweakChannel->m_nSoundGuid != -1 &&
+ !S_IsSoundStillPlaying( pTweakChannel->m_nSoundGuid ) )
+ {
+ VoiceTweak_EndVoiceTweakMode();
+ return;
+ }
+
+ char uchVoiceData[4096];
+ bool bFinal = false;
+ int nDataLength = Voice_GetCompressedData(uchVoiceData, sizeof(uchVoiceData), bFinal);
+
+ Voice_AddIncomingData(TWEAKMODE_CHANNELINDEX, uchVoiceData, nDataLength, 0);
+}
+
+
+void Voice_Idle(float frametime)
+{
+ if( voice_enable.GetInt() == 0 )
+ {
+ Voice_Deinit();
+ return;
+ }
+
+ if( g_bLocalPlayerTalkingAck )
+ {
+ g_LocalPlayerTalkingTimeout += frametime;
+ if(g_LocalPlayerTalkingTimeout > LOCALPLAYERTALKING_TIMEOUT)
+ {
+ g_bLocalPlayerTalkingAck = false;
+
+ // Tell the client DLL.
+ g_pSoundServices->OnChangeVoiceStatus(-2, FALSE);
+ }
+ }
+
+ // Precalculate these to speedup the voice fadeout.
+ g_nVoiceFadeSamples = max((int)(voice_fadeouttime.GetFloat() * g_VoiceSampleFormat.nSamplesPerSec ), 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
+ {
+ float oldpad = pChannel->m_TimePad;
+ pChannel->m_TimePad -= frametime;
+ if(oldpad > 0 && pChannel->m_TimePad <= 0)
+ {
+ // Start its audio.
+ pChannel->m_nViewEntityIndex = g_pSoundServices->GetViewEntity();
+ pChannel->m_nSoundGuid = VoiceSE_StartChannel( i, pChannel->m_iEntity, pChannel->m_bProximity, pChannel->m_nViewEntityIndex );
+ g_pSoundServices->OnChangeVoiceStatus(pChannel->m_iEntity, 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;
+ }
+}
+
+
+bool Voice_IsRecording()
+{
+ return g_bVoiceRecording && !g_bInTweakMode;
+}
+
+
+bool Voice_RecordStart(
+ const char *pUncompressedFile,
+ const char *pDecompressedFile,
+ const char *pMicInputFile)
+{
+ if( !g_pEncodeCodec && !g_bUsingSteamVoice )
+ return false;
+
+ g_VoiceWriter.Flush();
+
+ Voice_RecordStop();
+
+ if ( !g_bUsingSteamVoice )
+ {
+ g_pEncodeCodec->ResetState();
+ }
+
+ 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 )
+ {
+ if ( steamapicontext && steamapicontext->SteamFriends() && steamapicontext->SteamUser() )
+ {
+ // Tell Friends' Voice chat that the local user has started speaking
+ steamapicontext->SteamFriends()->SetInGameVoiceSpeaking( steamapicontext->SteamUser()->GetSteamID(), true );
+ }
+
+ g_pSoundServices->OnChangeVoiceStatus( -1, true ); // Tell the client DLL.
+ }
+ }
+
+ return g_bVoiceRecording;
+}
+
+
+void Voice_UserDesiresStop()
+{
+ if ( g_bVoiceRecordStopping )
+ return;
+
+ g_bVoiceRecordStopping = true;
+ g_pSoundServices->OnChangeVoiceStatus( -1, false ); // Tell the client DLL.
+
+ // If we're using Steam voice, we'll keep recording until Steam tells us we
+ // received all the data.
+ if ( g_bUsingSteamVoice )
+ {
+ steamapicontext->SteamUser()->StopVoiceRecording();
+ }
+ else
+ {
+ VoiceRecord_Stop();
+ }
+}
+
+
+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 )
+ {
+ if ( steamapicontext->SteamFriends() && steamapicontext->SteamUser() )
+ {
+ // Tell Friends' Voice chat that the local user has stopped speaking
+ steamapicontext->SteamFriends()->SetInGameVoiceSpeaking( steamapicontext->SteamUser()->GetSteamID(), false );
+ }
+ }
+
+ g_bVoiceRecording = false;
+ g_bVoiceRecordStopping = false;
+ return(true);
+}
+
+
+int Voice_GetCompressedData(char *pchDest, int nCount, bool bFinal)
+{
+ // Check g_bVoiceRecordStopping in case g_bUsingSteamVoice changes on us
+ // while waiting for the end of voice data.
+ if ( g_bUsingSteamVoice || g_bVoiceRecordStopping )
+ {
+ uint32 cbCompressedWritten = 0;
+ uint32 cbUncompressedWritten = 0;
+ uint32 cbCompressed = 0;
+ uint32 cbUncompressed = 0;
+ // We're going to always request steam give us the encoded stream at the optimal rate, unless our final output
+ // rate is lower than it. We'll pass our output rate when we actually extract the data, which Steam will
+ // happily upsample from its optimal rate for us.
+ int nEncodeRate = min( (int)steamapicontext->SteamUser()->GetVoiceOptimalSampleRate(), Voice_SamplesPerSec() );
+ EVoiceResult result = steamapicontext->SteamUser()->GetAvailableVoice( &cbCompressed, &cbUncompressed, nEncodeRate );
+ if ( result == k_EVoiceResultOK )
+ {
+ result = steamapicontext->SteamUser()->GetVoice( true, pchDest, nCount, &cbCompressedWritten,
+ g_pUncompressedFileData != NULL, g_pUncompressedFileData,
+ MAX_WAVEFILEDATA_LEN - g_nUncompressedDataBytes,
+ &cbUncompressedWritten, nEncodeRate );
+
+ if ( g_pUncompressedFileData )
+ {
+ g_nUncompressedDataBytes += cbUncompressedWritten;
+ }
+ g_pSoundServices->OnChangeVoiceStatus( -3, true );
+ }
+ else
+ {
+ if ( result == k_EVoiceResultNotRecording && g_bVoiceRecording )
+ {
+ Voice_RecordStop();
+ }
+
+ g_pSoundServices->OnChangeVoiceStatus( -3, false );
+ }
+ return cbCompressedWritten;
+ }
+
+ IVoiceCodec *pCodec = g_pEncodeCodec;
+ if( g_pVoiceRecord && pCodec )
+ {
+#ifdef VOICE_VOX_ENABLE
+ static ConVarRef voice_vox( "voice_vox" );
+#endif // VOICE_VOX_ENABLE
+
+ 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)
+ {
+ double curtime = Plat_FloatTime();
+ int nShouldGet = (curtime - g_MicStartTime) * Voice_SamplesPerSec();
+ gotten = min(sizeof(tempData)/BYTES_PER_SAMPLE,
+ (size_t)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 = curtime;
+ }
+#ifdef VOICE_VOX_ENABLE
+ else if ( gotten && voice_vox.GetBool() == true )
+ {
+ // 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;
+
+ if ( deltaData < voice_threshold.GetFloat() && maxData < voice_threshold.GetFloat() )
+ {
+ // -3 signals that we're silent
+ g_pSoundServices->OnChangeVoiceStatus( -3, false );
+ return 0;
+ }
+ }
+#endif // VOICE_VOX_ENABLE
+
+#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;
+ }
+#ifdef VOICE_VOX_ENABLE
+ // -3 signals that we're talking
+ g_pSoundServices->OnChangeVoiceStatus( -3, (nCompressedBytes > 0) );
+#endif // VOICE_VOX_ENABLE
+ return nCompressedBytes;
+ }
+ else
+ {
+#ifdef VOICE_VOX_ENABLE
+ // -3 signals that we're silent
+ g_pSoundServices->OnChangeVoiceStatus( -3, false );
+#endif // VOICE_VOX_ENABLE
+ 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)
+{
+ if(g_bInTweakMode)
+ return VOICE_CHANNEL_IN_TWEAK_MODE;
+
+ // 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 || g_bUsingSteamVoice ) )
+ {
+ // Won't exist in steam voice mode
+ if ( 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);
+ 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;
+}
+
+
+double UpsampleIntoBuffer(
+ const short *pSrc,
+ int nSrcSamples,
+ CCircularBuffer *pBuffer,
+ double startFraction,
+ double rate)
+{
+ double maxFraction = nSrcSamples - 1;
+
+ while(1)
+ {
+ if(startFraction >= maxFraction)
+ break;
+
+ int iSample = (int)startFraction;
+ double frac = startFraction - floor(startFraction);
+
+ double val1 = pSrc[iSample];
+ double val2 = pSrc[iSample+1];
+ short newSample = (short)(val1 + (val2 - val1) * frac);
+ pBuffer->Write(&newSample, sizeof(newSample));
+
+ startFraction += rate;
+ }
+
+ return startFraction - floor(startFraction);
+}
+
+
+//------------------ Copyright (c) 1999 Valve, LLC. ----------------------------
+// Purpose: Adds received voice data to
+// Input :
+// Output :
+//------------------------------------------------------------------------------
+int Voice_AddIncomingData(int nChannel, const char *pchData, int nCount, int iSequenceNumber)
+{
+ CVoiceChannel *pChannel;
+
+ // If in tweak mode, we call this during Idle with -1 as the channel, so any channel data from the network
+ // gets rejected.
+ if(g_bInTweakMode)
+ {
+ if(nChannel == TWEAKMODE_CHANNELINDEX)
+ nChannel = 0;
+ else
+ return 0;
+ }
+
+ if ( ( pChannel = GetVoiceChannel(nChannel)) == NULL || ( !g_bUsingSteamVoice && !pChannel->m_pVoiceCodec ) )
+ {
+ return(0);
+ }
+
+ 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.
+
+ // Decompress.
+ // @note Tom Bui: suggested destination buffer for Steam voice is 22kb
+ char decompressed[22528];
+
+#ifdef VOICE_SEND_RAW_TEST
+
+ int nDecompressed = nCount;
+ for ( int i=0; i < nDecompressed; i++ )
+ ((short*)decompressed)[i] = pchData[i] << 8;
+
+#else
+
+ int nDecompressed = 0;
+ if ( g_bUsingSteamVoice )
+ {
+ uint32 nBytesWritten = 0;
+ EVoiceResult result = steamapicontext->SteamUser()->DecompressVoice( pchData, nCount,
+ decompressed, sizeof( decompressed ),
+ &nBytesWritten, Voice_SamplesPerSec() );
+ if ( result == k_EVoiceResultOK )
+ {
+ nDecompressed = nBytesWritten / BYTES_PER_SAMPLE;
+ }
+ }
+ else
+ {
+ nDecompressed = pChannel->m_pVoiceCodec->Decompress(pchData, nCount, decompressed, sizeof(decompressed));
+ }
+
+#endif
+
+ if ( g_bInTweakMode )
+ {
+ short *data = (short *)decompressed;
+ g_VoiceTweakSpeakingVolume = 0;
+
+ // Find the highest value
+ for ( int i=0; i<nDecompressed; ++i )
+ {
+ g_VoiceTweakSpeakingVolume = max((int)abs(data[i]), g_VoiceTweakSpeakingVolume);
+ }
+
+ // Smooth it out
+ g_VoiceTweakSpeakingVolume &= 0xFE00;
+ }
+
+ pChannel->m_AutoGain.ProcessSamples((short*)decompressed, nDecompressed);
+
+ // Upsample into the dest buffer. We could do this in a mixer but it complicates the mixer.
+ pChannel->m_LastFraction = UpsampleIntoBuffer( (short*)decompressed,
+ nDecompressed,
+ &pChannel->m_Buffer,
+ pChannel->m_LastFraction,
+ (double)Voice_SamplesPerSec()/g_VoiceSampleFormat.nSamplesPerSec );
+ pChannel->m_LastSample = decompressed[nDecompressed];
+
+ // Write to our file buffer..
+ if(g_pDecompressedFileData)
+ {
+ int nToWrite = min(nDecompressed*2, MAX_WAVEFILEDATA_LEN - g_nDecompressedDataBytes);
+ memcpy(&g_pDecompressedFileData[g_nDecompressedDataBytes], decompressed, nToWrite);
+ g_nDecompressedDataBytes += nToWrite;
+ }
+
+ g_VoiceWriter.AddDecompressedData( pChannel, (const byte *)decompressed, nDecompressed * 2 );
+
+ if( voice_showincoming.GetInt() != 0 )
+ {
+ Msg("Voice - %d incoming samples added to channel %d\n", nDecompressed, nChannel);
+ }
+
+ return(nChannel);
+}
+
+
+#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_pMixerControls && voice_enable.GetBool() )
+ {
+ Voice_ForceInit();
+ }
+
+ if( !g_pMixerControls )
+ return 0;
+
+ Voice_EndAllChannels();
+ Voice_RecordStart(NULL, NULL, NULL);
+ Voice_AssignChannel(TWEAKMODE_ENTITYINDEX, false );
+ g_bInTweakMode = true;
+ InitMixerControls();
+
+ return 1;
+}
+
+void VoiceTweak_EndVoiceTweakMode()
+{
+ if(!g_bInTweakMode)
+ {
+ Assert(!"VoiceTweak_EndVoiceTweakMode called when not in tweak mode.");
+ return;
+ }
+
+ g_bInTweakMode = false;
+ Voice_RecordStop();
+}
+
+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 );
+ }
+}
+
+void Voice_ForceInit()
+{
+ if ( g_pMixerControls || !voice_enable.GetBool() )
+ {
+ // Nothing to do
+ return;
+ }
+
+ // Lacking a better default, just peak at what the server's sv_voicecodec is set to
+ static ConVarRef sv_voicecodec( "sv_voicecodec" );
+ if ( !Voice_InitWithDefault( sv_voicecodec.GetString() ) )
+ {
+ // Try ultimate fallback
+ Voice_InitWithDefault( VOICE_FALLBACK_CODEC );
+ }
+}
+
+float VoiceTweak_GetControlFloat(VoiceTweakControl iControl)
+{
+ Voice_ForceInit();
+
+ 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;
+}
+
+void Voice_Spatialize( channel_t *channel )
+{
+ if ( !g_bInTweakMode )
+ return;
+
+ Assert( channel->sfx );
+ Assert( channel->sfx->pSource );
+ Assert( channel->sfx->pSource->GetType() == CAudioSource::AUDIO_SOURCE_VOICE );
+
+ // Place the tweak mode sound back at the view entity
+ CVoiceChannel *pVoiceChannel = GetVoiceChannel( 0 );
+ Assert( pVoiceChannel );
+ if ( !pVoiceChannel )
+ return;
+
+ if ( pVoiceChannel->m_nSoundGuid != channel->guid )
+ return;
+
+ // No change
+ if ( g_pSoundServices->GetViewEntity() == pVoiceChannel->m_nViewEntityIndex )
+ return;
+
+ DevMsg( 1, "Voice_Spatialize changing voice tweak entity from %d to %d\n", pVoiceChannel->m_nViewEntityIndex, g_pSoundServices->GetViewEntity() );
+
+ pVoiceChannel->m_nViewEntityIndex = g_pSoundServices->GetViewEntity();
+ channel->soundsource = pVoiceChannel->m_nViewEntityIndex;
+}
+
+IVoiceTweak g_VoiceTweakAPI =
+{
+ VoiceTweak_StartVoiceTweakMode,
+ VoiceTweak_EndVoiceTweakMode,
+ VoiceTweak_SetControlFloat,
+ VoiceTweak_GetControlFloat,
+ VoiceTweak_IsStillTweaking,
+};
+
+