Skip to content

Commit 021b788

Browse files
cfsmp3claude
andcommitted
feat(scc): Add configurable frame rate and styled PAC codes for SCC output
This commit addresses the remaining items from issue #1191: 1. SCC Output Frame Rate: - Added scc_framerate to encoder_cfg and encoder_ctx structs - The --scc-framerate option now affects both input parsing AND output - Supports 24, 25, 29.97 (default), and 30 fps 2. Styled PAC (Preamble Address Code) Optimization: - Added support for styled PACs that encode color/font at column 0 - When captions start at column 0 with non-default style, uses a single styled PAC instead of indent PAC + mid-row code - More efficient output that matches professional SCC files Files changed: - ccx_common_option.h/c: Added scc_framerate to encoder_cfg - ccx_encoders_common.h/c: Added scc_framerate to encoder_ctx - ccx_encoders_scc.c: Added get_scc_fps(), styled PAC functions, and optimized write_cc_buffer_as_scenarist() - common.rs: Copy scc_framerate to enc_cfg Fixes #1191 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b0800a1 commit 021b788

File tree

6 files changed

+192
-4
lines changed

6 files changed

+192
-4
lines changed

src/lib_ccx/ccx_common_option.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ void init_options(struct ccx_s_options *options)
140140
options->enc_cfg.all_services_charset = NULL;
141141
options->enc_cfg.with_semaphore = 0;
142142
options->enc_cfg.force_dropframe = 0; // Assume No Drop Frame for MCC Encode.
143+
options->enc_cfg.scc_framerate = 0; // Default: 29.97fps for SCC output
143144
options->enc_cfg.extract_only_708 = 0;
144145

145146
options->settings_dtvcc.enabled = 0;

src/lib_ccx/ccx_common_option.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ struct encoder_cfg
7575
// MCC File
7676
int force_dropframe; // 1 if dropframe frame count should be used. defaults to no drop frame.
7777

78+
// SCC output framerate
79+
int scc_framerate; // SCC output framerate: 0=29.97 (default), 1=24, 2=25, 3=30
80+
7881
// text -> png (text render)
7982
char *render_font; // The font used to render text if needed (e.g. teletext->spupng)
8083
char *render_font_italics;

src/lib_ccx/ccx_encoders_common.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,7 @@ struct encoder_ctx *init_encoder(struct encoder_cfg *opt)
840840
ctx->segment_pending = 0;
841841
ctx->segment_last_key_frame = 0;
842842
ctx->nospupngocr = opt->nospupngocr;
843+
ctx->scc_framerate = opt->scc_framerate;
843844

844845
// Initialize teletext multi-page output arrays (issue #665)
845846
ctx->tlt_out_count = 0;

src/lib_ccx/ccx_encoders_common.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ struct encoder_ctx
153153
unsigned int cdp_hdr_seq;
154154
int force_dropframe;
155155

156+
// SCC output framerate
157+
int scc_framerate; // SCC output framerate: 0=29.97 (default), 1=24, 2=25, 3=30
158+
156159
int new_sentence; // Capitalize next letter?
157160

158161
int program_number;

src/lib_ccx/ccx_encoders_scc.c

Lines changed: 181 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -484,14 +484,156 @@ void write_control_code(const int fd, const unsigned char channel, const enum co
484484
* @param row 0-14 (inclusive)
485485
* @param column 0-31 (inclusive)
486486
*
487-
* //TODO: Preamble code need to take into account font as well
488-
*
487+
* Returns an indent-based preamble code (positions cursor at column with white color)
489488
*/
490489
enum control_code get_preamble_code(const unsigned char row, const unsigned char column)
491490
{
492491
return PREAMBLE_CC_START + 1 + (row * 8) + (column / 4);
493492
}
494493

494+
/**
495+
* Get byte2 value for a styled PAC (color/font at column 0)
496+
* Returns 0x40-0x4F or 0x60-0x6F depending on the style
497+
*
498+
* @param color The color to use
499+
* @param font The font style to use
500+
* @param use_high_range If true, use 0x60-0x6F range instead of 0x40-0x4F
501+
*
502+
* PAC style encoding (byte2):
503+
* 0x40/0x60: white, regular 0x41/0x61: white, underline
504+
* 0x42/0x62: green, regular 0x43/0x63: green, underline
505+
* 0x44/0x64: blue, regular 0x45/0x65: blue, underline
506+
* 0x46/0x66: cyan, regular 0x47/0x67: cyan, underline
507+
* 0x48/0x68: red, regular 0x49/0x69: red, underline
508+
* 0x4a/0x6a: yellow, regular 0x4b/0x6b: yellow, underline
509+
* 0x4c/0x6c: magenta, regular 0x4d/0x6d: magenta, underline
510+
* 0x4e/0x6e: white, italics 0x4f/0x6f: white, italic underline
511+
*/
512+
static unsigned char get_styled_pac_byte2(enum ccx_decoder_608_color_code color, enum font_bits font, bool use_high_range)
513+
{
514+
unsigned char base = use_high_range ? 0x60 : 0x40;
515+
unsigned char style_offset;
516+
517+
// Handle italics specially - they're always white
518+
if (font == FONT_ITALICS)
519+
return base + 0x0e;
520+
if (font == FONT_UNDERLINED_ITALICS)
521+
return base + 0x0f;
522+
523+
// Map color to base offset (0, 2, 4, 6, 8, 10, 12)
524+
switch (color)
525+
{
526+
case COL_WHITE:
527+
style_offset = 0x00;
528+
break;
529+
case COL_GREEN:
530+
style_offset = 0x02;
531+
break;
532+
case COL_BLUE:
533+
style_offset = 0x04;
534+
break;
535+
case COL_CYAN:
536+
style_offset = 0x06;
537+
break;
538+
case COL_RED:
539+
style_offset = 0x08;
540+
break;
541+
case COL_YELLOW:
542+
style_offset = 0x0a;
543+
break;
544+
case COL_MAGENTA:
545+
style_offset = 0x0c;
546+
break;
547+
default:
548+
// For unsupported colors (black, transparent, userdefined), fall back to white
549+
style_offset = 0x00;
550+
break;
551+
}
552+
553+
// Add 1 for underlined
554+
if (font == FONT_UNDERLINED)
555+
style_offset += 1;
556+
557+
return base + style_offset;
558+
}
559+
560+
/**
561+
* Check if the row uses high range (0x60-0x6F) or low range (0x40-0x4F) for styled PACs
562+
* Rows that have byte2 in 0x70-0x7F range for indents use 0x60-0x6F for styles
563+
*/
564+
static bool row_uses_high_range(unsigned char row)
565+
{
566+
// Based on the preamble code table:
567+
// Rows 2, 4, 6, 8, 10, 13, 15 use the "high" range (byte2 0x70-0x7F for indents)
568+
// which corresponds to 0x60-0x6F for styled PACs
569+
return (row == 1 || row == 3 || row == 5 || row == 7 || row == 9 || row == 12 || row == 14);
570+
}
571+
572+
/**
573+
* Write a styled PAC code (color/font at column 0) directly
574+
* This is more efficient than using indent PAC + mid-row code when at column 0
575+
*
576+
* @param fd File descriptor
577+
* @param channel Caption channel (1-4)
578+
* @param row Row number (0-14)
579+
* @param color Color to set
580+
* @param font Font style to set
581+
* @param disassemble If true, output assembly format
582+
* @param bytes_written Pointer to byte counter
583+
*/
584+
static void write_styled_preamble(const int fd, const unsigned char channel, const unsigned char row,
585+
enum ccx_decoder_608_color_code color, enum font_bits font,
586+
const bool disassemble, unsigned int *bytes_written)
587+
{
588+
// Get the preamble code for column 0 to obtain byte1
589+
enum control_code base_preamble = get_preamble_code(row, 0);
590+
unsigned char byte1 = odd_parity(get_first_byte(channel, base_preamble));
591+
592+
// Get styled byte2
593+
bool use_high_range = row_uses_high_range(row);
594+
unsigned char byte2 = odd_parity(get_styled_pac_byte2(color, font, use_high_range));
595+
596+
check_padding(fd, disassemble, bytes_written);
597+
598+
if (disassemble)
599+
{
600+
// Output assembly format like {0100Gr} for row 1, green
601+
const char *color_names[] = {"Wh", "Gr", "Bl", "Cy", "R", "Y", "Ma", "Wh", "Bk", "Wh"};
602+
const char *font_suffix = "";
603+
if (font == FONT_UNDERLINED)
604+
font_suffix = "U";
605+
else if (font == FONT_ITALICS)
606+
font_suffix = "I";
607+
else if (font == FONT_UNDERLINED_ITALICS)
608+
font_suffix = "IU";
609+
610+
fdprintf(fd, "{%02d00%s%s}", row + 1, color_names[color], font_suffix);
611+
}
612+
else
613+
{
614+
if (*bytes_written % 2 == 0)
615+
write_wrapped(fd, " ", 1);
616+
fdprintf(fd, "%02x%02x", byte1, byte2);
617+
}
618+
*bytes_written += 2;
619+
}
620+
621+
/**
622+
* Check if a styled PAC can be used (when color/font differs from white/regular and column is 0)
623+
*/
624+
static bool can_use_styled_pac(enum ccx_decoder_608_color_code color, enum font_bits font, unsigned char column)
625+
{
626+
// Styled PACs can only be used at column 0
627+
if (column != 0)
628+
return false;
629+
630+
// If style is already white/regular, no need for styled PAC
631+
if (color == COL_WHITE && font == FONT_REGULAR)
632+
return false;
633+
634+
return true;
635+
}
636+
495637
enum control_code get_tab_offset_code(const unsigned char column)
496638
{
497639
int offset = column % 4;
@@ -519,6 +661,23 @@ enum control_code get_font_code(enum font_bits font, enum ccx_decoder_608_color_
519661
}
520662
}
521663

664+
// Get frame rate value from scc_framerate setting
665+
// 0=29.97 (default), 1=24, 2=25, 3=30
666+
static float get_scc_fps(int scc_framerate)
667+
{
668+
switch (scc_framerate)
669+
{
670+
case 1:
671+
return 24.0f;
672+
case 2:
673+
return 25.0f;
674+
case 3:
675+
return 30.0f;
676+
default:
677+
return 29.97f;
678+
}
679+
}
680+
522681
void add_timestamp(const struct encoder_ctx *context, LLONG time, const bool disassemble)
523682
{
524683
write_wrapped(context->out->fh, context->encoded_crlf, context->encoded_crlf_length);
@@ -528,8 +687,9 @@ void add_timestamp(const struct encoder_ctx *context, LLONG time, const bool dis
528687
unsigned hour, minute, second, milli;
529688
millis_to_time(time, &hour, &minute, &second, &milli);
530689

531-
// SMPTE format
532-
float frame = milli * 29.97 / 1000;
690+
// SMPTE format - use configurable frame rate (issue #1191)
691+
float fps = get_scc_fps(context->scc_framerate);
692+
float frame = milli * fps / 1000;
533693
fdprintf(context->out->fh, "%02u:%02u:%02u:%02.f\t", hour, minute, second, frame);
534694
}
535695

@@ -578,6 +738,23 @@ int write_cc_buffer_as_scenarist(const struct eia608_screen *data, struct encode
578738
{
579739
if (switch_font || switch_color)
580740
{
741+
// Optimization (issue #1191): Use styled PAC when at column 0 with non-default style
742+
// This avoids needing a separate mid-row code
743+
if (column == 0 && can_use_styled_pac(data->colors[row][column], data->fonts[row][column], 0))
744+
{
745+
write_styled_preamble(context->out->fh, data->channel, row,
746+
data->colors[row][column], data->fonts[row][column],
747+
disassemble, &bytes_written);
748+
current_row = row;
749+
current_column = 0;
750+
current_font = data->fonts[row][column];
751+
current_color = data->colors[row][column];
752+
// Write the character and continue
753+
write_character(context->out->fh, data->characters[row][column], disassemble, &bytes_written);
754+
++current_column;
755+
continue;
756+
}
757+
581758
if (data->characters[row][column] == ' ')
582759
{
583760
// The MID-ROW code is going to move the cursor to the

src/rust/src/common.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ pub unsafe fn copy_from_rust(ccx_s_options: *mut ccx_s_options, options: Options
276276
(*ccx_s_options).out_interval = options.out_interval;
277277
(*ccx_s_options).segment_on_key_frames_only = options.segment_on_key_frames_only as _;
278278
(*ccx_s_options).scc_framerate = options.scc_framerate;
279+
// Also copy to enc_cfg so the encoder uses the same frame rate for SCC output
280+
(*ccx_s_options).enc_cfg.scc_framerate = options.scc_framerate;
279281
#[cfg(feature = "with_libcurl")]
280282
{
281283
if options.curlposturl.is_some() {
@@ -975,6 +977,7 @@ impl CType<encoder_cfg> for EncoderConfig {
975977
null_pointer()
976978
},
977979
extract_only_708: self.extract_only_708 as _,
980+
scc_framerate: 0, // Will be set from ccx_options.scc_framerate in copy_to_c
978981
}
979982
}
980983
}

0 commit comments

Comments
 (0)