Files
ccextractor/src/lib_ccx/ccx_decoders_608.c
Gaurav karmakar 689b27ce2f fix(608): handle pop-on to roll-up transition ending at EOF (#2268)
When a stream switches from pop-on to roll-up mode and ends before
check_roll_up() reports changes=1 (fewer lines than the roll-up window
requires), the CR handler never runs the backfill at line 836 and
current_visible_start_ms is still zero when flush_608_context fires
EraseDisplayedMemory. Result: the first emitted caption is timestamped
at 00:00:00,000.

Capture the FTS of the first character after the transition in a new
field ts_first_char_rollup_transition. Unlike ts_start_of_current_line,
this field is not overwritten by intermediate changes=0 CRs, so the
first-char time survives to the end-of-stream flush path.
write_cc_buffer uses it to backfill current_visible_start_ms when
rollup_from_popon is still set at emit time, and clears it together
with the flag. The normal popon->roll-up flow (where a scrolling CR
eventually fires) and every other caption path are unchanged.

Verified on c4dd893cb9d6...ts: first subtitle now starts at
00:00:13,913 (was 00:00:00,000). Regression tested against six master
samples the mentor flagged (0069dffd.mpg 03,603; 5cbb21ad.dvr-ms
00,534; 132d7df7.mov 12,812; 6395b281.asf 02,436; 8849331d.mp4 03,771;
b22260d0.ts 04,838) - all match the reference timings unchanged.

Co-authored-by: GAURAV KARMAKAR <gaurav.k@graeon.ai>
2026-04-21 22:53:37 -07:00

1380 lines
42 KiB
C

#include "ccx_decoders_608.h"
#include "ccx_common_common.h"
#include "ccx_common_structs.h"
#include "ccx_common_constants.h"
#include "ccx_common_timing.h"
#include "ccx_decoders_structs.h"
#include "ccx_decoders_xds.h"
static const int rowdata[] = {11, -1, 1, 2, 3, 4, 12, 13, 14, 15, 5, 6, 7, 8, 9, 10};
// Relationship between the first PAC byte and the row number
int in_xds_mode = 0;
// unsigned char str[2048]; // Another generic general purpose buffer
const unsigned char pac2_attribs[][3] = // Color, font, ident
{
{COL_WHITE, FONT_REGULAR, 0}, // 0x40 || 0x60
{COL_WHITE, FONT_UNDERLINED, 0}, // 0x41 || 0x61
{COL_GREEN, FONT_REGULAR, 0}, // 0x42 || 0x62
{COL_GREEN, FONT_UNDERLINED, 0}, // 0x43 || 0x63
{COL_BLUE, FONT_REGULAR, 0}, // 0x44 || 0x64
{COL_BLUE, FONT_UNDERLINED, 0}, // 0x45 || 0x65
{COL_CYAN, FONT_REGULAR, 0}, // 0x46 || 0x66
{COL_CYAN, FONT_UNDERLINED, 0}, // 0x47 || 0x67
{COL_RED, FONT_REGULAR, 0}, // 0x48 || 0x68
{COL_RED, FONT_UNDERLINED, 0}, // 0x49 || 0x69
{COL_YELLOW, FONT_REGULAR, 0}, // 0x4a || 0x6a
{COL_YELLOW, FONT_UNDERLINED, 0}, // 0x4b || 0x6b
{COL_MAGENTA, FONT_REGULAR, 0}, // 0x4c || 0x6c
{COL_MAGENTA, FONT_UNDERLINED, 0}, // 0x4d || 0x6d
{COL_WHITE, FONT_ITALICS, 0}, // 0x4e || 0x6e
{COL_WHITE, FONT_UNDERLINED_ITALICS, 0}, // 0x4f || 0x6f
{COL_WHITE, FONT_REGULAR, 0}, // 0x50 || 0x70
{COL_WHITE, FONT_UNDERLINED, 0}, // 0x51 || 0x71
{COL_WHITE, FONT_REGULAR, 4}, // 0x52 || 0x72
{COL_WHITE, FONT_UNDERLINED, 4}, // 0x53 || 0x73
{COL_WHITE, FONT_REGULAR, 8}, // 0x54 || 0x74
{COL_WHITE, FONT_UNDERLINED, 8}, // 0x55 || 0x75
{COL_WHITE, FONT_REGULAR, 12}, // 0x56 || 0x76
{COL_WHITE, FONT_UNDERLINED, 12}, // 0x57 || 0x77
{COL_WHITE, FONT_REGULAR, 16}, // 0x58 || 0x78
{COL_WHITE, FONT_UNDERLINED, 16}, // 0x59 || 0x79
{COL_WHITE, FONT_REGULAR, 20}, // 0x5a || 0x7a
{COL_WHITE, FONT_UNDERLINED, 20}, // 0x5b || 0x7b
{COL_WHITE, FONT_REGULAR, 24}, // 0x5c || 0x7c
{COL_WHITE, FONT_UNDERLINED, 24}, // 0x5d || 0x7d
{COL_WHITE, FONT_REGULAR, 28}, // 0x5e || 0x7e
{COL_WHITE, FONT_UNDERLINED, 28} // 0x5f || 0x7f
};
static const char *command_type[] =
{
"Unknown",
"EDM - EraseDisplayedMemory",
"RCL - ResumeCaptionLoading",
"EOC - End Of Caption",
"TO1 - Tab Offset, 1 column",
"TO2 - Tab Offset, 2 column",
"TO3 - Tab Offset, 3 column",
"RU2 - Roll up 2 rows",
"RU3 - Roll up 3 rows",
"RU4 - Roll up 4 rows",
"CR - Carriage Return",
"ENM - Erase non-displayed memory",
"BS - Backspace",
"RTD - Resume Text Display",
"AOF - Not Used (Alarm Off)",
"AON - Not Used (Alarm On)",
"DER - Delete to End of Row",
"RDC - Resume Direct Captioning",
"RU1 - Fake Roll up 1 rows"};
static const char *font_text[] =
{
"regular",
"italics",
"underlined",
"underlined italics"};
#if 0
static const char *cc_modes_text[] =
{
"Pop-Up captions"
};
#endif
const char *color_text[MAX_COLOR][2] =
{
{"white", ""},
{"green", "<font color=\"#00ff00\">"},
{"blue", "<font color=\"#0000ff\">"},
{"cyan", "<font color=\"#00ffff\">"},
{"red", "<font color=\"#ff0000\">"},
{"yellow", "<font color=\"#ffff00\">"},
{"magenta", "<font color=\"#ff00ff\">"},
{"userdefined", "<font color=\""},
{"black", ""},
{"transparent", ""}};
void clear_eia608_cc_buffer(ccx_decoder_608_context *context, struct eia608_screen *data)
{
for (int i = 0; i < CCX_DECODER_608_SCREEN_ROWS; i++)
{
memset(data->characters[i], ' ', CCX_DECODER_608_SCREEN_WIDTH);
data->characters[i][CCX_DECODER_608_SCREEN_WIDTH] = 0;
for (int j = 0; j < CCX_DECODER_608_SCREEN_WIDTH + 1; ++j)
{
data->colors[i][j] = context->settings->default_color;
data->fonts[i][j] = FONT_REGULAR;
}
data->row_used[i] = 0;
}
data->empty = 1;
}
void ccx_decoder_608_dinit_library(void **ctx)
{
freep(ctx);
}
ccx_decoder_608_context *ccx_decoder_608_init_library(struct ccx_decoder_608_settings *settings, int channel,
int field, int *halt,
int cc_to_stdout,
enum ccx_output_format output_format, struct ccx_common_timing_ctx *timing)
{
ccx_decoder_608_context *data = NULL;
data = malloc(sizeof(ccx_decoder_608_context));
if (!data)
return NULL;
data->cursor_column = 0;
data->cursor_row = 0;
data->visible_buffer = 1;
data->last_c1 = 0;
data->last_c2 = 0;
data->mode = MODE_POPON;
// data->current_visible_start_cc=0;
data->current_visible_start_ms = 0;
data->screenfuls_counter = 0;
data->channel = 1;
data->font = FONT_REGULAR;
data->rollup_base_row = 14;
data->ts_start_of_current_line = -1;
data->ts_last_char_received = -1;
data->new_channel = 1;
data->bytes_processed_608 = 0;
data->my_field = field;
data->my_channel = channel;
data->have_cursor_position = 0;
data->rollup_from_popon = 0;
data->ts_first_char_rollup_transition = -1;
data->output_format = output_format;
data->cc_to_stdout = cc_to_stdout;
data->textprinted = 0;
// Note: ts_start_of_current_line already set to -1 above
data->halt = halt;
data->settings = settings;
data->current_color = data->settings->default_color;
data->report = settings->report;
data->timing = timing;
clear_eia608_cc_buffer(data, &data->buffer1);
clear_eia608_cc_buffer(data, &data->buffer2);
return data;
}
struct eia608_screen *get_writing_buffer(ccx_decoder_608_context *context)
{
struct eia608_screen *use_buffer = NULL;
switch (context->mode)
{
case MODE_POPON: // Write on the non-visible buffer
if (context->visible_buffer == 1)
use_buffer = &context->buffer2;
else
use_buffer = &context->buffer1;
break;
case MODE_FAKE_ROLLUP_1: // Write directly to screen
case MODE_ROLLUP_2:
case MODE_ROLLUP_3:
case MODE_ROLLUP_4:
case MODE_PAINTON:
case MODE_TEXT:
// TODO: Fix this. Text uses a different buffer, and contains non-program information.
if (context->visible_buffer == 1)
use_buffer = &context->buffer1;
else
use_buffer = &context->buffer2;
break;
default:
ccx_common_logging.fatal_ftn(CCX_COMMON_EXIT_BUG_BUG, "Caption mode has an illegal value at get_writing_buffer(), this is a bug.\n");
}
return use_buffer;
}
void delete_to_end_of_row(ccx_decoder_608_context *context)
{
if (context->mode != MODE_TEXT)
{
if (context->cursor_row >= CCX_DECODER_608_SCREEN_ROWS)
return;
struct eia608_screen *use_buffer = get_writing_buffer(context);
for (int i = context->cursor_column; i <= CCX_DECODER_608_SCREEN_WIDTH - 1; i++)
{
// TODO: This can change the 'used' situation of a column, so we'd
// need to check and correct.
use_buffer->characters[context->cursor_row][i] = ' ';
use_buffer->colors[context->cursor_row][i] = context->settings->default_color;
use_buffer->fonts[context->cursor_row][i] = context->font;
}
}
}
void write_char(const unsigned char c, ccx_decoder_608_context *context)
{
if (context->mode != MODE_TEXT)
{
struct eia608_screen *use_buffer = get_writing_buffer(context);
/* printf ("\rWriting char [%c] at %s:%d:%d\n",c,
use_buffer == &wb->data608->buffer1?"B1":"B2",
wb->data608->cursor_row,wb->data608->cursor_column); */
if (context->cursor_row >= CCX_DECODER_608_SCREEN_ROWS || context->cursor_column >= CCX_DECODER_608_SCREEN_WIDTH)
return;
use_buffer->characters[context->cursor_row][context->cursor_column] = c;
use_buffer->colors[context->cursor_row][context->cursor_column] = context->current_color;
use_buffer->fonts[context->cursor_row][context->cursor_column] = context->font;
use_buffer->row_used[context->cursor_row] = 1;
if (use_buffer->empty)
{
// Don't set start time if we're in a transition from pop-on to roll-up
// In this case, start time will be set when CR causes scrolling
if (MODE_POPON != context->mode && !context->rollup_from_popon)
context->current_visible_start_ms = get_visible_start(context->timing, context->my_field);
}
use_buffer->empty = 0;
if (context->cursor_column < CCX_DECODER_608_SCREEN_WIDTH - 1)
context->cursor_column++;
if (context->ts_start_of_current_line == -1)
context->ts_start_of_current_line = get_fts(context->timing, context->my_field);
// First char after a pop-on -> roll-up transition: remember its FTS so
// write_cc_buffer can use it if EOF fires before a scrolling CR.
// Unlike ts_start_of_current_line, this field is NOT touched by
// intermediate CR commands (changes=0 CRs overwrite ts_start_of_current_line).
if (context->rollup_from_popon && context->ts_first_char_rollup_transition == -1)
context->ts_first_char_rollup_transition = get_fts(context->timing, context->my_field);
context->ts_last_char_received = get_fts(context->timing, context->my_field);
}
}
/* Handle MID-ROW CODES. */
void handle_text_attr(const unsigned char c1, const unsigned char c2, ccx_decoder_608_context *context)
{
// Handle channel change
context->channel = context->new_channel;
if (context->channel != context->my_channel)
return;
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "\r608: text_attr: %02X %02X", c1, c2);
if (((c1 != 0x11 && c1 != 0x19) ||
(c2 < 0x20 || c2 > 0x2f)))
{
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "\rThis is not a text attribute!\n");
}
else
{
int i = c2 - 0x20;
context->current_color = pac2_attribs[i][0];
context->font = pac2_attribs[i][1];
ccx_common_logging.debug_ftn(
CCX_DMT_DECODER_608,
" -- Color: %s, font: %s\n",
color_text[context->current_color][0],
font_text[context->font]);
// Mid-row codes should put a non-transparent space at the current position
// and advance the cursor
// so use write_char
write_char(0x20, context);
}
}
struct eia608_screen *get_current_visible_buffer(ccx_decoder_608_context *context)
{
struct eia608_screen *data;
if (context->visible_buffer == 1)
data = &context->buffer1;
else
data = &context->buffer2;
return data;
}
int write_cc_buffer(ccx_decoder_608_context *context, struct cc_subtitle *sub)
{
struct eia608_screen *data;
int wrote_something = 0;
LLONG start_time;
LLONG end_time;
if (context->settings->screens_to_process != -1 &&
context->screenfuls_counter >= context->settings->screens_to_process)
{
// We are done.
*context->halt = 1;
return 0;
}
data = get_current_visible_buffer(context);
if (context->mode == MODE_FAKE_ROLLUP_1 && // Use the actual start of data instead of last buffer change
context->ts_start_of_current_line != -1)
context->current_visible_start_ms = context->ts_start_of_current_line;
// Pop-on -> roll-up transition that never saw a scrolling CR (e.g. EOF
// with fewer lines than the roll-up window). The CR handler never ran,
// so back-fill current_visible_start_ms from the first-char FTS instead
// of emitting a caption starting at 0.
if (context->rollup_from_popon && context->ts_first_char_rollup_transition > 0)
{
context->current_visible_start_ms = context->ts_first_char_rollup_transition;
context->rollup_from_popon = 0;
context->ts_first_char_rollup_transition = -1;
}
start_time = context->current_visible_start_ms;
end_time = get_visible_end(context->timing, context->my_field);
sub->type = CC_608;
data->format = SFORMAT_CC_SCREEN;
data->start_time = 0;
data->end_time = 0;
data->mode = context->mode;
data->channel = context->channel;
data->my_field = context->my_field;
if (!data->empty && context->output_format != CCX_OF_NULL)
{
size_t new_size;
if (sub->nb_data + 1 > SIZE_MAX / sizeof(struct eia608_screen))
{
ccx_common_logging.log_ftn("Too many screens, cannot allocate more memory.\n");
return 0;
}
new_size = (sub->nb_data + 1) * sizeof(struct eia608_screen);
struct eia608_screen *new_data = (struct eia608_screen *)realloc(sub->data, new_size);
if (!new_data)
{
ccx_common_logging.log_ftn("Out of memory while reallocating screen buffer\n");
return 0;
}
sub->data = new_data;
sub->datatype = CC_DATATYPE_GENERIC;
memcpy(((struct eia608_screen *)sub->data) + sub->nb_data, data, sizeof(*data));
sub->nb_data++;
wrote_something = 1;
// Buffer until next packet to get correct timing
if (start_time == end_time && sub->got_output == 1)
{
sub->got_output = 0;
}
if (start_time < end_time)
{
int i = 0;
int nb_data = sub->nb_data;
data = (struct eia608_screen *)sub->data;
for (i = 0; (unsigned)i < sub->nb_data; i++)
{
if (!data->start_time)
break;
nb_data--;
data++;
}
for (i = 0; i < nb_data; i++)
{
// Don't update start time if Direct Rollup is selected
if (context->settings->direct_rollup)
{
data->start_time = start_time;
}
else
{
data->start_time = start_time + (((end_time - start_time) / nb_data) * i);
}
data->end_time = start_time + (((end_time - start_time) / nb_data) * (i + 1));
data++;
}
sub->got_output = 1;
}
}
return wrote_something;
}
int write_cc_line(ccx_decoder_608_context *context, struct cc_subtitle *sub)
{
struct eia608_screen *data;
LLONG start_time;
LLONG end_time;
int i = 0;
int wrote_something = 0;
data = get_current_visible_buffer(context);
start_time = context->ts_start_of_current_line;
end_time = get_fts(context->timing, context->my_field);
sub->type = CC_608;
data->format = SFORMAT_CC_LINE;
data->start_time = 0;
data->end_time = 0;
data->mode = context->mode;
data->channel = context->channel;
data->my_field = context->my_field;
if (!data->empty)
{
size_t new_size;
if (sub->nb_data + 1 > SIZE_MAX / sizeof(struct eia608_screen))
{
ccx_common_logging.log_ftn("Too many screens, cannot allocate more memory.\n");
return 0;
}
new_size = (sub->nb_data + 1) * sizeof(struct eia608_screen);
struct eia608_screen *new_data = (struct eia608_screen *)realloc(sub->data, new_size);
if (!new_data)
{
ccx_common_logging.log_ftn("Out of memory while reallocating screen buffer\n");
return 0;
}
sub->data = new_data;
memcpy(((struct eia608_screen *)sub->data) + sub->nb_data, data, sizeof(*data));
data = (struct eia608_screen *)sub->data + sub->nb_data;
sub->datatype = CC_DATATYPE_GENERIC;
sub->nb_data++;
for (i = 0; i < CCX_DECODER_608_SCREEN_ROWS; i++)
{
if (i == context->cursor_row)
data->row_used[i] = 1;
else
data->row_used[i] = 0;
}
wrote_something = 1;
if (start_time < end_time)
{
int nb_data = sub->nb_data;
data = (struct eia608_screen *)sub->data;
for (i = 0; (unsigned)i < sub->nb_data; i++)
{
if (!data->start_time)
break;
nb_data--;
data++;
}
for (i = 0; (int)i < nb_data; i++)
{
data->start_time = start_time + (((end_time - start_time) / nb_data) * i);
data->end_time = start_time + (((end_time - start_time) / nb_data) * (i + 1));
data++;
}
sub->got_output = 1;
}
}
return wrote_something;
}
// Check if a rollup would cause a line to go off the visible area
int check_roll_up(ccx_decoder_608_context *context)
{
int keep_lines = 0;
int firstrow = -1, lastrow = -1;
struct eia608_screen *use_buffer;
if (context->visible_buffer == 1)
use_buffer = &context->buffer1;
else
use_buffer = &context->buffer2;
switch (context->mode)
{
case MODE_FAKE_ROLLUP_1:
keep_lines = 1;
break;
case MODE_ROLLUP_2:
keep_lines = 2;
break;
case MODE_ROLLUP_3:
keep_lines = 3;
break;
case MODE_ROLLUP_4:
keep_lines = 4;
break;
case MODE_TEXT:
keep_lines = 7; // CFS: can be 7 to 15 according to the handbook. No idea how this is selected.
break;
default: // Shouldn't happen
return 0;
break;
}
if (use_buffer->row_used[0]) // If top line is used it will go off the screen no matter what
return 1;
int rows_orig = 0; // Number of rows in use right now
for (int i = 0; i < CCX_DECODER_608_SCREEN_ROWS; i++)
{
if (use_buffer->row_used[i])
{
rows_orig++;
if (firstrow == -1)
firstrow = i;
lastrow = i;
}
}
if (lastrow == -1) // Empty screen, nothing to rollup
return 0;
if ((lastrow - firstrow + 1) >= keep_lines)
return 1; // We have the roll-up area full, so yes
if ((firstrow - 1) <= context->cursor_row - keep_lines) // Roll up will delete those lines.
return 1;
return 0;
}
// Roll-up: Returns true if a line was rolled over the visible area (it disappears from screen), false
// if the rollup didn't delete any line.
int roll_up(ccx_decoder_608_context *context)
{
struct eia608_screen *use_buffer;
if (context->visible_buffer == 1)
use_buffer = &context->buffer1;
else
use_buffer = &context->buffer2;
int keep_lines;
switch (context->mode)
{
case MODE_FAKE_ROLLUP_1:
keep_lines = 1;
break;
case MODE_ROLLUP_2:
keep_lines = 2;
break;
case MODE_ROLLUP_3:
keep_lines = 3;
break;
case MODE_ROLLUP_4:
keep_lines = 4;
break;
case MODE_TEXT:
keep_lines = 7; // CFS: can be 7 to 15 according to the handbook. No idea how this is selected.
break;
default: // Shouldn't happen
keep_lines = 0;
break;
}
int firstrow = -1, lastrow = -1;
// Look for the last line used
int rows_orig = 0; // Number of rows in use right now
for (int i = 0; i < CCX_DECODER_608_SCREEN_ROWS; i++)
{
if (use_buffer->row_used[i])
{
rows_orig++;
if (firstrow == -1)
firstrow = i;
lastrow = i;
}
}
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "\rIn roll-up: %d lines used, first: %d, last: %d\n", rows_orig, firstrow, lastrow);
if (lastrow == -1) // Empty screen, nothing to rollup
return 0;
for (int j = lastrow - keep_lines + 1; j < lastrow; j++)
{
if (j >= 0)
{
memcpy(use_buffer->characters[j], use_buffer->characters[j + 1], CCX_DECODER_608_SCREEN_WIDTH + 1);
for (int i = 0; i < CCX_DECODER_608_SCREEN_WIDTH + 1; ++i)
{
use_buffer->colors[j][i] = use_buffer->colors[j + 1][i];
use_buffer->fonts[j][i] = use_buffer->fonts[j + 1][i];
}
use_buffer->row_used[j] = use_buffer->row_used[j + 1];
}
}
for (int j = 0; j < (1 + context->cursor_row - keep_lines); j++)
{
memset(use_buffer->characters[j], ' ', CCX_DECODER_608_SCREEN_WIDTH);
use_buffer->characters[j][CCX_DECODER_608_SCREEN_WIDTH] = 0;
for (int i = 0; i < CCX_DECODER_608_SCREEN_WIDTH; ++i)
{
use_buffer->colors[j][i] = context->settings->default_color;
use_buffer->fonts[j][i] = FONT_REGULAR;
}
use_buffer->row_used[j] = 0;
}
memset(use_buffer->characters[lastrow], ' ', CCX_DECODER_608_SCREEN_WIDTH);
use_buffer->characters[lastrow][CCX_DECODER_608_SCREEN_WIDTH] = 0;
for (int i = 0; i < CCX_DECODER_608_SCREEN_WIDTH; ++i)
{
use_buffer->colors[lastrow][i] = context->settings->default_color;
use_buffer->fonts[lastrow][i] = FONT_REGULAR;
}
use_buffer->row_used[lastrow] = 0;
// Sanity check
int rows_now = 0;
for (int i = 0; i < CCX_DECODER_608_SCREEN_ROWS; i++)
if (use_buffer->row_used[i])
rows_now++;
if (rows_now > keep_lines)
ccx_common_logging.log_ftn("Bug in roll_up, should have %d lines but I have %d.\n",
keep_lines, rows_now);
// If the buffer is now empty, let's set the flag
// This will allow write_char to set visible start time appropriately
if (0 == rows_now)
use_buffer->empty = 1;
return (rows_now != rows_orig);
}
void erase_memory(ccx_decoder_608_context *context, int displayed)
{
struct eia608_screen *buf;
if (displayed)
{
if (context->visible_buffer == 1)
buf = &context->buffer1;
else
buf = &context->buffer2;
}
else
{
if (context->visible_buffer == 1)
buf = &context->buffer2;
else
buf = &context->buffer1;
}
clear_eia608_cc_buffer(context, buf);
}
int is_current_row_empty(ccx_decoder_608_context *context)
{
struct eia608_screen *use_buffer;
if (context->visible_buffer == 1)
use_buffer = &context->buffer1;
else
use_buffer = &context->buffer2;
for (int i = 0; i < CCX_DECODER_608_SCREEN_WIDTH; i++)
{
if (use_buffer->characters[context->rollup_base_row][i] != ' ')
return 0;
}
return 1;
}
/* Process GLOBAL CODES */
void handle_command(unsigned char c1, const unsigned char c2, ccx_decoder_608_context *context, struct cc_subtitle *sub)
{
int changes = 0;
// Handle channel change
context->channel = context->new_channel;
if (context->channel != context->my_channel)
return;
enum command_code command = COM_UNKNOWN;
if (c1 == 0x15)
c1 = 0x14;
if ((c1 == 0x14 || c1 == 0x1C) && c2 == 0x2C)
command = COM_ERASEDISPLAYEDMEMORY;
if ((c1 == 0x14 || c1 == 0x1C) && c2 == 0x20)
command = COM_RESUMECAPTIONLOADING;
if ((c1 == 0x14 || c1 == 0x1C) && c2 == 0x2F)
command = COM_ENDOFCAPTION;
if ((c1 == 0x14 || c1 == 0x1C) && c2 == 0x22)
command = COM_ALARMOFF;
if ((c1 == 0x14 || c1 == 0x1C) && c2 == 0x23)
command = COM_ALARMON;
if ((c1 == 0x14 || c1 == 0x1C) && c2 == 0x24)
command = COM_DELETETOENDOFROW;
if ((c1 == 0x17 || c1 == 0x1F) && c2 == 0x21)
command = COM_TABOFFSET1;
if ((c1 == 0x17 || c1 == 0x1F) && c2 == 0x22)
command = COM_TABOFFSET2;
if ((c1 == 0x17 || c1 == 0x1F) && c2 == 0x23)
command = COM_TABOFFSET3;
if ((c1 == 0x14 || c1 == 0x1C) && c2 == 0x25)
command = COM_ROLLUP2;
if ((c1 == 0x14 || c1 == 0x1C) && c2 == 0x26)
command = COM_ROLLUP3;
if ((c1 == 0x14 || c1 == 0x1C) && c2 == 0x27)
command = COM_ROLLUP4;
if ((c1 == 0x14 || c1 == 0x1C) && c2 == 0x29)
command = COM_RESUMEDIRECTCAPTIONING;
if ((c1 == 0x14 || c1 == 0x1C) && c2 == 0x2D)
command = COM_CARRIAGERETURN;
if ((c1 == 0x14 || c1 == 0x1C) && c2 == 0x2E)
command = COM_ERASENONDISPLAYEDMEMORY;
if ((c1 == 0x14 || c1 == 0x1C) && c2 == 0x21)
command = COM_BACKSPACE;
if ((c1 == 0x14 || c1 == 0x1C) && c2 == 0x2b)
command = COM_RESUMETEXTDISPLAY;
if ((command == COM_ROLLUP2 || command == COM_ROLLUP3 || command == COM_ROLLUP4) && context->settings->force_rollup == 1)
command = COM_FAKE_RULLUP1;
if ((command == COM_ROLLUP3 || command == COM_ROLLUP4) && context->settings->force_rollup == 2)
command = COM_ROLLUP2;
else if (command == COM_ROLLUP4 && context->settings->force_rollup == 3)
command = COM_ROLLUP3;
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "\rCommand begin: %02X %02X (%s)\n", c1, c2, command_type[command]);
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "\rCurrent mode: %d Position: %d,%d VisBuf: %d\n", context->mode,
context->cursor_row, context->cursor_column, context->visible_buffer);
switch (command)
{
case COM_BACKSPACE:
if (context->cursor_column > 0)
{
context->cursor_column--;
get_writing_buffer(context)->characters[context->cursor_row][context->cursor_column] = ' ';
}
break;
case COM_TABOFFSET1:
if (context->cursor_column < CCX_DECODER_608_SCREEN_WIDTH - 1)
context->cursor_column++;
break;
case COM_TABOFFSET2:
context->cursor_column += 2;
if (context->cursor_column > CCX_DECODER_608_SCREEN_WIDTH - 1)
context->cursor_column = CCX_DECODER_608_SCREEN_WIDTH - 1;
break;
case COM_TABOFFSET3:
context->cursor_column += 3;
if (context->cursor_column > CCX_DECODER_608_SCREEN_WIDTH - 1)
context->cursor_column = CCX_DECODER_608_SCREEN_WIDTH - 1;
break;
case COM_RESUMECAPTIONLOADING:
context->mode = MODE_POPON;
break;
case COM_RESUMETEXTDISPLAY:
context->mode = MODE_TEXT;
break;
case COM_FAKE_RULLUP1:
case COM_ROLLUP2:
case COM_ROLLUP3:
case COM_ROLLUP4:
if (context->mode == MODE_POPON || context->mode == MODE_PAINTON)
{
/* CEA-608 C.10 Style Switching (regulatory)
[...]if pop-up or paint-on captioning is already present in
either memory it shall be erased[...] */
if (write_cc_buffer(context, sub))
context->screenfuls_counter++;
erase_memory(context, true);
// Track transition from pop-on/paint-on to roll-up for timing adjustment
// Start time will be set when CR causes scrolling (matching FFmpeg behavior)
context->rollup_from_popon = 1;
context->ts_start_of_current_line = -1;
context->ts_first_char_rollup_transition = -1;
}
erase_memory(context, false);
// If the reception of data for a row is interrupted by data for the alternate
// data channel or for text mode, the display of caption text will resume from the same
// cursor position if a roll-up caption command is received and no PAC is given [...]
if (context->mode != MODE_TEXT && context->have_cursor_position == 0)
{
// If no Preamble Address Code is received, the base row shall default to row 15
context->cursor_row = 14; // Default if the previous mode wasn't roll up already.
context->cursor_column = 0;
context->have_cursor_position = 1;
}
switch (command)
{
case COM_FAKE_RULLUP1:
context->mode = MODE_FAKE_ROLLUP_1;
break;
case COM_ROLLUP2:
context->mode = MODE_ROLLUP_2;
break;
case COM_ROLLUP3:
context->mode = MODE_ROLLUP_3;
break;
case COM_ROLLUP4:
context->mode = MODE_ROLLUP_4;
break;
default: // Impossible, but remove compiler warnings
break;
}
break;
case COM_CARRIAGERETURN:
if (context->mode == MODE_PAINTON) // CR has no effect on painton mode according to zvbis' code
break;
if (context->mode == MODE_POPON) // CFS: Not sure about this. Is there a valid reason for CR in popup?
{
context->cursor_column = 0;
if (context->cursor_row < CCX_DECODER_608_SCREEN_ROWS)
context->cursor_row++;
break;
}
if (context->output_format == CCX_OF_TRANSCRIPT)
{
write_cc_line(context, sub);
}
// In transcript mode, CR doesn't write the whole screen, to avoid
// repeated lines.
changes = check_roll_up(context);
if (changes)
{
// Handle pop-on to roll-up transition timing
// Use ts_start_of_current_line (when current line started) as the start time
// This matches FFmpeg's behavior of timestamping when the display changed
if (context->rollup_from_popon && context->ts_start_of_current_line > 0)
{
context->current_visible_start_ms = context->ts_start_of_current_line;
context->rollup_from_popon = 0;
context->ts_first_char_rollup_transition = -1;
}
// Only if the roll up would actually cause a line to disappear we write the buffer
if (context->output_format != CCX_OF_TRANSCRIPT)
{
if (write_cc_buffer(context, sub))
context->screenfuls_counter++;
if (context->settings->no_rollup)
erase_memory(context, true); // Make sure the lines we just wrote aren't written again
}
}
roll_up(context); // The roll must be done anyway of course.
// When in pop-on to roll-up transition with changes=0 (first CR, only 1 line),
// preserve the CR time so the next caption uses the display state change time,
// not the character typing time. This matches FFmpeg's timing behavior.
if (context->rollup_from_popon && !changes)
{
context->ts_start_of_current_line = get_fts(context->timing, context->my_field);
}
else
{
context->ts_start_of_current_line = -1; // Unknown.
}
if (changes)
context->current_visible_start_ms = get_visible_start(context->timing, context->my_field);
context->cursor_column = 0;
break;
case COM_ERASENONDISPLAYEDMEMORY:
erase_memory(context, false);
break;
case COM_ERASEDISPLAYEDMEMORY:
// Write it to disk before doing this, and make a note of the new
// time it became clear.
if (context->output_format == CCX_OF_TRANSCRIPT &&
(context->mode == MODE_FAKE_ROLLUP_1 ||
context->mode == MODE_ROLLUP_2 ||
context->mode == MODE_ROLLUP_3 ||
context->mode == MODE_ROLLUP_4))
{
// In transcript mode we just write the cursor line. The previous lines
// should have been written already, so writing everything produces
// duplicate lines.
write_cc_line(context, sub);
}
else
{
if (context->output_format == CCX_OF_TRANSCRIPT)
context->ts_start_of_current_line = context->current_visible_start_ms;
if (write_cc_buffer(context, sub))
context->screenfuls_counter++;
}
erase_memory(context, true);
context->current_visible_start_ms = get_visible_start(context->timing, context->my_field);
break;
case COM_ENDOFCAPTION: // Switch buffers
// The currently *visible* buffer is leaving, so now we know its ending
// time. Time to actually write it to file.
if (write_cc_buffer(context, sub))
context->screenfuls_counter++;
context->visible_buffer = (context->visible_buffer == 1) ? 2 : 1;
context->current_visible_start_ms = get_visible_start(context->timing, context->my_field);
context->cursor_column = 0;
context->cursor_row = 0;
context->current_color = context->settings->default_color;
context->font = FONT_REGULAR;
context->mode = MODE_POPON;
break;
case COM_DELETETOENDOFROW:
delete_to_end_of_row(context);
break;
case COM_ALARMOFF:
case COM_ALARMON:
// These two are unused according to Robson's, and we wouldn't be able to do anything useful anyway
break;
case COM_RESUMEDIRECTCAPTIONING:
context->mode = MODE_PAINTON;
// ccx_common_logging.log_ftn ("\nWarning: Received ResumeDirectCaptioning, this mode is almost impossible.\n");
// ccx_common_logging.log_ftn ("to transcribe to a text file.\n");
break;
default:
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "\rNot yet implemented.\n");
break;
}
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "\rCurrent mode: %d Position: %d,%d VisBuf: %d\n", context->mode,
context->cursor_row, context->cursor_column, context->visible_buffer);
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "\rCommand end: %02X %02X (%s)\n", c1, c2, command_type[command]);
}
void flush_608_context(ccx_decoder_608_context *context, struct cc_subtitle *sub)
{
// We issue a EraseDisplayedMemory here so if there's any captions pending
// they get written to Subtitle.
handle_command(0x14, 0x2c, context, sub); // EDM
}
// CEA-608, Anex F 1.1.1. - Character Set Table / Special Characters
void handle_double(const unsigned char c1, const unsigned char c2, ccx_decoder_608_context *context)
{
unsigned char c;
if (context->channel != context->my_channel)
return;
if (c2 >= 0x30 && c2 <= 0x3f)
{
c = c2 + 0x50; // So if c>=0x80 && c<=0x8f, it comes from here
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "\rDouble: %02X %02X --> %c\n", c1, c2, c);
write_char(c, context);
}
}
/* Process EXTENDED CHARACTERS */
unsigned char handle_extended(unsigned char hi, unsigned char lo, ccx_decoder_608_context *context)
{
// Handle channel change
if (context->new_channel > 2)
{
context->new_channel -= 2;
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "\nChannel correction, now %d\n", context->new_channel);
}
context->channel = context->new_channel;
if (context->channel != context->my_channel)
return 0;
// For lo values between 0x20-0x3f
unsigned char c = 0;
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "\rExtended: %02X %02X\n", hi, lo);
if (lo >= 0x20 && lo <= 0x3f && (hi == 0x12 || hi == 0x13))
{
switch (hi)
{
case 0x12:
c = lo + 0x70; // So if c>=0x90 && c<=0xaf it comes from here
break;
case 0x13:
c = lo + 0x90; // So if c>=0xb0 && c<=0xcf it comes from here
break;
}
// This column change is because extended characters replace
// the previous character (which is sent for basic decoders
// to show something similar to the real char)
if (context->cursor_column > 0)
context->cursor_column--;
write_char(c, context);
}
return 1;
}
/* Process PREAMBLE ACCESS CODES (PAC) */
void handle_pac(unsigned char c1, unsigned char c2, ccx_decoder_608_context *context)
{
// Handle channel change
if (context->new_channel > 2)
{
context->new_channel -= 2;
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "\nChannel correction, now %d\n", context->new_channel);
}
context->channel = context->new_channel;
if (context->channel != context->my_channel)
return;
int row = rowdata[((c1 << 1) & 14) | ((c2 >> 5) & 1)];
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "\rPAC: %02X %02X", c1, c2);
if (c2 >= 0x40 && c2 <= 0x5f)
{
c2 = c2 - 0x40;
}
else
{
if (c2 >= 0x60 && c2 <= 0x7f)
{
c2 = c2 - 0x60;
}
else
{
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "\rThis is not a PAC!!!!!\n");
return;
}
}
context->current_color = pac2_attribs[c2][0];
context->font = pac2_attribs[c2][1];
int indent = pac2_attribs[c2][2];
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, " -- Position: %d:%d, color: %s, font: %s\n", row,
indent, color_text[context->current_color][0], font_text[context->font]);
if (context->settings->default_color == COL_USERDEFINED && (context->current_color == COL_WHITE || context->current_color == COL_TRANSPARENT))
context->current_color = COL_USERDEFINED;
if (context->mode != MODE_TEXT)
{
// According to Robson, row info is discarded in text mode
// but column is accepted
context->cursor_row = row - 1; // Since the array is 0 based
}
context->rollup_base_row = row - 1;
context->cursor_column = indent;
context->have_cursor_position = 1;
if (context->mode == MODE_FAKE_ROLLUP_1 || context->mode == MODE_ROLLUP_2 ||
context->mode == MODE_ROLLUP_3 || context->mode == MODE_ROLLUP_4)
{
/* In roll-up, delete lines BELOW the PAC. Not sure (CFS) this is correct (possibly we may have to move the
buffer around instead) but it's better than leaving old characters in the buffer */
struct eia608_screen *use_buffer = get_writing_buffer(context); // &wb->data608->buffer1;
for (int j = row; j < CCX_DECODER_608_SCREEN_ROWS; j++)
{
if (use_buffer->row_used[j])
{
memset(use_buffer->characters[j], ' ', CCX_DECODER_608_SCREEN_WIDTH);
use_buffer->characters[j][CCX_DECODER_608_SCREEN_WIDTH] = 0;
for (int i = 0; i < CCX_DECODER_608_SCREEN_WIDTH; ++i)
{
use_buffer->colors[j][i] = context->settings->default_color;
use_buffer->fonts[j][i] = FONT_REGULAR;
}
use_buffer->row_used[j] = 0;
}
}
}
}
void handle_single(const unsigned char c1, ccx_decoder_608_context *context)
{
if (c1 < 0x20 || context->channel != context->my_channel)
return; // We don't allow special stuff here
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "%c", c1);
write_char(c1, context);
}
void erase_both_memories(ccx_decoder_608_context *context, struct cc_subtitle *sub)
{
erase_memory(context, false);
// For the visible memory, we write the contents to disk
// The currently *visible* buffer is leaving, so now we know its ending
// time. Time to actually write it to file.
if (write_cc_buffer(context, sub))
context->screenfuls_counter++;
context->current_visible_start_ms = get_visible_start(context->timing, context->my_field);
context->cursor_column = 0;
context->cursor_row = 0;
context->current_color = context->settings->default_color;
context->font = FONT_REGULAR;
erase_memory(context, true);
}
int check_channel(unsigned char c1, ccx_decoder_608_context *context)
{
int newchan = context->channel;
if (c1 >= 0x10 && c1 <= 0x17)
newchan = 1;
else if (c1 >= 0x18 && c1 <= 0x1e)
newchan = 2;
if (newchan != context->channel)
{
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "\nChannel change, now %d\n", newchan);
if (context->channel != 3) // Don't delete memories if returning from XDS.
{
// erase_both_memories (wb); // 47cfr15.119.pdf, page 859, part f
// CFS: Removed this because the specs say memories should be deleted if THE USER
// changes the channel.
}
}
return newchan;
}
/* Handle Command, special char or attribute and also check for
* channel changes.
* Returns 1 if something was written to screen, 0 otherwise */
int disCommand(unsigned char hi, unsigned char lo, ccx_decoder_608_context *context, struct cc_subtitle *sub)
{
int wrote_to_screen = 0;
/* Full channel changes are only allowed for "GLOBAL CODES",
* "OTHER POSITIONING CODES", "BACKGROUND COLOR CODES",
* "MID-ROW CODES".
* "PREAMBLE ACCESS CODES", "BACKGROUND COLOR CODES" and
* SPECIAL/SPECIAL CHARACTERS allow only switching
* between 1&3 or 2&4. */
context->new_channel = check_channel(hi, context);
// if (wb->data608->channel!=cc_channel)
// continue;
if (hi >= 0x18 && hi <= 0x1f)
hi = hi - 8;
switch (hi)
{
case 0x10:
if (lo >= 0x40 && lo <= 0x5f)
handle_pac(hi, lo, context);
break;
case 0x11:
if (lo >= 0x20 && lo <= 0x2f)
handle_text_attr(hi, lo, context);
if (lo >= 0x30 && lo <= 0x3f)
{
wrote_to_screen = 1;
handle_double(hi, lo, context);
}
if (lo >= 0x40 && lo <= 0x7f)
handle_pac(hi, lo, context);
break;
case 0x12:
case 0x13:
if (lo >= 0x20 && lo <= 0x3f)
{
wrote_to_screen = handle_extended(hi, lo, context);
}
if (lo >= 0x40 && lo <= 0x7f)
handle_pac(hi, lo, context);
break;
case 0x14:
case 0x15:
if (lo >= 0x20 && lo <= 0x2f)
handle_command(hi, lo, context, sub);
if (lo >= 0x40 && lo <= 0x7f)
handle_pac(hi, lo, context);
break;
case 0x16:
if (lo >= 0x40 && lo <= 0x7f)
handle_pac(hi, lo, context);
break;
case 0x17:
if (lo >= 0x21 && lo <= 0x23)
handle_command(hi, lo, context, sub);
if (lo >= 0x2e && lo <= 0x2f)
handle_text_attr(hi, lo, context);
if (lo >= 0x40 && lo <= 0x7f)
handle_pac(hi, lo, context);
break;
}
return wrote_to_screen;
}
/* If private data is NULL, then only XDS will be processed */
int process608(const unsigned char *data, int length, void *private_data, struct cc_subtitle *sub)
{
struct ccx_decoder_608_report *report = NULL;
struct lib_cc_decode *dec_ctx = private_data;
struct ccx_decoder_608_context *context;
int i;
if (dec_ctx->current_field == 1 && (dec_ctx->extract == 1 || dec_ctx->extract == 12))
{
context = dec_ctx->context_cc608_field_1;
}
else if (dec_ctx->current_field == 2 && (dec_ctx->extract == 2 || dec_ctx->extract == 12))
{
context = dec_ctx->context_cc608_field_2;
}
else if (dec_ctx->current_field == 2 && dec_ctx->extract == 1)
{
context = NULL;
}
else
{
return -1;
}
if (context)
{
report = context->report;
context->bytes_processed_608 += length;
}
if (!data)
{
return -1;
}
for (i = 0; i < length; i = i + 2)
{
unsigned char hi, lo;
int wrote_to_screen = 0;
hi = data[i] & 0x7F; // Get rid of parity bit
lo = data[i + 1] & 0x7F; // Get rid of parity bit
if (hi == 0 && lo == 0) // Just padding
continue;
// printf ("\r[%02X:%02X]\n",hi,lo);
if (hi >= 0x10 && hi <= 0x1e)
{
int ch = (hi <= 0x17) ? 1 : 2;
if (context == NULL || context->my_field == 2) // Originally: current_field from sequencing.c. Seems to be just to change channel, so context->my_field seems good.
ch += 2;
if (report)
report->cc_channels[ch - 1] = 1;
}
if (hi >= 0x01 && hi <= 0x0E && (context == NULL || context->my_field == 2)) // XDS can only exist in field 2.
{
if (context)
context->channel = 3;
if (!in_xds_mode)
{
ts_start_of_xds = get_fts(dec_ctx->timing, dec_ctx->current_field);
in_xds_mode = 1;
}
if (report)
report->xds = 1;
}
if (hi == 0x0F && in_xds_mode && (context == NULL || context->my_field == 2)) // End of XDS block
{
in_xds_mode = 0;
do_end_of_xds(sub, dec_ctx->xds_ctx, lo);
if (context)
context->channel = context->new_channel; // Switch from channel 3
continue;
}
if (hi >= 0x10 && hi <= 0x1F) // Non-character code or special/extended char
// http://www.theneitherworld.com/mcpoodle/SCC_TOOLS/DOCS/CC_CODES.HTML
// http://www.theneitherworld.com/mcpoodle/SCC_TOOLS/DOCS/CC_CHARS.HTML
{
if (!context || context->my_field == 2)
in_xds_mode = 0; // Back to normal (CEA 608-8.6.2)
if (!context) // Not XDS and we don't have a writebuffer, nothing else would have an effect
continue;
// We were writing characters before, start a new line for
// diagnostic output from disCommand()
if (context->textprinted == 1)
{
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "\n");
context->textprinted = 0;
}
if (context->last_c1 == hi && context->last_c2 == lo)
{
// Duplicate dual code, discard. Correct to do it only in
// non-XDS, XDS codes shall not be repeated.
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "Skipping command %02X,%02X Duplicate\n", hi, lo);
// Ignore only the first repetition
context->last_c1 = -1;
context->last_c2 = -1;
continue;
}
context->last_c1 = hi;
context->last_c2 = lo;
wrote_to_screen = disCommand(hi, lo, context, sub);
if (sub->got_output)
{
i += 2; // Otherwise we wouldn't be counting this byte pair
break;
}
}
else
{
if (in_xds_mode && (context == NULL || context->my_field == 2))
{
process_xds_bytes(dec_ctx->xds_ctx, hi, lo);
continue;
}
if (!context) // No XDS code after this point, and user doesn't want captions.
continue;
context->last_c1 = -1;
context->last_c2 = -1;
if (hi >= 0x20) // Standard characters (always in pairs)
{
// Only print if the channel is active
if (context->channel != context->my_channel)
continue;
if (context->textprinted == 0)
{
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "\n");
context->textprinted = 1;
}
handle_single(hi, context);
handle_single(lo, context);
wrote_to_screen = 1;
context->last_c1 = 0;
context->last_c2 = 0;
}
if (!context->textprinted && context->channel == context->my_channel)
{ // Current FTS information after the characters are shown
ccx_common_logging.debug_ftn(CCX_DMT_DECODER_608, "Current FTS: %s\n", print_mstime_static(get_fts(dec_ctx->timing, context->my_field)));
// printf(" N:%u", unsigned(fts_now) );
// printf(" G:%u", unsigned(fts_global) );
// printf(" F:%d %d %d %d\n",
// current_field, cb_field1, cb_field2, cb_708 );
}
if (wrote_to_screen && context->settings->direct_rollup && // If direct_rollup is enabled and
(context->mode == MODE_FAKE_ROLLUP_1 || // we are in rollup mode, write now.
context->mode == MODE_ROLLUP_2 ||
context->mode == MODE_ROLLUP_3 ||
context->mode == MODE_ROLLUP_4))
{
// We don't increase screenfuls_counter here.
write_cc_buffer(context, sub);
context->current_visible_start_ms = get_visible_start(context->timing, context->my_field);
}
}
if (wrote_to_screen && context->cc_to_stdout)
fflush(stdout);
} // for
return i;
}
/* Return a pointer to a string that holds the printable characters
* of the caption data block. FOR DEBUG PURPOSES ONLY! */
unsigned char *debug_608_to_ASC(unsigned char *cc_data, int channel)
{
static unsigned char output[3];
unsigned char cc_valid = (cc_data[0] & 4) >> 2;
unsigned char cc_type = cc_data[0] & 3;
unsigned char hi, lo;
output[0] = ' ';
output[1] = ' ';
output[2] = '\x00';
if (cc_valid && cc_type == channel)
{
hi = cc_data[1] & 0x7F; // Get rid of parity bit
lo = cc_data[2] & 0x7F; // Get rid of parity bit
if (hi >= 0x20)
{
output[0] = hi;
output[1] = (lo >= 0x20 ? lo : '.');
output[2] = '\x00';
}
else
{
output[0] = '<';
output[1] = '>';
output[2] = '\x00';
}
}
return output;
}