mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-03-07 11:51:49 +09:00
TAV: output FPS conversion
This commit is contained in:
@@ -140,6 +140,10 @@ typedef struct {
|
|||||||
int interlaced; // Interlaced mode (half-height internally, full height in header)
|
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)
|
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
|
// Audio encoding
|
||||||
int has_audio;
|
int has_audio;
|
||||||
int audio_quality; // TAD quality level (0-5)
|
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(" -o, --output FILE Output TAV file\n");
|
||||||
printf("\nVideo Options:\n");
|
printf("\nVideo Options:\n");
|
||||||
printf(" -s, --size WxH Frame size (auto-detected if omitted)\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, --quality N Quality level 0-5 (default: 3)\n");
|
||||||
printf(" -Q, --quantiser Y,Co,Cg Custom quantisers (advanced)\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(" -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(" --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(" -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(" --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("\nGOP Options:\n");
|
||||||
printf(" --temporal-dwt Enable 3D DWT GOP encoding (default)\n");
|
printf(" --temporal-dwt Enable 3D DWT GOP encoding (default)\n");
|
||||||
printf(" --intra-only Disable temporal compression (I-frames only)\n");
|
printf(" --intra-only Disable temporal compression (I-frames only)\n");
|
||||||
printf(" --gop-size N GOP size 8/16/24 (default: 24)\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("\nPerformance:\n");
|
||||||
printf(" -t, --threads N Parallel encoding threads (default: min(8, available CPUs))\n");
|
printf(" -t, --threads N Parallel encoding threads (default: min(8, available CPUs))\n");
|
||||||
printf(" 0 or 1 = single-threaded, 2-16 = multithreaded\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(" --preset-anime Anime mode (disable grain)\n");
|
||||||
printf("\nAudio:\n");
|
printf("\nAudio:\n");
|
||||||
printf(" --tad-audio Use TAD audio codec (default)\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(" --audio-quality N TAD audio quality 0-5 (default: matches video -q)\n");
|
||||||
printf(" --no-audio Disable audio encoding\n");
|
printf(" --no-audio Disable audio encoding\n");
|
||||||
printf(" --separate-audio-track Multiplex audio as separate track\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
|
* - FFmpeg outputs half-height frames via tinterlace+separatefields
|
||||||
* - Filtergraph: scale/crop to full size, then tinterlace weave halves
|
* - Filtergraph: scale/crop to full size, then tinterlace weave halves
|
||||||
* framerate, then separatefields restores framerate at half height
|
* 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,
|
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 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) {
|
if (interlaced) {
|
||||||
// Interlaced mode filtergraph:
|
// Interlaced mode filtergraph:
|
||||||
// 1. scale and crop to full size (width x full_height)
|
// 1. fps filter (if conversion requested) - applied first
|
||||||
// 2. tinterlace interleave_top:cvlpf - weave fields, halves framerate
|
// 2. scale and crop to full size (width x full_height)
|
||||||
// 3. separatefields - separate into half-height frames, doubles framerate back
|
// 3. tinterlace interleave_top:cvlpf - weave fields, halves framerate
|
||||||
// Final output: width x (full_height/2) at original 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),
|
snprintf(cmd, sizeof(cmd),
|
||||||
"ffmpeg -hide_banner -v quiet -i \"%s\" -f rawvideo -pix_fmt rgb24 -vf "
|
"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\" -",
|
"tinterlace=interleave_top:cvlpf,separatefields\" -",
|
||||||
input_file, width, full_height, width, full_height);
|
input_file, fps_filter, width, full_height, width, full_height);
|
||||||
} else {
|
} else {
|
||||||
// Progressive mode - simple scale and crop
|
// Progressive mode - optional fps conversion, then scale and crop
|
||||||
snprintf(cmd, sizeof(cmd),
|
snprintf(cmd, sizeof(cmd),
|
||||||
"ffmpeg -hide_banner -v quiet -i \"%s\" -f rawvideo -pix_fmt rgb24 -vf "
|
"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\" -",
|
||||||
input_file, width, height, width, height);
|
input_file, fps_filter, width, height, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
FILE *fp = popen(cmd, "r");
|
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.width,
|
||||||
cli->enc_params.height,
|
cli->enc_params.height,
|
||||||
cli->interlaced,
|
cli->interlaced,
|
||||||
cli->header_height);
|
cli->header_height,
|
||||||
|
cli->target_fps_num,
|
||||||
|
cli->target_fps_den);
|
||||||
if (!cli->ffmpeg_pipe) {
|
if (!cli->ffmpeg_pipe) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -1883,7 +1900,9 @@ static int encode_video(cli_context_t *cli) {
|
|||||||
cli->enc_params.width,
|
cli->enc_params.width,
|
||||||
cli->enc_params.height,
|
cli->enc_params.height,
|
||||||
cli->interlaced,
|
cli->interlaced,
|
||||||
cli->header_height);
|
cli->header_height,
|
||||||
|
cli->target_fps_num,
|
||||||
|
cli->target_fps_den);
|
||||||
if (!cli->ffmpeg_pipe) {
|
if (!cli->ffmpeg_pipe) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -2315,6 +2334,8 @@ int main(int argc, char *argv[]) {
|
|||||||
fprintf(stderr, "Error: Invalid fps format. Use NUM or NUM/DEN\n");
|
fprintf(stderr, "Error: Invalid fps format. Use NUM or NUM/DEN\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
cli.target_fps_num = num;
|
||||||
|
cli.target_fps_den = den;
|
||||||
cli.enc_params.fps_num = num;
|
cli.enc_params.fps_num = num;
|
||||||
cli.enc_params.fps_den = den;
|
cli.enc_params.fps_den = den;
|
||||||
break;
|
break;
|
||||||
@@ -2501,6 +2522,17 @@ int main(int argc, char *argv[]) {
|
|||||||
cli.header_height = cli.enc_params.height;
|
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
|
// Set audio quality to match video quality if not specified
|
||||||
if (cli.audio_quality < 0) {
|
if (cli.audio_quality < 0) {
|
||||||
cli.audio_quality = cli.enc_params.quality_level; // Match luma quality
|
cli.audio_quality = cli.enc_params.quality_level; // Match luma quality
|
||||||
|
|||||||
@@ -178,6 +178,8 @@ typedef struct {
|
|||||||
int height;
|
int height;
|
||||||
int fps_num;
|
int fps_num;
|
||||||
int fps_den;
|
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_interlaced;
|
||||||
int is_pal;
|
int is_pal;
|
||||||
int quality_index;
|
int quality_index;
|
||||||
@@ -230,6 +232,7 @@ static void print_usage(const char *program) {
|
|||||||
printf(" -o, --output FILE Output TAV-DT file\n");
|
printf(" -o, --output FILE Output TAV-DT file\n");
|
||||||
printf("\nOptions:\n");
|
printf("\nOptions:\n");
|
||||||
printf(" -q, --quality N Quality level 0-5 (default: 3)\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(" --ntsc Force NTSC format (720x480, default)\n");
|
||||||
printf(" --pal Force PAL format (720x576)\n");
|
printf(" --pal Force PAL format (720x576)\n");
|
||||||
printf(" --interlaced Interlaced output\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];
|
char video_size[32];
|
||||||
snprintf(video_size, sizeof(video_size), "%dx%d", enc->width, enc->height);
|
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
|
// Use same filtergraph as reference TAV encoder
|
||||||
char vf[256];
|
char vf[320];
|
||||||
snprintf(vf, sizeof(vf),
|
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->width, enc->height, enc->width, enc->height,
|
||||||
enc->is_interlaced ? ",setfield=tff" : "");
|
enc->is_interlaced ? ",setfield=tff" : "");
|
||||||
|
|
||||||
@@ -1295,6 +1306,7 @@ int main(int argc, char **argv) {
|
|||||||
{"input", required_argument, 0, 'i'},
|
{"input", required_argument, 0, 'i'},
|
||||||
{"output", required_argument, 0, 'o'},
|
{"output", required_argument, 0, 'o'},
|
||||||
{"quality", required_argument, 0, 'q'},
|
{"quality", required_argument, 0, 'q'},
|
||||||
|
{"fps", required_argument, 0, 'f'},
|
||||||
{"threads", required_argument, 0, 't'},
|
{"threads", required_argument, 0, 't'},
|
||||||
{"ntsc", no_argument, 0, 'N'},
|
{"ntsc", no_argument, 0, 'N'},
|
||||||
{"pal", no_argument, 0, 'P'},
|
{"pal", no_argument, 0, 'P'},
|
||||||
@@ -1307,7 +1319,7 @@ int main(int argc, char **argv) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
int opt;
|
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) {
|
switch (opt) {
|
||||||
case 'i':
|
case 'i':
|
||||||
enc.input_file = optarg;
|
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 < 0) enc.quality_index = 0;
|
||||||
if (enc.quality_index > 5) enc.quality_index = 5;
|
if (enc.quality_index > 5) enc.quality_index = 5;
|
||||||
break;
|
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': {
|
case 't': {
|
||||||
int threads = atoi(optarg);
|
int threads = atoi(optarg);
|
||||||
if (threads < 0) {
|
if (threads < 0) {
|
||||||
@@ -1367,6 +1391,7 @@ int main(int argc, char **argv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Probe input file for framerate
|
// Probe input file for framerate
|
||||||
|
int original_fps_num = 24, original_fps_den = 1;
|
||||||
char probe_cmd[4096];
|
char probe_cmd[4096];
|
||||||
snprintf(probe_cmd, sizeof(probe_cmd),
|
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'",
|
"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) {
|
if (probe) {
|
||||||
char line[256];
|
char line[256];
|
||||||
if (fgets(line, sizeof(line), probe)) {
|
if (fgets(line, sizeof(line), probe)) {
|
||||||
if (sscanf(line, "%d/%d", &enc.fps_num, &enc.fps_den) != 2) {
|
if (sscanf(line, "%d/%d", &original_fps_num, &original_fps_den) != 2) {
|
||||||
enc.fps_num = 24;
|
original_fps_num = 24;
|
||||||
enc.fps_den = 1;
|
original_fps_den = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pclose(probe);
|
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("\nTAV-DT Encoder (Revised Spec 2025-12-11)\n");
|
||||||
printf(" Format: %s %s\n", enc.is_pal ? "PAL" : "NTSC",
|
printf(" Format: %s %s\n", enc.is_pal ? "PAL" : "NTSC",
|
||||||
enc.is_interlaced ? "interlaced" : "progressive");
|
enc.is_interlaced ? "interlaced" : "progressive");
|
||||||
printf(" Resolution: %dx%d (internal: %dx%d)\n", enc.width, enc.height,
|
printf(" Resolution: %dx%d (internal: %dx%d)\n", enc.width, enc.height,
|
||||||
enc.width, enc.is_interlaced ? enc.height / 2 : 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(" Quality: %d\n", enc.quality_index);
|
||||||
printf(" GOP size: %d\n", DT_GOP_SIZE);
|
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)");
|
printf(" Payload FEC: %s\n", enc.fec_mode == FEC_MODE_LDPC ? "LDPC(255,223)" : "RS(255,223)");
|
||||||
|
|||||||
Reference in New Issue
Block a user