diff options
Diffstat (limited to 'core/src/commonTest/kotlin')
| -rw-r--r-- | core/src/commonTest/kotlin/coffee/liz/rookery/StackmatTest.kt | 83 |
1 files changed, 83 insertions, 0 deletions
diff --git a/core/src/commonTest/kotlin/coffee/liz/rookery/StackmatTest.kt b/core/src/commonTest/kotlin/coffee/liz/rookery/StackmatTest.kt new file mode 100644 index 0000000..f6071ff --- /dev/null +++ b/core/src/commonTest/kotlin/coffee/liz/rookery/StackmatTest.kt @@ -0,0 +1,83 @@ +package coffee.liz.rookery + +import coffee.liz.rookery.stackmat.StackmatDecoder +import coffee.liz.rookery.stackmat.StackmatPacket +import coffee.liz.rookery.stackmat.StackmatSession +import kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class StackmatTest { + private fun packetBytes(status: Char, digits: String): List<Int> = + listOf(status.code) + digits.map { it.code } + (64 + digits.sumOf { it - '0' }) + listOf('\n'.code, '\r'.code) + + /** Audio for a list of packets, the way a sound card hears it: optional inversion, AC coupling, noise. */ + private fun audio(packets: List<List<Int>>, rate: Int, invert: Boolean, noise: Float, rng: Random = Random(1)): FloatArray { + val bits = ArrayList<Boolean>() + repeat(40) { bits += true } + for (p in packets) { + for (b in p) { bits += false; for (k in 0 until 8) bits += (b shr k) and 1 == 1; bits += true } + repeat(30) { bits += true } // rest between packets + } + val spb = rate / 1200.0 + val n = (bits.size * spb).toInt() + val out = FloatArray(n) + var hp = 0f; var prev = 0f + val alpha = 1f - 1f / (0.004f * rate) // ~4 ms high-pass + for (i in 0 until n) { + val raw = (if (bits[(i / spb).toInt()] != invert) 0.5f else -0.5f) + hp = alpha * (hp + raw - prev); prev = raw + out[i] = hp + (rng.nextFloat() - 0.5f) * noise + } + return out + } + + private fun decode(samples: FloatArray, rate: Int, chunk: Int = 512): List<StackmatPacket> { + val got = mutableListOf<StackmatPacket>() + val d = StackmatDecoder(rate) { got += it } + var at = 0 + while (at < samples.size) { val n = minOf(chunk, samples.size - at); d.feed(samples.copyOfRange(at, at + n)); at += n } + return got + } + + private val sequence = listOf(packetBytes('I', "000000"), packetBytes('A', "000000"), packetBytes(' ', "001234"), packetBytes(' ', "012345"), packetBytes('S', "112345"), packetBytes('S', "112345")) + private val expected = listOf( + StackmatPacket('I', 0, 6), StackmatPacket('A', 0, 6), StackmatPacket(' ', 1_234, 6), StackmatPacket(' ', 12_345, 6), + StackmatPacket('S', 72_345, 6), StackmatPacket('S', 72_345, 6), + ) + + @Test fun decodesCleanSignalAtCommonRates() { + for (rate in listOf(44_100, 48_000, 16_000)) assertEquals(expected, decode(audio(sequence, rate, invert = false, noise = 0f), rate), "at $rate") + } + + @Test fun decodesInvertedNoisySignal() { + for (rate in listOf(44_100, 48_000)) { + assertEquals(expected, decode(audio(sequence, rate, invert = true, noise = 0.08f), rate), "inverted at $rate") + assertEquals(expected, decode(audio(sequence, rate, invert = false, noise = 0.08f), rate, chunk = 97), "noisy at $rate") + } + } + + @Test fun fiveDigitTimersAndBadChecksums() { + val gen3 = packetBytes('S', "12345") // 1:23.45 + val corrupt = packetBytes('S', "112345").toMutableList().also { it[3] = '9'.code } + val got = decode(audio(listOf(gen3, corrupt, gen3), 44_100, invert = false, noise = 0f), 44_100) + assertEquals(listOf(StackmatPacket('S', 83_450, 5), StackmatPacket('S', 83_450, 5)), got) + } + + @Test fun silenceAndNoiseDecodeToNothing() { + val rng = Random(4) + assertTrue(decode(FloatArray(48_000) { (rng.nextFloat() - 0.5f) * 0.01f }, 48_000).isEmpty()) + assertTrue(decode(FloatArray(48_000) { rng.nextFloat() - 0.5f }, 48_000).isEmpty()) + } + + @Test fun sessionRecordsEachSolveOnce() { + val solves = mutableListOf<Long>() + val s = StackmatSession { solves += it } + fun p(c: Char, ms: Long) = s.packet(StackmatPacket(c, ms, 6)) + p('S', 9_990) // a time already on the display when we connect: not ours + p('I', 0); p('A', 0); p(' ', 100); p(' ', 5_000); p('S', 12_340); p('S', 12_340); p('C', 12_340) + p('I', 0); p('S', 8_000) // the running packets were lost entirely + assertEquals(listOf(12_340L, 8_000L), solves) + } +} |
