Skip to content

Commit bc4aa6a

Browse files
personcopperfineVauff
authored andcommitted
feat: added voicechat recording primitive (#454)
1 parent b57591c commit bc4aa6a

4 files changed

Lines changed: 129 additions & 1 deletion

File tree

src/adminsystem.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1295,6 +1295,38 @@ CON_COMMAND_CHAT_FLAGS(setteam, "<name> <team (0-3)> - Set a player's team", ADM
12951295
if (iNumClients > 1)
12961296
PrintMultiAdminAction(nType, strCommandPlayerName, "moved", szAction);
12971297
}
1298+
1299+
CON_COMMAND_CHAT_FLAGS(savevc, "<name> save voicechat as PCM audio for test", ADMFLAG_CHEATS)
1300+
{
1301+
if (args.ArgC() < 2)
1302+
{
1303+
ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Usage: !savevc <name>");
1304+
return;
1305+
}
1306+
1307+
int iNumClients = 0;
1308+
int pSlots[MAXPLAYERS];
1309+
ETargetType nType;
1310+
ZEPlayer* pTargetPlayer = nullptr;
1311+
bool bIsAdmin = !player || player->GetZEPlayer()->IsAdminFlagSet(ADMFLAG_GENERIC);
1312+
std::string strTarget = (!bIsAdmin || args.ArgC() == 1) ? "@me" : args[1];
1313+
1314+
if (!g_playerManager->CanTargetPlayers(player, strTarget.c_str(), iNumClients, pSlots, NO_RANDOM | NO_MULTIPLE | NO_BOT, nType))
1315+
return;
1316+
1317+
CCSPlayerController* pTarget = CCSPlayerController::FromSlot(pSlots[0]);
1318+
pTargetPlayer = pTarget->GetZEPlayer();
1319+
1320+
std::vector<int16_t> audio = pTargetPlayer->GetVoiceChat();
1321+
1322+
std::ofstream file(
1323+
"audio.pcm",
1324+
std::ios::binary);
1325+
1326+
file.write(
1327+
reinterpret_cast<const char*>(audio.data()),
1328+
audio.size() * sizeof(int16_t));
1329+
}
12981330
#endif
12991331

13001332
void CAdmin::SetFlags(uint64 iFlags)

src/cs2fixes.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,6 +1244,7 @@ bool CS2Fixes::Hook_ProcessVoiceData(const CCLCMsg_VoiceData_t& msg)
12441244
RETURN_META_VALUE(MRES_IGNORED, true);
12451245

12461246
pPlayer->SetLastVoiceTime(GetGlobals()->curtime);
1247+
pPlayer->OnVoiceFrame(msg.audio());
12471248

12481249
RETURN_META_VALUE(MRES_IGNORED, true);
12491250
}

src/playermanager.cpp

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ CConVar<int> g_cvarAdminImmunityTargetting("cs2f_admin_immunity", FCVAR_NONE, "M
4747
CConVar<bool> g_cvarEnableMapSteamIds("cs2f_map_steamids_enable", FCVAR_NONE, "Whether to make Steam ID's available to maps", false);
4848

4949
ZEPlayerHandle::ZEPlayerHandle() :
50-
m_Index(INVALID_ZEPLAYERHANDLE_INDEX){};
50+
m_Index(INVALID_ZEPLAYERHANDLE_INDEX) {};
5151

5252
ZEPlayerHandle::ZEPlayerHandle(CPlayerSlot slot)
5353
{
@@ -96,6 +96,51 @@ ZEPlayer* ZEPlayerHandle::Get() const
9696
return pZEPlayer;
9797
}
9898

99+
std::vector<int16_t> ZEPlayer::GetVoiceChat()
100+
{
101+
int error = 0;
102+
OpusDecoder* decoder = opus_decoder_create(VOICECHAT_SAMPLERATE, VOICECHAT_CHANNELS, &error);
103+
if (error != OPUS_OK || decoder == nullptr || m_dequeVoicechat.empty())
104+
return std::vector<int16_t>();
105+
106+
std::vector<int16_t> pcm;
107+
// maximum packet duration (120ms; 5760 for 48kHz),
108+
const int frameBufLen = VOICECHAT_SAMPLERATE * 120 / 1000;
109+
std::vector<int16_t> frameBuf(frameBufLen * VOICECHAT_CHANNELS);
110+
111+
for (const auto& [_, packet] : m_dequeVoicechat)
112+
{
113+
const unsigned char* data =
114+
reinterpret_cast<const unsigned char*>(packet.data());
115+
int dataLen = static_cast<int>(packet.size());
116+
117+
int samples = opus_decode(
118+
decoder,
119+
data,
120+
dataLen,
121+
frameBuf.data(),
122+
frameBufLen,
123+
0);
124+
125+
if (samples < 0)
126+
{
127+
Panic("ZEPlayer::GetVoiceChat(): failed to parse opus: %s\n", opus_strerror(samples));
128+
129+
break;
130+
}
131+
else
132+
{
133+
pcm.insert(
134+
pcm.end(),
135+
frameBuf.begin(),
136+
frameBuf.begin() + samples * VOICECHAT_CHANNELS);
137+
}
138+
}
139+
140+
opus_decoder_destroy(decoder);
141+
return pcm;
142+
}
143+
99144
void ZEPlayer::OnSpawn()
100145
{
101146
SetSpeedMod(1.f);
@@ -759,6 +804,46 @@ void ZEPlayer::SetEntwatchHudSize(float flSize)
759804
pText->m_flFontSize = m_flEntwatchHudSize;
760805
}
761806

807+
void ZEPlayer::OnVoiceFrame(const CMsgVoiceAudio& msg)
808+
{
809+
// Message("OnVoiceFrame(...) "
810+
// "slot=%d "
811+
// "time=%f "
812+
// "format=%d "
813+
// "voice_data_size=%zu "
814+
// "sequence_bytes=%d "
815+
// "section_number=%u "
816+
// "sample_rate=%u "
817+
// "uncompressed_sample_offset=%u "
818+
// "num_packets=%u "
819+
// "voice_level=%f\n",
820+
// m_slot,
821+
// GetGlobals()->curtime,
822+
// static_cast<int>(msg.format()),
823+
// msg.voice_data().size(),
824+
// msg.sequence_bytes(),
825+
// msg.section_number(),
826+
// msg.sample_rate(),
827+
// msg.uncompressed_sample_offset(),
828+
// msg.num_packets(),
829+
// msg.voice_level());
830+
831+
// assume some things about the incoming voice chat
832+
// - all data is somewhat realtime: packets can be out of order, causing corruption; allow it
833+
// - the client controls the voice_level and sequence_bytes fields
834+
if ((msg.format() != VOICEDATA_FORMAT_OPUS) || (msg.num_packets() > UINT8_MAX) || (msg.voice_data().size() > UINT8_MAX))
835+
return;
836+
837+
// make a owned copy
838+
std::string voice_data = msg.voice_data();
839+
float time = GetGlobals()->curtime;
840+
m_dequeVoicechat.push_back(std::make_pair(time, voice_data));
841+
while ((!m_dequeVoicechat.empty()) && (m_dequeVoicechat.front().first < (time - VOICECHAT_SECONDS)))
842+
m_dequeVoicechat.pop_front();
843+
844+
return;
845+
}
846+
762847
void CPlayerManager::OnBotConnected(CPlayerSlot slot)
763848
{
764849
m_vecPlayers[slot.Get()] = new ZEPlayer(slot, true);

src/playermanager.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@
2525
#include "entity/cpointworldtext.h"
2626
#include "entity/lights.h"
2727
#include "gamesystem.h"
28+
#include "netmessages.h"
2829
#include "steam/isteamuser.h"
2930
#include "steam/steam_api_common.h"
3031
#include "steam/steamclientpublic.h"
3132
#include "utlvector.h"
33+
#include <deque>
34+
#include <opus/opus.h>
3235
#include <playerslot.h>
3336

3437
extern CConVar<bool> g_cvarFlashLightTransmitOthers;
@@ -56,6 +59,10 @@ extern CConVar<CUtlString> g_cvarFlashLightAttachment;
5659
#define ZSOUNDS_PREF_KEY_NAME "zsounds"
5760
#define INVALID_ZEPLAYERHANDLE_INDEX 0u
5861

62+
#define VOICECHAT_SAMPLERATE 48000
63+
#define VOICECHAT_CHANNELS 1
64+
#define VOICECHAT_SECONDS 60.0
65+
5966
static uint32 iZEPlayerHandleSerial = 0u; // this should actually be 3 bytes large, but no way enough players join in servers lifespan for this to be an issue
6067

6168
enum class ETargetType
@@ -273,6 +280,7 @@ class ZEPlayer
273280
void SetEntwatchHudSize(float flSize);
274281
void SetTopDefenderStatus(bool bStatus) { m_bTopDefender = bStatus; }
275282
void SetLastVoiceTime(float flTime) { m_flLastVoiceTime = flTime; }
283+
void OnVoiceFrame(const CMsgVoiceAudio& msg);
276284
void SetBeaconEnabledTime(float flTime) { m_flBeaconEnabledTime = flTime; }
277285

278286
uint64 GetAdminFlags() { return m_iAdminFlags; }
@@ -325,6 +333,7 @@ class ZEPlayer
325333
float GetEntwatchHudSize() { return m_flEntwatchHudSize; }
326334
bool GetTopDefenderStatus() { return m_bTopDefender; }
327335
float GetLastVoiceTime() { return m_flLastVoiceTime; }
336+
std::vector<int16_t> GetVoiceChat();
328337
float GetBeaconEnabledTime() { return m_flBeaconEnabledTime; }
329338

330339
void OnSpawn();
@@ -401,6 +410,7 @@ class ZEPlayer
401410
float m_flEntwatchHudSize;
402411
bool m_bTopDefender;
403412
float m_flLastVoiceTime;
413+
std::deque<std::pair<float, std::string>> m_dequeVoicechat;
404414
float m_flBeaconEnabledTime;
405415
};
406416

0 commit comments

Comments
 (0)