diff --git a/video_encoder/Makefile b/video_encoder/Makefile index 331f0cd..c3dcd55 100644 --- a/video_encoder/Makefile +++ b/video_encoder/Makefile @@ -145,9 +145,9 @@ test_mpeg_motion: test_mpeg_motion.cpp tests: $(TEST_TARGETS) # Build with debug symbols -debug: CFLAGS += -g -DDEBUG -fsanitize=address -debug: DBGFLAGS += -fsanitize=address -debug: tav_new #$(TARGETS) +debug: CFLAGS += -g -DDEBUG -fsanitize=address -fno-omit-frame-pointer +debug: DBGFLAGS += -fsanitize=address -fno-omit-frame-pointer +debug: $(TARGETS) # Clean build artifacts clean: diff --git a/video_encoder/lib/libtavdec/tav_video_decoder.c b/video_encoder/lib/libtavdec/tav_video_decoder.c index 33f71b2..acccd2b 100644 --- a/video_encoder/lib/libtavdec/tav_video_decoder.c +++ b/video_encoder/lib/libtavdec/tav_video_decoder.c @@ -1575,9 +1575,9 @@ int tav_video_decode_gop(tav_video_context_t *ctx, } } - // Apply grain synthesis to Y channel ONLY (using ORIGINAL dimensions - grain must match encoder's frame size) + // Apply grain synthesis to Y channel ONLY (use final dimensions to match allocated buffer) // Note: Grain synthesis is NOT applied to chroma channels - apply_grain_synthesis(gop_y[t], width, height, ctx->params.decomp_levels, t, + apply_grain_synthesis(gop_y[t], final_width, final_height, ctx->params.decomp_levels, t, QLUT[ctx->params.quantiser_y], ctx->params.encoder_preset); } diff --git a/video_encoder/src/decoder_tav.c b/video_encoder/src/decoder_tav.c index 7bdb0d1..aa4caf2 100644 --- a/video_encoder/src/decoder_tav.c +++ b/video_encoder/src/decoder_tav.c @@ -121,6 +121,8 @@ typedef struct { // TAV header info tav_header_t header; int perceptual_mode; + int interlaced; // 1 if video is interlaced (from video_flags bit 0) + int decode_height; // Actual decode height (half of header.height when interlaced) // Video decoder context tav_video_context_t *video_ctx; @@ -214,10 +216,24 @@ static int read_tav_header(decoder_context_t *ctx) { int base_version = ctx->header.version & 0x07; // Remove temporal wavelet flag ctx->perceptual_mode = (base_version == 5 || base_version == 6); + // Detect interlaced mode from video_flags bit 0 + ctx->interlaced = (ctx->header.video_flags & 0x01) ? 1 : 0; + + // Calculate decode height: half of header height for interlaced video + // The header stores the full display height, but encoded frames are half-height + if (ctx->interlaced) { + ctx->decode_height = ctx->header.height / 2; + } else { + ctx->decode_height = ctx->header.height; + } + if (ctx->verbose) { printf("=== TAV Header ===\n"); printf(" Version: %d\n", ctx->header.version); printf(" Resolution: %dx%d\n", ctx->header.width, ctx->header.height); + if (ctx->interlaced) { + printf(" Interlaced: yes (decode height: %d)\n", ctx->decode_height); + } printf(" FPS: %d\n", ctx->header.fps); printf(" Total frames: %u\n", ctx->header.total_frames); printf(" Wavelet filter: %d\n", ctx->header.wavelet_filter); @@ -260,63 +276,126 @@ static int spawn_ffmpeg(decoder_context_t *ctx) { // Child process - execute FFmpeg close(video_pipe_fd[1]); // Close write end + // For interlaced video: input is half-height fields, output is full-height interlaced + // For progressive video: input and output are both full-height char video_size[32]; char framerate[16]; - snprintf(video_size, sizeof(video_size), "%dx%d", ctx->header.width, ctx->header.height); + snprintf(video_size, sizeof(video_size), "%dx%d", ctx->header.width, ctx->decode_height); snprintf(framerate, sizeof(framerate), "%d", ctx->header.fps); // Redirect video pipe to fd 3 dup2(video_pipe_fd[0], 3); close(video_pipe_fd[0]); - if (ctx->output_raw) { - // Raw video output (no compression) - execl("/usr/bin/ffmpeg", "ffmpeg", - "-f", "rawvideo", - "-pixel_format", "rgb24", - "-video_size", video_size, - "-framerate", framerate, - "-i", "pipe:3", - "-f", "u8", - "-ar", "32000", - "-ac", "2", - "-i", ctx->audio_temp_file, - "-c:v", "rawvideo", - "-pixel_format", "rgb24", - "-c:a", "pcm_u8", - "-f", "matroska", - ctx->output_file, - "-y", - "-v", "warning", - (char*)NULL); + if (ctx->interlaced) { + // Interlaced mode: merge separate fields into interlaced frames + // tinterlace=interleave_top combines consecutive fields into interlaced frames + // Output will be full height (header.height) at half framerate + // Field order is set to top-field-first to match encoder + if (ctx->output_raw) { + // Raw video output (no compression) + execl("/usr/bin/ffmpeg", "ffmpeg", + "-f", "rawvideo", + "-pixel_format", "rgb24", + "-video_size", video_size, + "-framerate", framerate, + "-i", "pipe:3", + "-f", "u8", + "-ar", "32000", + "-ac", "2", + "-i", ctx->audio_temp_file, + "-vf", "tinterlace=interleave_top", + "-field_order", "tt", + "-c:v", "rawvideo", + "-pixel_format", "rgb24", + "-c:a", "pcm_u8", + "-f", "matroska", + ctx->output_file, + "-y", + "-v", "warning", + (char*)NULL); + } else { + // FFV1 output (lossless compression) with interlaced flag + execl("/usr/bin/ffmpeg", "ffmpeg", + "-f", "rawvideo", + "-pixel_format", "rgb24", + "-video_size", video_size, + "-framerate", framerate, + "-i", "pipe:3", + "-f", "u8", + "-ar", "32000", + "-ac", "2", + "-i", ctx->audio_temp_file, + "-vf", "tinterlace=interleave_top", + "-field_order", "tt", + "-color_range", "2", + "-c:v", "ffv1", + "-level", "3", + "-coder", "1", + "-context", "1", + "-g", "1", + "-slices", "24", + "-slicecrc", "1", + "-pixel_format", "rgb24", + "-color_range", "2", + "-c:a", "pcm_u8", + "-f", "matroska", + ctx->output_file, + "-y", + "-v", "warning", + (char*)NULL); + } } else { - // FFV1 output (lossless compression) - execl("/usr/bin/ffmpeg", "ffmpeg", - "-f", "rawvideo", - "-pixel_format", "rgb24", - "-video_size", video_size, - "-framerate", framerate, - "-i", "pipe:3", - "-f", "u8", - "-ar", "32000", - "-ac", "2", - "-i", ctx->audio_temp_file, - "-color_range", "2", - "-c:v", "ffv1", - "-level", "3", - "-coder", "1", - "-context", "1", - "-g", "1", - "-slices", "24", - "-slicecrc", "1", - "-pixel_format", "rgb24", - "-color_range", "2", - "-c:a", "pcm_u8", - "-f", "matroska", - ctx->output_file, - "-y", - "-v", "warning", - (char*)NULL); + // Progressive mode - simple passthrough + if (ctx->output_raw) { + // Raw video output (no compression) + execl("/usr/bin/ffmpeg", "ffmpeg", + "-f", "rawvideo", + "-pixel_format", "rgb24", + "-video_size", video_size, + "-framerate", framerate, + "-i", "pipe:3", + "-f", "u8", + "-ar", "32000", + "-ac", "2", + "-i", ctx->audio_temp_file, + "-c:v", "rawvideo", + "-pixel_format", "rgb24", + "-c:a", "pcm_u8", + "-f", "matroska", + ctx->output_file, + "-y", + "-v", "warning", + (char*)NULL); + } else { + // FFV1 output (lossless compression) + execl("/usr/bin/ffmpeg", "ffmpeg", + "-f", "rawvideo", + "-pixel_format", "rgb24", + "-video_size", video_size, + "-framerate", framerate, + "-i", "pipe:3", + "-f", "u8", + "-ar", "32000", + "-ac", "2", + "-i", ctx->audio_temp_file, + "-color_range", "2", + "-c:v", "ffv1", + "-level", "3", + "-coder", "1", + "-context", "1", + "-g", "1", + "-slices", "24", + "-slicecrc", "1", + "-pixel_format", "rgb24", + "-color_range", "2", + "-c:a", "pcm_u8", + "-f", "matroska", + ctx->output_file, + "-y", + "-v", "warning", + (char*)NULL); + } } fprintf(stderr, "Error: Failed to execute FFmpeg\n"); @@ -432,7 +511,8 @@ static int init_decoder_threads(decoder_context_t *ctx) { } // Pre-allocate frame buffers for each slot (assuming max GOP size of 32) - size_t frame_size = ctx->header.width * ctx->header.height * 3; + // Use decode_height for interlaced video (half of header height) + size_t frame_size = ctx->header.width * ctx->decode_height * 3; int max_gop_size = 32; for (int i = 0; i < ctx->num_slots; i++) { @@ -462,7 +542,7 @@ static int init_decoder_threads(decoder_context_t *ctx) { tav_video_params_t video_params = { .width = ctx->header.width, - .height = ctx->header.height, + .height = ctx->decode_height, // Use decode_height for interlaced video .decomp_levels = ctx->header.decomp_levels, .temporal_levels = 2, .wavelet_filter = ctx->header.wavelet_filter, diff --git a/video_encoder/src/encoder_tav.c b/video_encoder/src/encoder_tav.c index 1f79e94..0a22e2c 100644 --- a/video_encoder/src/encoder_tav.c +++ b/video_encoder/src/encoder_tav.c @@ -138,6 +138,8 @@ typedef struct { char *fontrom_high; int separate_audio_track; int use_native_audio; // PCM8 instead of TAD + 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) // Audio encoding int has_audio; @@ -318,6 +320,7 @@ static void print_usage(const char *program) { printf(" --fontrom-low FILE Font ROM for low ASCII (.chr)\n"); printf(" --fontrom-high FILE Font ROM for high ASCII (.chr)\n"); printf(" --suppress-xhdr Suppress Extended Header packet (enabled by default)\n"); + printf(" --interlaced Enable interlaced video mode (half-height encoding)\n"); printf(" -v, --verbose Verbose output\n"); printf(" --help Show this help\n"); printf("\nExamples:\n"); @@ -385,12 +388,35 @@ static int get_video_info(const char *input_file, int *width, int *height, /** * Open FFmpeg pipe for reading RGB24 frames. + * + * When interlaced=1: + * - full_height is the full display height (written to header) + * - 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 */ -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) { char cmd[MAX_PATH * 2]; - 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); + + 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 + 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," + "tinterlace=interleave_top:cvlpf,separatefields\" -", + input_file, width, full_height, width, full_height); + } else { + // Progressive mode - simple 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); + } FILE *fp = popen(cmd, "r"); if (!fp) { @@ -427,8 +453,15 @@ static int read_rgb_frame(FILE *fp, uint8_t *rgb_frame, size_t frame_size) { /** * Write TAV file header. + * + * When interlaced mode is enabled: + * - header_height should be the full display height (e.g., 448) + * - params->height is the internal encoding height (e.g., 224) + * - video_flags bit 0 is set to indicate interlaced */ -static int write_tav_header(FILE *fp, const tav_encoder_params_t *params, int has_audio, int has_subtitles) { +static int write_tav_header(FILE *fp, const tav_encoder_params_t *params, + int has_audio, int has_subtitles, + int interlaced, int header_height) { // Magic (8 bytes: \x1FTSVMTAV) fwrite(TAV_MAGIC, 1, 8, fp); @@ -469,8 +502,11 @@ static int write_tav_header(FILE *fp, const tav_encoder_params_t *params, int ha fwrite(&width, sizeof(uint16_t), 1, fp); // Height (uint16_t, 2 bytes) + // For interlaced mode, write the full display height (header_height) + // For progressive mode, write params->height // Write 0 if height exceeds 65535 (extended dimensions will be in XDIM) - uint16_t height = (params->height > 65535) ? 0 : (uint16_t)params->height; + int actual_height = interlaced ? header_height : params->height; + uint16_t height = (actual_height > 65535) ? 0 : (uint16_t)actual_height; fwrite(&height, sizeof(uint16_t), 1, fp); // FPS (uint8_t, 1 byte) - simplified to just fps_num @@ -499,7 +535,9 @@ static int write_tav_header(FILE *fp, const tav_encoder_params_t *params, int ha fputc(extra_flags, fp); // Video flags (uint8_t, 1 byte) - uint8_t video_flags = 0; // Progressive, non-NTSC, lossy + // Bit 0 = interlaced, Bit 1 = NTSC framerate, Bit 2 = lossless, etc. + uint8_t video_flags = 0; + if (interlaced) video_flags |= 0x01; // Bit 0: interlaced fputc(video_flags, fp); // Quality level (uint8_t, 1 byte) @@ -1379,7 +1417,9 @@ static int encode_video_mt(cli_context_t *cli) { printf("Opening FFmpeg pipe...\n"); cli->ffmpeg_pipe = open_ffmpeg_pipe(cli->input_file, cli->enc_params.width, - cli->enc_params.height); + cli->enc_params.height, + cli->interlaced, + cli->header_height); if (!cli->ffmpeg_pipe) { return -1; } @@ -1468,11 +1508,14 @@ static int encode_video_mt(cli_context_t *cli) { } // Write TAV header - write_tav_header(cli->output_fp, &cli->enc_params, cli->has_audio, cli->subtitles != NULL); + write_tav_header(cli->output_fp, &cli->enc_params, cli->has_audio, cli->subtitles != NULL, + cli->interlaced, cli->header_height); // Write Extended Header (unless suppressed) + // For interlaced mode, use header_height for XDIM if needed + int xhdr_height = cli->interlaced ? cli->header_height : cli->enc_params.height; if (!cli->suppress_xhdr) { - cli->extended_header_offset = write_extended_header(cli, cli->enc_params.width, cli->enc_params.height); + cli->extended_header_offset = write_extended_header(cli, cli->enc_params.width, xhdr_height); if (cli->extended_header_offset < 0) { fprintf(stderr, "Warning: Failed to write Extended Header\n"); } @@ -1838,7 +1881,9 @@ static int encode_video(cli_context_t *cli) { printf("Opening FFmpeg pipe...\n"); cli->ffmpeg_pipe = open_ffmpeg_pipe(cli->input_file, cli->enc_params.width, - cli->enc_params.height); + cli->enc_params.height, + cli->interlaced, + cli->header_height); if (!cli->ffmpeg_pipe) { return -1; } @@ -1925,11 +1970,14 @@ static int encode_video(cli_context_t *cli) { } // Write TAV header (with actual encoder params) - write_tav_header(cli->output_fp, &cli->enc_params, cli->has_audio, cli->subtitles != NULL); + write_tav_header(cli->output_fp, &cli->enc_params, cli->has_audio, cli->subtitles != NULL, + cli->interlaced, cli->header_height); // Write Extended Header (unless suppressed) + // For interlaced mode, use header_height for XDIM if needed + int xhdr_height_st = cli->interlaced ? cli->header_height : cli->enc_params.height; if (!cli->suppress_xhdr) { - cli->extended_header_offset = write_extended_header(cli, cli->enc_params.width, cli->enc_params.height); + cli->extended_header_offset = write_extended_header(cli, cli->enc_params.width, xhdr_height_st); if (cli->extended_header_offset < 0) { fprintf(stderr, "Warning: Failed to write Extended Header\n"); } @@ -2237,6 +2285,7 @@ int main(int argc, char *argv[]) { {"tiled", no_argument, 0, 1029}, {"suppress-xhdr", no_argument, 0, 1030}, {"threads", required_argument, 0, 't'}, + {"interlaced", no_argument, 0, 1031}, {"help", no_argument, 0, '?'}, {0, 0, 0, 0} }; @@ -2384,6 +2433,9 @@ int main(int argc, char *argv[]) { case 1030: // --suppress-xhdr cli.suppress_xhdr = 1; break; + case 1031: // --interlaced + cli.interlaced = 1; + break; case 't': { // --threads int threads = atoi(optarg); if (threads < 0 || threads > MAX_THREADS) { @@ -2435,6 +2487,20 @@ int main(int argc, char *argv[]) { } } + // Handle interlaced mode: store full height for header, use half-height internally + if (cli.interlaced) { + // Store full height for the header + cli.header_height = cli.enc_params.height; + // Use half-height internally (FFmpeg will output half-height frames) + cli.enc_params.height = cli.enc_params.height / 2; + printf("Interlaced mode: header=%dx%d, internal=%dx%d\n", + cli.enc_params.width, cli.header_height, + cli.enc_params.width, cli.enc_params.height); + } else { + // Progressive mode: header_height equals internal height + cli.header_height = cli.enc_params.height; + } + // 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