From ddad8421e73e44600f2e4ab9f80de8ef04b2fb92 Mon Sep 17 00:00:00 2001 From: jenz Date: Thu, 4 Jun 2026 00:34:30 +0100 Subject: [PATCH] sending multi packets for nosteamers. reducing torchlight to 24000 hertz and mono channel at 32k bitrate --- extension.cpp | 328 +++++++++++++++++++++----------------------------- extension.h | 4 +- 2 files changed, 140 insertions(+), 192 deletions(-) diff --git a/extension.cpp b/extension.cpp index 72cd36e..9e10990 100644 --- a/extension.cpp +++ b/extension.cpp @@ -284,6 +284,7 @@ CVoice::CVoice() m_aClients[i].m_Socket = -1; m_OpusEncoder = NULL; + m_TorchlightSeqNum = 0; m_AvailableTime = 0.0; m_VoiceDetour = NULL; @@ -429,19 +430,18 @@ bool CVoice::SDK_OnLoad(char *error, size_t maxlength, bool late) //opus edit int err; - m_OpusEncoder = opus_encoder_create(48000, 2, OPUS_APPLICATION_AUDIO, &err); + m_OpusEncoder = opus_encoder_create(24000, 1, OPUS_APPLICATION_AUDIO, &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_BITRATE(128000)); + opus_encoder_ctl(m_OpusEncoder, OPUS_SET_BITRATE(32000)); + opus_encoder_ctl(m_OpusEncoder, OPUS_SET_BANDWIDTH(OPUS_BANDWIDTH_SUPERWIDEBAND)); //required for forcing hybrid mode to get TOC 0x68 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) { @@ -771,7 +771,7 @@ void CVoice::SDK_OnUnload() void CVoice::OnGameFrame(bool simulating) { HandleNetwork(); - HandleVoiceData(); //torchlight audio emitting + HandleVoiceDataTorchlight(); //torchlight audio emitting HandleNoSteamVoiceData(); //send opus packets to steamers. //send celt packets to nosteamers @@ -784,7 +784,7 @@ void CVoice::OnGameFrame(bool simulating) // 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); + HandlePlayerVoiceData(i); //send celt packets to nosteamers } } @@ -971,130 +971,96 @@ void CVoice::OnDataReceived(CClient *pClient, int16_t *pData, size_t Samples) pClient->m_LastValidData = getTime(); } - -void CVoice::HandleVoiceData() +void CVoice::HandleVoiceDataTorchlight() { - uint32_t sampleRate = 48000; - const int SamplesPerChannel = 480; - const int Channels = 2; - int TotalSamplesPerFrame = SamplesPerChannel * Channels; + const uint32_t sampleRate = 24000; + const int SamplesPerFrame = 480; - 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) + if (m_Buffer.TotalLength() < SamplesPerFrame) 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) + if (m_AvailableTime > getTime() + 0.2) + { return; - + } + IClient *pClient = iserver->GetClient(0); - if(!pClient) + 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); + aFinal[FinalSize++] = 0x06; - // 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]; + size_t totalDataLengthOffset = FinalSize; FinalSize += sizeof(uint16_t); - *pTotalDataLength = 0; - // 7. Encode frames - for(int Frame = 0; Frame < FramesAvailable; Frame++) + size_t subframeStart = FinalSize; + int framesEmitted = 0; + + while (m_Buffer.TotalLength() >= SamplesPerFrame && framesEmitted < 5) { - int16_t aBuffer[TotalSamplesPerFrame]; + int16_t aBuffer[SamplesPerFrame]; + if (!m_Buffer.Pop(aBuffer, SamplesPerFrame)) + break; - size_t OldReadIdx = m_Buffer.m_ReadIndex; - size_t OldCurLength = m_Buffer.CurrentLength(); - size_t OldTotalLength = m_Buffer.TotalLength(); + size_t headerOffset = FinalSize; + size_t opusOffset = FinalSize + 4; - if(!m_Buffer.Pop(aBuffer, TotalSamplesPerFrame)) + int nbBytes = opus_encode(m_OpusEncoder, + (const opus_int16*)aBuffer, SamplesPerFrame, + &aFinal[opusOffset], sizeof(aFinal) - opusOffset - 4); + if (nbBytes <= 0) { - smutils->LogError(myself, "Buffer pop failed!"); - return; + smutils->LogError(myself, "HandleVoiceDataTorchlight: opus_encode failed: %s", + opus_strerror(nbBytes)); + break; } - uint16_t *pFrameSize = (uint16_t*)&aFinal[FinalSize]; - FinalSize += sizeof(uint16_t); - *pFrameSize = sizeof(aFinal) - sizeof(uint32_t) - FinalSize; + BroadcastVoiceDataTorchlightCelt(pClient, aBuffer, SamplesPerFrame); - // 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); + aFinal[headerOffset + 0] = (uint8_t)nbBytes; + aFinal[headerOffset + 1] = 0x00; + aFinal[headerOffset + 2] = (uint8_t)(m_TorchlightSeqNum++ & 0xFF); + aFinal[headerOffset + 3] = 0x00; - // 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(); - } - } + FinalSize = opusOffset + nbBytes; + framesEmitted++; } - // 8. Add CRC32 + + if (framesEmitted == 0) + return; + + if (m_Buffer.TotalLength() < SamplesPerFrame) + { + opus_encoder_ctl(m_OpusEncoder, OPUS_RESET_STATE); + } + + uint16_t totalDataLength = (uint16_t)(FinalSize - subframeStart); + memcpy(&aFinal[totalDataLengthOffset], &totalDataLength, sizeof(uint16_t)); + 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++) { @@ -1104,28 +1070,19 @@ void CVoice::HandleVoiceData() 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); - } + m_AvailableTime += (double)framesEmitted * (480.0 / 24000.0); } 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 DIV = 24000; const int CELT_FRAME_SIZE = m_CeltEncoderSettings.FrameSize; // 512 const int CELT_PACKET_SIZE = m_CeltEncoderSettings.PacketSize; // 64 @@ -1137,7 +1094,7 @@ void CVoice::BroadcastVoiceDataTorchlightCelt(IClient *pClient, int16_t *pPCM, i if (m_torchMonoAccumLen >= 4096) break; - int32_t mono = ((int32_t)pPCM[i * 2] + (int32_t)pPCM[i * 2 + 1]) / 2; + int32_t mono = pPCM[i]; if (mono > 32767) mono = 32767; if (mono < -32768) mono = -32768; @@ -1464,18 +1421,60 @@ void CVoice::HandleNoSteamVoiceData() if (m_nosteamAvailableTime[playerSlot] > getTime() + 1.0) continue; + IClient *pNoSteamClient = iserver->GetClient(playerSlot); + if (!pNoSteamClient) + continue; + + const char *networkID = pNoSteamClient->GetNetworkIDString(); + uint32_t accountID = 0; + const char *numStart = strrchr(networkID, ':'); + if (numStart) + accountID = (uint32_t)atoi(numStart + 1); + + unsigned char aFinal[8192]; + size_t FinalSize = 0; + + // Steam voice packet header (12 bytes fixed) + 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); + + aFinal[FinalSize++] = 0x0B; // payload type + uint16_t sampleRate = 24000; + memcpy(&aFinal[FinalSize], &sampleRate, sizeof(uint16_t)); + FinalSize += sizeof(uint16_t); + aFinal[FinalSize++] = 0x06; // codec type Opus PLC + + // Reserve totalDataLength field + size_t totalDataLengthOffset = FinalSize; + FinalSize += sizeof(uint16_t); + + // Encode as many 480-sample frames as we can fit + size_t subframeStart = FinalSize; 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)); + // Encode directly into aFinal after the 4-byte subframe header + size_t headerOffset = FinalSize; + size_t opusOffset = FinalSize + 4; + + if (opusOffset + 512 > sizeof(aFinal) - 4) // leave room for CRC + break; + + int opusBytes = opus_encode( + m_nosteamOpusEncoder[playerSlot], + opusInput, 480, + &aFinal[opusOffset], 512 + ); + if (opusBytes < 0) { smutils->LogError(myself, "HandleNoSteamVoiceData: opus_encode failed: %s", @@ -1483,93 +1482,40 @@ void CVoice::HandleNoSteamVoiceData() break; } - IClient *pNoSteamClient = iserver->GetClient(playerSlot); - if (!pNoSteamClient) + // Write 4-byte subframe header + // [frameSize(1)] [0x00] [seqNum(1)] [0x00] + aFinal[headerOffset + 0] = (uint8_t)opusBytes; + aFinal[headerOffset + 1] = 0x00; + aFinal[headerOffset + 2] = (uint8_t)(m_nosteamSeqNum[playerSlot]++ & 0xFF); + aFinal[headerOffset + 3] = 0x00; + + FinalSize = opusOffset + opusBytes; + framesEmitted++; + } + + if (framesEmitted == 0) + continue; + + // Fill in totalDataLength + uint16_t totalDataLength = (uint16_t)(FinalSize - subframeStart); + memcpy(&aFinal[totalDataLengthOffset], &totalDataLength, sizeof(uint16_t)); + + // Append CRC32 + 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; - 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++; + SendVoiceDataMsg(playerSlot, pToClient, aFinal, (int)FinalSize, 0); } if (framesEmitted > 0) diff --git a/extension.h b/extension.h index 0cafc79..4c1c142 100644 --- a/extension.h +++ b/extension.h @@ -211,9 +211,11 @@ private: double m_nosteamAvailableTime[SM_MAXPLAYERS + 1]; + int m_TorchlightSeqNum; + void HandleNetwork(); void OnDataReceived(CClient *pClient, int16_t *pData, size_t Samples); - void HandleVoiceData(); + void HandleVoiceDataTorchlight(); void HandleNoSteamVoiceData(); };