mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-03-07 11:51:49 +09:00
tavenc/dec: interlaced mode
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user