summaryrefslogtreecommitdiff
path: root/core/src/commonTest/kotlin/coffee/liz/rookery/StackmatTest.kt
blob: f6071ff29c1796b05aca9391f2f65a431a80d64c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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)
    }
}