MythTV master
cc608decoder.cpp
Go to the documentation of this file.
1// -*- Mode: c++ -*-
2
3// Some of the XDS was inspired by code in TVTime. -- dtk 03/30/2006
5
6#include <algorithm>
7#include <vector>
8
9// Qt headers
10#include <QStringList>
11#include <QCoreApplication>
12
13// MythTV headers
15
16#define DEBUG_XDS 0
17
18static const std::array<const uint8_t,256> vbi_bit_reverse
19{
20 0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0,
21 0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0,
22 0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8,
23 0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8,
24 0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4,
25 0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4,
26 0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec,
27 0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc,
28 0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2,
29 0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2,
30 0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea,
31 0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa,
32 0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6,
33 0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6,
34 0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee,
35 0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe,
36 0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1,
37 0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1,
38 0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9,
39 0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9,
40 0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5,
41 0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5,
42 0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed,
43 0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd,
44 0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3,
45 0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3,
46 0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb,
47 0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb,
48 0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7,
49 0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7,
50 0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef,
51 0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff
52};
53
54static const std::array<const std::string, 8> formats
55{
56 "Full format 4:3, 576 lines",
57 "Letterbox 14:9 centre, 504 lines",
58 "Letterbox 14:9 top, 504 lines",
59 "Letterbox 16:9 centre, 430 lines",
60 "Letterbox 16:9 top, 430 lines",
61 "Letterbox > 16:9 centre",
62 "Full format 14:9 centre, 576 lines",
63 "Anamorphic 16:9, 576 lines"
64};
65
66static const std::array<const std::string,4> subtitles
67{
68 "none",
69 "in active image area",
70 "out of active image area",
71 "?"
72};
73
74static void init_xds_program_type(CC608ProgramType& xds_program_type);
75
77 : m_reader(ccr),
78 m_rbuf(new unsigned char[sizeof(ccsubtitle)+255])
79{
80 // fill translation table
81 for (uint i = 0; i < 128; i++)
82 m_stdChar[i] = QChar(i);
83 m_stdChar[42] = QLatin1Char(0xE1); // á
84 m_stdChar[92] = QLatin1Char(0xE9); // é
85 m_stdChar[94] = QLatin1Char(0xED); // í
86 m_stdChar[95] = QLatin1Char(0xF3); // ó
87 m_stdChar[96] = QLatin1Char(0xFA); // ú
88 m_stdChar[123] = QLatin1Char(0xE7); // ç
89 m_stdChar[124] = QLatin1Char(0xF7); // ÷
90 m_stdChar[125] = QLatin1Char(0xD1); // Ñ
91 m_stdChar[126] = QLatin1Char(0xF1); // ñ
92 m_stdChar[127] = QChar(0x2588); // full block
93
95}
96
98{
99 delete [] m_rbuf;
100}
101
102void CC608Decoder::FormatCC(std::chrono::milliseconds tc, int code1, int code2)
103{
104 FormatCCField(tc, 0, code1);
105 FormatCCField(tc, 1, code2);
106}
107
108void CC608Decoder::GetServices(std::chrono::seconds seconds, CC608Seen& seen) const
109{
110 auto then = SystemClock::now() - seconds;
111 for (uint i = 0; i < 4; i++)
112 seen[i] = (m_lastSeen[i] >= then);
113}
114
115static const std::array<const int,16> rowdata =
116{
117 11, -1, 1, 2, 3, 4, 12, 13,
118 14, 15, 5, 6, 7, 8, 9, 10
119};
120
121static const std::array<const QChar,16> specialchar =
122{
123 QLatin1Char(0xAE), QLatin1Char(0xB0), QLatin1Char(0xBD), QLatin1Char(0xBF), // ®°½¿
124 QChar(0x2122), QLatin1Char(0xA2), QLatin1Char(0xA3), QChar(0x266A), // ™¢£♪
125 QLatin1Char(0xE0), QLatin1Char(' '), QLatin1Char(0xE8), QLatin1Char(0xE2), // à èâ
126 QLatin1Char(0xEA), QLatin1Char(0xEE), QLatin1Char(0xF4), QLatin1Char(0xFB) // êîôû
127};
128
129static const std::array<const QChar,32> extendedchar2 =
130{
131 QLatin1Char(0xC1), QLatin1Char(0xC9), QLatin1Char(0xD3), QLatin1Char(0xDA), // ÁÉÓÚ
132 QLatin1Char(0xDC), QLatin1Char(0xFC), QLatin1Char('`'), QLatin1Char(0xA1), // Üü`¡
133 QLatin1Char('*'), QLatin1Char('\''), QChar(0x2014), QLatin1Char(0xA9), // *'-©
134 QChar(0x2120), QLatin1Char(0xB7), QChar(0x201C), QChar(0x201D), // ℠·“”
135 QLatin1Char(0xC0), QLatin1Char(0xC2), QLatin1Char(0xC7), QLatin1Char(0xC8), // ÀÂÇÈ
136 QLatin1Char(0xCA), QLatin1Char(0xCB), QLatin1Char(0xEB), QLatin1Char(0xCE), // ÊËëÎ
137 QLatin1Char(0xCF), QLatin1Char(0xEF), QLatin1Char(0xD4), QLatin1Char(0xD9), // ÏïÔÙ
138 QLatin1Char(0xF9), QLatin1Char(0xDB), QLatin1Char(0xAB), QLatin1Char(0xBB) // ùÛ«»
139};
140
141static const std::array<const QChar,32> extendedchar3 =
142{
143 QLatin1Char(0xC3), QLatin1Char(0xE3), QLatin1Char(0xCD), QLatin1Char(0xCC), // ÃãÍÌ
144 QLatin1Char(0xEC), QLatin1Char(0xD2), QLatin1Char(0xF2), QLatin1Char(0xD5), // ìÒòÕ
145 QLatin1Char(0xF5), QLatin1Char('{'), QLatin1Char('}'), QLatin1Char('\\'), // õ{}
146 QLatin1Char('^'), QLatin1Char('_'), QLatin1Char(0xA6), QLatin1Char('~'), // ^_¦~
147 QLatin1Char(0xC4), QLatin1Char(0xE4), QLatin1Char(0xD6), QLatin1Char(0xF6), // ÄäÖö
148 QLatin1Char(0xDF), QLatin1Char(0xA5), QLatin1Char(0xA4), QLatin1Char('|'), // ߥ¤|
149 QLatin1Char(0xC5), QLatin1Char(0xE5), QLatin1Char(0xD8), QLatin1Char(0xF8), // Å娸
150 QChar(0x250C), QChar(0x2510), QChar(0x2514), QChar(0x2518) // ┌┐└┘
151};
152
153void CC608Decoder::FormatTextCode(std::chrono::milliseconds tc, size_t field, size_t mode, size_t len, int b1, int b2)
154{
155 if (mode == std::numeric_limits<std::size_t>::max())
156 return;
157
158 m_lastCodeTc[field] += 33ms;
159 m_timeCode[mode] = tc;
160
161 // commit row number only when first text code
162 // comes in
163 if (m_newRow[mode])
164 NewRowCC(mode, len);
165
166 m_ccBuf[mode] += CharCC(b1);
167 m_col[mode]++;
168 if (b2 & 0x60)
169 {
170 m_ccBuf[mode] += CharCC(b2);
171 m_col[mode]++;
172 }
173}
174
175void CC608Decoder::FormatControlCode(std::chrono::milliseconds tc, size_t field, int b1, int b2)
176{
177 m_lastCodeTc[field] += 67ms;
178
179 int newccmode = (b1 >> 3) & 1;
180 int newtxtmode = m_txtMode[(field*2) + newccmode];
181 if ((b1 & 0x06) == 0x04)
182 {
183 switch (b2)
184 {
185 case 0x29:
186 case 0x2C:
187 case 0x20:
188 case 0x2F:
189 case 0x25:
190 case 0x26:
191 case 0x27:
192 // CC1,2
193 newtxtmode = 0;
194 break;
195 case 0x2A:
196 case 0x2B:
197 // TXT1,2
198 newtxtmode = 1;
199 break;
200 }
201 }
202 m_ccMode[field] = newccmode;
203 m_txtMode[(field*2) + newccmode] = newtxtmode;
204 size_t mode = (field << 2) | (newtxtmode << 1) | m_ccMode[field];
205
206 m_timeCode[mode] = tc;
207 size_t len = m_ccBuf[mode].length();
208
209 if (b2 & 0x40) //preamble address code (row & indent)
210 {
211 if (newtxtmode)
212 // no address codes in TXT mode?
213 return;
214
215 m_newRow[mode] = rowdata[((b1 << 1) & 14) | ((b2 >> 5) & 1)];
216 if (m_newRow[mode] == -1)
217 // bogus code?
218 m_newRow[mode] = m_lastRow[mode] + 1;
219
220 if (b2 & 0x10) //row contains indent flag
221 {
222 m_newCol[mode] = (b2 & 0x0E) << 1;
223 // Encode as 0x7020 or 0x7021 depending on the
224 // underline flag.
225 m_newAttr[mode] = (b2 & 0x1) + 0x20;
226 LOG(VB_VBI, LOG_INFO,
227 QString("cc608 preamble indent, b2=%1")
228 .arg(b2, 2, 16));
229 }
230 else
231 {
232 m_newCol[mode] = 0;
233 m_newAttr[mode] = (b2 & 0xf) + 0x10;
234 // Encode as 0x7010 through 0x702f for the 16 possible
235 // values of b2.
236 LOG(VB_VBI, LOG_INFO,
237 QString("cc608 preamble color change, b2=%1")
238 .arg(b2, 2, 16));
239 }
240
241 // row, indent, attribute settings are not final
242 // until text code arrives
243 return;
244 }
245
246 switch (b1 & 0x07)
247 {
248 case 0x00: //attribute
249#if 0
250 LOG(VB_VBI, LOG_DEBUG,
251 QString("<ATTRIBUTE %1 %2>").arg(b1).arg(b2));
252#endif
253 break;
254 case 0x01: //midrow or char
255 if (m_newRow[mode])
256 NewRowCC(mode, len);
257
258 switch (b2 & 0x70)
259 {
260 case 0x20: //midrow attribute change
261 LOG(VB_VBI, LOG_INFO,
262 QString("cc608 mid-row color change, b2=%1")
263 .arg(b2, 2, 16));
264 // Encode as 0x7000 through 0x700f for the
265 // 16 possible values of b2.
266 m_ccBuf[mode] += ' ';
267 m_ccBuf[mode] += QChar(0x7000 + (b2 & 0xf));
268 m_col[mode]++;
269 break;
270 case 0x30: //special character..
271 m_ccBuf[mode] += specialchar[b2 & 0x0f];
272 m_col[mode]++;
273 break;
274 }
275 break;
276 case 0x02: //extended char
277 // extended char is preceded by alternate char
278 // - if there's no alternate, it could be noise
279 if (!len)
280 break;
281
282 if (b2 & 0x30)
283 {
284 m_ccBuf[mode].remove(len - 1, 1);
285 m_ccBuf[mode] += extendedchar2[b2 - 0x20];
286 break;
287 }
288 break;
289 case 0x03: //extended char
290 // extended char is preceded by alternate char
291 // - if there's no alternate, it could be noise
292 if (!len)
293 break;
294
295 if (b2 & 0x30)
296 {
297 m_ccBuf[mode].remove(len - 1, 1);
298 m_ccBuf[mode] += extendedchar3[b2 - 0x20];
299 break;
300 }
301 break;
302 case 0x04: //misc
303 case 0x05: //misc + F
304#if 0
305 LOG(VB_VBI, LOG_DEBUG,
306 QString("ccmode %1 cmd %2").arg(m_ccMode)
307 .arg(b2, 2, 16, '0'));
308#endif
309 switch (b2)
310 {
311 case 0x21: //backspace
312 // add backspace if line has been encoded already
313 if (m_newRow[mode])
314 len = NewRowCC(mode, len);
315
316 if (len == 0 ||
317 m_ccBuf[mode].startsWith("\b"))
318 {
319 m_ccBuf[mode] += '\b';
320 m_col[mode]--;
321 }
322 else
323 {
324 m_ccBuf[mode].remove(len - 1, 1);
325 m_col[mode]--;
326 }
327 break;
328 case 0x25: //2 row caption
329 case 0x26: //3 row caption
330 case 0x27: //4 row caption
331 if (m_style[mode] == CC_STYLE_PAINT && len)
332 {
333 // flush
334 BufferCC(mode, len, 0);
335 m_ccBuf[mode] = "";
336 m_row[mode] = 0;
337 m_col[mode] = 0;
338 }
339 else if (m_style[mode] == CC_STYLE_POPUP)
340 {
341 ResetCC(mode);
342 }
343
344 m_rowCount[mode] = b2 - 0x25 + 2;
345 m_style[mode] = CC_STYLE_ROLLUP;
346 break;
347 case 0x2D: //carriage return
348 if (m_style[mode] != CC_STYLE_ROLLUP)
349 break;
350
351 if (m_newRow[mode])
352 m_row[mode] = m_newRow[mode];
353
354 // flush if there is text or need to scroll
355 // TODO: decode ITV (WebTV) link in TXT2
356 if (len || (m_row[mode] != 0 && !m_lineCont[mode] &&
357 (!newtxtmode || m_row[mode] >= 16)))
358 {
359 BufferCC(mode, len, 0);
360 }
361
362 if (newtxtmode)
363 {
364 if (m_row[mode] < 16)
365 m_newRow[mode] = m_row[mode] + 1;
366 else
367 // scroll up previous lines
368 m_newRow[mode] = 16;
369 }
370
371 m_ccBuf[mode] = "";
372 m_col[mode] = 0;
373 m_lineCont[mode] = 0;
374 break;
375
376 case 0x29:
377 // resume direct caption (paint-on style)
378 if (m_style[mode] == CC_STYLE_ROLLUP && len)
379 {
380 // flush
381 BufferCC(mode, len, 0);
382 m_ccBuf[mode] = "";
383 m_row[mode] = 0;
384 m_col[mode] = 0;
385 }
386 else if (m_style[mode] == CC_STYLE_POPUP)
387 {
388 ResetCC(mode);
389 }
390
391 m_style[mode] = CC_STYLE_PAINT;
392 m_rowCount[mode] = 0;
393 m_lineCont[mode] = 0;
394 break;
395
396 case 0x2B: //resume text display
397 m_resumeText[mode] = 1;
398 if (m_row[mode] == 0)
399 {
400 m_newRow[mode] = 1;
401 m_newCol[mode] = 0;
402 m_newAttr[mode] = 0;
403 }
404 m_style[mode] = CC_STYLE_ROLLUP;
405 break;
406 case 0x2C: //erase displayed memory
407 if (m_ignoreTimeCode ||
408 (tc - m_lastClr[mode]) > 5s ||
409 m_lastClr[mode] == 0ms)
410 {
411 // don't overflow the frontend with
412 // too many redundant erase codes
413 BufferCC(mode, 0, 1);
414 }
415 if (m_style[mode] != CC_STYLE_POPUP)
416 {
417 m_row[mode] = 0;
418 m_col[mode] = 0;
419 }
420 m_lineCont[mode] = 0;
421 break;
422
423 case 0x20: //resume caption (pop-up style)
424 if (m_style[mode] != CC_STYLE_POPUP)
425 {
426 if (len)
427 // flush
428 BufferCC(mode, len, 0);
429 m_ccBuf[mode] = "";
430 m_row[mode] = 0;
431 m_col[mode] = 0;
432 }
433 m_style[mode] = CC_STYLE_POPUP;
434 m_rowCount[mode] = 0;
435 m_lineCont[mode] = 0;
436 break;
437 case 0x2F: //end caption + swap memory
438 if (m_style[mode] != CC_STYLE_POPUP)
439 {
440 if (len)
441 // flush
442 BufferCC(mode, len, 0);
443 }
444 else if (m_ignoreTimeCode ||
445 (tc - m_lastClr[mode]) > 5s ||
446 m_lastClr[mode] == 0ms)
447 {
448 // clear and flush
449 BufferCC(mode, len, 1);
450 }
451 else if (len)
452 {
453 // flush
454 BufferCC(mode, len, 0);
455 }
456 m_ccBuf[mode] = "";
457 m_row[mode] = 0;
458 m_col[mode] = 0;
459 m_style[mode] = CC_STYLE_POPUP;
460 m_rowCount[mode] = 0;
461 m_lineCont[mode] = 0;
462 break;
463
464 case 0x2A: //text restart
465 // clear display
466 BufferCC(mode, 0, 1);
467 ResetCC(mode);
468 // TXT starts at row 1
469 m_newRow[mode] = 1;
470 m_newCol[mode] = 0;
471 m_newAttr[mode] = 0;
472 m_style[mode] = CC_STYLE_ROLLUP;
473 break;
474
475 case 0x2E: //erase non-displayed memory
476 ResetCC(mode);
477 break;
478 }
479 break;
480 case 0x07: //misc (TAB)
481 if (m_newRow[mode])
482 {
483 m_newCol[mode] += (b2 & 0x03);
484 NewRowCC(mode, len);
485 }
486 else
487 {
488 // illegal?
489 for (int x = 0; x < (b2 & 0x03); x++)
490 {
491 m_ccBuf[mode] += ' ';
492 m_col[mode]++;
493 }
494 }
495 break;
496 }
497}
498
499void CC608Decoder::FormatCCField(std::chrono::milliseconds tc, size_t field, int data)
500{
501 size_t len = 0;
502 size_t mode = 0;
503
504 if (data == -1) // invalid data. flush buffers to be safe.
505 {
506 // TODO: flush reader buffer
507 if (m_ccMode[field] != -1)
508 {
509 for (mode = field*4; mode < ((field*4) + 4); mode++)
510 ResetCC(mode);
511 m_xds[field] = 0;
512 m_badVbi[field] = 0;
513 m_ccMode[field] = -1;
514 m_txtMode[field*2] = 0;
515 m_txtMode[(field*2) + 1] = 0;
516 }
517 return;
518 }
519
520 if ((m_lastFormatData[field&1] == data) &&
521 (m_lastFormatTc[field&1] == tc))
522 {
523 LOG(VB_VBI, LOG_DEBUG, "Format CC -- Duplicate");
524 return;
525 }
526
527 m_lastFormatTc[field&1] = tc;
528 m_lastFormatData[field&1] = data;
529
530 int b1 = data & 0x7f;
531 int b2 = (data >> 8) & 0x7f;
532#if 1
533 LOG(VB_VBI, LOG_DEBUG,
534 QString("Format CC @%1/%2 = %3 %4, %5/%6 = '%7' '%8'")
535 .arg(tc.count()).arg(field)
536 .arg((data&0xff), 2, 16)
537 .arg((data&0xff00)>>8, 2, 16)
538 .arg(b1, 2, 16, QChar('0'))
539 .arg(b2, 2, 16, QChar('0'))
540 .arg(QChar((b1 & 0x60) ? b1 : '_'))
541 .arg(QChar((b2 & 0x60) ? b2 : '_')));
542#endif
543 if (m_ccMode[field] >= 0)
544 {
545 mode = field << 2 |
546 (m_txtMode[(field*2) + m_ccMode[field]] << 1) |
547 m_ccMode[field];
548 if (mode != std::numeric_limits<std::size_t>::max())
549 len = m_ccBuf[mode].length();
550 else
551 len = 0;
552 }
553 else
554 {
555 mode = std::numeric_limits<std::size_t>::max();
556 len = 0;
557 }
558
559 if (FalseDup(tc, field, data))
560 {
562 return;
563 }
564 else if (XDSDecode(field, b1, b2))
565 {
566 return;
567 }
568 else if (b1 & 0x60)
569 {
570 // 0x20 <= b1 <= 0x7F
571 // text codes
572 FormatTextCode(tc, field, mode, len, b1, b2);
573 }
574 else if ((b1 & 0x10) && (b2 > 0x1F))
575 {
576 // 0x10 <= b1 <= 0x1F
577 // control codes
578 FormatControlCode(tc, field, b1, b2);
579 }
580
581 for (size_t mode2 = field*4; mode2 < ((field*4) + 4); mode2++)
582 {
583 size_t len2 = m_ccBuf[mode2].length();
584 if ((m_ignoreTimeCode || ((tc - m_timeCode[mode2]) > 100ms)) &&
585 (m_style[mode2] != CC_STYLE_POPUP) && len2)
586 {
587 // flush unfinished line if waiting too long
588 // in paint-on or scroll-up mode
589 m_timeCode[mode2] = tc;
590 BufferCC(mode2, len2, 0);
591 m_ccBuf[mode2] = "";
592 m_row[mode2] = m_lastRow[mode2];
593 m_lineCont[mode2] = 1;
594 }
595 }
596
597 if (data != m_lastCode[field])
598 {
599 m_lastCode[field] = data;
600 m_lastCodeTc[field] = tc;
601 }
602 m_lastTc[field] = tc;
603}
604
605bool CC608Decoder::FalseDup(std::chrono::milliseconds tc, int field, int data)
606{
607 int b1 = data & 0x7f;
608 int b2 = (data >> 8) & 0x7f;
609
611 {
612 // most digital streams with encoded VBI
613 // have duplicate control codes;
614 // suppress every other repeated control code
615 if ((data == m_lastCode[field]) &&
616 ((b1 & 0x70) == 0x10))
617 {
618 m_lastCode[field] = -1;
619 return true;
620 }
621 return false;
622 }
623
624 // bttv-0.9 VBI reads are pretty reliable (1 read/33367us).
625 // bttv-0.7 reads don't seem to work as well so if read intervals
626 // vary from this, be more conservative in detecting duplicate
627 // CC codes.
628 std::chrono::milliseconds dup_text_fudge = 0ms;
629 std::chrono::milliseconds dup_ctrl_fudge = 0ms;
630 if (m_badVbi[field] < 100 && b1 != 0 && b2 != 0)
631 {
632 std::chrono::milliseconds d = tc - m_lastTc[field];
633 if (d < 25ms || d > 42ms)
634 m_badVbi[field]++;
635 else if (m_badVbi[field] > 0)
636 m_badVbi[field]--;
637 }
638 if (m_badVbi[field] < 4)
639 {
640 // this should pick up all codes
641 dup_text_fudge = -2ms;
642 // this should pick up 1st, 4th, 6th, 8th, ... codes
643 dup_ctrl_fudge = 33ms - 4ms;
644 }
645 else
646 {
647 dup_text_fudge = 4ms;
648 dup_ctrl_fudge = 33ms - 4ms;
649 }
650
651 if (data == m_lastCode[field])
652 {
653 if ((b1 & 0x70) == 0x10)
654 {
655 if (tc > (m_lastCodeTc[field] + 67ms + dup_ctrl_fudge))
656 return false;
657 }
658 else if (b1)
659 {
660 // text, XDS
661 if (tc > (m_lastCodeTc[field] + 33ms + dup_text_fudge))
662 return false;
663 }
664
665 return true;
666 }
667
668 return false;
669}
670
671void CC608Decoder::ResetCC(size_t mode)
672{
673// m_lastRow[mode] = 0;
674// m_newRow[mode] = 0;
675// m_newCol[mode] = 0;
676// m_timeCode[mode] = 0;
677 m_row[mode] = 0;
678 m_col[mode] = 0;
679 m_rowCount[mode] = 0;
680// m_style[mode] = CC_STYLE_POPUP;
681 m_lineCont[mode] = 0;
682 m_resumeText[mode] = 0;
683 m_lastClr[mode] = 0ms;
684 m_ccBuf[mode] = "";
685}
686
687QString CC608Decoder::ToASCII(const QString &cc608str, bool suppress_unknown)
688{
689 QString ret = "";
690
691 for (const auto& cp : std::as_const(cc608str))
692 {
693 int cpu = cp.unicode();
694 if (cpu == 0)
695 break;
696 switch (cpu)
697 {
698 case 0x2120 : ret += "(SM)"; break;
699 case 0x2122 : ret += "(TM)"; break;
700 case 0x2014 : ret += "(--)"; break;
701 case 0x201C : ret += "``"; break;
702 case 0x201D : ret += "''"; break;
703 case 0x250C : ret += "|-"; break;
704 case 0x2510 : ret += "-|"; break;
705 case 0x2514 : ret += "|_"; break;
706 case 0x2518 : ret += "_|"; break;
707 case 0x2588 : ret += "[]"; break;
708 case 0x266A : ret += "o/~"; break;
709 case '\b' : ret += "\\b"; break;
710 default :
711 if (cpu >= 0x7000 && cpu < 0x7000 + 0x30)
712 {
713 if (!suppress_unknown)
714 ret += QString("[%1]").arg(cpu, 2, 16);
715 }
716 else if (cpu <= 0x80)
717 {
718 ret += QString(cp.toLatin1());
719 }
720 else if (!suppress_unknown)
721 {
722 ret += QString("{%1}").arg(cpu, 2, 16);
723 }
724 }
725 }
726
727 return ret;
728}
729
730void CC608Decoder::BufferCC(size_t mode, int len, int clr)
731{
732 QByteArray tmpbuf;
733 if (len)
734 {
735 // calculate UTF-8 encoding length
736 tmpbuf = m_ccBuf[mode].toUtf8();
737#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
738 len = std::min(tmpbuf.length(), 255);
739#else
740 len = std::min(tmpbuf.length(), static_cast<qsizetype>(255));
741#endif
742 }
743
744 unsigned char *bp = m_rbuf;
745 *(bp++) = m_row[mode];
746 *(bp++) = m_rowCount[mode];
747 *(bp++) = m_style[mode];
748 // overload resumetext field
749 unsigned char f = m_resumeText[mode];
750 f |= mode << 4;
751 if (m_lineCont[mode])
752 f |= CC_LINE_CONT;
753 *(bp++) = f;
754 *(bp++) = clr;
755 *(bp++) = len;
756 if (len)
757 {
758 memcpy(bp,
759 tmpbuf.constData(),
760 len);
761 len += sizeof(ccsubtitle);
762 }
763 else
764 {
765 len = sizeof(ccsubtitle);
766 }
767
768 if ((len != 0) && VERBOSE_LEVEL_CHECK(VB_VBI, LOG_INFO))
769 {
770 LOG(VB_VBI, LOG_INFO, QString("### %1 %2 %3 %4 %5 %6 %7 - '%8'")
771 .arg(m_timeCode[mode].count(), 10)
772 .arg(m_row[mode], 2).arg(m_rowCount[mode])
773 .arg(m_style[mode]).arg(f, 2, 16)
774 .arg(clr).arg(len, 3)
775 .arg(ToASCII(QString::fromUtf8(tmpbuf.constData(), len), false)));
776 }
777
778 m_reader->AddTextData(m_rbuf, len, m_timeCode[mode], 'C');
779 uint8_t ccmode = m_rbuf[3] & CC_MODE_MASK;
780 int stream = -1;
781 switch (ccmode)
782 {
783 case CC_CC1: stream = 0; break;
784 case CC_CC2: stream = 1; break;
785 case CC_CC3: stream = 2; break;
786 case CC_CC4: stream = 3; break;
787 }
788 if (stream >= 0)
789 m_lastSeen[stream] = std::chrono::system_clock::now();
790
791 m_resumeText[mode] = 0;
792 if (clr && !len)
793 m_lastClr[mode] = m_timeCode[mode];
794 else if (len)
795 m_lastClr[mode] = 0ms;
796}
797
798int CC608Decoder::NewRowCC(size_t mode, int len)
799{
800 if (m_style[mode] == CC_STYLE_ROLLUP)
801 {
802 // previous line was likely missing a carriage return
803 m_row[mode] = m_newRow[mode];
804 if (len)
805 {
806 BufferCC(mode, len, 0);
807 m_ccBuf[mode] = "";
808 len = 0;
809 }
810 m_col[mode] = 0;
811 m_lineCont[mode] = 0;
812 }
813 else
814 {
815 // popup/paint style
816
817 if (m_row[mode] == 0)
818 {
819 if (len == 0)
820 m_row[mode] = m_newRow[mode];
821 else
822 {
823 // previous line was missing a row address
824 // - assume it was one row up
825 m_ccBuf[mode] += '\n';
826 len++;
827 if (m_row[mode] == 0)
828 m_row[mode] = m_newRow[mode] - 1;
829 else
830 m_row[mode]--;
831 }
832 }
833 else if (m_newRow[mode] > m_lastRow[mode])
834 {
835 // next line can be more than one row away
836 for (int i = 0; i < (m_newRow[mode] - m_lastRow[mode]); i++)
837 {
838 m_ccBuf[mode] += '\n';
839 len++;
840 }
841 m_col[mode] = 0;
842 }
843 else if (m_newRow[mode] == m_lastRow[mode])
844 {
845 // same row
846 if (m_newCol[mode] >= m_col[mode])
847 {
848 // new line appends to current line
849 m_newCol[mode] -= m_col[mode];
850 }
851 else
852 {
853 // new line overwrites current line;
854 // could be legal (overwrite spaces?) but
855 // more likely we have bad address codes
856 // - just move to next line; may exceed row 15
857 // but frontend will adjust
858 m_ccBuf[mode] += '\n';
859 len++;
860 m_col[mode] = 0;
861 }
862 }
863 else
864 {
865 // next line goes upwards (not legal?)
866 // - flush
867 BufferCC(mode, len, 0);
868 m_ccBuf[mode] = "";
869 m_row[mode] = m_newRow[mode];
870 m_col[mode] = 0;
871 m_lineCont[mode] = 0;
872 len = 0;
873 }
874 }
875
876 m_lastRow[mode] = m_newRow[mode];
877 m_newRow[mode] = 0;
878
879 int limit = m_newCol[mode];
880 for (int x = 0; x < limit; x++)
881 {
882 m_ccBuf[mode] += ' ';
883 len++;
884 m_col[mode]++;
885 }
886
887 if (m_newAttr[mode])
888 {
889 m_ccBuf[mode] += QChar(m_newAttr[mode] + 0x7000);
890 len++;
891 }
892
893 m_newCol[mode] = 0;
894 m_newAttr[mode] = 0;
895
896 return len;
897}
898
899
900static bool IsPrintable(char c)
901{
902 return ((c) & 0x7F) >= 0x20 && ((c) & 0x7F) <= 0x7E;
903}
904
905static char Printable(char c)
906{
907 return IsPrintable(c) ? ((c) & 0x7F) : '.';
908}
909
910#if 0
911static int OddParity(unsigned char c)
912{
913 c ^= (c >> 4); c ^= (c >> 2); c ^= (c >> 1);
914 return c & 1;
915}
916#endif
917
918// // // // // // // // // // // // // // // // // // // // // // // //
919// // // // // // // // // // // VPS // // // // // // // // // // //
920// // // // // // // // // // // // // // // // // // // // // // // //
921
922static constexpr int PIL_TIME(int day, int mon, int hour, int min)
923{ return (day << 15) + (mon << 11) + (hour << 6) + min; }
924
925static void DumpPIL(int pil)
926{
927 int day = (pil >> 15);
928 int mon = (pil >> 11) & 0xF;
929 int hour = (pil >> 6 ) & 0x1F;
930 int min = (pil ) & 0x3F;
931
932 if (pil == PIL_TIME(0, 15, 31, 63))
933 LOG(VB_VBI, LOG_INFO, " PDC: Timer-control (no PDC)");
934 else if (pil == PIL_TIME(0, 15, 30, 63))
935 LOG(VB_VBI, LOG_INFO, " PDC: Recording inhibit/terminate");
936 else if (pil == PIL_TIME(0, 15, 29, 63))
937 LOG(VB_VBI, LOG_INFO, " PDC: Interruption");
938 else if (pil == PIL_TIME(0, 15, 28, 63))
939 LOG(VB_VBI, LOG_INFO, " PDC: Continue");
940 else if (pil == PIL_TIME(31, 15, 31, 63))
941 LOG(VB_VBI, LOG_INFO, " PDC: No time");
942 else
943 LOG(VB_VBI, LOG_INFO, QString(" PDC: %1, 200X-%2-%3 %4:%5")
944 .arg(pil).arg(mon).arg(day).arg(hour).arg(min));
945#undef PIL_TIME
946}
947
948void CC608Decoder::DecodeVPS(const unsigned char *buf)
949{
950 int c = vbi_bit_reverse[buf[1]];
951
952 if ((int8_t) c < 0)
953 {
954 m_vpsLabel[m_vpsL] = 0;
956 m_vpsL = 0;
957 }
958 c &= 0x7F;
960 m_vpsL = (m_vpsL + 1) % 16;
961
962 LOG(VB_VBI, LOG_INFO, QString("VPS: 3-10: %1 %2 %3 %4 %5 %6 %7 %8 (\"%9\")")
963 .arg(buf[0]).arg(buf[1]).arg(buf[2]).arg(buf[3]).arg(buf[4])
964 .arg(buf[5]).arg(buf[6]).arg(buf[7]).arg(m_vpsPrLabel.data()));
965
966 int pcs = buf[2] >> 6;
967 int cni = + ((buf[10] & 3) << 10)
968 + ((buf[11] & 0xC0) << 2)
969 + ((buf[8] & 0xC0) << 0)
970 + (buf[11] & 0x3F);
971 int pil = ((buf[8] & 0x3F) << 14) + (buf[9] << 6) + (buf[10] >> 2);
972 int pty = buf[12];
973
974 LOG(VB_VBI, LOG_INFO, QString("CNI: %1 PCS: %2 PTY: %3 ")
975 .arg(cni).arg(pcs).arg(pty));
976
977 DumpPIL(pil);
978
979 // c & 0xf0;
980}
981
982// // // // // // // // // // // // // // // // // // // // // // // //
983// // // // // // // // // // // WSS // // // // // // // // // // //
984// // // // // // // // // // // // // // // // // // // // // // // //
985
986void CC608Decoder::DecodeWSS(const unsigned char *buf)
987{
988 static const std::array<const int,8> kWssBits { 0, 0, 0, 1, 0, 1, 1, 1 };
989 uint wss = 0;
990
991 for (uint i = 0; i < 16; i++)
992 {
993 uint b1 = kWssBits[buf[i] & 7];
994 uint b2 = kWssBits[(buf[i] >> 3) & 7];
995
996 if (b1 == b2)
997 return;
998 wss |= b2 << i;
999 }
1000 unsigned char parity = wss & 0xf;
1001 parity ^= parity >> 2;
1002 parity ^= parity >> 1;
1003
1004 LOG(VB_VBI, LOG_INFO,
1005 QString("WSS: %1; %2 mode; %3 color coding;\n\t\t\t"
1006 " %4 helper; reserved b7=%5; %6\n\t\t\t"
1007 " open subtitles: %7; %scopyright %8; copying %9")
1008 .arg(QString::fromStdString(formats[wss & 7]),
1009 (wss & 0x0010) ? "film" : "camera",
1010 (wss & 0x0020) ? "MA/CP" : "standard",
1011 (wss & 0x0040) ? "modulated" : "no",
1012 (wss & 0x0080) ? "1" : "0",
1013 (wss & 0x0100) ? "have TTX subtitles; " : "",
1014 QString::fromStdString(subtitles[(wss >> 9) & 3]),
1015 (wss & 0x0800) ? "surround sound; " : "",
1016 (wss & 0x1000) ? "asserted" : "unknown")
1017 // Qt < 5.14 only allows a max of nin string arguments in a single call
1018 .arg((wss & 0x2000) ? "restricted" : "not restricted")); // clazy:exclude=qstring-arg
1019
1020 if (parity & 1)
1021 {
1022 m_wssFlags = wss;
1023 m_wssValid = true;
1024 }
1025}
1026
1027QString CC608Decoder::XDSDecodeString(const std::vector<unsigned char> &buf,
1028 uint start, uint end) const
1029{
1030#if DEBUG_XDS
1031 for (uint i = start; (i < buf.size()) && (i < end); i++)
1032 {
1033 LOG(VB_VBI, LOG_INFO, QString("%1: 0x%2 -> 0x%3 %4")
1034 .arg(i,2)
1035 .arg(buf[i],2,16,QChar('0'))
1036 .arg(CharCC(buf[i]).unicode(),2,16,QChar('0'))
1037 .arg(CharCC(buf[i])));
1038 }
1039#endif // DEBUG_XDS
1040
1041 QString tmp = "";
1042 for (uint i = start; (i < buf.size()) && (i < end); i++)
1043 {
1044 if (buf[i] > 0x0)
1045 tmp += CharCC(buf[i]);
1046 }
1047
1048#if DEBUG_XDS
1049 LOG(VB_VBI, LOG_INFO, QString("XDSDecodeString: '%1'").arg(tmp));
1050#endif // DEBUG_XDS
1051
1052 return tmp.trimmed();
1053}
1054
1055static bool is_better(const QString &newStr, const QString &oldStr)
1056{
1057 if (!newStr.isEmpty() && newStr != oldStr &&
1058 (newStr != oldStr.left(newStr.length())))
1059 {
1060 if (oldStr.isEmpty())
1061 return true;
1062
1063 // check if the string contains any bogus characters
1064 return std::all_of(newStr.cbegin(), newStr.cend(),
1065 [](auto ch){ return ch.toLatin1() >= 0x20; } );
1066 }
1067 return false;
1068}
1069
1071{
1072 QMutexLocker locker(&m_xdsLock);
1073 return m_xdsRatingSystems[(future) ? 1 : 0];
1074}
1075
1076uint CC608Decoder::GetRating(uint i, bool future) const
1077{
1078 QMutexLocker locker(&m_xdsLock);
1079 return m_xdsRating[(future) ? 1 : 0][i & 0x3] & 0x7;
1080}
1081
1082QString CC608Decoder::GetRatingString(uint i, bool future) const
1083{
1084 QMutexLocker locker(&m_xdsLock);
1085
1086 const std::array<const QString,4> prefix { "MPAA-", "TV-", "CE-", "CF-" };
1087 const std::array<const std::array<const QString,8>,4> mainStr
1088 {{
1089 { "NR", "G", "PG", "PG-13", "R", "NC-17", "X", "NR" },
1090 { "NR", "Y", "Y7", "G", "PG", "14", "MA", "NR" },
1091 { "E", "C", "C8+", "G", "PG", "14+", "18+", "NR" },
1092 { "E", "G", "8+", "13+", "16+", "18+", "NR", "NR" },
1093 }};
1094
1095 QString main = prefix[i] + mainStr[i][GetRating(i, future)];
1096
1097 if (kRatingTPG == i)
1098 {
1099 uint cf = (future) ? 1 : 0;
1100 if (!(m_xdsRating[cf][i]&0xF0))
1101 return main;
1102
1103 main += " ";
1104 // TPG flags
1105 if (m_xdsRating[cf][i] & 0x80)
1106 main += "D"; // Dialog
1107 if (m_xdsRating[cf][i] & 0x40)
1108 main += "V"; // Violence
1109 if (m_xdsRating[cf][i] & 0x20)
1110 main += "S"; // Sex
1111 if (m_xdsRating[cf][i] & 0x10)
1112 main += "L"; // Language
1113 }
1114
1115 return main;
1116}
1117
1118QString CC608Decoder::GetProgramName(bool future) const
1119{
1120 QMutexLocker locker(&m_xdsLock);
1121 return m_xdsProgramName[(future) ? 1 : 0];
1122}
1123
1124QString CC608Decoder::GetProgramType(bool future) const
1125{
1126 QMutexLocker locker(&m_xdsLock);
1127 const std::vector<uint> &program_type = m_xdsProgramType[(future) ? 1 : 0];
1128 QString tmp = "";
1129
1130 for (size_t i = 0; i < program_type.size(); i++)
1131 {
1132 if (i != 0)
1133 tmp += ", ";
1134 tmp += m_xdsProgramTypeString[program_type[i]];
1135 }
1136
1137 return tmp;
1138}
1139
1140QString CC608Decoder::GetXDS(const QString &key) const
1141{
1142 QMutexLocker locker(&m_xdsLock);
1143
1144 if (key == "ratings")
1145 return QString::number(GetRatingSystems(false));
1146 if (key.startsWith("has_rating_"))
1147 return ((1<<key.right(1).toUInt()) & GetRatingSystems(false))?"1":"0";
1148 if (key.startsWith("rating_"))
1149 return GetRatingString(key.right(1).toUInt(), false);
1150
1151 if (key == "future_ratings")
1152 return QString::number(GetRatingSystems(true));
1153 if (key.startsWith("has_future_rating_"))
1154 return ((1<<key.right(1).toUInt()) & GetRatingSystems(true))?"1":"0";
1155 if (key.startsWith("future_rating_"))
1156 return GetRatingString(key.right(1).toUInt(), true);
1157
1158 if (key == "programname")
1159 return GetProgramName(false);
1160 if (key == "future_programname")
1161 return GetProgramName(true);
1162
1163 if (key == "programtype")
1164 return GetProgramType(false);
1165 if (key == "future_programtype")
1166 return GetProgramType(true);
1167
1168 if (key == "callsign")
1169 return m_xdsNetCall;
1170 if (key == "channame")
1171 return m_xdsNetName;
1172 if (key == "tsid")
1173 return QString::number(m_xdsTsid);
1174
1175 return {};
1176}
1177
1178static std::array<const int,16> b1_to_service
1179{ -1, // 0x0
1180 0, 0, //0x1,0x2 -- Current
1181 1, 1, //0x3,0x4 -- Future
1182 2, 2, //0x5,0x6 -- Channel
1183 3, 3, //0x7,0x8 -- Misc
1184 4, 4, //0x9,0xA -- Public Service
1185 5, 5, //0xB,0xC -- Reserved
1186 6, 6, //0xD,0xE -- Private Data
1187 -1, // 0xF
1188};
1189
1190bool CC608Decoder::XDSDecode([[maybe_unused]] int field, int b1, int b2)
1191{
1192 if (field == 0)
1193 return false; // XDS is only on second field
1194
1195#if DEBUG_XDS
1196 LOG(VB_VBI, LOG_INFO,
1197 QString("XDSDecode: 0x%1 0x%2 '%3%4' xds[%5]=%6 in XDS %7")
1198 .arg(b1,2,16,QChar('0')).arg(b2,2,16,QChar('0'))
1199 .arg((CharCC(b1).unicode()>0x20) ? CharCC(b1) : QChar(' '))
1200 .arg((CharCC(b2).unicode()>0x20) ? CharCC(b2) : QChar(' '))
1201 .arg(field).arg(m_xds[field])
1202 .arg(m_xdsCurService));
1203#endif // DEBUG_XDS
1204
1205 if (m_xdsCurService < 0)
1206 {
1207 if (b1 > 0x0f)
1208 return false;
1209
1211
1212 if (m_xdsCurService < 0)
1213 return false;
1214
1215 if (b1 & 1)
1216 {
1217 m_xdsBuf[m_xdsCurService].clear(); // if start of service clear buffer
1218#if DEBUG_XDS
1219 LOG(VB_VBI, LOG_INFO, QString("XDSDecode: Starting XDS %1").arg(m_xdsCurService));
1220#endif // DEBUG_XDS
1221 }
1222 }
1223 else if ((0x0 < b1) && (b1 < 0x0f))
1224 { // switch to different service
1226#if DEBUG_XDS
1227 LOG(VB_VBI, LOG_INFO, QString("XDSDecode: Resuming XDS %1").arg(m_xdsCurService));
1228#endif // DEBUG_XDS
1229 }
1230
1231 if (m_xdsCurService < 0)
1232 return false;
1233
1234 m_xdsBuf[m_xdsCurService].push_back(b1);
1235 m_xdsBuf[m_xdsCurService].push_back(b2);
1236
1237 if (b1 == 0x0f) // end of packet
1238 {
1239#if DEBUG_XDS
1240 LOG(VB_VBI, LOG_INFO, QString("XDSDecode: Ending XDS %1").arg(m_xdsCurService));
1241#endif // DEBUG_XDS
1244 m_xdsBuf[m_xdsCurService].clear();
1245 m_xdsCurService = -1;
1246 }
1247 else if ((0x10 <= b1) && (b1 <= 0x1f)) // suspension of XDS packet
1248 {
1249#if DEBUG_XDS
1250 LOG(VB_VBI, LOG_INFO, QString("XDSDecode: Suspending XDS %1 on 0x%2")
1251 .arg(m_xdsCurService).arg(b1,2,16,QChar('0')));
1252#endif // DEBUG_XDS
1253 m_xdsCurService = -1;
1254 }
1255
1256 return true;
1257}
1258
1259void CC608Decoder::XDSPacketParse(const std::vector<unsigned char> &xds_buf)
1260{
1261 QMutexLocker locker(&m_xdsLock);
1262
1263 bool handled = false;
1264 int xds_class = xds_buf[0];
1265
1266 if (!xds_class)
1267 return;
1268
1269 if ((xds_class == 0x01) || (xds_class == 0x03)) // cont code 2 and 4, resp.
1270 handled = XDSPacketParseProgram(xds_buf, (xds_class == 0x03));
1271 else if (xds_class == 0x05) // cont code: 0x06
1272 handled = XDSPacketParseChannel(xds_buf);
1273 else if ((xds_class == 0x07) || // cont code: 0x08 // misc.
1274 (xds_class == 0x09) || // cont code: 0x0a // public (aka weather)
1275 (xds_class == 0x0b)) // cont code: 0x0c // reserved
1276 ;
1277 else if (xds_class == 0x0d) // cont code: 0x0e
1278 handled = true; // undefined
1279
1280 if (!handled)
1281 {
1282#if DEBUG_XDS
1283 LOG(VB_VBI, LOG_INFO, QString("XDS: ") +
1284 QString("Unhandled packet (0x%1 0x%2) sz(%3) '%4'")
1285 .arg(xds_buf[0],0,16).arg(xds_buf[1],0,16)
1286 .arg(xds_buf.size())
1287 .arg(XDSDecodeString(xds_buf, 2, xds_buf.size() - 2)));
1288#endif
1289 }
1290}
1291
1292bool CC608Decoder::XDSPacketCRC(const std::vector<unsigned char> &xds_buf)
1293{
1294 /* Check the checksum for validity of the packet. */
1295 int sum = 0;
1296 for (size_t i = 0; i < xds_buf.size() - 1; i++)
1297 sum += xds_buf[i];
1298
1299 if ((((~sum) & 0x7f) + 1) != xds_buf[xds_buf.size() - 1])
1300 {
1302
1303 LOG(VB_VBI, LOG_ERR, QString("XDS: failed CRC %1 of %2")
1305
1306 return false;
1307 }
1308
1310 return true;
1311}
1312
1314 const std::vector<unsigned char> &xds_buf, bool future)
1315{
1316 bool handled = true;
1317 int b2 = xds_buf[1];
1318 int cf = (future) ? 1 : 0;
1319 QString loc = (future) ? "XDS: Future " : "XDS: Current ";
1320
1321 if ((b2 == 0x01) && (xds_buf.size() >= 6))
1322 {
1323 uint min = xds_buf[2] & 0x3f;
1324 uint hour = xds_buf[3] & 0x0f;
1325 uint day = xds_buf[4] & 0x1f;
1326 uint month = xds_buf[5] & 0x0f;
1327 month = (month < 1 || month > 12) ? 0 : month;
1328
1329 LOG(VB_VBI, LOG_INFO, loc +
1330 QString("Start Time %1/%2 %3:%4%5")
1331 .arg(month).arg(day).arg(hour).arg(min / 10).arg(min % 10));
1332 }
1333 else if ((b2 == 0x02) && (xds_buf.size() >= 4))
1334 {
1335 uint length_min = xds_buf[2] & 0x3f;
1336 uint length_hour = xds_buf[3] & 0x3f;
1337 uint length_elapsed_min = 0;
1338 uint length_elapsed_hour = 0;
1339 uint length_elapsed_secs = 0;
1340 if (xds_buf.size() > 6)
1341 {
1342 length_elapsed_min = xds_buf[4] & 0x3f;
1343 length_elapsed_hour = xds_buf[5] & 0x3f;
1344 }
1345 if (xds_buf.size() > 8 && xds_buf[7] == 0x40)
1346 length_elapsed_secs = xds_buf[6] & 0x3f;
1347
1348 QString msg = QString("Program Length %1:%2%3 "
1349 "Time in Show %4:%5%6.%7%8")
1350 .arg(length_hour).arg(length_min / 10).arg(length_min % 10)
1351 .arg(length_elapsed_hour)
1352 .arg(length_elapsed_min / 10).arg(length_elapsed_min % 10)
1353 .arg(length_elapsed_secs / 10).arg(length_elapsed_secs % 10);
1354
1355 LOG(VB_VBI, LOG_INFO, loc + msg);
1356 }
1357 else if ((b2 == 0x03) && (xds_buf.size() >= 6))
1358 {
1359 QString tmp = XDSDecodeString(xds_buf, 2, xds_buf.size() - 2);
1360 if (is_better(tmp, m_xdsProgramName[cf]))
1361 {
1362 m_xdsProgramName[cf] = tmp;
1363 LOG(VB_VBI, LOG_INFO, loc + QString("Program Name: '%1'")
1364 .arg(GetProgramName(future)));
1365 }
1366 }
1367 else if ((b2 == 0x04) && (xds_buf.size() >= 6))
1368 {
1369 std::vector<uint> program_type;
1370 for (size_t i = 2; i < xds_buf.size() - 2; i++)
1371 {
1372 int cur = xds_buf[i] - 0x20;
1373 if (cur >= 0 && cur < 96)
1374 program_type.push_back(cur);
1375 }
1376
1377 bool unchanged = m_xdsProgramType[cf].size() == program_type.size();
1378 for (uint i = 0; (i < program_type.size()) && unchanged; i++)
1379 unchanged = m_xdsProgramType[cf][i] == program_type[i];
1380
1381 if (!unchanged)
1382 {
1383 m_xdsProgramType[cf] = program_type;
1384 LOG(VB_VBI, LOG_INFO, loc + QString("Program Type '%1'")
1385 .arg(GetProgramType(future)));
1386 }
1387 }
1388 else if ((b2 == 0x05) && (xds_buf.size() >= 4))
1389 {
1390 uint movie_rating = xds_buf[2] & 0x7;
1391 uint rating_system = (xds_buf[2] >> 3) & 0x7;
1392 uint tv_rating = xds_buf[3] & 0x7;
1393 uint VSL = xds_buf[3] & (0x7 << 3);
1394 uint sel = VSL | rating_system;
1395 if (sel == 3)
1396 {
1397 if (!(kHasCanEnglish & m_xdsRatingSystems[cf]) ||
1398 (tv_rating != GetRating(kRatingCanEnglish, future)))
1399 {
1401 m_xdsRating[cf][kRatingCanEnglish] = tv_rating;
1402 LOG(VB_VBI, LOG_INFO, loc + QString("VChip %1")
1403 .arg(GetRatingString(kRatingCanEnglish, future)));
1404 }
1405 }
1406 else if (sel == 7)
1407 {
1408 if (!(kHasCanFrench & m_xdsRatingSystems[cf]) ||
1409 (tv_rating != GetRating(kRatingCanFrench, future)))
1410 {
1412 m_xdsRating[cf][kRatingCanFrench] = tv_rating;
1413 LOG(VB_VBI, LOG_INFO, loc + QString("VChip %1")
1414 .arg(GetRatingString(kRatingCanFrench, future)));
1415 }
1416 }
1417 else if (sel == 0x13 || sel == 0x1f)
1418 {
1419 ; // Reserved according to TVTime code
1420 }
1421 else if ((rating_system & 0x3) == 1)
1422 {
1423 if (!(kHasTPG & m_xdsRatingSystems[cf]) ||
1424 (tv_rating != GetRating(kRatingTPG, future)))
1425 {
1426 uint f = ((xds_buf[0]<<3) & 0x80) | ((xds_buf[1]<<1) & 0x70);
1428 m_xdsRating[cf][kRatingTPG] = tv_rating | f;
1429 LOG(VB_VBI, LOG_INFO, loc + QString("VChip %1")
1430 .arg(GetRatingString(kRatingTPG, future)));
1431 }
1432 }
1433 else if (rating_system == 0)
1434 {
1435 if (!(kHasMPAA & m_xdsRatingSystems[cf]) ||
1436 (movie_rating != GetRating(kRatingMPAA, future)))
1437 {
1439 m_xdsRating[cf][kRatingMPAA] = movie_rating;
1440 LOG(VB_VBI, LOG_INFO, loc + QString("VChip %1")
1441 .arg(GetRatingString(kRatingMPAA, future)));
1442 }
1443 }
1444 else
1445 {
1446 LOG(VB_VBI, LOG_ERR, loc +
1447 QString("VChip Unhandled -- rs(%1) rating(%2:%3)")
1448 .arg(rating_system).arg(tv_rating).arg(movie_rating));
1449 }
1450 }
1451#if 0
1452 else if (b2 == 0x07)
1453 ; // caption
1454 else if (b2 == 0x08)
1455 ; // cgms
1456 else if (b2 == 0x09)
1457 ; // unknown
1458 else if (b2 == 0x0c)
1459 ; // misc
1460 else if (b2 == 0x10 || b2 == 0x13 || b2 == 0x15 || b2 == 0x16 ||
1461 b2 == 0x91 || b2 == 0x92 || b2 == 0x94 || b2 == 0x97)
1462 ; // program description
1463 else if (b2 == 0x86)
1464 ; // audio
1465 else if (b2 == 0x89)
1466 ; // aspect
1467 else if (b2 == 0x8c)
1468 ; // program data
1469#endif
1470 else
1471 {
1472 handled = false;
1473 }
1474
1475 return handled;
1476}
1477
1478bool CC608Decoder::XDSPacketParseChannel(const std::vector<unsigned char> &xds_buf)
1479{
1480 bool handled = true;
1481
1482 int b2 = xds_buf[1];
1483 if ((b2 == 0x01) && (xds_buf.size() >= 6))
1484 {
1485 QString tmp = XDSDecodeString(xds_buf, 2, xds_buf.size() - 2);
1487 {
1488 LOG(VB_VBI, LOG_INFO, QString("XDS: Network Name '%1'").arg(tmp));
1489 m_xdsNetName = tmp;
1490 }
1491 }
1492 else if ((b2 == 0x02) && (xds_buf.size() >= 6))
1493 {
1494 QString tmp = XDSDecodeString(xds_buf, 2, xds_buf.size() - 2);
1495 if (is_better(tmp, m_xdsNetCall) && (tmp.indexOf(" ") < 0))
1496 {
1497 LOG(VB_VBI, LOG_INFO, QString("XDS: Network Call '%1'").arg(tmp));
1498 m_xdsNetCall = tmp;
1499 }
1500 }
1501 else if ((b2 == 0x04) && (xds_buf.size() >= 6))
1502 {
1503 uint tsid = (xds_buf[2] << 24 | xds_buf[3] << 16 |
1504 xds_buf[4] << 8 | xds_buf[5]);
1505 if (tsid != m_xdsTsid)
1506 {
1507 LOG(VB_VBI, LOG_INFO, QString("XDS: TSID 0x%1").arg(tsid,0,16));
1508 m_xdsTsid = tsid;
1509 }
1510 }
1511 else
1512 {
1513 handled = false;
1514 }
1515
1516 return handled;
1517}
1518
1519static void init_xds_program_type(CC608ProgramType& xds_program_type)
1520{
1521 xds_program_type[0] = QCoreApplication::translate("(Categories)",
1522 "Education");
1523 xds_program_type[1] = QCoreApplication::translate("(Categories)",
1524 "Entertainment");
1525 xds_program_type[2] = QCoreApplication::translate("(Categories)",
1526 "Movie");
1527 xds_program_type[3] = QCoreApplication::translate("(Categories)",
1528 "News");
1529 xds_program_type[4] = QCoreApplication::translate("(Categories)",
1530 "Religious");
1531 xds_program_type[5] = QCoreApplication::translate("(Categories)",
1532 "Sports");
1533 xds_program_type[6] = QCoreApplication::translate("(Categories)",
1534 "Other");
1535 xds_program_type[7] = QCoreApplication::translate("(Categories)",
1536 "Action");
1537 xds_program_type[8] = QCoreApplication::translate("(Categories)",
1538 "Advertisement");
1539 xds_program_type[9] = QCoreApplication::translate("(Categories)",
1540 "Animated");
1541 xds_program_type[10] = QCoreApplication::translate("(Categories)",
1542 "Anthology");
1543 xds_program_type[11] = QCoreApplication::translate("(Categories)",
1544 "Automobile");
1545 xds_program_type[12] = QCoreApplication::translate("(Categories)",
1546 "Awards");
1547 xds_program_type[13] = QCoreApplication::translate("(Categories)",
1548 "Baseball");
1549 xds_program_type[14] = QCoreApplication::translate("(Categories)",
1550 "Basketball");
1551 xds_program_type[15] = QCoreApplication::translate("(Categories)",
1552 "Bulletin");
1553 xds_program_type[16] = QCoreApplication::translate("(Categories)",
1554 "Business");
1555 xds_program_type[17] = QCoreApplication::translate("(Categories)",
1556 "Classical");
1557 xds_program_type[18] = QCoreApplication::translate("(Categories)",
1558 "College");
1559 xds_program_type[19] = QCoreApplication::translate("(Categories)",
1560 "Combat");
1561 xds_program_type[20] = QCoreApplication::translate("(Categories)",
1562 "Comedy");
1563 xds_program_type[21] = QCoreApplication::translate("(Categories)",
1564 "Commentary");
1565 xds_program_type[22] = QCoreApplication::translate("(Categories)",
1566 "Concert");
1567 xds_program_type[23] = QCoreApplication::translate("(Categories)",
1568 "Consumer");
1569 xds_program_type[24] = QCoreApplication::translate("(Categories)",
1570 "Contemporary");
1571 xds_program_type[25] = QCoreApplication::translate("(Categories)",
1572 "Crime");
1573 xds_program_type[26] = QCoreApplication::translate("(Categories)",
1574 "Dance");
1575 xds_program_type[27] = QCoreApplication::translate("(Categories)",
1576 "Documentary");
1577 xds_program_type[28] = QCoreApplication::translate("(Categories)",
1578 "Drama");
1579 xds_program_type[29] = QCoreApplication::translate("(Categories)",
1580 "Elementary");
1581 xds_program_type[30] = QCoreApplication::translate("(Categories)",
1582 "Erotica");
1583 xds_program_type[31] = QCoreApplication::translate("(Categories)",
1584 "Exercise");
1585 xds_program_type[32] = QCoreApplication::translate("(Categories)",
1586 "Fantasy");
1587 xds_program_type[33] = QCoreApplication::translate("(Categories)",
1588 "Farm");
1589 xds_program_type[34] = QCoreApplication::translate("(Categories)",
1590 "Fashion");
1591 xds_program_type[35] = QCoreApplication::translate("(Categories)",
1592 "Fiction");
1593 xds_program_type[36] = QCoreApplication::translate("(Categories)",
1594 "Food");
1595 xds_program_type[37] = QCoreApplication::translate("(Categories)",
1596 "Football");
1597 xds_program_type[38] = QCoreApplication::translate("(Categories)",
1598 "Foreign");
1599 xds_program_type[39] = QCoreApplication::translate("(Categories)",
1600 "Fundraiser");
1601 xds_program_type[40] = QCoreApplication::translate("(Categories)",
1602 "Game/Quiz");
1603 xds_program_type[41] = QCoreApplication::translate("(Categories)",
1604 "Garden");
1605 xds_program_type[42] = QCoreApplication::translate("(Categories)",
1606 "Golf");
1607 xds_program_type[43] = QCoreApplication::translate("(Categories)",
1608 "Government");
1609 xds_program_type[44] = QCoreApplication::translate("(Categories)",
1610 "Health");
1611 xds_program_type[45] = QCoreApplication::translate("(Categories)",
1612 "High School");
1613 xds_program_type[46] = QCoreApplication::translate("(Categories)",
1614 "History");
1615 xds_program_type[47] = QCoreApplication::translate("(Categories)",
1616 "Hobby");
1617 xds_program_type[48] = QCoreApplication::translate("(Categories)",
1618 "Hockey");
1619 xds_program_type[49] = QCoreApplication::translate("(Categories)",
1620 "Home");
1621 xds_program_type[50] = QCoreApplication::translate("(Categories)",
1622 "Horror");
1623 xds_program_type[51] = QCoreApplication::translate("(Categories)",
1624 "Information");
1625 xds_program_type[52] = QCoreApplication::translate("(Categories)",
1626 "Instruction");
1627 xds_program_type[53] = QCoreApplication::translate("(Categories)",
1628 "International");
1629 xds_program_type[54] = QCoreApplication::translate("(Categories)",
1630 "Interview");
1631 xds_program_type[55] = QCoreApplication::translate("(Categories)",
1632 "Language");
1633 xds_program_type[56] = QCoreApplication::translate("(Categories)",
1634 "Legal");
1635 xds_program_type[57] = QCoreApplication::translate("(Categories)",
1636 "Live");
1637 xds_program_type[58] = QCoreApplication::translate("(Categories)",
1638 "Local");
1639 xds_program_type[59] = QCoreApplication::translate("(Categories)",
1640 "Math");
1641 xds_program_type[60] = QCoreApplication::translate("(Categories)",
1642 "Medical");
1643 xds_program_type[61] = QCoreApplication::translate("(Categories)",
1644 "Meeting");
1645 xds_program_type[62] = QCoreApplication::translate("(Categories)",
1646 "Military");
1647 xds_program_type[63] = QCoreApplication::translate("(Categories)",
1648 "Miniseries");
1649 xds_program_type[64] = QCoreApplication::translate("(Categories)",
1650 "Music");
1651 xds_program_type[65] = QCoreApplication::translate("(Categories)",
1652 "Mystery");
1653 xds_program_type[66] = QCoreApplication::translate("(Categories)",
1654 "National");
1655 xds_program_type[67] = QCoreApplication::translate("(Categories)",
1656 "Nature");
1657 xds_program_type[68] = QCoreApplication::translate("(Categories)",
1658 "Police");
1659 xds_program_type[69] = QCoreApplication::translate("(Categories)",
1660 "Politics");
1661 xds_program_type[70] = QCoreApplication::translate("(Categories)",
1662 "Premiere");
1663 xds_program_type[71] = QCoreApplication::translate("(Categories)",
1664 "Prerecorded");
1665 xds_program_type[72] = QCoreApplication::translate("(Categories)",
1666 "Product");
1667 xds_program_type[73] = QCoreApplication::translate("(Categories)",
1668 "Professional");
1669 xds_program_type[74] = QCoreApplication::translate("(Categories)",
1670 "Public");
1671 xds_program_type[75] = QCoreApplication::translate("(Categories)",
1672 "Racing");
1673 xds_program_type[76] = QCoreApplication::translate("(Categories)",
1674 "Reading");
1675 xds_program_type[77] = QCoreApplication::translate("(Categories)",
1676 "Repair");
1677 xds_program_type[78] = QCoreApplication::translate("(Categories)",
1678 "Repeat");
1679 xds_program_type[79] = QCoreApplication::translate("(Categories)",
1680 "Review");
1681 xds_program_type[80] = QCoreApplication::translate("(Categories)",
1682 "Romance");
1683 xds_program_type[81] = QCoreApplication::translate("(Categories)",
1684 "Science");
1685 xds_program_type[82] = QCoreApplication::translate("(Categories)",
1686 "Series");
1687 xds_program_type[83] = QCoreApplication::translate("(Categories)",
1688 "Service");
1689 xds_program_type[84] = QCoreApplication::translate("(Categories)",
1690 "Shopping");
1691 xds_program_type[85] = QCoreApplication::translate("(Categories)",
1692 "Soap Opera");
1693 xds_program_type[86] = QCoreApplication::translate("(Categories)",
1694 "Special");
1695 xds_program_type[87] = QCoreApplication::translate("(Categories)",
1696 "Suspense");
1697 xds_program_type[88] = QCoreApplication::translate("(Categories)",
1698 "Talk");
1699 xds_program_type[89] = QCoreApplication::translate("(Categories)",
1700 "Technical");
1701 xds_program_type[90] = QCoreApplication::translate("(Categories)",
1702 "Tennis");
1703 xds_program_type[91] = QCoreApplication::translate("(Categories)",
1704 "Travel");
1705 xds_program_type[92] = QCoreApplication::translate("(Categories)",
1706 "Variety");
1707 xds_program_type[93] = QCoreApplication::translate("(Categories)",
1708 "Video");
1709 xds_program_type[94] = QCoreApplication::translate("(Categories)",
1710 "Weather");
1711 xds_program_type[95] = QCoreApplication::translate("(Categories)",
1712 "Western");
1713}
static const std::array< const uint8_t, 256 > vbi_bit_reverse
static std::array< const int, 16 > b1_to_service
static const std::array< const QChar, 32 > extendedchar2
static const std::array< const std::string, 4 > subtitles
static const std::array< const int, 16 > rowdata
static const std::array< const QChar, 16 > specialchar
static void DumpPIL(int pil)
static bool IsPrintable(char c)
static void init_xds_program_type(CC608ProgramType &xds_program_type)
static char Printable(char c)
static const std::array< const std::string, 8 > formats
static constexpr int PIL_TIME(int day, int mon, int hour, int min)
static const std::array< const QChar, 32 > extendedchar3
static bool is_better(const QString &newStr, const QString &oldStr)
std::array< bool, 4 > CC608Seen
Definition: cc608decoder.h:59
@ kHasCanEnglish
Definition: cc608decoder.h:78
@ kHasCanFrench
Definition: cc608decoder.h:79
@ kHasMPAA
Definition: cc608decoder.h:76
@ kHasTPG
Definition: cc608decoder.h:77
std::array< QString, 96 > CC608ProgramType
Definition: cc608decoder.h:60
static constexpr uint8_t CC_LINE_CONT
Definition: cc608decoder.h:45
@ CC_CC4
Definition: cc608decoder.h:54
@ CC_CC3
Definition: cc608decoder.h:53
@ CC_CC2
Definition: cc608decoder.h:50
@ CC_CC1
Definition: cc608decoder.h:49
@ kRatingCanFrench
Definition: cc608decoder.h:86
@ kRatingCanEnglish
Definition: cc608decoder.h:85
@ kRatingMPAA
Definition: cc608decoder.h:83
@ kRatingTPG
Definition: cc608decoder.h:84
@ CC_STYLE_PAINT
Definition: cc608decoder.h:40
@ CC_STYLE_POPUP
Definition: cc608decoder.h:39
@ CC_STYLE_ROLLUP
Definition: cc608decoder.h:41
static constexpr uint8_t CC_MODE_MASK
Definition: cc608decoder.h:46
CC608PerField m_badVbi
Definition: cc608decoder.h:142
uint m_xdsCrcPassed
Definition: cc608decoder.h:184
CC608PerFieldTc m_lastFormatTc
Definition: cc608decoder.h:170
CC608Input * m_reader
Definition: cc608decoder.h:135
QString m_xdsNetName
Definition: cc608decoder.h:194
bool m_ignoreTimeCode
Definition: cc608decoder.h:137
CC608PerMode m_lineCont
Definition: cc608decoder.h:160
QString GetRatingString(uint i, bool future) const
CC608PerField m_xds
Definition: cc608decoder.h:147
void XDSPacketParse(const std::vector< unsigned char > &xds_buf)
CC608PerFieldTc m_lastTc
Definition: cc608decoder.h:143
QString GetProgramType(bool future) const
bool XDSDecode(int field, int b1, int b2)
static QString ToASCII(const QString &cc608, bool suppress_unknown)
std::array< uint, 2 > m_xdsRatingSystems
Definition: cc608decoder.h:188
QString GetXDS(const QString &key) const
CC608PerMode m_col
Definition: cc608decoder.h:157
QString XDSDecodeString(const std::vector< unsigned char > &buf, uint start, uint end) const
std::array< QString, 2 > m_xdsProgramName
Definition: cc608decoder.h:190
uint m_xdsCrcFailed
Definition: cc608decoder.h:185
void DecodeWSS(const unsigned char *buf)
CC608PerField m_lastFormatData
Definition: cc608decoder.h:171
CC608PerMode m_resumeText
Definition: cc608decoder.h:161
CC608PerModeTc m_lastClr
Definition: cc608decoder.h:162
std::array< QChar, 128 > m_stdChar
Definition: cc608decoder.h:166
void FormatControlCode(std::chrono::milliseconds tc, size_t field, int b1, int b2)
void FormatTextCode(std::chrono::milliseconds tc, size_t field, size_t mode, size_t len, int b1, int b2)
QRecursiveMutex m_xdsLock
Definition: cc608decoder.h:187
std::array< std::array< uint, 4 >, 2 > m_xdsRating
Definition: cc608decoder.h:189
void FormatCC(std::chrono::milliseconds tc, int code1, int code2)
bool XDSPacketParseChannel(const std::vector< unsigned char > &xds_buf)
bool XDSPacketParseProgram(const std::vector< unsigned char > &xds_buf, bool future)
void DecodeVPS(const unsigned char *buf)
QChar CharCC(int code) const
Definition: cc608decoder.h:118
CC608PerMode m_lastRow
Definition: cc608decoder.h:151
QString m_xdsNetCall
Definition: cc608decoder.h:193
CC608PerField m_lastCode
Definition: cc608decoder.h:144
bool FalseDup(std::chrono::milliseconds tc, int field, int data)
std::array< QString, 8 > m_ccBuf
Definition: cc608decoder.h:163
QString GetProgramName(bool future) const
CC608ProgramType m_xdsProgramTypeString
Definition: cc608decoder.h:197
uint GetRatingSystems(bool future) const
CC608PerFieldTc m_lastCodeTc
Definition: cc608decoder.h:145
void GetServices(std::chrono::seconds seconds, CC608Seen &seen) const
void BufferCC(size_t mode, int len, int clr)
std::array< std::vector< unsigned char >, 7 > m_xdsBuf
Definition: cc608decoder.h:183
void ResetCC(size_t mode)
CC608PerMode m_newAttr
Definition: cc608decoder.h:154
bool XDSPacketCRC(const std::vector< unsigned char > &xds_buf)
std::array< std::vector< uint >, 2 > m_xdsProgramType
Definition: cc608decoder.h:191
unsigned char * m_rbuf
Definition: cc608decoder.h:169
std::array< char, 20 > m_vpsLabel
Definition: cc608decoder.h:175
CC608Decoder(CC608Input *ccr)
std::array< int, 4 > m_txtMode
Definition: cc608decoder.h:148
CC608PerMode m_style
Definition: cc608decoder.h:159
CC608PerMode m_newRow
Definition: cc608decoder.h:152
CC608PerMode m_rowCount
Definition: cc608decoder.h:158
std::array< SystemTime, 4 > m_lastSeen
Definition: cc608decoder.h:139
CC608PerField m_ccMode
Definition: cc608decoder.h:146
uint GetRating(uint i, bool future) const
CC608PerMode m_newCol
Definition: cc608decoder.h:153
std::array< char, 20 > m_vpsPrLabel
Definition: cc608decoder.h:174
CC608PerModeTc m_timeCode
Definition: cc608decoder.h:155
int NewRowCC(size_t mode, int len)
void FormatCCField(std::chrono::milliseconds tc, size_t field, int data)
CC608PerMode m_row
Definition: cc608decoder.h:156
virtual void AddTextData(unsigned char *buf, int len, std::chrono::milliseconds timecode, char type)=0
unsigned int uint
Definition: compat.h:68
static uint32_t * tmp
Definition: goom_core.cpp:28
static const iso6937table * d
static bool VERBOSE_LEVEL_CHECK(uint64_t mask, LogLevel_t level)
Definition: mythlogging.h:29
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39