Fix shared microphone crackle from timestamp jitter

This commit is contained in:
evgeny committed 2026-10-22 16:53:25 +02:00
1 parent c9a3a1334b
commit bb380441a3
4 files changed
+216 -31

No files matched your search

@@ -5,8 +5,8 @@ import org.junit.Test
class MediaCaptureTimelineTest {
private val origin = 1_000_000_000L
private fun frame(time: Long, value: Int, epoch: Long = 1) =
MicrophoneCapture.Frame(ShortArray(1920) { value.toShort() }, time, 0, generation = epoch)
private fun frame(time: Long, value: Int, epoch: Long = 1, sequence: Long = (time - origin) * 48000 / 1_000_000_000) =
MicrophoneCapture.Frame(ShortArray(1920) { value.toShort() }, time, sequence, generation = epoch)
private fun pull(timeline: MediaCaptureTimeline) = timeline.pull(timeline.deadlineNs)
@Test fun switchingKeepsTimeAndSequenceAndRejectsOldDevice() {
@@ -68,4 +68,93 @@ class MediaCaptureTimelineTest {
assertFalse(timeline.offer(frame(origin + 480_000_000L, 2)))
assertEquals(1L, timeline.lateFrames)
}
@Test fun timestampJitterDoesNotInsertSilenceOrChangeAnySample() {
for (jitter in listOf(20_000L, 100_000L, 350_000L)) {
val timeline = MediaCaptureTimeline(origin)
timeline.reset(1)
repeat(510) { index ->
val pcm = ShortArray(1920) { ((index * 1920 + it) % 30000 + 1).toShort() }
val offset = if (index == 0) 0 else if (index % 2 == 0) jitter else -jitter
assertTrue(timeline.offer(MicrophoneCapture.Frame(pcm, origin + index * 20_000_000L + offset,
index * 960L, generation = 1)))
if (index >= 10) {
val output = pull(timeline)
val expected = ShortArray(1920) { (((index - 10) * 1920 + it) % 30000 + 1).toShort() }
assertArrayEquals("jitter=$jitter block=$index", expected, output.pcm)
assertEquals(0L, timeline.missingFrames)
assertEquals(0, timeline.ratePpm)
}
}
}
}
@Test fun actualSequenceGapIsSilenceAndLaterSoundKeepsItsPosition() {
val timeline = MediaCaptureTimeline(origin)
timeline.reset(1)
assertTrue(timeline.offer(frame(origin, 11)))
assertTrue(timeline.offer(frame(origin + 40_000_000, 22)))
assertTrue(pull(timeline).pcm.all { it == 11.toShort() })
assertTrue(pull(timeline).pcm.all { it == 0.toShort() })
assertTrue(pull(timeline).pcm.all { it == 22.toShort() })
assertEquals(960L, timeline.missingFrames)
}
@Test fun slowClockDriftIsResampledWithoutHolesOrSharpEdges() {
for (ppm in listOf(-300.0, 300.0)) {
val timeline = MediaCaptureTimeline(origin)
timeline.reset(1)
var sent = 0
var previous = 0
repeat(15000) { output ->
val until = output * 20_000_000L + MediaCaptureTimeline.RESERVE_NS
while (sent * 20_000_000.0 * (1 + ppm / 1_000_000) <= until) {
val pcm = ShortArray(1920) { channel ->
(10000 + 4000 * kotlin.math.sin(2 * Math.PI * 200 * (sent * 960L + channel / 2) / 48000)).toInt().toShort()
}
assertTrue(timeline.offer(MicrophoneCapture.Frame(pcm,
origin + (sent * 20_000_000.0 * (1 + ppm / 1_000_000)).toLong(), sent * 960L, generation = 1)))
sent++
}
val frame = pull(timeline)
for (i in 0 until 960) {
val sample = frame.pcm[i * 2].toInt()
assertTrue("zero/hole ppm=$ppm block=$output", sample >= 5999)
if (previous != 0) assertTrue("click ppm=$ppm block=$output", kotlin.math.abs(sample - previous) <= 110)
assertEquals(frame.pcm[i * 2], frame.pcm[i * 2 + 1])
previous = sample
}
assertEquals(0L, timeline.missingFrames)
}
assertTrue("clock correction ppm=$ppm actual=${timeline.ratePpm}", kotlin.math.abs(timeline.ratePpm + ppm) < 20)
}
}
@Test fun bluetoothTimestampJumpsDoNotInterruptContinuousPcm() {
val timeline = MediaCaptureTimeline(origin)
timeline.reset(1)
assertTrue(timeline.offer(frame(origin, 11)))
assertTrue(timeline.offer(frame(origin + 247_000_000, 22, sequence = 960)))
assertTrue(timeline.offer(frame(origin + 40_000_000, 33, sequence = 1920)))
assertTrue(pull(timeline).pcm.all { it == 11.toShort() })
assertTrue(pull(timeline).pcm.all { it == 22.toShort() })
assertTrue(pull(timeline).pcm.all { it == 33.toShort() })
assertEquals(2L, timeline.timestampJumps)
assertEquals(0L, timeline.missingFrames)
assertEquals(0, timeline.ratePpm)
}
@Test fun realCaptureStallReanchorsFreshPcmInsteadOfRejectingEveryFollowingBlock() {
val timeline = MediaCaptureTimeline(origin)
timeline.reset(1)
assertTrue(timeline.offer(MicrophoneCapture.Frame(ShortArray(1920) { 11 }, origin, 0, origin + 20_000_000, 1)))
assertTrue(pull(timeline).pcm.all { it == 11.toShort() })
repeat(10) { pull(timeline) }
assertTrue(timeline.offer(MicrophoneCapture.Frame(ShortArray(1920) { 22 }, origin + 240_000_000,
960, origin + 260_000_000, 1)))
assertTrue(pull(timeline).pcm.all { it == 0.toShort() })
assertTrue(pull(timeline).pcm.all { it == 22.toShort() })
assertEquals(1L, timeline.timestampJumps)
assertEquals(0L, timeline.lateFrames)
}
}