diff --git a/src/adminsystem.cpp b/src/adminsystem.cpp index de883c24..8788622f 100644 --- a/src/adminsystem.cpp +++ b/src/adminsystem.cpp @@ -1295,6 +1295,38 @@ CON_COMMAND_CHAT_FLAGS(setteam, " - Set a player's team", ADM if (iNumClients > 1) PrintMultiAdminAction(nType, strCommandPlayerName, "moved", szAction); } + +CON_COMMAND_CHAT_FLAGS(savevc, " save voicechat as PCM audio for test", ADMFLAG_CHEATS) +{ + if (args.ArgC() < 2) + { + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Usage: !savevc "); + return; + } + + int iNumClients = 0; + int pSlots[MAXPLAYERS]; + ETargetType nType; + ZEPlayer* pTargetPlayer = nullptr; + bool bIsAdmin = !player || player->GetZEPlayer()->IsAdminFlagSet(ADMFLAG_GENERIC); + std::string strTarget = (!bIsAdmin || args.ArgC() == 1) ? "@me" : args[1]; + + if (!g_playerManager->CanTargetPlayers(player, strTarget.c_str(), iNumClients, pSlots, NO_RANDOM | NO_MULTIPLE | NO_BOT, nType)) + return; + + CCSPlayerController* pTarget = CCSPlayerController::FromSlot(pSlots[0]); + pTargetPlayer = pTarget->GetZEPlayer(); + + std::vector audio = pTargetPlayer->GetVoiceChat(); + + std::ofstream file( + "audio.pcm", + std::ios::binary); + + file.write( + reinterpret_cast(audio.data()), + audio.size() * sizeof(int16_t)); +} #endif void CAdmin::SetFlags(uint64 iFlags) diff --git a/src/cs2fixes.cpp b/src/cs2fixes.cpp index 6dee2ab3..bfeabdd7 100644 --- a/src/cs2fixes.cpp +++ b/src/cs2fixes.cpp @@ -1244,6 +1244,7 @@ bool CS2Fixes::Hook_ProcessVoiceData(const CCLCMsg_VoiceData_t& msg) RETURN_META_VALUE(MRES_IGNORED, true); pPlayer->SetLastVoiceTime(GetGlobals()->curtime); + pPlayer->OnVoiceFrame(msg.audio()); RETURN_META_VALUE(MRES_IGNORED, true); } diff --git a/src/playermanager.cpp b/src/playermanager.cpp index 14212c5f..4bf136ed 100644 --- a/src/playermanager.cpp +++ b/src/playermanager.cpp @@ -47,7 +47,7 @@ CConVar g_cvarAdminImmunityTargetting("cs2f_admin_immunity", FCVAR_NONE, "M CConVar g_cvarEnableMapSteamIds("cs2f_map_steamids_enable", FCVAR_NONE, "Whether to make Steam ID's available to maps", false); ZEPlayerHandle::ZEPlayerHandle() : - m_Index(INVALID_ZEPLAYERHANDLE_INDEX){}; + m_Index(INVALID_ZEPLAYERHANDLE_INDEX) {}; ZEPlayerHandle::ZEPlayerHandle(CPlayerSlot slot) { @@ -96,6 +96,51 @@ ZEPlayer* ZEPlayerHandle::Get() const return pZEPlayer; } +std::vector ZEPlayer::GetVoiceChat() +{ + int error = 0; + OpusDecoder* decoder = opus_decoder_create(VOICECHAT_SAMPLERATE, VOICECHAT_CHANNELS, &error); + if (error != OPUS_OK || decoder == nullptr || m_dequeVoicechat.empty()) + return std::vector(); + + std::vector pcm; + // maximum packet duration (120ms; 5760 for 48kHz), + const int frameBufLen = VOICECHAT_SAMPLERATE * 120 / 1000; + std::vector frameBuf(frameBufLen * VOICECHAT_CHANNELS); + + for (const auto& [_, packet] : m_dequeVoicechat) + { + const unsigned char* data = + reinterpret_cast(packet.data()); + int dataLen = static_cast(packet.size()); + + int samples = opus_decode( + decoder, + data, + dataLen, + frameBuf.data(), + frameBufLen, + 0); + + if (samples < 0) + { + Panic("ZEPlayer::GetVoiceChat(): failed to parse opus: %s\n", opus_strerror(samples)); + + break; + } + else + { + pcm.insert( + pcm.end(), + frameBuf.begin(), + frameBuf.begin() + samples * VOICECHAT_CHANNELS); + } + } + + opus_decoder_destroy(decoder); + return pcm; +} + void ZEPlayer::OnSpawn() { SetSpeedMod(1.f); @@ -759,6 +804,46 @@ void ZEPlayer::SetEntwatchHudSize(float flSize) pText->m_flFontSize = m_flEntwatchHudSize; } +void ZEPlayer::OnVoiceFrame(const CMsgVoiceAudio& msg) +{ + // Message("OnVoiceFrame(...) " + // "slot=%d " + // "time=%f " + // "format=%d " + // "voice_data_size=%zu " + // "sequence_bytes=%d " + // "section_number=%u " + // "sample_rate=%u " + // "uncompressed_sample_offset=%u " + // "num_packets=%u " + // "voice_level=%f\n", + // m_slot, + // GetGlobals()->curtime, + // static_cast(msg.format()), + // msg.voice_data().size(), + // msg.sequence_bytes(), + // msg.section_number(), + // msg.sample_rate(), + // msg.uncompressed_sample_offset(), + // msg.num_packets(), + // msg.voice_level()); + + // assume some things about the incoming voice chat + // - all data is somewhat realtime: packets can be out of order, causing corruption; allow it + // - the client controls the voice_level and sequence_bytes fields + if ((msg.format() != VOICEDATA_FORMAT_OPUS) || (msg.num_packets() > UINT8_MAX) || (msg.voice_data().size() > UINT8_MAX)) + return; + + // make a owned copy + std::string voice_data = msg.voice_data(); + float time = GetGlobals()->curtime; + m_dequeVoicechat.push_back(std::make_pair(time, voice_data)); + while ((!m_dequeVoicechat.empty()) && (m_dequeVoicechat.front().first < (time - VOICECHAT_SECONDS))) + m_dequeVoicechat.pop_front(); + + return; +} + void CPlayerManager::OnBotConnected(CPlayerSlot slot) { m_vecPlayers[slot.Get()] = new ZEPlayer(slot, true); diff --git a/src/playermanager.h b/src/playermanager.h index 835b0468..6870b7b0 100644 --- a/src/playermanager.h +++ b/src/playermanager.h @@ -25,10 +25,13 @@ #include "entity/cpointworldtext.h" #include "entity/lights.h" #include "gamesystem.h" +#include "netmessages.h" #include "steam/isteamuser.h" #include "steam/steam_api_common.h" #include "steam/steamclientpublic.h" #include "utlvector.h" +#include +#include #include extern CConVar g_cvarFlashLightTransmitOthers; @@ -56,6 +59,10 @@ extern CConVar g_cvarFlashLightAttachment; #define ZSOUNDS_PREF_KEY_NAME "zsounds" #define INVALID_ZEPLAYERHANDLE_INDEX 0u +#define VOICECHAT_SAMPLERATE 48000 +#define VOICECHAT_CHANNELS 1 +#define VOICECHAT_SECONDS 60.0 + 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 enum class ETargetType @@ -273,6 +280,7 @@ class ZEPlayer void SetEntwatchHudSize(float flSize); void SetTopDefenderStatus(bool bStatus) { m_bTopDefender = bStatus; } void SetLastVoiceTime(float flTime) { m_flLastVoiceTime = flTime; } + void OnVoiceFrame(const CMsgVoiceAudio& msg); void SetBeaconEnabledTime(float flTime) { m_flBeaconEnabledTime = flTime; } uint64 GetAdminFlags() { return m_iAdminFlags; } @@ -325,6 +333,7 @@ class ZEPlayer float GetEntwatchHudSize() { return m_flEntwatchHudSize; } bool GetTopDefenderStatus() { return m_bTopDefender; } float GetLastVoiceTime() { return m_flLastVoiceTime; } + std::vector GetVoiceChat(); float GetBeaconEnabledTime() { return m_flBeaconEnabledTime; } void OnSpawn(); @@ -401,6 +410,7 @@ class ZEPlayer float m_flEntwatchHudSize; bool m_bTopDefender; float m_flLastVoiceTime; + std::deque> m_dequeVoicechat; float m_flBeaconEnabledTime; };