clone

a desktop tv and program guide for over-the-air broadcasts: what's on air, what your tuner gets, live tv, recordings

main
airwaves / internal / cc / cea608_test.go
cea608_test.go 12.0K
1package cc23import (4 "fmt"5 "math/bits"6 "strings"7 "testing"8 "time"9)1011func TestTrackBytes(t *testing.T) {12 track := NewTrack([]Cue{{Start: time.Second, End: 2 * time.Second, Text: "Hi"}}, 30)13 want := map[int64][2]byte{14 0: {0x94, 0x2c}, 1: {0x94, 0x2c}, // EDM: a clean screen15 21: {0x94, 0x20}, 22: {0x94, 0x20}, // RCL: pop-on16 23: {0x94, 0xae}, 24: {0x94, 0xae}, // ENM17 25: {0x94, 0x76}, 26: {0x94, 0x76}, // PAC: row 15, indent 1218 27: {0x97, 0x23}, 28: {0x97, 0x23}, // tab offset 3: column 1519 29: {0xc8, 0xe9}, // "Hi"20 30: {0x94, 0x2f}, 31: {0x94, 0x2f}, // EOC at one second21 60: {0x94, 0x2c}, 61: {0x94, 0x2c}, // EDM at two22 }23 for f := int64(-2); f < 100; f++ {24 b1, b2 := track.Pair(f)25 w, ok := want[f]26 if !ok {27 w = [2]byte{Padding, Padding}28 }29 if [2]byte{b1, b2} != w {30 t.Errorf("frame %d: % x, want % x", f, []byte{b1, b2}, w[:])31 }32 }33 var none *Track34 if b1, b2 := none.Pair(5); b1 != Padding || b2 != Padding {35 t.Error("a nil track sent something")36 }37}3839func TestParity(t *testing.T) {40 for b := range 128 {41 if p := parity(byte(b)); bits.OnesCount8(p)%2 != 1 || p&0x7f != byte(b) {42 t.Errorf("parity(%#x) = %#x", b, p)43 }44 }45}4647func TestCharset(t *testing.T) {48 for _, set := range []struct{ a, b string }{49 {"ÁÉÓÚÜü\x00¡*\x00—©℠•“”ÀÂÇÈÊËëÎÏïÔÙùÛ«»", "AEOUUu !- -cs.\"\"AACEEEeIIiOUuU\"\""},50 {"ÃãÍÌìÒòÕõ{}\\^_|~ÄäÖöߥ¤¦Å娸┌┐└┘", "AaIIiOoOo()/ -:-AaOosY$:AaOo++++"},51 } {52 if n, m := len([]rune(set.a)), len(set.b); n != 32 || m != 32 {53 t.Errorf("extended set of %d with %d stand-ins", n, m)54 }55 }56 for r, c := range charset {57 if c.alt != 0 {58 if alt, ok := charset[rune(c.alt)]; !ok || alt.code[1] != 0 {59 t.Errorf("%q stands in for %q but isn't basic", c.alt, r)60 }61 }62 }63 for _, r := range "*\\^_{|}~" {64 if c := charset[r]; c.code[1] == 0 {65 t.Errorf("%q is sent as basic %#x, which 608 shows as something else", r, c.code[0])66 }67 }68 for r := range substitutes {69 if _, ok := charset[r]; ok {70 t.Errorf("%q is substituted though 608 has it", r)71 }72 }73}7475// screen608 is a CEA-608 pop-on decoder, enough to check what a track76// shows and when.77type screen608 struct {78 shown, hidden [16][Columns]rune79 row, col int80 prev [2]byte81 rollUp bool82 text string // on screen83 since int6484 events []shownCaption85}8687type shownCaption struct {88 start, end int6489 text string90}9192func decodeTrack(t *testing.T, track *Track, frames int64) []shownCaption {93 t.Helper()94 back := map[[2]byte]rune{}95 for r, c := range charset {96 back[c.code] = r97 }98 var s screen60899 var update func(int64)100 var f int64101 put := func(r rune) {102 if s.col >= Columns {103 return104 }105 if s.rollUp {106 s.shown[s.row][s.col] = r107 s.col++108 update(f)109 return110 }111 s.hidden[s.row][s.col] = r112 s.col++113 }114 update = func(f int64) {115 var lines []string116 for _, row := range s.shown {117 if l := strings.TrimSpace(strings.ReplaceAll(string(row[:]), "\x00", " ")); l != "" {118 lines = append(lines, l)119 }120 }121 text := strings.Join(lines, "\n")122 if text == s.text {123 return124 }125 if s.text != "" {126 s.events = append(s.events, shownCaption{s.since, f, s.text})127 }128 s.text, s.since = text, f129 }130 rows := map[[2]byte]int{}131 for r := 1; r <= 15; r++ {132 p := pac(r, 0)133 rows[[2]byte{p[0], p[1] &^ 0x1f}] = r134 }135 for f = range frames {136 b1, b2 := track.Pair(f)137 if bits.OnesCount8(b1)%2 != 1 || bits.OnesCount8(b2)%2 != 1 {138 t.Fatalf("frame %d: % x lacks odd parity", f, []byte{b1, b2})139 }140 c := [2]byte{b1 & 0x7f, b2 & 0x7f}141 if c == [2]byte{} {142 continue143 }144 if c[0] >= 0x20 {145 s.prev = [2]byte{}146 put(back[[2]byte{c[0]}])147 if c[1] != 0 {148 put(back[[2]byte{c[1]}])149 }150 continue151 }152 if c == s.prev {153 continue // the second sending154 }155 s.prev = c156 switch {157 case c == [2]byte{0x14, rcl}:158 s.rollUp = false159 case c == [2]byte{0x14, ru2}:160 if !s.rollUp {161 s.shown, s.hidden = [16][Columns]rune{}, [16][Columns]rune{}162 update(f)163 }164 s.rollUp = true165 case c == [2]byte{0x14, cr}:166 for r := 1; r < 16; r++ {167 s.shown[r-1] = s.shown[r]168 if r-1 < s.row-1 {169 s.shown[r-1] = [Columns]rune{} // two rows only170 }171 }172 s.shown[15] = [Columns]rune{}173 s.col = 0174 update(f)175 case c == [2]byte{0x14, enm}:176 s.hidden = [16][Columns]rune{}177 case c == [2]byte{0x14, eoc}:178 s.shown, s.hidden = s.hidden, s.shown179 update(f)180 case c == [2]byte{0x14, edm}:181 s.shown = [16][Columns]rune{}182 update(f)183 case c[0] == 0x17 && c[1] >= 0x21 && c[1] <= 0x23:184 s.col += int(c[1] - 0x20)185 case c[1] >= 0x40:186 r, ok := rows[[2]byte{c[0], c[1] &^ 0x1f}]187 if !ok {188 t.Fatalf("frame %d: bad PAC % x", f, c)189 }190 s.row, s.col = r, int(c[1]&0x0e)*2191 case c[0] == 0x11:192 put(back[c])193 case c[0] == 0x12 || c[0] == 0x13:194 s.col = max(s.col-1, 0)195 put(back[c])196 default:197 t.Fatalf("frame %d: unexpected % x", f, c)198 }199 }200 update(frames)201 return s.events202}203204func TestTrackSchedule(t *testing.T) {205 long := "A caption long enough to need\nmost of a second to load in"206 s := func(sec float64) time.Duration { return time.Duration(sec * float64(time.Second)) }207 cues := []Cue{208 {Start: s(0), End: s(1), Text: "From the start"}, // can't load before it, so late209 {Start: s(2), End: s(3), Text: "Two to three"}, // ordinary210 {Start: s(3), End: s(5), Text: "Straight on, no blank"}, // loaded while the last shows211 {Start: s(5.1), End: s(6), Text: "A small gap"}, // the last stays on through it, and it runs on to be read212 {Start: s(10), End: s(10.5), Text: "Short"}, //213 {Start: s(11), End: s(12), Text: long}, // loaded around the blank before it, late214 {Start: s(13), End: s(17), Text: "Long"}, //215 {Start: s(17.6), End: s(19), Text: long}, // loaded before the blank, and runs on to the next216 {Start: s(20), End: s(20.3), Text: "¡Olé! ♪ ½ — «ça»"}, // special and extended characters217 {Start: s(20.3), End: s(20.6), Text: "Too fast: one"}, //218 {Start: s(20.6), End: s(20.9), Text: "Too fast: and two"}, //219 }220 got := decodeTrack(t, NewTrack(cues, 30), 30*30)221 frame := func(d time.Duration) int64 { return int64(d * 30 / time.Second) }222 var lines []string223 for _, e := range got {224 lines = append(lines, fmt.Sprintf("%d-%d %q", e.start, e.end, e.text))225 }226 t.Logf("shown:\n%s", strings.Join(lines, "\n"))227 if len(got) != len(cues) {228 t.Fatalf("%d captions shown, want %d", len(got), len(cues))229 }230 for i, c := range cues {231 g := got[i]232 if g.text != c.Text {233 t.Errorf("caption %d shows %q, want %q", i, g.text, c.Text)234 }235 if g.start < frame(c.Start) {236 t.Errorf("caption %d early: frame %d, want %d", i, g.start, frame(c.Start))237 }238 }239 exact := map[int][2]int64{1: {60, 90}, 2: {90, 153}, 3: {153, 183}, 6: {390, 510}, 7: {528, 600}}240 for i, w := range exact {241 if g := got[i]; g.start != w[0] || g.end != w[1] {242 t.Errorf("caption %d shown %d-%d, want %d-%d", i, g.start, g.end, w[0], w[1])243 }244 }245 // Late ones are late by their loading time, no more.246 if g := got[0]; g.start > 2+int64(len(loadCaption(cues[0].Text))) {247 t.Errorf("first caption at frame %d", g.start)248 }249 if g := got[5]; g.start > frame(cues[4].Start+time.Second)+2+int64(len(loadCaption(long))) {250 t.Errorf("caption 5 at frame %d", g.start)251 }252}253254// A caption shortly after another clears it in among its own loading, so255// it still shows on time.256func TestPopOnAfterShortGap(t *testing.T) {257 s := func(sec float64) time.Duration { return time.Duration(sec * float64(time.Second)) }258 cues := []Cue{259 {Start: s(0.5), End: s(3.7), Text: "but yet we still continue to wander on."},260 {Start: s(3.95), End: s(6), Text: "We always find ourselves crying out."},261 }262 got := decodeTrack(t, NewTrack(cues, 30), 300)263 if len(got) != 2 || got[0].end != 111 || got[1].start != 119 {264 t.Errorf("shown %+v, want the first cleared at frame 111 and the second on at 119", got)265 }266}267268// Tuning in mid-sentence shows a line of what was just said, then words as269// they're said.270func TestRollUpTuningIn(t *testing.T) {271 s := func(sec float64) time.Duration { return time.Duration(sec * float64(time.Second)) }272 before := []Word{{s(-3), "and"}, {s(-2.8), "I"}, {s(-2.5), "don't"}, {s(-2.2), "believe"}, {s(-2), "that"},273 {s(-1.5), "it's"}, {s(-1.2), "actually"}, {s(-0.8), "haunted."}, {s(-0.4), "So,"}, {s(-0.2), "I"}}274 cues := Shift([]Cue{275 {Start: s(-3), End: s(1), Words: before},276 {Start: s(0.5), End: s(3), Words: []Word{{s(0.5), "don't"}, {s(0.8), "want"}}},277 }, 0)278 got := decodeTrack(t, NewTrack(cues, 30), 120)279 if len(got) == 0 || len(got[len(got)-1].text) > 2*Columns {280 t.Fatalf("shown %+v", got)281 }282 final := got[len(got)-1]283 if final.text != "it's actually haunted. So, I\ndon't want" {284 t.Errorf("shows %q", final.text)285 }286 for _, e := range got {287 if strings.Contains(e.text, "believe") {288 t.Errorf("shows %q, from well before tuning in", e.text)289 }290 }291 if cues[0].Words[0].Text != "and" {292 t.Error("NewTrack changed its argument")293 }294}295296func TestRollUpBytes(t *testing.T) {297 track := NewTrack([]Cue{{Start: time.Second, End: 2 * time.Second, Text: "Hi you",298 Words: []Word{{time.Second, "Hi"}, {1500 * time.Millisecond, "you"}}}}, 30)299 want := map[int64][2]byte{300 0: {0x94, 0x2c}, 1: {0x94, 0x2c}, // EDM301 30: {0x94, 0x25}, 31: {0x94, 0x25}, // RU2: roll-up, two rows302 32: {0x94, 0x70}, 33: {0x94, 0x70}, // PAC: row 15, column 0303 34: {0xc8, 0xe9}, // "Hi" as it's said304 45: {0x20, 0x79}, 46: {0xef, 0x75}, // " you", half a second on305 60: {0x94, 0x2c}, 61: {0x94, 0x2c}, // EDM at the end306 }307 for f := int64(0); f < 100; f++ {308 b1, b2 := track.Pair(f)309 w, ok := want[f]310 if !ok {311 w = [2]byte{Padding, Padding}312 }313 if [2]byte{b1, b2} != w {314 t.Errorf("frame %d: % x, want % x", f, []byte{b1, b2}, w[:])315 }316 }317}318319// Words timed one by one roll up as they're said, two rows at a time, and320// a pause clears the screen.321func TestRollUp(t *testing.T) {322 s := func(sec float64) time.Duration { return time.Duration(sec * float64(time.Second)) }323 words := func(at ...any) (out []Word) {324 for i := 0; i < len(at); i += 2 {325 out = append(out, Word{s(at[i].(float64)), at[i+1].(string)})326 }327 return out328 }329 cues := []Cue{330 {Start: s(1), End: s(4), Words: words(1.0, "Hello", 1.3, "there", 1.6, "everybody,", 2.0, "welcome", 2.3, "back", 2.5, "to", 2.6, "the", 2.8, "channel.")},331 {Start: s(4.2), End: s(6), Words: words(4.2, "Today", 4.5, "we", 4.7, "play")},332 {Start: s(9), End: s(10), Words: words(9.0, "Bye")},333 }334 got := decodeTrack(t, NewTrack(cues, 30), 400)335 var lines []string336 at := map[string][2]int64{}337 for _, e := range got {338 lines = append(lines, fmt.Sprintf("%d-%d %q", e.start, e.end, e.text))339 at[e.text] = [2]int64{e.start, e.end}340 }341 t.Logf("shown:\n%s", strings.Join(lines, "\n"))342 for text, when := range map[string][2]int64{343 "Hello": {34, 36},344 "Hello there": {39, 41},345 "Hello there everybody, welcome\nback": {69, 73},346 "back to the channel. Today we\nplay": {141, 145},347 "Bye": {274, 276},348 } {349 got, ok := at[text]350 if !ok || got[0] < when[0] || got[0] > when[1] {351 t.Errorf("%q shown from %v, want frame %d to %d", text, got, when[0], when[1])352 }353 }354 if w := at["back to the channel. Today we\nplay"]; w[1] != 180 {355 t.Errorf("not cleared at the pause: %v", w)356 }357 if w := at["Bye"]; w[1] != 300 {358 t.Errorf("not cleared at the end: %v", w)359 }360}361362func TestControlCodesSentTwice(t *testing.T) {363 cues := []Cue{364 {Start: time.Second, End: 2 * time.Second, Text: "♪♪ ♪ «Ça» ü"},365 {Start: 2 * time.Second, End: 3 * time.Second, Text: "Line one\nline two"},366 {Start: 4 * time.Second, End: 5 * time.Second, Text: "ÄÖÜ"},367 }368 track := NewTrack(cues, 30)369 var run [][2]byte370 check := func(f int64) {371 if len(run) > 0 && run[0][0]&0x70 == 0x10 && len(run) != 2 {372 t.Errorf("frame %d: % x sent %d times in a row", f, run[0], len(run))373 }374 }375 for f := range int64(200) {376 b1, b2 := track.Pair(f)377 p := [2]byte{b1, b2}378 if len(run) > 0 && run[0] == p {379 run = append(run, p)380 continue381 }382 check(f)383 run = [][2]byte{p}384 }385 check(200)386}