From 7d46e3b9ebec5dad07293362c159ab46142cca0a Mon Sep 17 00:00:00 2001 From: jenz Date: Mon, 1 Jun 2026 01:37:09 +0100 Subject: [PATCH] further attempts at improving the voice chat quality for nosteamers from steam players, but still not good --- extension.cpp | 291 ++++++++++++++++++++++--------------------------- extension.h | 2 +- ringbuffer.cpp | 30 +++++ 3 files changed, 161 insertions(+), 162 deletions(-) diff --git a/extension.cpp b/extension.cpp index 28fe0fb..c8985b8 100644 --- a/extension.cpp +++ b/extension.cpp @@ -196,8 +196,6 @@ DETOUR_DECL_MEMBER1(ProcessVoiceData, bool, void *, msg) if (g_aFrameVoiceBytes[clientIndex] > NET_MAX_VOICE_BYTES_FRAME) return true; - g_fLastVoiceData[clientIndex] = gpGlobals->curtime; - // Get IClient for sending IClient *pClient = iserver->GetClient(playerSlot); if (!pClient) @@ -233,6 +231,7 @@ DETOUR_DECL_MEMBER1(ProcessVoiceData, bool, void *, msg) { //convert steam OPUS packet to CELT for no steam clients g_Interface.PushPlayerVoiceData(playerSlot, nBytes, voiceDataBuffer); + g_Interface.HandlePlayerVoiceData(playerSlot); //send celt packets to nosteamers. // Send steam Opus packet to Steam clients int maxClients = iserver->GetClientCount(); for (int i = 0; i < maxClients; i++) @@ -253,18 +252,12 @@ DETOUR_DECL_MEMBER1(ProcessVoiceData, bool, void *, msg) SendVoiceDataMsg(playerSlot, pToClient, (unsigned char *)voiceDataBuffer, nBytes, voiceMsg->m_xuid); } } + g_fLastVoiceData[clientIndex] = gpGlobals->curtime; return true; } DETOUR_DECL_STATIC4(SV_BroadcastVoiceData, void, IClient *, pClient, int, nBytes, char *, data, int64, xuid) { - //if(g_Interface.OnBroadcastVoiceData(pClient, nBytes, data)) - /* - if (1 == 2) //we dont do dis no more - { - DETOUR_STATIC_CALL(SV_BroadcastVoiceData)(pClient, nBytes, data, xuid); - } - */ } #ifdef _WIN32 @@ -505,8 +498,7 @@ bool CVoice::SDK_OnLoad(char *error, size_t maxlength, bool late) m_CeltEncoderSettings.FrameSize = 512; m_CeltEncoderSettings.PacketSize = 64; m_CeltEncoderSettings.Complexity = 10; - m_CeltEncoderSettings.FrameTime = (double)m_CeltEncoderSettings.FrameSize - / (double)m_CeltEncoderSettings.SampleRate_Hz; + m_CeltEncoderSettings.FrameTime = (double)m_CeltEncoderSettings.FrameSize / (double)m_CeltEncoderSettings.SampleRate_Hz; int theError; //torchlight celt @@ -812,8 +804,7 @@ void CVoice::SDK_OnUnload() void CVoice::OnGameFrame(bool simulating) { HandleNetwork(); - HandleVoiceData(); - HandlePlayerVoiceData(); //send celt packets to nosteamers. + HandleVoiceData(); //torchlight audio emitting HandleNoSteamVoiceData(); //send opus packets to steamers. // Reset per-client voice byte counter to 0 every frame. @@ -822,28 +813,7 @@ void CVoice::OnGameFrame(bool simulating) bool CVoice::OnBroadcastVoiceData(IClient *pClient, int nBytes, char *data) { - // Reject empty packets - if(nBytes < 1) - return false; - - int client = pClient->GetPlayerSlot() + 1; - - if (client < 1 || client > SM_MAXPLAYERS) - { - return false; - } - - // 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[client] += 5 + nBytes; - - if (g_aFrameVoiceBytes[client] > NET_MAX_VOICE_BYTES_FRAME) - { - return false; - } - - g_fLastVoiceData[client] = gpGlobals->curtime; - + //not actually used anymore anyways return true; } @@ -1244,20 +1214,16 @@ void CVoice::PushPlayerVoiceData(int playerSlot, int nBytes, char *data) if (!m_pCeltCodecPlayer[playerSlot]) { - // First time this player speaks - create encoder int theError; - m_pCeltCodecPlayer[playerSlot] = celt_encoder_create_custom( - m_pCeltModePlayer, 1, &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)); + 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]) @@ -1266,32 +1232,14 @@ void CVoice::PushPlayerVoiceData(int playerSlot, int nBytes, char *data) m_PlayerOpusDecoder[playerSlot] = opus_decoder_create(24000, 1, &err); if (err < 0) { - smutils->LogError(myself, "PushPlayerVoiceData: opus_decoder_create failed: %s", - opus_strerror(err)); + smutils->LogError(myself, "PushPlayerVoiceData: opus_decoder_create failed: %s", opus_strerror(err)); return; } } - const int OPUS_SAMPLE_RATE = 24000; const unsigned char *p = (const unsigned char *)data; - /* - // debugging real steam clients voice packets. - int dumpLen = nBytes < 32 ? nBytes : 32; - char hexBuf[32 * 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, "Steam packet in PushPlayerVoiceData: nBytes=%d first %d bytes: %s", nBytes, dumpLen, hexBuf); - */ - - //if its not opus packets we dont process it + // Verify raw network packets match standard Valve Opus voice signatures if (nBytes < 18 || p[8] != 0x0B || (p[11] != 0x05 && p[11] != 0x06)) return; @@ -1320,140 +1268,161 @@ void CVoice::PushPlayerVoiceData(int playerSlot, int nBytes, char *data) const unsigned char *opusFrame = p + offset; - int16_t pcmBuf[5760]; + // Maximum safe decoded PCM space buffer allocation (120ms frame at 24kHz = 2880 samples) + int16_t pcmBuf[2880]; int decoded = opus_decode(m_PlayerOpusDecoder[playerSlot], opusFrame, frameLen - 2, - pcmBuf, 5760, 0); + pcmBuf, 2880, 0); if (decoded <= 0) { - smutils->LogError(myself, "PushPlayerVoiceData: opus_decode failed: %s", - opus_strerror(decoded)); + smutils->LogError(myself, "PushPlayerVoiceData: opus_decode failed: %s", opus_strerror(decoded)); return; } - const int STEP = m_CeltEncoderSettings.SampleRate_Hz; - const int DIV = OPUS_SAMPLE_RATE; + // PHASE-PERFECT FIXED POINT RESAMPLING (24000 Hz -> 22050 Hz) + // Shift left into 16.16 fixed-point space to prevent rounding/truncation drift errors + uint32_t step_fp = ((uint32_t)24000 << 16) / (uint32_t)m_CeltEncoderSettings.SampleRate_Hz; + uint32_t curr_fp = m_playerResampleAccum[playerSlot]; // Treat tracking value storage as raw fixed-point register - for (int i = 0; i < decoded; i++) + // Allocate temporary staging stack vector array to drop into ring-buffer cleanly in one pass + int16_t resampledStaging[2880]; + int outSamplesCount = 0; + + while (true) { - m_playerResampleAccum[playerSlot] += STEP; - while (m_playerResampleAccum[playerSlot] >= DIV) - { - m_playerResampleAccum[playerSlot] -= DIV; - int16_t sample = pcmBuf[i]; - if (m_playerVoiceBuffer[playerSlot].CurrentFree() > 0) - m_playerVoiceBuffer[playerSlot].Push(&sample, 1); - } + uint32_t srcIndex = curr_fp >> 16; + if (srcIndex >= (uint32_t)decoded) + break; + + // Linear Interpolation over adjacent samples to prevent digital harmonic hiss + int16_t s1 = pcmBuf[srcIndex]; + int16_t s2 = (srcIndex + 1 < (uint32_t)decoded) ? pcmBuf[srcIndex + 1] : s1; + + uint32_t frac = curr_fp & 0xFFFF; + int32_t interpolatedSample = s1 + (((int32_t)(s2 - s1) * (int32_t)frac) >> 16); + + resampledStaging[outSamplesCount++] = (int16_t)interpolatedSample; + curr_fp += step_fp; } - // If buffer still doesn't have enough for one CELT frame, use Opus PLC to fill - if (m_playerVoiceBuffer[playerSlot].TotalLength() < (size_t)m_CeltEncoderSettings.FrameSize) + // Normalize state trackers back relative to zero base offset + m_playerResampleAccum[playerSlot] = curr_fp - ((uint32_t)decoded << 16); + + // Bulk push data down onto RingBuffer payload safely in one operation + if (outSamplesCount > 0 && (size_t)outSamplesCount <= m_playerVoiceBuffer[playerSlot].CurrentFree()) { - int16_t plcBuf[5760]; - int plcDecoded = opus_decode(m_PlayerOpusDecoder[playerSlot], - NULL, 0, // NULL = PLC mode - plcBuf, 480, 0); - if (plcDecoded > 0) - { - for (int i = 0; i < plcDecoded; i++) - { - m_playerResampleAccum[playerSlot] += m_CeltEncoderSettings.SampleRate_Hz; - while (m_playerResampleAccum[playerSlot] >= 24000) - { - m_playerResampleAccum[playerSlot] -= 24000; - int16_t sample = plcBuf[i]; - if (m_playerVoiceBuffer[playerSlot].CurrentFree() > 0) - m_playerVoiceBuffer[playerSlot].Push(&sample, 1); - } - } - } + m_playerVoiceBuffer[playerSlot].Push(resampledStaging, outSamplesCount); } } - //smutils->LogMessage(myself, "PushPlayerVoiceData: pushed, bufLen=%d", (int)m_playerVoiceBuffer[playerSlot].TotalLength()); } -void CVoice::HandlePlayerVoiceData() +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(); - for (int playerSlot = 0; playerSlot < maxClients; playerSlot++) + if (!m_pCeltCodecPlayer[playerSlot]) + return; + + size_t currentBufferLength = m_playerVoiceBuffer[playerSlot].TotalLength(); + if (currentBufferLength < (size_t)CELT_FRAME_SIZE) + return; + + // Use Engine Time instead of System Time to stay perfectly in sync with server tickrates + double now = (double)gpGlobals->curtime; + + double timeSinceLastVoice = now - g_fLastVoiceData[playerSlot + 1]; + if (timeSinceLastVoice > 0.5) // Reduced to 500ms for responsiveness { - if (!m_pCeltCodecPlayer[playerSlot]) - continue; + m_playerAvailableTime[playerSlot + 1] = 0.0; + + // Completely clear internal codec history matrices to ensure clean starts + if (m_pCeltCodecPlayer[playerSlot]) { + celt_encoder_ctl(m_pCeltCodecPlayer[playerSlot], CELT_RESET_STATE_REQUEST, NULL); + } + if (m_PlayerOpusDecoder[playerSlot]) { + opus_decoder_ctl(m_PlayerOpusDecoder[playerSlot], OPUS_RESET_STATE); + } + } - if (m_playerVoiceBuffer[playerSlot].TotalLength() < (size_t)CELT_FRAME_SIZE) - continue; + if (m_playerAvailableTime[playerSlot + 1] == 0.0) + { + m_playerAvailableTime[playerSlot + 1] = now; + return; + } - float timeAvailable = (float)m_playerVoiceBuffer[playerSlot].TotalLength() - / (float)m_CeltEncoderSettings.SampleRate_Hz; + double elapsed = now - m_playerAvailableTime[playerSlot + 1]; + int framesToEmit = (int)(elapsed / m_CeltEncoderSettings.FrameTime); + // DYNAMIC JITTER BUFFER CATCH-UP + // If the server lag spikes and calculates a massive frame burst, check what is actually in the buffer. + // There's no point trying to emit 37 frames if the client has only sent 4 frames of real data! + int framesInRealBuffer = (int)(currentBufferLength / (size_t)CELT_FRAME_SIZE); - //warmup encoder with 5 frames - if (m_playerAvailableTime[playerSlot] == 0.0 && - m_playerVoiceBuffer[playerSlot].TotalLength() >= (size_t)CELT_FRAME_SIZE * 11) + if (framesToEmit > framesInRealBuffer) + { + framesToEmit = framesInRealBuffer; + } + //smutils->LogMessage(myself, "framesToEmit: %d", framesToEmit); + + // Smooth-cap the maximum frames processed per server frame to avoid robotic bursts + // 4 frames = ~92ms of audio, which is an ideal ceiling for single-frame catchups. + if (framesToEmit > 4) + { + framesToEmit = 4; + } + + if (framesToEmit <= 0) + return; + + int framesProcessed = 0; + + while (framesProcessed < framesToEmit && + m_playerVoiceBuffer[playerSlot].TotalLength() >= (size_t)CELT_FRAME_SIZE) + { + int16_t celtInput[CELT_FRAME_SIZE]; + if (!m_playerVoiceBuffer[playerSlot].Pop(celtInput, CELT_FRAME_SIZE)) { - for (int w = 0; w < 10; w++) - { - int16_t warmupInput[CELT_FRAME_SIZE]; - if (!m_playerVoiceBuffer[playerSlot].Pop(warmupInput, CELT_FRAME_SIZE)) - break; - unsigned char discard[64]; - celt_encode(m_pCeltCodecPlayer[playerSlot], warmupInput, CELT_FRAME_SIZE, discard, 64); - } - m_playerAvailableTime[playerSlot] = getTime(); + break; } - //if (m_playerAvailableTime[playerSlot] < getTime() && timeAvailable < 0.2) - if (m_playerAvailableTime[playerSlot] < getTime() && timeAvailable < 0.05) - continue; - - if (m_playerAvailableTime[playerSlot] > getTime() + 1.0) - continue; - - int framesEmitted = 0; - while (m_playerVoiceBuffer[playerSlot].TotalLength() >= (size_t)CELT_FRAME_SIZE) + unsigned char celtPacket[CELT_PACKET_SIZE]; + int celtBytes = celt_encode(m_pCeltCodecPlayer[playerSlot], celtInput, + CELT_FRAME_SIZE, celtPacket, CELT_PACKET_SIZE); + if (celtBytes > 0) { - //smutils->LogMessage(myself, "framesEmitted: %d", framesEmitted); - if (framesEmitted >= 1) - break; - - int16_t celtInput[CELT_FRAME_SIZE]; - if (!m_playerVoiceBuffer[playerSlot].Pop(celtInput, CELT_FRAME_SIZE)) - break; - - unsigned char celtPacket[CELT_PACKET_SIZE]; - 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++) { - 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); - } - framesEmitted++; - } - else - { - smutils->LogError(myself, "HandlePlayerVoiceData: celt_encode failed: %d", celtBytes); - break; + 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); } } - - if (framesEmitted > 0) + else { - if (m_playerAvailableTime[playerSlot] < getTime()) - m_playerAvailableTime[playerSlot] = getTime(); - m_playerAvailableTime[playerSlot] += (double)framesEmitted * m_CeltEncoderSettings.FrameTime; + smutils->LogError(myself, "HandlePlayerVoiceData: celt_encode failed: %d", celtBytes); + break; } + + framesProcessed++; + } + + // Advance our tracking clock safely based on what we processed + if (framesProcessed > 0) + { + m_playerAvailableTime[playerSlot + 1] += (double)framesProcessed * m_CeltEncoderSettings.FrameTime; + } + else if (framesToEmit > 0) + { + // If we wanted to emit frames but the buffer was empty, pull the clock forward + // to 'now' so we don't build up a permanent structural timing lag. + m_playerAvailableTime[playerSlot + 1] = now; } } diff --git a/extension.h b/extension.h index d5c46d6..9086e80 100644 --- a/extension.h +++ b/extension.h @@ -143,6 +143,7 @@ public: void PushPlayerVoiceData(int playerSlot, int nBytes, char *data); void BroadcastVoiceDataCelt(IClient *pClient, int16_t *pPCM, int nSamples); void TranscodeNoSteamToSteam(int playerSlot, int nBytes, char *data); + void HandlePlayerVoiceData(int playerSlot); private: int m_ListenSocket; @@ -215,7 +216,6 @@ private: void HandleNetwork(); void OnDataReceived(CClient *pClient, int16_t *pData, size_t Samples); void HandleVoiceData(); - void HandlePlayerVoiceData(); void HandleNoSteamVoiceData(); }; diff --git a/ringbuffer.cpp b/ringbuffer.cpp index 2df026c..11d0efa 100644 --- a/ringbuffer.cpp +++ b/ringbuffer.cpp @@ -65,6 +65,7 @@ void CRingBuffer::Mix(int16_t *pData, size_t Samples) } } +/* bool CRingBuffer::Push(int16_t *pData, size_t Samples) { if(Samples > CurrentFree()) @@ -126,6 +127,35 @@ bool CRingBuffer::Push(int16_t *pData, size_t Samples) return true; } +*/ + +bool CRingBuffer::Push(int16_t *pData, size_t Samples) +{ + if(Samples > CurrentFree()) + return false; // Safely drop if buffer overflows, preventing distortion + + if(m_WriteIndex + Samples > m_BufferSize) + { + size_t TowardsEnd = m_BufferSize - m_WriteIndex; + memcpy(&m_aBuffer[m_WriteIndex], pData, TowardsEnd * sizeof(*m_aBuffer)); + m_WriteIndex = 0; + + size_t Left = Samples - TowardsEnd; + memcpy(m_aBuffer, &pData[TowardsEnd], Left * sizeof(*m_aBuffer)); + m_WriteIndex = Left; + } + else + { + memcpy(&m_aBuffer[m_WriteIndex], pData, Samples * sizeof(*m_aBuffer)); + m_WriteIndex += Samples; + } + + if(m_WriteIndex == m_BufferSize) + m_WriteIndex = 0; + + m_Length += Samples; + return true; +} size_t CRingBuffer::TotalLength() {