summaryrefslogtreecommitdiff
path: root/core/src/commonTest/kotlin/coffee/liz/rookery
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/commonTest/kotlin/coffee/liz/rookery')
-rw-r--r--core/src/commonTest/kotlin/coffee/liz/rookery/StackmatTest.kt83
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)
+ }
+}