TEV/TAV: SSF-TC impl

This commit is contained in:
minjaesong
2025-11-06 01:18:19 +09:00
parent af3679921d
commit 00c882aa8d
7 changed files with 403 additions and 118 deletions

View File

@@ -18,7 +18,7 @@
#include <limits.h>
#include <float.h>
#define ENCODER_VENDOR_STRING "Encoder-TAV 20251031 (3d-dwt,tad)"
#define ENCODER_VENDOR_STRING "Encoder-TAV 20251106 (3d-dwt,tad,ssf-tc)"
// TSVM Advanced Video (TAV) format constants
#define TAV_MAGIC "\x1F\x54\x53\x56\x4D\x54\x41\x56" // "\x1FTSVM TAV"
@@ -56,7 +56,7 @@
#define TAV_PACKET_AUDIO_MP2 0x20 // MP2 audio
#define TAV_PACKET_AUDIO_PCM8 0x21 // 8-bit PCM audio (zstd compressed)
#define TAV_PACKET_AUDIO_TAD 0x24 // TAD audio (DWT-based perceptual codec)
#define TAV_PACKET_SUBTITLE 0x30 // Subtitle packet
#define TAV_PACKET_SUBTITLE_TC 0x31 // Subtitle packet with timecode (SSF-TC format)
#define TAV_PACKET_AUDIO_TRACK 0x40 // Separate audio track (full MP2 file)
#define TAV_PACKET_EXTENDED_HDR 0xEF // Extended header packet
#define TAV_PACKET_GOP_SYNC 0xFC // GOP sync packet (N frames decoded)
@@ -8349,30 +8349,42 @@ static void free_subtitle_list(subtitle_entry_t *list) {
}
// Write subtitle packet (copied from TEV)
static int write_subtitle_packet(FILE *output, uint32_t index, uint8_t opcode, const char *text) {
// Calculate packet size
// Write SSF-TC subtitle packet to output
static int write_subtitle_packet_tc(FILE *output, uint32_t index, uint8_t opcode, const char *text, uint64_t timecode_ns) {
// Calculate packet size: index (3 bytes) + timecode (8 bytes) + opcode (1 byte) + text + null terminator
size_t text_len = text ? strlen(text) : 0;
size_t packet_size = 3 + 1 + text_len + 1; // index (3 bytes) + opcode + text + null terminator
size_t packet_size = 3 + 8 + 1 + text_len + 1;
// Write packet type and size
uint8_t packet_type = TAV_PACKET_SUBTITLE;
uint8_t packet_type = TAV_PACKET_SUBTITLE_TC;
fwrite(&packet_type, 1, 1, output);
uint32_t size32 = (uint32_t)packet_size;
fwrite(&size32, 4, 1, output);
// Write subtitle data
// Write subtitle index (24-bit, little-endian)
uint8_t index_bytes[3] = {
(uint8_t)(index & 0xFF),
(uint8_t)((index >> 8) & 0xFF),
(uint8_t)((index >> 16) & 0xFF)
};
fwrite(index_bytes, 3, 1, output);
// Write timecode (64-bit, little-endian)
uint8_t timecode_bytes[8];
for (int i = 0; i < 8; i++) {
timecode_bytes[i] = (timecode_ns >> (i * 8)) & 0xFF;
}
fwrite(timecode_bytes, 8, 1, output);
// Write opcode
fwrite(&opcode, 1, 1, output);
// Write text if present
if (text && text_len > 0) {
fwrite(text, 1, text_len, output);
}
// Write null terminator
uint8_t null_terminator = 0;
fwrite(&null_terminator, 1, 1, output);
@@ -9034,47 +9046,59 @@ static int process_audio_for_gop(tav_encoder_t *enc, int *frame_numbers, int num
return 1;
}
// Process subtitles for current frame (copied and adapted from TEV)
static int process_subtitles(tav_encoder_t *enc, int frame_num, FILE *output) {
if (!enc->subtitles) {
return 1; // No subtitles to process
}
// Write all subtitles upfront in SSF-TC format (called before first frame)
static int write_all_subtitles_tc(tav_encoder_t *enc, FILE *output) {
if (!enc->subtitles) return 0;
int bytes_written = 0;
int subtitle_count = 0;
// Check if we need to show a new subtitle
if (!enc->subtitle_visible) {
subtitle_entry_t *sub = enc->current_subtitle;
if (!sub) sub = enc->subtitles; // Start from beginning if not set
// Find next subtitle to show
while (sub && sub->start_frame <= frame_num) {
if (sub->end_frame > frame_num) {
// This subtitle should be shown
if (sub != enc->current_subtitle) {
enc->current_subtitle = sub;
enc->subtitle_visible = 1;
bytes_written += write_subtitle_packet(output, 0, 0x01, sub->text);
if (enc->verbose) {
printf("Frame %d: Showing subtitle: %.50s%s\n",
frame_num, sub->text, strlen(sub->text) > 50 ? "..." : "");
}
}
break;
}
sub = sub->next;
}
// Convert frame timing to nanoseconds
// For NTSC: frame_time = 1001000000 / 30000 nanoseconds
// For others: frame_time = 1e9 / fps nanoseconds
uint64_t frame_time_ns;
if (enc->is_ntsc_framerate) {
frame_time_ns = 1001000000ULL / 30000ULL; // NTSC: 30000/1001 fps
} else {
frame_time_ns = (uint64_t)(1000000000.0 / enc->fps);
}
// Check if we need to hide current subtitle
if (enc->subtitle_visible && enc->current_subtitle) {
if (frame_num >= enc->current_subtitle->end_frame) {
enc->subtitle_visible = 0;
bytes_written += write_subtitle_packet(output, 0, 0x02, NULL);
if (enc->verbose) {
printf("Frame %d: Hiding subtitle\n", frame_num);
}
// Iterate through all subtitles and write them with timecodes
subtitle_entry_t *sub = enc->subtitles;
while (sub) {
// Calculate timecodes for show and hide events
uint64_t show_timecode;
uint64_t hide_timecode;
if (enc->is_ntsc_framerate) {
// NTSC: time = frame * 1001000000 / 30000
show_timecode = ((uint64_t)sub->start_frame * 1001000000ULL) / 30000ULL;
hide_timecode = ((uint64_t)sub->end_frame * 1001000000ULL) / 30000ULL;
} else {
show_timecode = (uint64_t)sub->start_frame * frame_time_ns;
hide_timecode = (uint64_t)sub->end_frame * frame_time_ns;
}
// Write show subtitle event
bytes_written += write_subtitle_packet_tc(output, 0, 0x01, sub->text, show_timecode);
// Write hide subtitle event
bytes_written += write_subtitle_packet_tc(output, 0, 0x02, NULL, hide_timecode);
subtitle_count++;
if (enc->verbose) {
printf("SSF-TC: Subtitle %d: show at %.3fs, hide at %.3fs: %.50s%s\n",
subtitle_count,
show_timecode / 1000000000.0,
hide_timecode / 1000000000.0,
sub->text, strlen(sub->text) > 50 ? "..." : "");
}
sub = sub->next;
}
if (enc->verbose && subtitle_count > 0) {
printf("Wrote %d SSF-TC subtitle events (%d bytes)\n", subtitle_count * 2, bytes_written);
}
return bytes_written;
@@ -10330,6 +10354,11 @@ int main(int argc, char *argv[]) {
}
}
// Write all subtitles upfront in SSF-TC format (before first frame)
if (enc->subtitles) {
write_all_subtitles_tc(enc, enc->output_fp);
}
if (enc->output_fps != enc->fps) {
printf("Frame rate conversion enabled: %d fps output\n", enc->output_fps);
}
@@ -10544,8 +10573,8 @@ int main(int argc, char *argv[]) {
// Process audio for this frame
process_audio(enc, true_frame_count, enc->output_fp);
// Process subtitles for this frame
process_subtitles(enc, true_frame_count, enc->output_fp);
// Note: Subtitles are now written upfront in SSF-TC format (see write_all_subtitles_tc)
// process_subtitles() is no longer called here
}
if (enc->enable_temporal_dwt) {
@@ -10965,9 +10994,9 @@ int main(int argc, char *argv[]) {
// Skip when temporal DWT is enabled (audio handled in GOP flush)
if (!enc->enable_temporal_dwt && enc->is_ntsc_framerate && (frame_count % 1000 == 500)) {
true_frame_count++;
// Process audio and subtitles for the duplicated frame to maintain sync
// Process audio for the duplicated frame to maintain sync
process_audio(enc, true_frame_count, enc->output_fp);
process_subtitles(enc, true_frame_count, enc->output_fp);
// Note: Subtitles are now written upfront in SSF-TC format (see write_all_subtitles_tc)
uint8_t sync_packet_ntsc = TAV_PACKET_SYNC_NTSC;
fwrite(&sync_packet_ntsc, 1, 1, enc->output_fp);

View File

@@ -33,7 +33,7 @@
#define TEV_PACKET_IFRAME 0x10 // Intra frame (keyframe)
#define TEV_PACKET_PFRAME 0x11 // Predicted frame
#define TEV_PACKET_AUDIO_MP2 0x20 // MP2 audio
#define TEV_PACKET_SUBTITLE 0x30 // Subtitle packet
#define TEV_PACKET_SUBTITLE_TC 0x31 // Subtitle packet with timecode (SSF-TC format)
#define TEV_PACKET_SYNC 0xFF // Sync packet
// Utility macros
@@ -1834,71 +1834,86 @@ static void free_subtitle_list(subtitle_entry_t *list) {
}
}
// Write subtitle packet to output
static int write_subtitle_packet(FILE *output, uint32_t index, uint8_t opcode, const char *text) {
// Calculate packet size
// Write SSF-TC subtitle packet to output
static int write_subtitle_packet_tc(FILE *output, uint32_t index, uint8_t opcode, const char *text, uint64_t timecode_ns) {
// Calculate packet size: index (3 bytes) + timecode (8 bytes) + opcode (1 byte) + text + null terminator
size_t text_len = text ? strlen(text) : 0;
size_t packet_size = 3 + 1 + text_len + 1; // index (3 bytes) + opcode + text + null terminator
size_t packet_size = 3 + 8 + 1 + text_len + 1;
// Write packet type and size
uint8_t packet_type = TEV_PACKET_SUBTITLE;
uint8_t packet_type = TEV_PACKET_SUBTITLE_TC;
fwrite(&packet_type, 1, 1, output);
fwrite(&packet_size, 4, 1, output);
// Write subtitle packet data
// Write subtitle index (24-bit, little-endian)
uint8_t index_bytes[3];
index_bytes[0] = index & 0xFF;
index_bytes[1] = (index >> 8) & 0xFF;
index_bytes[2] = (index >> 16) & 0xFF;
fwrite(index_bytes, 1, 3, output);
// Write timecode (64-bit, little-endian)
uint8_t timecode_bytes[8];
for (int i = 0; i < 8; i++) {
timecode_bytes[i] = (timecode_ns >> (i * 8)) & 0xFF;
}
fwrite(timecode_bytes, 1, 8, output);
// Write opcode
fwrite(&opcode, 1, 1, output);
// Write text if present
if (text && text_len > 0) {
fwrite(text, 1, text_len, output);
}
// Write null terminator
uint8_t null_term = 0x00;
fwrite(&null_term, 1, 1, output);
return packet_size + 5; // packet_size + packet_type + size field
}
// Process subtitles for the current frame
static int process_subtitles(tev_encoder_t *enc, int frame_num, FILE *output) {
// Write all subtitles upfront in SSF-TC format (called before first frame)
static int write_all_subtitles_tc(tev_encoder_t *enc, FILE *output) {
if (!enc->has_subtitles) return 0;
int bytes_written = 0;
// Check if any subtitles need to be shown at this frame
subtitle_entry_t *sub = enc->current_subtitle;
while (sub && sub->start_frame <= frame_num) {
if (sub->start_frame == frame_num) {
// Show subtitle
bytes_written += write_subtitle_packet(output, 0, 0x01, sub->text);
if (enc->verbose) {
printf("Frame %d: Showing subtitle: %.50s%s\n",
frame_num, sub->text, strlen(sub->text) > 50 ? "..." : "");
}
int subtitle_count = 0;
// Convert frame timing to nanoseconds
// Frame time = 1e9 / fps nanoseconds
uint64_t frame_time_ns = (uint64_t)(1000000000.0 / enc->output_fps);
// Iterate through all subtitles and write them with timecodes
subtitle_entry_t *sub = enc->subtitle_list;
while (sub) {
// Calculate timecodes for show and hide events
uint64_t show_timecode = (uint64_t)sub->start_frame * frame_time_ns;
uint64_t hide_timecode = (uint64_t)sub->end_frame * frame_time_ns;
// Write show subtitle event
bytes_written += write_subtitle_packet_tc(output, 0, 0x01, sub->text, show_timecode);
// Write hide subtitle event
bytes_written += write_subtitle_packet_tc(output, 0, 0x02, NULL, hide_timecode);
subtitle_count++;
if (enc->verbose) {
printf("SSF-TC: Subtitle %d: show at %.3fs, hide at %.3fs: %.50s%s\n",
subtitle_count,
show_timecode / 1000000000.0,
hide_timecode / 1000000000.0,
sub->text, strlen(sub->text) > 50 ? "..." : "");
}
if (sub->end_frame == frame_num) {
// Hide subtitle
bytes_written += write_subtitle_packet(output, 0, 0x02, NULL);
if (enc->verbose) {
printf("Frame %d: Hiding subtitle\n", frame_num);
}
}
// Move to next subtitle if we're past the end of current one
if (sub->end_frame <= frame_num) {
enc->current_subtitle = sub->next;
}
sub = sub->next;
}
if (enc->verbose && subtitle_count > 0) {
printf("Wrote %d SSF-TC subtitle events (%d bytes)\n", subtitle_count * 2, bytes_written);
}
return bytes_written;
}
@@ -2868,6 +2883,12 @@ int main(int argc, char *argv[]) {
// Write TEV header
write_tev_header(output, enc);
// Write all subtitles upfront in SSF-TC format (before first frame)
if (enc->has_subtitles) {
write_all_subtitles_tc(enc, output);
}
gettimeofday(&enc->start_time, NULL);
printf("Encoding video with %s 4:2:0 format...\n", enc->ictcp_mode ? "ICtCp" : "YCoCg-R");
@@ -2962,8 +2983,8 @@ int main(int argc, char *argv[]) {
// Process audio for this frame
process_audio(enc, frame_count, output);
// Process subtitles for this frame
process_subtitles(enc, frame_count, output);
// Note: Subtitles are now written upfront in SSF-TC format (see write_all_subtitles_tc)
// process_subtitles() is no longer called here
// Encode frame
// Pass field parity for interlaced mode, -1 for progressive mode

View File

@@ -26,8 +26,8 @@
#define TAV_PACKET_AUDIO_MP2 0x20
#define TAV_PACKET_AUDIO_PCM8 0x21
#define TAV_PACKET_AUDIO_TAD 0x24
#define TAV_PACKET_SUBTITLE 0x30
#define TAV_PACKET_SUBTITLE_KAR 0x31
#define TAV_PACKET_SUBTITLE 0x30 // Legacy SSF (frame-locked)
#define TAV_PACKET_SUBTITLE_TC 0x31 // SSF-TC (timecode-based)
#define TAV_PACKET_AUDIO_TRACK 0x40
#define TAV_PACKET_VIDEO_CH2_I 0x70
#define TAV_PACKET_VIDEO_CH2_P 0x71
@@ -119,8 +119,8 @@ const char* get_packet_type_name(uint8_t type) {
case TAV_PACKET_AUDIO_MP2: return "AUDIO MP2";
case TAV_PACKET_AUDIO_PCM8: return "AUDIO PCM8 (zstd)";
case TAV_PACKET_AUDIO_TAD: return "AUDIO TAD (zstd)";
case TAV_PACKET_SUBTITLE: return "SUBTITLE (Simple)";
case TAV_PACKET_SUBTITLE_KAR: return "SUBTITLE (Karaoke)";
case TAV_PACKET_SUBTITLE: return "SUBTITLE (SSF frame-locked)";
case TAV_PACKET_SUBTITLE_TC: return "SUBTITLE (SSF-TC timecoded)";
case TAV_PACKET_AUDIO_TRACK: return "AUDIO TRACK (Separate MP2)";
case TAV_PACKET_EXIF: return "METADATA (EXIF)";
case TAV_PACKET_ID3V1: return "METADATA (ID3v1)";
@@ -151,7 +151,7 @@ int should_display_packet(uint8_t type, display_options_t *opts) {
(type >= 0x70 && type <= 0x7F))) return 1;
if (opts->show_audio && (type == TAV_PACKET_AUDIO_MP2 || type == TAV_PACKET_AUDIO_PCM8 ||
type == TAV_PACKET_AUDIO_TAD || type == TAV_PACKET_AUDIO_TRACK)) return 1;
if (opts->show_subtitles && (type == TAV_PACKET_SUBTITLE || type == TAV_PACKET_SUBTITLE_KAR)) return 1;
if (opts->show_subtitles && (type == TAV_PACKET_SUBTITLE || type == TAV_PACKET_SUBTITLE_TC)) return 1;
if (opts->show_timecode && type == TAV_PACKET_TIMECODE) return 1;
if (opts->show_metadata && (type >= 0xE0 && type <= 0xE4)) return 1;
if (opts->show_sync && (type == TAV_PACKET_SYNC || type == TAV_PACKET_SYNC_NTSC)) return 1;
@@ -160,7 +160,7 @@ int should_display_packet(uint8_t type, display_options_t *opts) {
return 0;
}
void print_subtitle_packet(FILE *fp, uint32_t size, int is_karaoke, int verbose) {
void print_subtitle_packet(FILE *fp, uint32_t size, int is_timecoded, int verbose) {
if (!verbose) {
fseek(fp, size, SEEK_CUR);
return;
@@ -174,10 +174,26 @@ void print_subtitle_packet(FILE *fp, uint32_t size, int is_karaoke, int verbose)
index |= (byte << (i * 8));
}
// Read timecode if SSF-TC (0x31)
uint64_t timecode_ns = 0;
int header_size = 4; // 3 bytes index + 1 byte opcode
if (is_timecoded) {
uint8_t timecode_bytes[8];
if (fread(timecode_bytes, 1, 8, fp) != 8) return;
for (int i = 0; i < 8; i++) {
timecode_ns |= ((uint64_t)timecode_bytes[i]) << (i * 8);
}
header_size += 8; // Add 8 bytes for timecode
}
uint8_t opcode;
if (fread(&opcode, 1, 1, fp) != 1) return;
printf(" [Index=%u, Opcode=0x%02X", index, opcode);
printf(" [Index=%u", index);
if (is_timecoded) {
printf(", Time=%.3fs", timecode_ns / 1000000000.0);
}
printf(", Opcode=0x%02X", opcode);
switch (opcode) {
case 0x01: printf(" (SHOW)"); break;
@@ -193,7 +209,7 @@ void print_subtitle_packet(FILE *fp, uint32_t size, int is_karaoke, int verbose)
printf("]");
// Read and display text content for SHOW commands
int remaining = size - 4; // Already read 3 (index) + 1 (opcode)
int remaining = size - header_size; // Already read index + timecode (if any) + opcode
if ((opcode == 0x01 || (opcode >= 0x10 && opcode <= 0x2F) || (opcode >= 0x30 && opcode <= 0x41)) && remaining > 0) {
char *text = malloc(remaining + 1);
if (text && fread(text, 1, remaining, fp) == remaining) {
@@ -788,14 +804,14 @@ static const char* VERDESC[] = {"null", "YCoCg tiled, uniform", "ICtCp tiled, un
}
case TAV_PACKET_SUBTITLE:
case TAV_PACKET_SUBTITLE_KAR: {
case TAV_PACKET_SUBTITLE_TC: {
stats.subtitle_count++;
uint32_t size;
if (fread(&size, sizeof(uint32_t), 1, fp) != 1) break;
if (!opts.summary_only && display) {
printf(" - size=%u bytes", size);
print_subtitle_packet(fp, size, packet_type == TAV_PACKET_SUBTITLE_KAR, opts.verbose);
print_subtitle_packet(fp, size, packet_type == TAV_PACKET_SUBTITLE_TC, opts.verbose);
} else {
fseek(fp, size, SEEK_CUR);
}