|
| 1 | +package app.echoloop |
| 2 | + |
| 3 | +import org.junit.Assert.assertEquals |
| 4 | +import org.junit.Assert.assertFalse |
| 5 | +import org.junit.Assert.assertTrue |
| 6 | +import org.junit.Test |
| 7 | + |
| 8 | +/** |
| 9 | + * AdaptiveVadThreshold 的 JVM 单元测试。 |
| 10 | + * |
| 11 | + * RMS 数值均为增益前的原始电平(与 WavRecorder.onBuffer 回调一致)。 |
| 12 | + */ |
| 13 | +class AdaptiveVadThresholdTest { |
| 14 | + |
| 15 | + private fun AdaptiveVadThreshold.feed(rms: Float, count: Int): List<Boolean> = |
| 16 | + List(count) { isVoiced(rms) } |
| 17 | + |
| 18 | + @Test |
| 19 | + fun `没有任何样本时使用最高门槛(等同旧固定门槛)`() { |
| 20 | + val vad = AdaptiveVadThreshold() |
| 21 | + assertEquals(AdaptiveVadThreshold.MAX_THRESHOLD, vad.threshold, 0f) |
| 22 | + assertFalse(vad.isVoiced(0.014f)) |
| 23 | + assertTrue(AdaptiveVadThreshold().isVoiced(0.015f)) |
| 24 | + } |
| 25 | + |
| 26 | + // 回归:用户日志中整段原始 RMS 仅约 0.007–0.011,旧固定门槛 0.015 |
| 27 | + // 把正在说话的小声用户判成静音,5s 兜底提前结束录音。 |
| 28 | + @Test |
| 29 | + fun `安静环境下小声说话被判为有声`() { |
| 30 | + val vad = AdaptiveVadThreshold() |
| 31 | + vad.feed(0.0015f, 8) // 开口前约 0.5s 环境底噪 |
| 32 | + |
| 33 | + val voiced = vad.feed(0.008f, 30) |
| 34 | + |
| 35 | + assertTrue("小声语音应全部判为有声", voiced.all { it }) |
| 36 | + assertTrue(vad.threshold < 0.008f) |
| 37 | + } |
| 38 | + |
| 39 | + @Test |
| 40 | + fun `嘈杂环境门槛封顶在最高值,不会无限升高`() { |
| 41 | + val vad = AdaptiveVadThreshold() |
| 42 | + vad.feed(0.012f, 20) // 底噪 × 倍数 = 0.036,远超上限 |
| 43 | + |
| 44 | + assertEquals(AdaptiveVadThreshold.MAX_THRESHOLD, vad.threshold, 0f) |
| 45 | + assertFalse("底噪本身不应判为有声", vad.isVoiced(0.012f)) |
| 46 | + assertTrue("达到旧门槛的语音必须判为有声", vad.isVoiced(0.02f)) |
| 47 | + } |
| 48 | + |
| 49 | + @Test |
| 50 | + fun `极安静环境门槛不低于最低值,避免微弱噪声被当成说话`() { |
| 51 | + val vad = AdaptiveVadThreshold() |
| 52 | + vad.feed(0.0003f, 20) |
| 53 | + |
| 54 | + assertEquals(AdaptiveVadThreshold.MIN_THRESHOLD, vad.threshold, 0f) |
| 55 | + assertFalse(vad.isVoiced(0.003f)) |
| 56 | + } |
| 57 | + |
| 58 | + @Test |
| 59 | + fun `录音启动时的全零 buffer 不参与底噪估计`() { |
| 60 | + val vad = AdaptiveVadThreshold() |
| 61 | + vad.feed(0f, 5) |
| 62 | + vad.feed(0.003f, 10) |
| 63 | + |
| 64 | + // 底噪按 0.003 估计:门槛 = 0.0075,而不是被全零拉到最低值 |
| 65 | + assertEquals(0.0075f, vad.threshold, 1e-6f) |
| 66 | + } |
| 67 | + |
| 68 | + // 取向「宁可多录几秒,也不要截断」:中等底噪下,略高于门槛的弱语音仍判为有声。 |
| 69 | + @Test |
| 70 | + fun `中等底噪下弱语音仍判为有声`() { |
| 71 | + val vad = AdaptiveVadThreshold() |
| 72 | + vad.feed(0.004f, 10) |
| 73 | + |
| 74 | + assertEquals(0.010f, vad.threshold, 1e-6f) |
| 75 | + assertTrue(vad.isVoiced(0.011f)) |
| 76 | + assertFalse(vad.isVoiced(0.004f)) |
| 77 | + } |
| 78 | + |
| 79 | + @Test |
| 80 | + fun `开口后长时间连续说话门槛不会被语音抬高`() { |
| 81 | + val vad = AdaptiveVadThreshold() |
| 82 | + vad.feed(0.0015f, 8) |
| 83 | + |
| 84 | + // 约 19s 连续语音(无明显停顿) |
| 85 | + val voiced = vad.feed(0.008f, 300) |
| 86 | + |
| 87 | + assertTrue(voiced.all { it }) |
| 88 | + } |
| 89 | + |
| 90 | + @Test |
| 91 | + fun `reset 后恢复初始门槛`() { |
| 92 | + val vad = AdaptiveVadThreshold() |
| 93 | + vad.feed(0.0015f, 10) |
| 94 | + vad.reset() |
| 95 | + |
| 96 | + assertEquals(AdaptiveVadThreshold.MAX_THRESHOLD, vad.threshold, 0f) |
| 97 | + assertEquals(null, vad.noiseFloor) |
| 98 | + } |
| 99 | +} |
0 commit comments