fix(rtp): keep the voice activity bit out of the audio level's sign - #1032
Merged
algesten merged 2 commits intoAug 28, 2026
Merged
Conversation
When serializing the RFC 6464 `ssrc-audio-level` extension, the V bit was forced to 1 for every non-zero level, so `voice_activity: Some(false)` was written — and read back — as `Some(true)`. `audio_level` is negative, so masking before negating keeps the sign in place: for -37, `0x7f & v1` is 0x5B (= 128 - 37) and `-(0x5B) as u8` is 0xA5 (= 128 + 37), whose bit 7 is already set. OR-ing the flag in afterwards can never clear it. The low seven bits happen to come out right, which is why the level always survived and only the flag was lost. Taking the magnitude first fixes it. Checked exhaustively: of the 256 (level, voice_activity) pairs on the scale, 127 used to round-trip with the wrong flag — all of them with the flag false and a non-zero level — and all 256 round-trip correctly now. The two golden-byte tests in `rtp::header` encoded the old behaviour: they build a header with `voice_activity: Some(false)` and expected 170 (0xAA) where the correct byte is 42 (0x2A). Updated, with a note saying why. The parsing tests are untouched — 0xAA on the wire really does mean "voice, level 42". Found while building an SFU on str0m: our own test asserted that a declared "not speech" flag survives forwarding, and it came back as speech every time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
algesten
approved these changes
Aug 28, 2026
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.
What
When serializing the RFC 6464
ssrc-audio-levelextension, the voice activity(V) bit is forced to
1for every non-zero level. A header written withvoice_activity: Some(false)is parsed back asSome(true).The audio level itself is always written correctly — only the flag is lost.
Why
audio_levelis negative, so masking it before negating leaves the sign inplace. For
v1 = -37:v1as bits0xDB0x7f & v10x5B= 128 − 37-(0x5B)asu80xA5= 128 + 37Bit 7 is already set before the flag term is OR-ed in, and OR cannot clear it.
The low seven bits come out as the magnitude, which is why the level survived
and only the flag was lost — and why this went unnoticed: with
voice_activity: Some(true)everything matches.Fix
Checked exhaustively over the scale: of the 256
(level, voice_activity)pairs,127 used to round-trip with the wrong flag — all with the flag
falseand anon-zero level — and all 256 round-trip correctly now. The added test
audio_level_round_tripwalks the whole scale and fails onmain(
left: Some(true), right: Some(false)).Test changes
The two golden-byte tests in
rtp::headerencoded the old behaviour: they buildheaders with
voice_activity: Some(false)and expect170(0xAA) where thecorrect byte is
42(0x2A). Updated with a note explaining why, so the changedoes not read as a loosened expectation.
The parsing tests are deliberately untouched:
0xAAon the wire really doesmean "voice, level 42", and the parse side needs no change.
cargo test --libis green (707 passed),cargo fmt --checkclean.How it was found
Building an SFU on str0m. Our own test asserted that a declared "not speech"
flag survives forwarding, and it came back as speech every time. For an SFU this
matters: the point of RFC 6464 is to make forwarding decisions without
decrypting or decoding, and speaker detection that trusts the V bit cannot tell
speech from steady background noise.