Fix shared microphone crackle from timestamp jitter
This commit is contained in:
1 parent
c9a3a1334b
commit
bb380441a3
4 files changed
+216
-31
No files matched your search
+91
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user