Conceal a lost voice packet with 20 ms, not 120 ms

A lost packet was concealed by decoding "nothing" into a buffer sized
for the longest Opus frame, and Opus fills whatever it is given: every
lost packet played 120 ms of made-up audio in place of 20 ms, piling up
delay on that speaker's line.

Decoders now conceal through their own call that takes the length to
make up, and the stream asks for the length of the last packet it got.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 08:18:06 +00:00
parent cc0243a7c6
commit 75d9606231
5 changed files with 44 additions and 24 deletions

View File

@@ -4,17 +4,20 @@ import com.ts3client.audio.opus.OpusCodec;
import com.ts3client.audio.opus.OpusDecoder;
import com.ts3client.audio.opus.OpusEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Stands in for libopus: a "packet" is one byte, decoded to a 20 ms frame holding that byte
* divided by 100 in every sample. Concealment decodes to -1, so it is easy to spot.
* divided by 100 in every sample. Concealment yields -1, so it is easy to spot.
*/
final class FakeOpus implements OpusCodec {
static final float CONCEALED = -1f;
int resets;
final List<Integer> concealedFrameSizes = new ArrayList<>();
static byte[] packet(int value) {
return new byte[]{(byte) value};
@@ -30,11 +33,17 @@ final class FakeOpus implements OpusCodec {
return new OpusDecoder() {
@Override
public int decode(byte[] packet, float[] out) {
float v = packet == null ? CONCEALED : packet[0] / 100f;
Arrays.fill(out, 0, VoiceFormat.FRAME_SIZE * channels, v);
Arrays.fill(out, 0, VoiceFormat.FRAME_SIZE * channels, packet[0] / 100f);
return VoiceFormat.FRAME_SIZE;
}
@Override
public int conceal(float[] out, int frameSize) {
concealedFrameSizes.add(frameSize);
Arrays.fill(out, 0, frameSize * channels, CONCEALED);
return frameSize;
}
@Override
public void reset() {
resets++;

View File

@@ -50,6 +50,8 @@ class VoiceStreamTest {
assertEquals(0.01f, pull());
assertEquals(FakeOpus.CONCEALED, pull());
assertEquals(0.03f, pull());
assertEquals(List.of(VoiceFormat.FRAME_SIZE), opus.concealedFrameSizes,
"a lost packet is concealed at the length of the one before, not the longest Opus frame");
}
@Test