/** * vim: set ts=4 : * ============================================================================= * SourceMod Sample Extension * Copyright (C) 2004-2008 AlliedModders LLC. All rights reserved. * ============================================================================= * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, version 3.0, as published by the * Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . * * As a special exception, AlliedModders LLC gives you permission to link the * code of this program (as well as its derivative works) to "Half-Life 2," the * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software * by the Valve Corporation. You must obey the GNU General Public License in * all respects for all other code used. Additionally, AlliedModders LLC grants * this exception to all derivative works. AlliedModders LLC defines further * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), * or . * * Version: $Id$ */ //#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include "inetmessage.h" #include "CDetour/detours.h" #include "extension.h" // voice packets are sent over unreliable netchannel //#define NET_MAX_DATAGRAM_PAYLOAD 4000 // = maximum unreliable payload size // voice packetsize = 64 | netchannel overflows at >4000 bytes // with 22050 samplerate and 512 frames per packet -> 23.22ms per packet // SVC_VoiceData overhead = 5 bytes // sensible limit of 8 packets per frame = 552 bytes -> 185.76ms of voice data per frame //#define NET_MAX_VOICE_BYTES_FRAME (8 * (5 + 64)) #define NET_MAX_VOICE_BYTES_FRAME (10 * (5 + 64)) ConVar g_SmVoiceAddr("sm_voice_addr", "127.0.0.1", FCVAR_PROTECTED, "Voice server listen ip address."); ConVar g_SmVoicePort("sm_voice_port", "27020", FCVAR_PROTECTED, "Voice server listen port.", true, 1025.0, true, 65535.0); /** * @file extension.cpp * @brief Implement extension code here. */ template inline T min_ext(T a, T b) { return a> 8); } return ~crc; } CVoice g_Interface; SMEXT_LINK(&g_Interface); CGlobalVars *gpGlobals = NULL; ISDKTools *g_pSDKTools = NULL; IServer *iserver = NULL; double g_fLastVoiceData[SM_MAXPLAYERS + 1]; int g_aFrameVoiceBytes[SM_MAXPLAYERS + 1]; bool g_bIsNoSteam[SM_MAXPLAYERS + 1]; bool g_bClientMuted[SM_MAXPLAYERS + 1][SM_MAXPLAYERS + 1]; // [muter][mutee] static void SendVoiceDataMsg(int fromClientSlot, IClient *pToClient, unsigned char *data, int nBytes, int64 xuid) { SVC_VoiceData msg; msg.m_bProximity = false; msg.m_nLength = nBytes * 8; // length in bits msg.m_xuid = xuid; msg.m_nFromClient = fromClientSlot; msg.m_DataOut = data; pToClient->SendNetMsg(msg); } #define Bits2Bytes(b) ((b+7)>>3) /* objdump -d /home/gameservers/css_dev/bin/engine_srv.so | grep -A 10 "_ZNK11CBaseClient13GetPlayerSlotEv" 00081a90 <_ZNK11CBaseClient13GetPlayerSlotEv>: 81a90: 55 push %ebp 81a91: 89 e5 mov %esp,%ebp 81a93: 8b 45 08 mov 0x8(%ebp),%eax 81a96: 5d pop %ebp 81a97: 8b 40 0c mov 0xc(%eax),%eax 81a9a: c3 ret 81a9b: 8d 74 26 00 lea 0x0(%esi,%eiz,1),%esi 81a9f: 90 nop 00081aa0 <_ZThn4_NK11CBaseClient13GetPlayerSlotEv>: mov 0xc(%eax),%eax -> offset is 12 bytes (0x0c) */ DETOUR_DECL_MEMBER1(ProcessVoiceData, bool, void *, msg) { //in member class we use "this" to obtain the client. int playerSlot = *(int*)((char*)this + 12); //m_nClientSlot member CBaseClient which CGameClient inherits from. int clientIndex = playerSlot + 1; CLC_VoiceData *voiceMsg = (CLC_VoiceData *)msg; char voiceDataBuffer[4096]; int bitsRead = voiceMsg->m_DataIn.ReadBitsClamped(voiceDataBuffer, voiceMsg->m_nLength); int nBytes = Bits2Bytes(bitsRead); //still need this super important check for preventing empty voice packets spam. if (nBytes < 1) return true; if (clientIndex < 1 || clientIndex > SM_MAXPLAYERS) return true; // Reject voice packet if we'd send more than NET_MAX_VOICE_BYTES_FRAME voice bytes from this client in the current frame. // 5 = SVC_VoiceData header/overhead g_aFrameVoiceBytes[clientIndex] += 5 + nBytes; if (g_aFrameVoiceBytes[clientIndex] > NET_MAX_VOICE_BYTES_FRAME) return true; // Get IClient for sending IClient *pClient = iserver->GetClient(playerSlot); if (!pClient) return true; // Detect NoSteam if (g_bIsNoSteam[clientIndex]) { //convert nosteam CELT packet to OPUS for steam clients g_Interface.TranscodeNoSteamToSteam(playerSlot, nBytes, voiceDataBuffer); //send nosteam CELT packet to no steam clients int maxClients = iserver->GetClientCount(); for (int i = 0; i < maxClients; i++) { IClient *pToClient = iserver->GetClient(i); if (!pToClient || !pToClient->IsConnected() || !pToClient->IsActive() || pToClient->IsFakeClient()) continue; bool bSelf = (i == playerSlot); if (bSelf) continue; if (!g_bIsNoSteam[i + 1]) continue; if (g_bClientMuted[i + 1][clientIndex]) continue; //smutils->LogMessage(myself, "in ProcessVoiceData before SendVoiceDataMsg"); SendVoiceDataMsg(playerSlot, pToClient, (unsigned char *)voiceDataBuffer, nBytes, voiceMsg->m_xuid); } } else { //convert steam OPUS packet to CELT for no steam clients g_Interface.PushPlayerVoiceData(playerSlot, nBytes, voiceDataBuffer); // Send steam Opus packet to Steam clients int maxClients = iserver->GetClientCount(); for (int i = 0; i < maxClients; i++) { IClient *pToClient = iserver->GetClient(i); if (!pToClient || !pToClient->IsConnected() || !pToClient->IsActive() || pToClient->IsFakeClient()) continue; bool bSelf = (i == playerSlot); if (bSelf) continue; if (g_bIsNoSteam[i + 1]) continue; if (g_bClientMuted[i + 1][clientIndex]) continue; //smutils->LogMessage(myself, "in ProcessVoiceData before SendVoiceDataMsg"); SendVoiceDataMsg(playerSlot, pToClient, (unsigned char *)voiceDataBuffer, nBytes, voiceMsg->m_xuid); } } return true; } DETOUR_DECL_STATIC4(SV_BroadcastVoiceData, void, IClient *, pClient, int, nBytes, char *, data, int64, xuid) { } double getTime() { struct timespec tv; if(clock_gettime(CLOCK_REALTIME, &tv) != 0) return 0; return (tv.tv_sec + (tv.tv_nsec / 1000000000.0)); } void OnGameFrame(bool simulating) { g_Interface.OnGameFrame(simulating); } CVoice::CVoice() { m_ListenSocket = -1; m_PollFds = 0; for(int i = 1; i < 1 + MAX_CLIENTS; i++) m_aPollFds[i].fd = -1; for(int i = 0; i < MAX_CLIENTS; i++) m_aClients[i].m_Socket = -1; m_OpusEncoder = NULL; m_AvailableTime = 0.0; m_VoiceDetour = NULL; m_ProcessVoiceDataDetour = NULL; m_SV_BroadcastVoiceData = NULL; m_TorchlightOpusDecoder = NULL; for (int i = 0; i <= SM_MAXPLAYERS; i++) { m_PlayerOpusDecoder[i] = NULL; } for (int i = 0; i <= SM_MAXPLAYERS; i++) { m_pCeltDecoder[i] = NULL; } m_pCeltMode = NULL; m_pCeltCodec = NULL; m_pCeltModePlayer = NULL; m_torchMonoAccumLen = 0; m_torchResampleAccum = 0; memset(m_torchMonoAccum, 0, sizeof(m_torchMonoAccum)); memset(m_nosteamSeqNum, 0, sizeof(m_nosteamSeqNum)); memset(g_bIsNoSteam, 0, sizeof(g_bIsNoSteam)); memset(m_playerAvailableTime, 0, sizeof(m_playerAvailableTime)); for (int i = 0; i <= SM_MAXPLAYERS; i++) m_pCeltCodecPlayer[i] = NULL; memset(g_bClientMuted, 0, sizeof(g_bClientMuted)); memset(m_nosteamResampleAccum, 0, sizeof(m_nosteamResampleAccum)); memset(m_nosteamAvailableTime, 0, sizeof(m_nosteamAvailableTime)); for (int i = 0; i <= SM_MAXPLAYERS; i++) m_nosteamOpusEncoder[i] = NULL; } bool CVoice::SDK_OnLoad(char *error, size_t maxlength, bool late) { // Setup engine-specific data. Dl_info info; void *engineFactory = (void *)g_SMAPI->GetEngineFactory(false); if(dladdr(engineFactory, &info) == 0) { g_SMAPI->Format(error, maxlength, "dladdr(engineFactory) failed."); return false; } void *pEngineSo = dlopen(info.dli_fname, RTLD_NOW); if(pEngineSo == NULL) { g_SMAPI->Format(error, maxlength, "dlopen(%s) failed.", info.dli_fname); return false; } int engineVersion = g_SMAPI->GetSourceEngineBuild(); void *adrVoiceData = NULL; switch (engineVersion) { case SOURCE_ENGINE_CSGO: #ifdef _WIN32 adrVoiceData = memutils->FindPattern(pEngineSo, "\x55\x8B\xEC\x81\xEC\xD0\x00\x00\x00\x53\x56\x57", 12); #else adrVoiceData = memutils->ResolveSymbol(pEngineSo, "_Z21SV_BroadcastVoiceDataP7IClientiPcx"); #endif break; case SOURCE_ENGINE_LEFT4DEAD2: #ifdef _WIN32 adrVoiceData = memutils->FindPattern(pEngineSo, "\x55\x8B\xEC\x83\xEC\x70\xA1\x2A\x2A\x2A\x2A\x33\xC5\x89\x45\xFC\xA1\x2A\x2A\x2A\x2A\x53\x56", 23); #else adrVoiceData = memutils->ResolveSymbol(pEngineSo, "_Z21SV_BroadcastVoiceDataP7IClientiPcx"); #endif break; case SOURCE_ENGINE_NUCLEARDAWN: #ifdef _WIN32 adrVoiceData = memutils->FindPattern(pEngineSo, "\x55\x8B\xEC\xA1\x2A\x2A\x2A\x2A\x83\xEC\x58\x57\x33\xFF", 14); #else adrVoiceData = memutils->ResolveSymbol(pEngineSo, "_Z21SV_BroadcastVoiceDataP7IClientiPcx"); #endif break; case SOURCE_ENGINE_INSURGENCY: #ifdef _WIN32 adrVoiceData = memutils->FindPattern(pEngineSo, "\x55\x8B\xEC\x83\xEC\x74\x68\x2A\x2A\x2A\x2A\x8D\x4D\xE4\xE8", 15); #else adrVoiceData = memutils->ResolveSymbol(pEngineSo, "_Z21SV_BroadcastVoiceDataP7IClientiPcx"); #endif break; case SOURCE_ENGINE_TF2: case SOURCE_ENGINE_CSS: case SOURCE_ENGINE_HL2DM: case SOURCE_ENGINE_DODS: case SOURCE_ENGINE_SDK2013: #ifdef _WIN32 adrVoiceData = memutils->FindPattern(pEngineSo, "\x55\x8B\xEC\xA1\x2A\x2A\x2A\x2A\x83\xEC\x50\x83\x78\x30", 14); #else adrVoiceData = memutils->ResolveSymbol(pEngineSo, "_Z21SV_BroadcastVoiceDataP7IClientiPcx"); #endif break; default: g_SMAPI->Format(error, maxlength, "Unsupported game."); dlclose(pEngineSo); return false; } //2026 signature of CGameClient::ProcessVoiceData void *adrProcessVoiceData = memutils->ResolveSymbol(pEngineSo, "_ZN11CGameClient16ProcessVoiceDataEP13CLC_VoiceData"); dlclose(pEngineSo); m_SV_BroadcastVoiceData = (t_SV_BroadcastVoiceData)adrVoiceData; if(!m_SV_BroadcastVoiceData) { g_SMAPI->Format(error, maxlength, "SV_BroadcastVoiceData sigscan failed."); return false; } // Setup voice detour. CDetourManager::Init(g_pSM->GetScriptingEngine(), NULL); m_VoiceDetour = DETOUR_CREATE_STATIC(SV_BroadcastVoiceData, adrVoiceData); if (!m_VoiceDetour) { g_SMAPI->Format(error, maxlength, "SV_BroadcastVoiceData detour failed."); return false; } m_VoiceDetour->EnableDetour(); //2026 detouring CGameClient::ProcessVoiceData m_ProcessVoiceDataDetour = DETOUR_CREATE_MEMBER(ProcessVoiceData, adrProcessVoiceData); if (!m_ProcessVoiceDataDetour) { g_SMAPI->Format(error, maxlength, "ProcessVoiceData detour failed."); return false; } m_ProcessVoiceDataDetour->EnableDetour(); //opus edit int err; m_OpusEncoder = opus_encoder_create(48000, 2, OPUS_APPLICATION_AUDIO, &err); if (err<0) { smutils->LogError(myself, "failed to create encode: %s", opus_strerror(err)); return false; } opus_encoder_ctl(m_OpusEncoder, OPUS_SET_BITRATE(128000)); opus_encoder_ctl(m_OpusEncoder, OPUS_SET_BANDWIDTH(OPUS_BANDWIDTH_FULLBAND)); opus_encoder_ctl(m_OpusEncoder, OPUS_SET_SIGNAL(OPUS_SIGNAL_MUSIC)); //MDCT mode opus_encoder_ctl(m_OpusEncoder, OPUS_SET_LSB_DEPTH(16)); opus_encoder_ctl(m_OpusEncoder, OPUS_SET_PACKET_LOSS_PERC(0)); opus_encoder_ctl(m_OpusEncoder, OPUS_SET_FORCE_CHANNELS(2)); if (err<0) { smutils->LogError(myself, "failed to set bitrate: %s\n", opus_strerror(err)); return false; } // Opus decoder for torchlight (48000 Hz stereo) int err2; m_TorchlightOpusDecoder = opus_decoder_create(48000, 2, &err2); if (err2 < 0) { smutils->LogError(myself, "failed to create torchlight opus decoder: %s", opus_strerror(err2)); return false; } // CELT encoder (22050 Hz mono, 512 samples/frame, 64 byte packets) m_CeltEncoderSettings.SampleRate_Hz = 22050; //m_CeltEncoderSettings.TargetBitRate_Kbps = 64; m_CeltEncoderSettings.TargetBitRate_Kbps = 48; m_CeltEncoderSettings.FrameSize = 512; m_CeltEncoderSettings.PacketSize = 64; m_CeltEncoderSettings.Complexity = 10; m_CeltEncoderSettings.FrameTime = (double)m_CeltEncoderSettings.FrameSize / (double)m_CeltEncoderSettings.SampleRate_Hz; int theError; //torchlight celt m_pCeltMode = celt_mode_create(m_CeltEncoderSettings.SampleRate_Hz, m_CeltEncoderSettings.FrameSize, &theError); if (!m_pCeltMode) { g_SMAPI->Format(error, maxlength, "celt_mode_create error: %d", theError); SDK_OnUnload(); return false; } m_pCeltCodec = celt_encoder_create_custom(m_pCeltMode, 1, &theError); if (!m_pCeltCodec) { g_SMAPI->Format(error, maxlength, "celt_encoder_create_custom error: %d", theError); SDK_OnUnload(); return false; } celt_encoder_ctl(m_pCeltCodec, CELT_RESET_STATE_REQUEST, NULL); celt_encoder_ctl(m_pCeltCodec, CELT_SET_BITRATE(m_CeltEncoderSettings.TargetBitRate_Kbps * 1000)); celt_encoder_ctl(m_pCeltCodec, CELT_SET_COMPLEXITY(m_CeltEncoderSettings.Complexity)); //Player celt m_pCeltModePlayer = celt_mode_create(m_CeltEncoderSettings.SampleRate_Hz, m_CeltEncoderSettings.FrameSize, &theError); if (!m_pCeltModePlayer) { g_SMAPI->Format(error, maxlength, "celt_mode_create 2 error: %d", theError); SDK_OnUnload(); return false; } return true; } bool CVoice::SDK_OnMetamodLoad(ISmmAPI *ismm, char *error, size_t maxlen, bool late) { GET_V_IFACE_CURRENT(GetEngineFactory, g_pCVar, ICvar, CVAR_INTERFACE_VERSION); gpGlobals = ismm->GetCGlobals(); ConVar_Register(0, this); return true; } bool CVoice::RegisterConCommandBase(ConCommandBase *pVar) { /* Always call META_REGCVAR instead of going through the engine. */ return META_REGCVAR(pVar); } cell_t IsClientTalking(IPluginContext *pContext, const cell_t *params) { int client = params[1]; if(client < 1 || client > SM_MAXPLAYERS) { return pContext->ThrowNativeError("Client index %d is invalid", client); } double d = gpGlobals->curtime - g_fLastVoiceData[client]; if(d < 0) // mapchange return false; if(d > 0.33) return false; return true; } cell_t Native_SetClientNoSteam(IPluginContext *pContext, const cell_t *params) { int client = params[1]; bool IsSteam = params[2]; if (client < 1 || client > SM_MAXPLAYERS) return pContext->ThrowNativeError("Invalid client index %d", client); g_bIsNoSteam[client] = IsSteam; return 1; } cell_t Native_SendCeltVoiceInit(IPluginContext *pContext, const cell_t *params) { int client = params[1]; if (client < 1 || client > SM_MAXPLAYERS) return pContext->ThrowNativeError("Invalid client index %d", client); IClient *pClient = iserver->GetClient(client - 1); if (!pClient || !pClient->IsConnected()) return 0; SVC_VoiceInit msg("vaudio_celt", 22050); pClient->SendNetMsg(msg); //smutils->LogMessage(myself, "Sent SVC_VoiceInit vaudio_celt to client %d", client); return 1; } cell_t Native_ClientMutedOtherClient(IPluginContext *pContext, const cell_t *params) { int clientmuter = params[1]; int clientmutee = params[2]; if (clientmuter < 1 || clientmuter > SM_MAXPLAYERS) return pContext->ThrowNativeError("Invalid clientmuter index %d", clientmuter); if (clientmutee < 0 || clientmutee > SM_MAXPLAYERS) //we allow muting the SourceTV return pContext->ThrowNativeError("Invalid clientmutee index %d", clientmutee); g_bClientMuted[clientmuter][clientmutee] = params[3]; //true or false //smutils->LogMessage(myself, "native g_bClientMuted: %i. clientmuter: %i. clientmutee: %i", g_bClientMuted[clientmuter][clientmutee], clientmuter, clientmutee); return 1; } const sp_nativeinfo_t MyNatives[] = { { "IsClientTalking", IsClientTalking }, { "SetClientNoSteam", Native_SetClientNoSteam }, { "SendCeltVoiceInit", Native_SendCeltVoiceInit }, { "ClientMutedOtherClient", Native_ClientMutedOtherClient }, { NULL, NULL } }; static void ListenSocketAction(void *pData) { CVoice *pThis = (CVoice *)pData; pThis->ListenSocket(); } void CVoice::SDK_OnAllLoaded() { sharesys->AddNatives(myself, MyNatives); sharesys->RegisterLibrary(myself, "Voice"); SM_GET_LATE_IFACE(SDKTOOLS, g_pSDKTools); if(g_pSDKTools == NULL) { smutils->LogError(myself, "SDKTools interface not found"); SDK_OnUnload(); return; } iserver = g_pSDKTools->GetIServer(); if(iserver == NULL) { smutils->LogError(myself, "Failed to get IServer interface from SDKTools!"); SDK_OnUnload(); return; } // Init tcp server m_ListenSocket = socket(AF_INET, SOCK_STREAM, 0); if(m_ListenSocket < 0) { smutils->LogError(myself, "Failed creating socket."); SDK_OnUnload(); return; } int yes = 1; if(setsockopt(m_ListenSocket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) < 0) { smutils->LogError(myself, "Failed setting SO_REUSEADDR on socket."); SDK_OnUnload(); return; } // This doesn't seem to work right away ... engine->ServerCommand("exec sourcemod/extension.Voice.cfg\n"); engine->ServerExecute(); // ... delay starting listen server to next frame smutils->AddFrameAction(ListenSocketAction, this); } void CVoice::ListenSocket() { if(m_PollFds > 0) return; sockaddr_in bindAddr; memset(&bindAddr, 0, sizeof(bindAddr)); bindAddr.sin_family = AF_INET; inet_aton(g_SmVoiceAddr.GetString(), &bindAddr.sin_addr); bindAddr.sin_port = htons(g_SmVoicePort.GetInt()); smutils->LogMessage(myself, "Binding to %s:%d!\n", g_SmVoiceAddr.GetString(), g_SmVoicePort.GetInt()); if(bind(m_ListenSocket, (sockaddr *)&bindAddr, sizeof(sockaddr_in)) < 0) { smutils->LogError(myself, "Failed binding to socket (%d '%s').", errno, strerror(errno)); SDK_OnUnload(); return; } if(listen(m_ListenSocket, MAX_CLIENTS) < 0) { smutils->LogError(myself, "Failed listening on socket."); SDK_OnUnload(); return; } m_aPollFds[0].fd = m_ListenSocket; m_aPollFds[0].events = POLLIN; m_PollFds++; smutils->AddGameFrameHook(::OnGameFrame); } void CVoice::SDK_OnUnload() { smutils->RemoveGameFrameHook(::OnGameFrame); if(m_VoiceDetour) { m_VoiceDetour->Destroy(); m_VoiceDetour = NULL; } if (m_ProcessVoiceDataDetour) { m_ProcessVoiceDataDetour->Destroy(); m_ProcessVoiceDataDetour = NULL; } if(m_ListenSocket != -1) { close(m_ListenSocket); m_ListenSocket = -1; } for(int Client = 0; Client < MAX_CLIENTS; Client++) { if(m_aClients[Client].m_Socket != -1) { close(m_aClients[Client].m_Socket); m_aClients[Client].m_Socket = -1; } } opus_encoder_destroy(m_OpusEncoder); if (m_TorchlightOpusDecoder) { opus_decoder_destroy(m_TorchlightOpusDecoder); m_TorchlightOpusDecoder = NULL; } for (int i = 0; i <= SM_MAXPLAYERS; i++) { if (m_PlayerOpusDecoder[i]) { opus_decoder_destroy(m_PlayerOpusDecoder[i]); m_PlayerOpusDecoder[i] = NULL; } } if (m_pCeltCodec) { celt_encoder_destroy(m_pCeltCodec); m_pCeltCodec = NULL; } if (m_pCeltMode) { celt_mode_destroy(m_pCeltMode); m_pCeltMode = NULL; } for (int i = 0; i <= SM_MAXPLAYERS; i++) { if (m_pCeltCodecPlayer[i]) { celt_encoder_destroy(m_pCeltCodecPlayer[i]); m_pCeltCodecPlayer[i] = NULL; } } if (m_pCeltModePlayer) { celt_mode_destroy(m_pCeltModePlayer); m_pCeltModePlayer = NULL; } for (int i = 0; i <= SM_MAXPLAYERS; i++) { if (m_pCeltDecoder[i]) { celt_decoder_destroy(m_pCeltDecoder[i]); m_pCeltDecoder[i] = NULL; } } for (int i = 0; i <= SM_MAXPLAYERS; i++) { if (m_nosteamOpusEncoder[i]) { opus_encoder_destroy(m_nosteamOpusEncoder[i]); m_nosteamOpusEncoder[i] = NULL; } } } void CVoice::OnGameFrame(bool simulating) { HandleNetwork(); HandleVoiceData(); //torchlight audio emitting HandleNoSteamVoiceData(); //send opus packets to steamers. //send celt packets to nosteamers int maxClients = iserver->GetClientCount(); for (int i = 0; i < maxClients; i++) { if (!m_pCeltCodecPlayer[i]) continue; // Keep draining 512-sample blocks until the buffer has less than one full frame remaining while (m_playerVoiceBuffer[i].TotalLength() >= m_CeltEncoderSettings.FrameSize) { HandlePlayerVoiceData(i); } } // Reset per-client voice byte counter to 0 every frame. memset(g_aFrameVoiceBytes, 0, sizeof(g_aFrameVoiceBytes)); } void CVoice::HandleNetwork() { if(m_ListenSocket == -1) return; int PollRes = poll(m_aPollFds, m_PollFds, 0); if(PollRes <= 0) return; // Accept new clients if(m_aPollFds[0].revents & POLLIN) { // Find slot int Client; for(Client = 0; Client < MAX_CLIENTS; Client++) { if(m_aClients[Client].m_Socket == -1) break; } // no free slot if(Client != MAX_CLIENTS) { sockaddr_in addr; socklen_t size = sizeof(sockaddr_in); int Socket = accept(m_ListenSocket, (sockaddr *)&addr, &size); m_aClients[Client].m_Socket = Socket; m_aClients[Client].m_BufferWriteIndex = 0; m_aClients[Client].m_LastLength = 0; m_aClients[Client].m_LastValidData = 0.0; m_aClients[Client].m_New = true; m_aClients[Client].m_UnEven = false; m_aPollFds[m_PollFds].fd = Socket; m_aPollFds[m_PollFds].events = POLLIN | POLLHUP; m_aPollFds[m_PollFds].revents = 0; m_PollFds++; //smutils->LogMessage(myself, "Client %d connected!\n", Client); } } bool CompressPollFds = false; for(int PollFds = 1; PollFds < m_PollFds; PollFds++) { int Client = -1; for(Client = 0; Client < MAX_CLIENTS; Client++) { if(m_aClients[Client].m_Socket == m_aPollFds[PollFds].fd) break; } if(Client == -1) continue; CClient *pClient = &m_aClients[Client]; // Connection shutdown prematurely ^C // Make sure to set SO_LINGER l_onoff = 1, l_linger = 0 if(m_aPollFds[PollFds].revents & POLLHUP) { close(pClient->m_Socket); pClient->m_Socket = -1; m_aPollFds[PollFds].fd = -1; CompressPollFds = true; //smutils->LogMessage(myself, "Client %d disconnected!(2)\n", Client); continue; } // Data available? if(!(m_aPollFds[PollFds].revents & POLLIN)) continue; size_t BytesAvailable; if(ioctl(pClient->m_Socket, FIONREAD, &BytesAvailable) == -1) continue; if(pClient->m_New) { pClient->m_BufferWriteIndex = m_Buffer.GetReadIndex(); pClient->m_New = false; } m_Buffer.SetWriteIndex(pClient->m_BufferWriteIndex); // Don't recv() when we can't fit data into the ringbuffer unsigned char aBuf[32768]; if(min_ext(BytesAvailable, sizeof(aBuf)) > m_Buffer.CurrentFree() * sizeof(int16_t)) continue; // Edge case: previously received data is uneven and last recv'd byte has to be prepended int Shift = 0; if(pClient->m_UnEven) { Shift = 1; aBuf[0] = pClient->m_Remainder; pClient->m_UnEven = false; } ssize_t Bytes = recv(pClient->m_Socket, &aBuf[Shift], sizeof(aBuf) - Shift, 0); if(Bytes <= 0) { close(pClient->m_Socket); pClient->m_Socket = -1; m_aPollFds[PollFds].fd = -1; CompressPollFds = true; //smutils->LogMessage(myself, "Client %d disconnected!(1)\n", Client); continue; } Bytes += Shift; // Edge case: data received is uneven (can't be divided by two) // store last byte, drop it here and prepend it right before the next recv if(Bytes & 1) { pClient->m_UnEven = true; pClient->m_Remainder = aBuf[Bytes - 1]; Bytes -= 1; } // Got data! OnDataReceived(pClient, (int16_t *)aBuf, Bytes / sizeof(int16_t)); pClient->m_LastLength = m_Buffer.CurrentLength(); pClient->m_BufferWriteIndex = m_Buffer.GetWriteIndex(); } if(CompressPollFds) { for(int PollFds = 1; PollFds < m_PollFds; PollFds++) { if(m_aPollFds[PollFds].fd != -1) continue; for(int PollFds_ = PollFds; PollFds_ < 1 + MAX_CLIENTS; PollFds_++) m_aPollFds[PollFds_].fd = m_aPollFds[PollFds_ + 1].fd; PollFds--; m_PollFds--; } } } void CVoice::OnDataReceived(CClient *pClient, int16_t *pData, size_t Samples) { // Check for empty input ssize_t DataStartsAt = -1; for(size_t i = 0; i < Samples; i++) { if(pData[i] == 0) continue; DataStartsAt = i; break; } // Discard empty data if last vaild data was more than a second ago. if(pClient->m_LastValidData + 1.0 < getTime()) { // All empty if(DataStartsAt == -1) return; // Data starts here pData += DataStartsAt; Samples -= DataStartsAt; } if(!m_Buffer.PushTorch(pData, Samples)) { smutils->LogError(myself, "Buffer push failed!!! Samples: %u, Free: %u\n", Samples, m_Buffer.CurrentFree()); return; } pClient->m_LastValidData = getTime(); } void CVoice::HandleVoiceData() { uint32_t sampleRate = 48000; const int SamplesPerChannel = 480; const int Channels = 2; int TotalSamplesPerFrame = SamplesPerChannel * Channels; int FramesAvailable = m_Buffer.TotalLength() / TotalSamplesPerFrame; if(!FramesAvailable) return; //int FramesMax = 5; int FramesMax = 2; //used to be 5 bool reset_state = false; if (FramesAvailable <= FramesMax) { reset_state = true; } FramesAvailable = min_ext(FramesAvailable, FramesMax); // Allow less buffering after audio has started playing if (m_Buffer.TotalLength() < TotalSamplesPerFrame) return; float TimeAvailable = (float)m_Buffer.TotalLength() / sampleRate; if (m_AvailableTime < getTime() && TimeAvailable < 0.2) return; double maxBuffer = (m_AvailableTime < getTime()) ? 0.2 : 0.04; if (m_AvailableTime > getTime() + maxBuffer) return; IClient *pClient = iserver->GetClient(0); if(!pClient) return; unsigned char aFinal[8192]; size_t FinalSize = 0; // 1. Steam Account ID (4 bytes) uint32_t steamID = 1; memcpy(&aFinal[FinalSize], &steamID, sizeof(uint32_t)); FinalSize += sizeof(uint32_t); // 2. Steam Community (4 bytes) uint32_t steamCommunity = 0x01100001; memcpy(&aFinal[FinalSize], &steamCommunity, sizeof(uint32_t)); FinalSize += sizeof(uint32_t); // 3. Payload Type 11 (1 byte) aFinal[FinalSize++] = 0x0B; // 4. Sample Rate (2 bytes little-endian) memcpy(&aFinal[FinalSize], &sampleRate, sizeof(uint16_t)); FinalSize += sizeof(uint16_t); // 5. Payload Type 5 for Opus. 6 for opus plc (1 byte) aFinal[FinalSize++] = 0x05; // 6. Reserve space for total data length (2 bytes little-endian) uint16_t *pTotalDataLength = (uint16_t*)&aFinal[FinalSize]; FinalSize += sizeof(uint16_t); *pTotalDataLength = 0; // 7. Encode frames for(int Frame = 0; Frame < FramesAvailable; Frame++) { int16_t aBuffer[TotalSamplesPerFrame]; size_t OldReadIdx = m_Buffer.m_ReadIndex; size_t OldCurLength = m_Buffer.CurrentLength(); size_t OldTotalLength = m_Buffer.TotalLength(); if(!m_Buffer.Pop(aBuffer, TotalSamplesPerFrame)) { smutils->LogError(myself, "Buffer pop failed!"); return; } uint16_t *pFrameSize = (uint16_t*)&aFinal[FinalSize]; FinalSize += sizeof(uint16_t); *pFrameSize = sizeof(aFinal) - sizeof(uint32_t) - FinalSize; // Encode with Opus //pcm opus_int16*: Input signal (interleaved if 2 channels). length is frame_size*channels*sizeof(opus_int16) int nbBytes = opus_encode(m_OpusEncoder, (const opus_int16*)aBuffer, SamplesPerChannel, &aFinal[FinalSize], *pFrameSize); if (nbBytes <= 1) { smutils->LogError(myself, "Opus encode failed: %s", opus_strerror(nbBytes)); return; } BroadcastVoiceDataTorchlightCelt(pClient, aBuffer, SamplesPerChannel); // Write frame size *pFrameSize = (uint16_t)nbBytes; *pTotalDataLength += sizeof(uint16_t) + nbBytes; FinalSize += nbBytes; // Check for buffer underruns for(int Client = 0; Client < MAX_CLIENTS; Client++) { CClient *pClient = &m_aClients[Client]; if(pClient->m_Socket == -1 || pClient->m_New == true) continue; m_Buffer.SetWriteIndex(pClient->m_BufferWriteIndex); if(m_Buffer.CurrentLength() > pClient->m_LastLength) { pClient->m_BufferWriteIndex = m_Buffer.GetReadIndex(); m_Buffer.SetWriteIndex(pClient->m_BufferWriteIndex); pClient->m_LastLength = m_Buffer.CurrentLength(); } } } // 8. Add CRC32 uint32_t crc32_value = UTIL_CRC32(aFinal, FinalSize); memcpy(&aFinal[FinalSize], &crc32_value, sizeof(uint32_t)); FinalSize += sizeof(uint32_t); int maxClients = iserver->GetClientCount(); for (int i = 0; i < maxClients; i++) { IClient *pToClient = iserver->GetClient(i); if (!pToClient || !pToClient->IsConnected() || !pToClient->IsActive() || pToClient->IsFakeClient()) continue; if (g_bClientMuted[i + 1][pClient->GetPlayerSlot()]) continue; if (!g_bIsNoSteam[i + 1]) { //sending opus packets to steam users. SendVoiceDataMsg(pClient->GetPlayerSlot(), pToClient, aFinal, FinalSize, 0); } } if (m_AvailableTime < getTime()) m_AvailableTime = getTime(); m_AvailableTime += (double)FramesAvailable * 0.01; if (reset_state) { opus_encoder_ctl(m_OpusEncoder, OPUS_RESET_STATE); } } void CVoice::BroadcastVoiceDataTorchlightCelt(IClient *pClient, int16_t *pPCM, int nSamples) { // pPCM is stereo interleaved at 48000 Hz (SamplesPerChannel * 2 total samples) // Downsample to mono 22050 Hz and accumulate into m_torchMonoAccum const int STEP = m_CeltEncoderSettings.SampleRate_Hz; // 22050 const int DIV = 48000; const int CELT_FRAME_SIZE = m_CeltEncoderSettings.FrameSize; // 512 const int CELT_PACKET_SIZE = m_CeltEncoderSettings.PacketSize; // 64 int fromSlot = pClient->GetPlayerSlot(); int maxClients = iserver->GetClientCount(); for (int i = 0; i < nSamples; i++) { if (m_torchMonoAccumLen >= 4096) break; int32_t mono = ((int32_t)pPCM[i * 2] + (int32_t)pPCM[i * 2 + 1]) / 2; if (mono > 32767) mono = 32767; if (mono < -32768) mono = -32768; m_torchResampleAccum += STEP; while (m_torchResampleAccum >= DIV) { m_torchResampleAccum -= DIV; if (m_torchMonoAccumLen < 4096) m_torchMonoAccum[m_torchMonoAccumLen++] = (int16_t)mono; } } while (m_torchMonoAccumLen >= CELT_FRAME_SIZE) { unsigned char celtPacket[CELT_PACKET_SIZE]; int celtBytes = celt_encode(m_pCeltCodec, m_torchMonoAccum, CELT_FRAME_SIZE, celtPacket, CELT_PACKET_SIZE); if (celtBytes > 0) { for (int i = 0; i < maxClients; i++) { IClient *pToClient = iserver->GetClient(i); if (!pToClient || !pToClient->IsConnected() || !pToClient->IsActive() || pToClient->IsFakeClient()) continue; if (!g_bIsNoSteam[i + 1]) //i dont actually understand why the index is one off here. continue; if (g_bClientMuted[i + 1][fromSlot]) continue; //smutils->LogMessage(myself, "right before SendVoiceDataMsg"); SendVoiceDataMsg(fromSlot, pToClient, celtPacket, celtBytes, 0); } } else { smutils->LogError(myself, "BroadcastVoiceDataTorchlightCelt: celt_encode failed: %d", celtBytes); } m_torchMonoAccumLen -= CELT_FRAME_SIZE; if (m_torchMonoAccumLen > 0) memmove(m_torchMonoAccum, m_torchMonoAccum + CELT_FRAME_SIZE, m_torchMonoAccumLen * sizeof(int16_t)); } } void CVoice::PushPlayerVoiceData(int playerSlot, int nBytes, char *data) { if (playerSlot < 0 || playerSlot >= SM_MAXPLAYERS) return; if (!m_pCeltCodecPlayer[playerSlot]) { int theError; m_pCeltCodecPlayer[playerSlot] = celt_encoder_create_custom(m_pCeltModePlayer, 1, &theError); if (!m_pCeltCodecPlayer[playerSlot]) { smutils->LogError(myself, "PushPlayerVoiceData: celt_encoder_create_custom failed: %d", theError); return; } celt_encoder_ctl(m_pCeltCodecPlayer[playerSlot], CELT_RESET_STATE_REQUEST, NULL); celt_encoder_ctl(m_pCeltCodecPlayer[playerSlot], CELT_SET_BITRATE(m_CeltEncoderSettings.TargetBitRate_Kbps * 1000)); celt_encoder_ctl(m_pCeltCodecPlayer[playerSlot], CELT_SET_COMPLEXITY(m_CeltEncoderSettings.Complexity)); } if (!m_PlayerOpusDecoder[playerSlot]) { int err; m_PlayerOpusDecoder[playerSlot] = opus_decoder_create(24000, 1, &err); if (err < 0) { smutils->LogError(myself, "PushPlayerVoiceData: opus_decoder_create failed: %s", opus_strerror(err)); return; } } double now = (double)gpGlobals->curtime; double timeSinceLastVoice = now - g_fLastVoiceData[playerSlot + 1]; if (timeSinceLastVoice > 0.5 && g_fLastVoiceData[playerSlot + 1] != 0.0) { celt_encoder_ctl(m_pCeltCodecPlayer[playerSlot], CELT_RESET_STATE_REQUEST, NULL); opus_decoder_ctl(m_PlayerOpusDecoder[playerSlot], OPUS_RESET_STATE); // Clear out any stale, partial remaining samples left over in the queue while(m_playerVoiceBuffer[playerSlot].TotalLength() > 0) { int16_t trash[512]; size_t toPop = m_playerVoiceBuffer[playerSlot].TotalLength() > 512 ? 512 : m_playerVoiceBuffer[playerSlot].TotalLength(); m_playerVoiceBuffer[playerSlot].Pop(trash, toPop); } //smutils->LogMessage(myself, "Voice states flushed cleanly. playerSlot: %d", playerSlot); } // Update the timestamp to mark this active packet's arrival g_fLastVoiceData[playerSlot + 1] = now; const unsigned char *p = (const unsigned char *)data; /* int dumpLen = nBytes < 24 ? nBytes : 24; char hexBuf[24 * 3 + 1]; int pos = 0; for (int i = 0; i < dumpLen; i++) { static const char hex[] = "0123456789ABCDEF"; hexBuf[pos++] = hex[(p[i] >> 4) & 0xF]; hexBuf[pos++] = hex[p[i] & 0xF]; hexBuf[pos++] = ' '; } hexBuf[pos] = '\0'; smutils->LogMessage(myself, "[INBOUND STEAM] nBytes=%d, First bytes: %s", nBytes, hexBuf); */ uint16_t totalDataLength; memcpy(&totalDataLength, p + 12, sizeof(uint16_t)); if ((int)(14 + totalDataLength + 4) != nBytes) return; uint32_t expectedCRC; memcpy(&expectedCRC, p + nBytes - 4, sizeof(uint32_t)); if (UTIL_CRC32(p, nBytes - 4) != expectedCRC) return; int offset = 14; int end = 14 + (int)totalDataLength; if (offset + 4 <= end) { uint16_t trueAudioPayloadLen; memcpy(&trueAudioPayloadLen, p + 12, sizeof(uint16_t)); // Frame 1 always starts exactly at offset 18 int frame1Start = 18; if (frame1Start >= end) return; const unsigned char *frame1Ptr = p + frame1Start; // Find the start of Frame 2 by scanning for the next 0x68 TOC marker int frame2Start = -1; // A standard 24kHz Opus frame will practically never be shorter than 30 bytes for (int i = frame1Start + 30; i < end - 4; i++) { if (p[i] == 0x68) //TOC -> 0x68 { // Verify if this is the start of Frame 2 frame2Start = i; break; } } int16_t pcmFrameBuffer[960]; int totalDecodedSamples = 0; // Calculate explicit size for Frame 1 int frame1Size = (frame2Start != -1) ? (frame2Start - frame1Start) : (end - frame1Start); if (frame1Size > 0) { //smutils->LogMessage(myself, "[VOICE-FIX] Frame 1 Determined Size: %d bytes. TOC: 0x%02X", frame1Size, frame1Ptr[0]); int samples1 = opus_decode(m_PlayerOpusDecoder[playerSlot], frame1Ptr, frame1Size, &pcmFrameBuffer[0], 480, 0); if (samples1 > 0) totalDecodedSamples += samples1; } // Process Frame 2 if a second TOC marker was identified if (frame2Start != -1) { const unsigned char *frame2Ptr = p + frame2Start; int frame2Size = end - frame2Start; // Frame 2 spans to the end of the payload buffer if (frame2Size > 0) { //smutils->LogMessage(myself, "[VOICE-FIX] Frame 2 Determined Size: %d bytes. TOC: 0x%02X", frame2Size, frame2Ptr[0]); int samples2 = opus_decode(m_PlayerOpusDecoder[playerSlot], frame2Ptr, frame2Size, &pcmFrameBuffer[totalDecodedSamples], 480, 0); if (samples2 > 0) totalDecodedSamples += samples2; } } int16_t resampledFrameBuffer[960]; int totalResampledSamples = 0; if (totalDecodedSamples > 0) { // The exact conversion ratio factor is 147 / 160 // 480 samples at 24kHz converts precisely into 441 samples at 22.05kHz // 960 samples (2 frames) converts precisely into 882 samples totalResampledSamples = (totalDecodedSamples * 147) / 160; for (int i = 0; i < totalResampledSamples; i++) { // Determine where this output sample lands on the input timeline double srcPosition = (double)i * 160.0 / 147.0; int srcIndex = (int)srcPosition; double fraction = srcPosition - (double)srcIndex; if (srcIndex + 1 < totalDecodedSamples) { // Linear interpolate between the two closest matching input samples int16_t sampleA = pcmFrameBuffer[srcIndex]; int16_t sampleB = pcmFrameBuffer[srcIndex + 1]; resampledFrameBuffer[i] = (int16_t)(sampleA + fraction * (sampleB - sampleA)); } else { // Handle boundary edge case for the final trailing sample resampledFrameBuffer[i] = pcmFrameBuffer[srcIndex]; } } } // --- PUSH SOUND TIMELINES SEQUENTIALLY --- if (totalResampledSamples > 0) { size_t freeSpace = m_playerVoiceBuffer[playerSlot].CurrentFree(); if ((size_t)totalResampledSamples <= freeSpace) { // Push the perfectly timed 22050Hz stream to the CELT ring buffer m_playerVoiceBuffer[playerSlot].Push(resampledFrameBuffer, totalResampledSamples); } } } } void CVoice::HandlePlayerVoiceData(int playerSlot) { const int CELT_FRAME_SIZE = m_CeltEncoderSettings.FrameSize; const int CELT_PACKET_SIZE = m_CeltEncoderSettings.PacketSize; int maxClients = iserver->GetClientCount(); if (!m_pCeltCodecPlayer[playerSlot]) return; size_t currentBufferLength = m_playerVoiceBuffer[playerSlot].TotalLength(); if (currentBufferLength < (size_t)CELT_FRAME_SIZE) { return; // Not enough data yet } // We have a solid block! Extract exactly one frame's worth of samples int16_t celtInput[512]; if (!m_playerVoiceBuffer[playerSlot].Pop(celtInput, CELT_FRAME_SIZE)) { return; } // Run the encoder pass on our clean block unsigned char celtPacket[64]; int celtBytes = celt_encode(m_pCeltCodecPlayer[playerSlot], celtInput, CELT_FRAME_SIZE, celtPacket, CELT_PACKET_SIZE); if (celtBytes > 0) { for (int i = 0; i < maxClients; i++) { IClient *pToClient = iserver->GetClient(i); if (!pToClient || !pToClient->IsConnected() || !pToClient->IsActive()) continue; if (!g_bIsNoSteam[i + 1]) continue; if (g_bClientMuted[i + 1][playerSlot + 1]) continue; SendVoiceDataMsg(playerSlot, pToClient, celtPacket, celtBytes, 0); } } else { smutils->LogError(myself, "HandlePlayerVoiceData: celt_encode failed: %d", celtBytes); } } void CVoice::TranscodeNoSteamToSteam(int playerSlot, int nBytes, char *data) { if (playerSlot < 0 || playerSlot >= SM_MAXPLAYERS) return; if (!m_pCeltDecoder[playerSlot]) { int err; m_pCeltDecoder[playerSlot] = celt_decoder_create_custom(m_pCeltModePlayer, 1, &err); if (!m_pCeltDecoder[playerSlot]) { smutils->LogError(myself, "TranscodeNoSteamToSteam: celt_decoder_create_custom failed: %d", err); return; } } if (!m_nosteamOpusEncoder[playerSlot]) { int err; m_nosteamOpusEncoder[playerSlot] = opus_encoder_create(24000, 1, OPUS_APPLICATION_AUDIO, &err); if (err < 0) { smutils->LogError(myself, "TranscodeNoSteamToSteam: opus_encoder_create failed: %s", opus_strerror(err)); return; } /* 0x68 TOC -> 01101000 -> c bits (6-7): 00 -> s bit (5): 0 (Mono) -> config bits (0-4): 01101 (13) Mode: Hybrid (SILK + CELT) Bandwidth: SWB Frame Size: 20 ms BITRATE 640000 changes TOC to 0xD8 */ opus_encoder_ctl(m_nosteamOpusEncoder[playerSlot], OPUS_SET_BITRATE(32000)); opus_encoder_ctl(m_nosteamOpusEncoder[playerSlot], OPUS_SET_SIGNAL(OPUS_SIGNAL_VOICE)); } int16_t pcmBuf[512]; int decoded = celt_decode(m_pCeltDecoder[playerSlot], (const unsigned char *)data, nBytes, pcmBuf, m_CeltEncoderSettings.FrameSize); if (decoded < 0) { smutils->LogError(myself, "TranscodeNoSteamToSteam: celt_decode failed: %d", decoded); return; } // Resample 22050 -> 24000 and push into ring buffer const int STEP = 24000; const int DIV = 22050; for (int i = 0; i < decoded; i++) { m_nosteamResampleAccum[playerSlot] += STEP; while (m_nosteamResampleAccum[playerSlot] >= DIV) { m_nosteamResampleAccum[playerSlot] -= DIV; int16_t sample = pcmBuf[i]; if (m_nosteamOpusPCMBuffer[playerSlot].CurrentFree() > 0) m_nosteamOpusPCMBuffer[playerSlot].Push(&sample, 1); } } } void CVoice::HandleNoSteamVoiceData() { int maxClients = iserver->GetClientCount(); for (int playerSlot = 0; playerSlot < maxClients; playerSlot++) { if (!m_nosteamOpusEncoder[playerSlot]) continue; if (m_nosteamOpusPCMBuffer[playerSlot].TotalLength() < 480) continue; float timeAvailable = (float)m_nosteamOpusPCMBuffer[playerSlot].TotalLength() / 24000.0f; if (m_nosteamAvailableTime[playerSlot] < getTime() && timeAvailable < 0.1) continue; if (m_nosteamAvailableTime[playerSlot] > getTime() + 1.0) continue; int framesEmitted = 0; while (m_nosteamOpusPCMBuffer[playerSlot].TotalLength() >= 480) { if (framesEmitted >= 1) break; int16_t opusInput[480]; if (!m_nosteamOpusPCMBuffer[playerSlot].Pop(opusInput, 480)) break; unsigned char opusBuf[512]; int opusBytes = opus_encode(m_nosteamOpusEncoder[playerSlot], opusInput, 480, opusBuf, sizeof(opusBuf)); if (opusBytes < 0) { smutils->LogError(myself, "HandleNoSteamVoiceData: opus_encode failed: %s", opus_strerror(opusBytes)); break; } IClient *pNoSteamClient = iserver->GetClient(playerSlot); if (!pNoSteamClient) continue; const char *networkID = pNoSteamClient->GetNetworkIDString(); uint32_t accountID = 0; // Parse [U:1:XXXXXXXX] format const char *numStart = strrchr(networkID, ':'); if (numStart) accountID = (uint32_t)atoi(numStart + 1); // Build modern Steam voice packet with working routing blocks unsigned char aFinal[8192]; size_t FinalSize = 0; // Build Steam voice packet header uint32_t steamID = accountID; uint32_t steamCommunity = 0x01100001; memcpy(&aFinal[FinalSize], &steamID, sizeof(uint32_t)); FinalSize += sizeof(uint32_t); memcpy(&aFinal[FinalSize], &steamCommunity, sizeof(uint32_t)); FinalSize += sizeof(uint32_t); // 2. Payload Type (1 byte) aFinal[FinalSize++] = 0x0B; // 3. Sample Rate (2 bytes little-endian) uint16_t sampleRate = 24000; memcpy(&aFinal[FinalSize], &sampleRate, sizeof(uint16_t)); FinalSize += sizeof(uint16_t); // 4. Codec Type (1 byte) - 0x06 for Opus PLC aFinal[FinalSize++] = 0x06; // 5. Track where Size 1 lives size_t totalDataLengthOffset = FinalSize; FinalSize += sizeof(uint16_t); // Reserve 2 bytes // 6. Sub-Frame Header size (2 bytes) - MUST be exactly raw opusBytes count uint16_t frameSize = (uint16_t)opusBytes; memcpy(&aFinal[FinalSize], &frameSize, sizeof(uint16_t)); FinalSize += sizeof(uint16_t); // 7. Sequence Number (2 bytes) uint16_t seq = m_nosteamSeqNum[playerSlot]++; memcpy(&aFinal[FinalSize], &seq, sizeof(uint16_t)); FinalSize += sizeof(uint16_t); // 8. Raw Opus Audio Payload memcpy(&aFinal[FinalSize], opusBuf, opusBytes); FinalSize += opusBytes; uint16_t totalRemainingBytes = (uint16_t)(sizeof(uint16_t) + sizeof(uint16_t) + opusBytes); memcpy(&aFinal[totalDataLengthOffset], &totalRemainingBytes, sizeof(uint16_t)); // 10. Compute and append CRC32 checksum over the complete array built so far uint32_t crc = UTIL_CRC32(aFinal, FinalSize); memcpy(&aFinal[FinalSize], &crc, sizeof(uint32_t)); FinalSize += sizeof(uint32_t); for (int i = 0; i < maxClients; i++) { IClient *pToClient = iserver->GetClient(i); if (!pToClient || !pToClient->IsConnected() || !pToClient->IsActive() || pToClient->IsFakeClient()) continue; if (g_bIsNoSteam[i + 1]) continue; if (g_bClientMuted[i + 1][playerSlot + 1]) continue; // debugging voice packets /* int dumpLen = (int)FinalSize < 32 ? (int)FinalSize : 32; char hexBuf[32 * 3 + 1]; int pos = 0; for (int i = 0; i < dumpLen; i++) { static const char hex[] = "0123456789ABCDEF"; hexBuf[pos++] = hex[(aFinal[i] >> 4) & 0xF]; hexBuf[pos++] = hex[aFinal[i] & 0xF]; hexBuf[pos++] = ' '; } hexBuf[pos] = '\0'; smutils->LogMessage(myself, "CVoice::HandleNoSteamVoiceData packet: FinalSize=%d first %d bytes: %s", (int)FinalSize, dumpLen, hexBuf); */ SendVoiceDataMsg(playerSlot, pToClient, aFinal, (int)FinalSize, 0); } framesEmitted++; } if (framesEmitted > 0) { if (m_nosteamAvailableTime[playerSlot] < getTime()) m_nosteamAvailableTime[playerSlot] = getTime(); m_nosteamAvailableTime[playerSlot] += (double)framesEmitted * (480.0 / 24000.0); } } }