diff --git a/video_encoder/src/encoder_tav.c b/video_encoder/src/encoder_tav.c index 6ef2bdb..808d656 100644 --- a/video_encoder/src/encoder_tav.c +++ b/video_encoder/src/encoder_tav.c @@ -140,6 +140,10 @@ typedef struct { int interlaced; // Interlaced mode (half-height internally, full height in header) int header_height; // Height to write to header (may differ from enc_params.height when interlaced) + // Framerate conversion + int target_fps_num; // Target output framerate numerator (0 = no conversion) + int target_fps_den; // Target output framerate denominator + // Audio encoding int has_audio; int audio_quality; // TAD quality level (0-5) @@ -277,19 +281,19 @@ static void print_usage(const char *program) { printf(" -o, --output FILE Output TAV file\n"); printf("\nVideo Options:\n"); printf(" -s, --size WxH Frame size (auto-detected if omitted)\n"); - printf(" -f, --fps NUM/DEN Framerate (e.g., 60/1, 30000/1001)\n"); + printf(" -f, --fps NUM/DEN Output Framerate (e.g., 60/1, 30000/1001)\n"); printf(" -q, --quality N Quality level 0-5 (default: 3)\n"); printf(" -Q, --quantiser Y,Co,Cg Custom quantisers (advanced)\n"); printf(" -w, --wavelet N Spatial wavelet: 0=5/3, 1=9/7 (default), 2=13/7, 16=DD-4, 255=Haar\n"); printf(" --temporal-wavelet N Temporal wavelet: 0=Haar (default), 1=CDF 5/3\n"); printf(" -c, --colour-space N Colour space: 0=YCoCg-R (default), 1=ICtCp\n"); printf(" --decomp-levels N Spatial DWT levels (0=auto, default: 6)\n"); - printf(" --temporal-levels N Temporal DWT levels (0=auto, default: 2)\n"); +// printf(" --temporal-levels N Temporal DWT levels (0=auto, default: 2)\n"); printf("\nGOP Options:\n"); printf(" --temporal-dwt Enable 3D DWT GOP encoding (default)\n"); printf(" --intra-only Disable temporal compression (I-frames only)\n"); printf(" --gop-size N GOP size 8/16/24 (default: 24)\n"); - printf(" --single-pass Disable scene change detection\n"); +// printf(" --single-pass Disable scene change detection\n"); printf("\nPerformance:\n"); printf(" -t, --threads N Parallel encoding threads (default: min(8, available CPUs))\n"); printf(" 0 or 1 = single-threaded, 2-16 = multithreaded\n"); @@ -309,7 +313,7 @@ static void print_usage(const char *program) { printf(" --preset-anime Anime mode (disable grain)\n"); printf("\nAudio:\n"); printf(" --tad-audio Use TAD audio codec (default)\n"); - printf(" --pcm8-audio Use native PCM8 audio\n"); + printf(" --pcm8-audio Use TSVM-native PCM8 audio\n"); printf(" --audio-quality N TAD audio quality 0-5 (default: matches video -q)\n"); printf(" --no-audio Disable audio encoding\n"); printf(" --separate-audio-track Multiplex audio as separate track\n"); @@ -393,28 +397,39 @@ static int get_video_info(const char *input_file, int *width, int *height, * - FFmpeg outputs half-height frames via tinterlace+separatefields * - Filtergraph: scale/crop to full size, then tinterlace weave halves * framerate, then separatefields restores framerate at half height + * + * When target_fps_num > 0: + * - Applies fps filter at the start to convert to target framerate */ static FILE* open_ffmpeg_pipe(const char *input_file, int width, int height, - int interlaced, int full_height) { + int interlaced, int full_height, + int target_fps_num, int target_fps_den) { char cmd[MAX_PATH * 2]; + char fps_filter[64] = ""; + + // Build fps filter string if conversion is requested (applied first) + if (target_fps_num > 0 && target_fps_den > 0) { + snprintf(fps_filter, sizeof(fps_filter), "fps=%d/%d,", target_fps_num, target_fps_den); + } if (interlaced) { // Interlaced mode filtergraph: - // 1. scale and crop to full size (width x full_height) - // 2. tinterlace interleave_top:cvlpf - weave fields, halves framerate - // 3. separatefields - separate into half-height frames, doubles framerate back - // Final output: width x (full_height/2) at original framerate + // 1. fps filter (if conversion requested) - applied first + // 2. scale and crop to full size (width x full_height) + // 3. tinterlace interleave_top:cvlpf - weave fields, halves framerate + // 4. separatefields - separate into half-height frames, doubles framerate back + // Final output: width x (full_height/2) at target framerate snprintf(cmd, sizeof(cmd), "ffmpeg -hide_banner -v quiet -i \"%s\" -f rawvideo -pix_fmt rgb24 -vf " - "\"scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d," + "\"%sscale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d," "tinterlace=interleave_top:cvlpf,separatefields\" -", - input_file, width, full_height, width, full_height); + input_file, fps_filter, width, full_height, width, full_height); } else { - // Progressive mode - simple scale and crop + // Progressive mode - optional fps conversion, then scale and crop snprintf(cmd, sizeof(cmd), "ffmpeg -hide_banner -v quiet -i \"%s\" -f rawvideo -pix_fmt rgb24 -vf " - "\"scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d\" -", - input_file, width, height, width, height); + "\"%sscale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d\" -", + input_file, fps_filter, width, height, width, height); } FILE *fp = popen(cmd, "r"); @@ -1419,7 +1434,9 @@ static int encode_video_mt(cli_context_t *cli) { cli->enc_params.width, cli->enc_params.height, cli->interlaced, - cli->header_height); + cli->header_height, + cli->target_fps_num, + cli->target_fps_den); if (!cli->ffmpeg_pipe) { return -1; } @@ -1883,7 +1900,9 @@ static int encode_video(cli_context_t *cli) { cli->enc_params.width, cli->enc_params.height, cli->interlaced, - cli->header_height); + cli->header_height, + cli->target_fps_num, + cli->target_fps_den); if (!cli->ffmpeg_pipe) { return -1; } @@ -2315,6 +2334,8 @@ int main(int argc, char *argv[]) { fprintf(stderr, "Error: Invalid fps format. Use NUM or NUM/DEN\n"); return 1; } + cli.target_fps_num = num; + cli.target_fps_den = den; cli.enc_params.fps_num = num; cli.enc_params.fps_den = den; break; @@ -2501,6 +2522,17 @@ int main(int argc, char *argv[]) { cli.header_height = cli.enc_params.height; } + // Report fps conversion if enabled + if (cli.target_fps_num > 0 && cli.original_fps_num > 0) { + if (cli.target_fps_num != cli.original_fps_num || cli.target_fps_den != cli.original_fps_den) { + printf("Framerate conversion: %d/%d -> %d/%d\n", + cli.original_fps_num, cli.original_fps_den, + cli.target_fps_num, cli.target_fps_den); + } + } else if (cli.target_fps_num > 0) { + printf("Output framerate: %d/%d\n", cli.target_fps_num, cli.target_fps_den); + } + // Set audio quality to match video quality if not specified if (cli.audio_quality < 0) { cli.audio_quality = cli.enc_params.quality_level; // Match luma quality diff --git a/video_encoder/src/encoder_tav_dt.c b/video_encoder/src/encoder_tav_dt.c index 13c0548..4578a03 100644 --- a/video_encoder/src/encoder_tav_dt.c +++ b/video_encoder/src/encoder_tav_dt.c @@ -178,6 +178,8 @@ typedef struct { int height; int fps_num; int fps_den; + int target_fps_num; // Target output framerate numerator (0 = no conversion) + int target_fps_den; // Target output framerate denominator int is_interlaced; int is_pal; int quality_index; @@ -230,6 +232,7 @@ static void print_usage(const char *program) { printf(" -o, --output FILE Output TAV-DT file\n"); printf("\nOptions:\n"); printf(" -q, --quality N Quality level 0-5 (default: 3)\n"); + printf(" -f, --fps NUM/DEN Output framerate (e.g., 30/1, 24000/1001)\n"); printf(" --ntsc Force NTSC format (720x480, default)\n"); printf(" --pal Force PAL format (720x576)\n"); printf(" --interlaced Interlaced output\n"); @@ -431,10 +434,18 @@ static FILE *spawn_ffmpeg_video(dt_encoder_t *enc, pid_t *pid) { char video_size[32]; snprintf(video_size, sizeof(video_size), "%dx%d", enc->width, enc->height); + // Build fps filter prefix if conversion is requested + char fps_filter[64] = ""; + if (enc->target_fps_num > 0 && enc->target_fps_den > 0) { + snprintf(fps_filter, sizeof(fps_filter), "fps=%d/%d,", + enc->target_fps_num, enc->target_fps_den); + } + // Use same filtergraph as reference TAV encoder - char vf[256]; + char vf[320]; snprintf(vf, sizeof(vf), - "scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d%s", + "%sscale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d%s", + fps_filter, enc->width, enc->height, enc->width, enc->height, enc->is_interlaced ? ",setfield=tff" : ""); @@ -1295,6 +1306,7 @@ int main(int argc, char **argv) { {"input", required_argument, 0, 'i'}, {"output", required_argument, 0, 'o'}, {"quality", required_argument, 0, 'q'}, + {"fps", required_argument, 0, 'f'}, {"threads", required_argument, 0, 't'}, {"ntsc", no_argument, 0, 'N'}, {"pal", no_argument, 0, 'P'}, @@ -1307,7 +1319,7 @@ int main(int argc, char **argv) { }; int opt; - while ((opt = getopt_long(argc, argv, "i:o:q:t:vhNPI", long_options, NULL)) != -1) { + while ((opt = getopt_long(argc, argv, "i:o:q:f:t:vhNPI", long_options, NULL)) != -1) { switch (opt) { case 'i': enc.input_file = optarg; @@ -1320,6 +1332,18 @@ int main(int argc, char **argv) { if (enc.quality_index < 0) enc.quality_index = 0; if (enc.quality_index > 5) enc.quality_index = 5; break; + case 'f': { + int num, den = 1; + if (sscanf(optarg, "%d/%d", &num, &den) < 1) { + fprintf(stderr, "Error: Invalid fps format. Use NUM or NUM/DEN\n"); + return 1; + } + enc.target_fps_num = num; + enc.target_fps_den = den; + enc.fps_num = num; + enc.fps_den = den; + break; + } case 't': { int threads = atoi(optarg); if (threads < 0) { @@ -1367,6 +1391,7 @@ int main(int argc, char **argv) { } // Probe input file for framerate + int original_fps_num = 24, original_fps_den = 1; char probe_cmd[4096]; snprintf(probe_cmd, sizeof(probe_cmd), "ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate -of default=nw=1:nk=1 '%s'", @@ -1376,20 +1401,35 @@ int main(int argc, char **argv) { if (probe) { char line[256]; if (fgets(line, sizeof(line), probe)) { - if (sscanf(line, "%d/%d", &enc.fps_num, &enc.fps_den) != 2) { - enc.fps_num = 24; - enc.fps_den = 1; + if (sscanf(line, "%d/%d", &original_fps_num, &original_fps_den) != 2) { + original_fps_num = 24; + original_fps_den = 1; } } pclose(probe); } + // If user didn't specify target fps, use probed fps + if (enc.target_fps_num == 0) { + enc.fps_num = original_fps_num; + enc.fps_den = original_fps_den; + } + printf("\nTAV-DT Encoder (Revised Spec 2025-12-11)\n"); printf(" Format: %s %s\n", enc.is_pal ? "PAL" : "NTSC", enc.is_interlaced ? "interlaced" : "progressive"); printf(" Resolution: %dx%d (internal: %dx%d)\n", enc.width, enc.height, enc.width, enc.is_interlaced ? enc.height / 2 : enc.height); - printf(" Framerate: %d/%d\n", enc.fps_num, enc.fps_den); + + // Report fps conversion if enabled + if (enc.target_fps_num > 0 && + (enc.target_fps_num != original_fps_num || enc.target_fps_den != original_fps_den)) { + printf(" Framerate: %d/%d -> %d/%d (conversion)\n", + original_fps_num, original_fps_den, + enc.target_fps_num, enc.target_fps_den); + } else { + printf(" Framerate: %d/%d\n", enc.fps_num, enc.fps_den); + } printf(" Quality: %d\n", enc.quality_index); printf(" GOP size: %d\n", DT_GOP_SIZE); printf(" Payload FEC: %s\n", enc.fec_mode == FEC_MODE_LDPC ? "LDPC(255,223)" : "RS(255,223)");