🤖 AI: A channel slot reused by a new client can have the previous occupant's audio attributed to it. On an -R server the stale frame lands in the new client's own recording, at full amplitude.
Root cause. Between disconnect and the new occupant negotiating transport properties a channel sits at CT_NONE, so decoder selection leaves CurOpusDecoder null. In that window nothing writes the channel's decode buffer: the OPUS decode is skipped for want of a decoder, and bIsRawAudio is false because iCeltNumCodedBytes has been reset to CELT_MINIMUM_NUM_BYTES. vecvecsData is indexed by position in the active-channel list rather than by channel ID, so it still holds the previous occupant's decoded audio — which is then passed to the recorder and read by the mix.
Reproduced 3 of 3 on 5146a072. Client A sends a 440 Hz tone and disconnects; after a gap client B takes the freed slot and sends only silence. B's own pre-identification recording is exactly 128 samples of A's tone — one OPUS frame, DOUBLE_SYSTEM_FRAME_SIZE_SAMPLES — at 440 Hz energy fraction 0.999 or above. A's own stub is silent in the same runs, so this is not a recorder artifact. Reconnect gaps of 100, 250 and 500 ms all reproduce it.
Scope: recordings. vecvecsData also feeds the mix, so the same stale samples are a candidate for the live path — but they do not survive there audibly. On a server run without -R, what a passive listener receives is statistically unchanged by the fix below: the residual artifact at slot handover measures about 59 dB under the source (peak sample 29 of 32767) both with it and without.
Fix — contribute silence in the no-decoder branch:
else
{
CurOpusDecoder = nullptr;
// CT_NONE: nothing else writes this buffer, and it is indexed by position in
// the active-channel list, so it still holds the previous occupant's audio
memset ( &vecvecsData[iChanCnt][0], 0, vecvecsData[iChanCnt].Size() * sizeof ( int16_t ) );
}
With that applied, B's stub reads 0.0 in 3 of 3 runs and A's own tone still records normally.
One subtlety: zero the whole worst-case buffer rather than iClientFrameSizeSamples worth. That variable is still zero in this branch, since it is only set in the CT_OPUS and CT_OPUS64 arms, so a memset scaled by it writes no bytes at all.
🤖 This message was written by AI and reviewed by @mcfnord.
🤖 AI: A channel slot reused by a new client can have the previous occupant's audio attributed to it. On an
-Rserver the stale frame lands in the new client's own recording, at full amplitude.Root cause. Between disconnect and the new occupant negotiating transport properties a channel sits at
CT_NONE, so decoder selection leavesCurOpusDecodernull. In that window nothing writes the channel's decode buffer: the OPUS decode is skipped for want of a decoder, andbIsRawAudiois false becauseiCeltNumCodedByteshas been reset toCELT_MINIMUM_NUM_BYTES.vecvecsDatais indexed by position in the active-channel list rather than by channel ID, so it still holds the previous occupant's decoded audio — which is then passed to the recorder and read by the mix.Reproduced 3 of 3 on
5146a072. Client A sends a 440 Hz tone and disconnects; after a gap client B takes the freed slot and sends only silence. B's own pre-identification recording is exactly 128 samples of A's tone — one OPUS frame,DOUBLE_SYSTEM_FRAME_SIZE_SAMPLES— at 440 Hz energy fraction 0.999 or above. A's own stub is silent in the same runs, so this is not a recorder artifact. Reconnect gaps of 100, 250 and 500 ms all reproduce it.Scope: recordings.
vecvecsDataalso feeds the mix, so the same stale samples are a candidate for the live path — but they do not survive there audibly. On a server run without-R, what a passive listener receives is statistically unchanged by the fix below: the residual artifact at slot handover measures about 59 dB under the source (peak sample 29 of 32767) both with it and without.Fix — contribute silence in the no-decoder branch:
With that applied, B's stub reads 0.0 in 3 of 3 runs and A's own tone still records normally.
One subtlety: zero the whole worst-case buffer rather than
iClientFrameSizeSamplesworth. That variable is still zero in this branch, since it is only set in theCT_OPUSandCT_OPUS64arms, so amemsetscaled by it writes no bytes at all.🤖 This message was written by AI and reviewed by @mcfnord.