File size: 2,497 Bytes
843a4b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { describe, it, expect } from "vitest";
import {
    tokensToOnsets,
    textToSchedule,
    onsetsToMorse,
    decodeOnsets,
} from "../../lib/wire.js";
import { morseToTokens } from "../../lib/morse.js";
import { makeTiming } from "../../lib/timing.js";

const T = makeTiming(120); // dah=240 elem=480 letter=960 word=1680

describe("tokensToOnsets", () => {
    it("dot = 1 impulse", () => {
        expect(tokensToOnsets(["dot"], T)).toEqual([0]);
    });

    it("dash = 2 impulses spaced by dahGap", () => {
        expect(tokensToOnsets(["dah"], T)).toEqual([0, T.dahGapMs]);
    });

    it("A (.-) spaces the dash element after elemGap from the dot", () => {
        // dot@0, elemGap, dah@(elem), dah2@(elem+dah)
        expect(tokensToOnsets(morseToTokens(".-"), T)).toEqual([
            0,
            T.elemGapMs,
            T.elemGapMs + T.dahGapMs,
        ]);
    });
});

describe("onsetsToMorse round-trip (ideal timing)", () => {
    const samples = ["SOS", "HELLO WORLD", "HI YOU", "CQ CQ", "E T 1 9", "ABCDE"];
    for (const text of samples) {
        it(`recovers "${text}"`, () => {
            const { morse, onsets } = textToSchedule(text, T);
            expect(onsetsToMorse(onsets, T)).toBe(morse);
            expect(decodeOnsets(onsets, T).text).toBe(text);
        });
    }
});

describe("onsetsToMorse with timing jitter", () => {
    // Deterministic pseudo-jitter (no Math.random): perturb each onset by a
    // bounded sawtooth. Should still decode because gap classes are well
    // separated (geometric midpoints).
    function jitter(onsets, maxMs) {
        return onsets.map((t, i) => t + (((i * 37) % 11) - 5) / 5 * maxMs);
    }

    it("decodes SOS with +-30ms jitter", () => {
        const { onsets, text } = { ...textToSchedule("SOS", T) };
        const jittered = jitter(onsets, 30);
        expect(decodeOnsets(jittered, T).text).toBe("SOS");
        expect(text).toBeUndefined; // sanity: textToSchedule has no .text
    });

    it("decodes a sentence with +-40ms jitter", () => {
        const { onsets } = textToSchedule("HELLO WORLD", T);
        expect(decodeOnsets(jitter(onsets, 40), T).text).toBe("HELLO WORLD");
    });
});

describe("edge cases", () => {
    it("empty onsets -> empty", () => {
        expect(onsetsToMorse([], T)).toBe("");
        expect(decodeOnsets([], T)).toEqual({ morse: "", text: "" });
    });

    it("single impulse -> E", () => {
        expect(decodeOnsets([0], T).text).toBe("E");
    });
});