app/src/test/kotlin/com/honjimaku/subrep/SegmenterTest.kt (4108 bytes)
1 package com.honjimaku.subrep 2 3 import io.kotest.property.Arb 4 import io.kotest.property.arbitrary.float 5 import io.kotest.property.arbitrary.int 6 import io.kotest.property.arbitrary.list 7 import io.kotest.property.checkAll 8 import kotlinx.coroutines.runBlocking 9 import org.junit.Assert.assertEquals 10 import org.junit.Assert.assertNull 11 import org.junit.Assert.assertTrue 12 import org.junit.Test 13 import kotlin.random.Random 14 15 class SegmenterTest { 16 17 private val rate = 16_000 18 19 private fun silence(ms: Int) = FloatArray(rate * ms / 1000) 20 21 /** Loud noise, as speech would be: well above the gate. */ 22 private fun speech(ms: Int, random: Random) = FloatArray(rate * ms / 1000) { random.nextFloat() * 0.6f - 0.3f } 23 24 /** Feeds [sound] in frames of 100 ms, as the capture does, and returns the pieces. */ 25 private fun pieces(segmenter: Segmenter, sound: FloatArray): List<FloatArray> { 26 val out = ArrayList<FloatArray>() 27 val frame = rate / 10 28 var at = 0 29 while (at < sound.size) { 30 val n = minOf(frame, sound.size - at) 31 segmenter.feed(sound.copyOfRange(at, at + n), n, out) 32 at += n 33 } 34 return out 35 } 36 37 @Test 38 fun silenceGivesNoPiece(): Unit = runBlocking { 39 checkAll(Arb.int(0..30_000), Arb.float(0f, 0.003f)) { ms, amplitude -> 40 val random = Random(ms) 41 val quiet = FloatArray(rate * ms / 1000) { random.nextFloat() * 2 * amplitude - amplitude } 42 val segmenter = Segmenter() 43 assertTrue(pieces(segmenter, quiet).isEmpty()) 44 assertNull(segmenter.flush()) 45 } 46 } 47 48 /** One burst of speech between silences is one piece: the speech, a little before, and the pause after. */ 49 @Test 50 fun oneBurstOfSpeechIsOnePiece(): Unit = runBlocking { 51 checkAll(Arb.int(500..9_000)) { ms -> 52 val random = Random(ms) 53 val sound = silence(1_000) + speech(ms, random) + silence(2_000) 54 val out = pieces(Segmenter(), sound) 55 assertEquals("pieces for $ms ms", 1, out.size) 56 val samples = out[0].size 57 // The piece has the speech, up to 320 ms before it, the pause of 650 ms after it, and block rounding. 58 assertTrue("$samples samples for $ms ms", samples >= rate * ms / 1000) 59 assertTrue("$samples samples for $ms ms", samples <= rate * (ms + 320 + 650 + 2 * 32) / 1000) 60 } 61 } 62 63 @Test 64 fun aShortNoiseIsDropped() { 65 val random = Random(1) 66 val sound = silence(1_000) + speech(200, random) + silence(2_000) 67 assertTrue(pieces(Segmenter(), sound).isEmpty()) 68 } 69 70 /** Speech that goes on is cut at the maximum, and each cut piece is kept. */ 71 @Test 72 fun noPieceIsLongerThanTheMaximum(): Unit = runBlocking { 73 checkAll(Arb.list(Arb.int(100..15_000), 1..6)) { lengths -> 74 val random = Random(lengths.hashCode()) 75 var sound = silence(500) 76 for ((i, ms) in lengths.withIndex()) sound += if (i % 2 == 0) speech(ms, random) else silence(ms) 77 sound += silence(1_500) 78 val out = pieces(Segmenter(), sound) 79 for (piece in out) assertTrue("${piece.size} samples", piece.size <= rate * 11 + rate * 32 / 1000) 80 // A burst under 450 ms is dropped by design. A cut at the maximum can leave a rest 81 // under 450 ms behind too, so each piece may cost up to half a second of speech. 82 val spoken = lengths.filterIndexed { i, ms -> i % 2 == 0 && ms >= 600 }.sum() 83 val kept = out.sumOf { it.size } 84 assertTrue("kept $kept samples of $spoken ms of speech", kept >= rate * spoken / 1000 - (out.size + 1) * rate / 2) 85 } 86 } 87 88 @Test 89 fun theOpenPieceComesOutAtTheEnd() { 90 val random = Random(2) 91 val segmenter = Segmenter() 92 val out = pieces(segmenter, silence(500) + speech(1_500, random)) 93 assertTrue(out.isEmpty()) 94 val last = segmenter.flush() 95 assertTrue(last != null && last.size >= rate * 1_500 / 1000) 96 assertNull(segmenter.flush()) 97 } 98 }