tavenc/dec: interlaced mode

This commit is contained in:
minjaesong
2025-12-09 08:40:42 +09:00
parent efdb915208
commit 017aef26ab
4 changed files with 214 additions and 68 deletions

View File

@@ -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:

View File

@@ -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);
}

View File

@@ -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,15 +276,77 @@ 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->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 {
// Progressive mode - simple passthrough
if (ctx->output_raw) {
// Raw video output (no compression)
execl("/usr/bin/ffmpeg", "ffmpeg",
@@ -318,6 +396,7 @@ static int spawn_ffmpeg(decoder_context_t *ctx) {
"-v", "warning",
(char*)NULL);
}
}
fprintf(stderr, "Error: Failed to execute FFmpeg\n");
exit(1);
@@ -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,

View File

@@ -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];
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\" -",
"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