Fix choppy voice at low FPS caused by per-frame decode cap - #5186
Open
QueryOfficial wants to merge 2 commits into
Open
Fix choppy voice at low FPS caused by per-frame decode cap#5186QueryOfficial wants to merge 2 commits into
QueryOfficial wants to merge 2 commits into
Conversation
9e28567 (multitheftauto#5175) capped remote voice decoding at 6 frames per game frame to bound CPU under relay floods. Speex emits one 20 ms frame per packet at every sample rate, so a speaker produces 50 packets per second; any listener rendering below 50 FPS, or receiving a coalesced burst, exceeds the cap and the surplus packets are silently dropped. Each dropped packet leaves a 20 ms hole, the BASS push stream stalls on it, and onClientPlayerVoiceStop fires mid-sentence. In practice voice became choppy for everyone in crowded areas, where FPS is lowest. Credit now accrues in real time at the voice frame rate and is spent per decode, capped at a burst ceiling. Legitimate speech never outruns real time so it is never throttled, regardless of the listener's frame rate; a flood still fills the ceiling and is then held to real time, so the CPU bound remains. The ceiling is exposed as max_voice_decode_burst (6-200, default 25 = 500 ms of audio) and sent to clients in the join packet behind eBitStreamVersion::VoiceDecodeBurstLimit. Either side lacking the version falls back to the default.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replace the fixed per-game-frame voice decode cap introduced in #5175 with a real-time token bucket, and expose the burst ceiling as a new server setting
max_voice_decode_burst.CClientPlayerVoice::DecodeAndBuffercurrently drops every packet past the 6th one received in a single game frame from the same speaker. This PR replaces that counter with credit that accrues at the voice frame rate (one 20 ms frame per 20 ms) and is spent per decode, capped at a configurable ceiling. Steady speech can never outrun real time, so it is never throttled regardless of the listener's frame rate; a flood still fills the ceiling and is then held to real time, so the CPU bound that #5175 introduced is preserved.Changes:
CClientPlayerVoice:m_voiceFramesThisPulse→TakeDecodeCredit()(real-time credit, burst ceiling).DoPulseno longer resets a counter.CVoiceRecorder: stores the ceiling received from the server; newVOICE_FRAME_DURATION_MSandVOICE_DECODE_BURST_DEFAULTconstants.CMainConfig: newmax_voice_decode_burst(6–200, default 25 = 500 ms of audio), in theSIntSettingtable so it also works withsetServerConfigSetting.CPlayerJoinCompletePacket/CPacketHandler: the ceiling is sent in the join packet, gated by a neweBitStreamVersion::VoiceDecodeBurstLimit. Either side lacking the version falls back to the default, so old server + new client and new server + old client both keep working.mtaserver.conf: documented the new setting next to the othermax_voice_*entries.Motivation
Since 9e28567 (#5175) landed, remote voice has become choppy for many players, most noticeably in crowded areas. #5175 was also backported to
release/1.6.0in 0ce13e2, so the bug is live in the 1.6 nightlies.Speex emits one 20 ms frame per packet at every sample rate, so a speaker sends ~50 packets per second. The cap of 6 decodes per game frame is only safe while the listener renders at ≥ 50 FPS with perfectly even packet arrival. In practice:
Everything past the 6th is silently discarded. Each dropped packet leaves a 20 ms hole; the BASS push stream has no jitter buffer, stalls on the hole, and fires
onClientPlayerVoiceStopmid-sentence. Speech that used to arrive as one continuous stream now arrives as many 250–350 ms fragments.Observed on a live server with a client-side counter on
onClientPlayerVoiceStart/Stopper speaker:The local player's own voice goes through the same
DecodeAndBufferpath but arrives one frame at a time with no network batching, which is why it is the only stream that stays intact — it never reaches the cap. Everyone else does.The DoS concern behind #5175 is legitimate; the problem is that a per-frame counter conflates "flood" with "listener is slower than the sender". A time-based budget separates the two: it bounds decode work per unit of real time rather than per rendered frame.
max_voice_decode_burstlets operators trade the two off explicitly. 25 (500 ms) is comfortably above any legitimate burst — anything longer than that has already stalled the stream, so decoding it late buys nothing — while still capping a flood at 25 decodes per frame per speaker (~1 ms of Speex work). The minimum of 6 reproduces current behaviour.Test plan
Reproduce the bug first, on current master or a 1.6 nightly, so the fix has a baseline:
onClientPlayerVoiceStart/onClientPlayerVoiceStopper speaker (segment count, and average time between start and stop).fpslimit 25. Have 3–5 other players talk at once for ~30 seconds.Then with this change:
fpslimit 25, 3–5 concurrent speakers. Segment count should be one per actual transmission;onClientPlayerVoiceStopshould fire only on key release.setServerConfigSetting("max_voice_decode_burst", 6), reconnect. Choppiness at low FPS should return, confirming the setting is live and 6 reproduces current behaviour. Set back to 25, reconnect: resolved. Note the value is applied at join, so a reconnect is required after changing it.For future regression checks: keep the per-speaker segment counter, cap FPS at 25, and confirm segment count stays at one per transmission.
Checklist