further attempts at improving the voice chat quality for nosteamers from steam players, but still not good

This commit is contained in:
jenz 2026-06-01 01:37:09 +01:00
parent 2184fd917a
commit 7d46e3b9eb
3 changed files with 161 additions and 162 deletions

View File

@ -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;
}
}

View File

@ -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();
};

View File

@ -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()
{