tavenc: tiling on uniform

This commit is contained in:
minjaesong
2025-12-07 23:27:07 +09:00
parent 0907e22f53
commit 9e2c9e6efd
4 changed files with 322 additions and 143 deletions

View File

@@ -27,7 +27,8 @@ LIBTAVENC_OBJ = lib/libtavenc/tav_encoder_lib.o \
lib/libtavenc/tav_encoder_dwt.o \ lib/libtavenc/tav_encoder_dwt.o \
lib/libtavenc/tav_encoder_quantize.o \ lib/libtavenc/tav_encoder_quantize.o \
lib/libtavenc/tav_encoder_ezbc.o \ lib/libtavenc/tav_encoder_ezbc.o \
lib/libtavenc/tav_encoder_utils.o lib/libtavenc/tav_encoder_utils.o \
lib/libtavenc/tav_encoder_tile.o
# libtavdec - TAV decoder library # libtavdec - TAV decoder library
LIBTAVDEC_OBJ = lib/libtavdec/tav_video_decoder.o LIBTAVDEC_OBJ = lib/libtavdec/tav_video_decoder.o

View File

@@ -153,38 +153,12 @@ int tav_encoder_validate_context(tav_encoder_context_t *ctx);
// Video Encoding // Video Encoding
// ============================================================================= // =============================================================================
/** /*
* Encode a single RGB24 frame. * DEPRECATED: tav_encoder_encode_frame() and tav_encoder_flush() have been
* * removed. Use tav_encoder_encode_gop() instead, which works for both
* Frames are buffered internally until a GOP is full, then encoded and returned. * single-threaded and multi-threaded modes. The CLI should buffer frames
* For GOP encoding: returns NULL until GOP is complete. * and call encode_gop() when a full GOP is ready.
* For intra-only: returns packet immediately.
*
* Thread-safety: NOT thread-safe. Caller must serialize calls to encode_frame().
*
* @param ctx Encoder context
* @param rgb_frame RGB24 frame data (planar: [R...][G...][B...]), width×height×3 bytes
* @param frame_pts Presentation timestamp (frame number or time)
* @param packet Output packet pointer (NULL if GOP not yet complete)
* @return 1 if packet ready, 0 if buffering for GOP, -1 on error
*/ */
int tav_encoder_encode_frame(tav_encoder_context_t *ctx,
const uint8_t *rgb_frame,
int64_t frame_pts,
tav_encoder_packet_t **packet);
/**
* Flush encoder and encode any remaining buffered frames.
*
* Call at end of encoding to output final GOP (even if not full).
* Returns packets one at a time through repeated calls.
*
* @param ctx Encoder context
* @param packet Output packet pointer (NULL when no more packets)
* @return 1 if packet ready, 0 if no more packets, -1 on error
*/
int tav_encoder_flush(tav_encoder_context_t *ctx,
tav_encoder_packet_t **packet);
/** /**
* Encode a complete GOP (Group of Pictures) directly. * Encode a complete GOP (Group of Pictures) directly.

View File

@@ -13,6 +13,7 @@
#include "tav_encoder_quantize.h" #include "tav_encoder_quantize.h"
#include "tav_encoder_ezbc.h" #include "tav_encoder_ezbc.h"
#include "tav_encoder_utils.h" #include "tav_encoder_utils.h"
#include "tav_encoder_tile.h"
#include "encoder_tad.h" #include "encoder_tad.h"
#include <stdio.h> #include <stdio.h>
@@ -390,26 +391,26 @@ tav_encoder_context_t *tav_encoder_create(const tav_encoder_params_t *params) {
// Auto mode: use monoblock for <= D1 PAL, tiled for larger // Auto mode: use monoblock for <= D1 PAL, tiled for larger
if (ctx->width > TAV_MONOBLOCK_MAX_WIDTH || ctx->height > TAV_MONOBLOCK_MAX_HEIGHT) { if (ctx->width > TAV_MONOBLOCK_MAX_WIDTH || ctx->height > TAV_MONOBLOCK_MAX_HEIGHT) {
ctx->monoblock = 0; ctx->monoblock = 0;
if (ctx->verbose) { // if (ctx->verbose) {
printf("Auto-selected Padded Tiling mode: %dx%d exceeds D1 PAL threshold (%dx%d)\n", printf("Auto-selected Padded Tiling mode: %dx%d exceeds D1 PAL threshold (%dx%d)\n",
ctx->width, ctx->height, TAV_MONOBLOCK_MAX_WIDTH, TAV_MONOBLOCK_MAX_HEIGHT); ctx->width, ctx->height, TAV_MONOBLOCK_MAX_WIDTH, TAV_MONOBLOCK_MAX_HEIGHT);
} // }
} else { } else {
ctx->monoblock = 1; ctx->monoblock = 1;
if (ctx->verbose) { // if (ctx->verbose) {
printf("Auto-selected Monoblock mode: %dx%d within D1 PAL threshold\n", printf("Auto-selected Monoblock mode: %dx%d within D1 PAL threshold\n",
ctx->width, ctx->height); ctx->width, ctx->height);
} // }
} }
} else if (ctx->monoblock == 0) { } else if (ctx->monoblock == 0) {
if (ctx->verbose) { // if (ctx->verbose) {
printf("Forced Padded Tiling mode (--tiled)\n"); printf("Forced Padded Tiling mode (--tiled)\n");
} // }
} else { } else {
// monoblock == 1: force monoblock even for large dimensions // monoblock == 1: force monoblock even for large dimensions
if (ctx->verbose) { // if (ctx->verbose) {
printf("Forced Monoblock mode (--monoblock)\n"); printf("Forced Monoblock mode (--monoblock)\n");
} // }
} }
// Calculate tile dimensions based on monoblock setting // Calculate tile dimensions based on monoblock setting
@@ -421,10 +422,10 @@ tav_encoder_context_t *tav_encoder_create(const tav_encoder_params_t *params) {
// Padded Tiling mode: multiple tiles of TILE_SIZE_X × TILE_SIZE_Y // Padded Tiling mode: multiple tiles of TILE_SIZE_X × TILE_SIZE_Y
ctx->tiles_x = (ctx->width + TAV_TILE_SIZE_X - 1) / TAV_TILE_SIZE_X; ctx->tiles_x = (ctx->width + TAV_TILE_SIZE_X - 1) / TAV_TILE_SIZE_X;
ctx->tiles_y = (ctx->height + TAV_TILE_SIZE_Y - 1) / TAV_TILE_SIZE_Y; ctx->tiles_y = (ctx->height + TAV_TILE_SIZE_Y - 1) / TAV_TILE_SIZE_Y;
if (ctx->verbose) { // if (ctx->verbose) {
printf("Padded Tiling mode: %dx%d tiles (%d total)\n", printf("Padded Tiling mode: %dx%d tiles (%d total)\n",
ctx->tiles_x, ctx->tiles_y, ctx->tiles_x * ctx->tiles_y); ctx->tiles_x, ctx->tiles_y, ctx->tiles_x * ctx->tiles_y);
} // }
} }
// Calculate decomp levels if auto (0) // Calculate decomp levels if auto (0)
@@ -655,9 +656,17 @@ void tav_encoder_get_stats(tav_encoder_context_t *ctx, tav_encoder_stats_t *stat
} }
// ============================================================================= // =============================================================================
// Frame Encoding (Single-threaded implementation for now) // Frame Encoding - DEPRECATED, use tav_encoder_encode_gop() instead
// ============================================================================= // =============================================================================
/*
* tav_encoder_encode_frame() is deprecated and will be removed.
* Use tav_encoder_encode_gop() which works for both single-threaded and
* multi-threaded modes. The CLI should buffer frames and call encode_gop()
* when a full GOP is ready.
*/
#if 0 // DEPRECATED - kept for reference, will be deleted
int tav_encoder_encode_frame(tav_encoder_context_t *ctx, int tav_encoder_encode_frame(tav_encoder_context_t *ctx,
const uint8_t *rgb_frame, const uint8_t *rgb_frame,
int64_t frame_pts, int64_t frame_pts,
@@ -733,11 +742,19 @@ int tav_encoder_encode_frame(tav_encoder_context_t *ctx,
"Multi-threaded encoding not yet implemented"); "Multi-threaded encoding not yet implemented");
return -1; return -1;
} }
#endif // DEPRECATED
// ============================================================================= // =============================================================================
// Flush Encoder // Flush Encoder - DEPRECATED, CLI handles partial GOPs directly
// ============================================================================= // =============================================================================
/*
* tav_encoder_flush() is deprecated and will be removed.
* The CLI should track remaining frames and call tav_encoder_encode_gop()
* directly for partial GOPs at the end of encoding.
*/
#if 0 // DEPRECATED - kept for reference, will be deleted
int tav_encoder_flush(tav_encoder_context_t *ctx, int tav_encoder_flush(tav_encoder_context_t *ctx,
tav_encoder_packet_t **packet) { tav_encoder_packet_t **packet) {
if (!ctx || !packet) { if (!ctx || !packet) {
@@ -847,6 +864,7 @@ int tav_encoder_flush(tav_encoder_context_t *ctx,
return 0; // No more packets return 0; // No more packets
} }
#endif // DEPRECATED
void tav_encoder_free_packet(tav_encoder_packet_t *packet) { void tav_encoder_free_packet(tav_encoder_packet_t *packet) {
if (!packet) return; if (!packet) return;
@@ -1359,72 +1377,167 @@ static int encode_gop_intra_only(tav_encoder_context_t *ctx, gop_slot_t *slot) {
return -1; return -1;
} }
// Allocate work buffers for single frame // Step 1: RGB to YCoCg-R (or ICtCp) for full frame
float *work_y = tav_calloc(num_pixels, sizeof(float)); float *frame_y = tav_calloc(num_pixels, sizeof(float));
float *work_co = tav_calloc(num_pixels, sizeof(float)); float *frame_co = tav_calloc(num_pixels, sizeof(float));
float *work_cg = tav_calloc(num_pixels, sizeof(float)); float *frame_cg = tav_calloc(num_pixels, sizeof(float));
int16_t *quant_y = tav_calloc(num_pixels, sizeof(int16_t));
int16_t *quant_co = tav_calloc(num_pixels, sizeof(int16_t));
int16_t *quant_cg = tav_calloc(num_pixels, sizeof(int16_t));
// Step 1: RGB to YCoCg-R (or ICtCp) rgb_to_colour_space_frame(ctx, slot->rgb_frames[0], frame_y, frame_co, frame_cg, width, height);
rgb_to_colour_space_frame(ctx, slot->rgb_frames[0], work_y, work_co, work_cg, width, height);
// Step 2: Apply 2D DWT // Get quantiser values from QLUT indices
tav_dwt_2d_forward(work_y, width, height, ctx->decomp_levels, ctx->wavelet_type);
tav_dwt_2d_forward(work_co, width, height, ctx->decomp_levels, ctx->wavelet_type);
tav_dwt_2d_forward(work_cg, width, height, ctx->decomp_levels, ctx->wavelet_type);
// Step 3: Quantize coefficients
// ctx->quantiser_y/co/cg contain QLUT indices, lookup actual quantiser values
int base_quantiser_y = QLUT[ctx->quantiser_y]; int base_quantiser_y = QLUT[ctx->quantiser_y];
int base_quantiser_co = QLUT[ctx->quantiser_co]; int base_quantiser_co = QLUT[ctx->quantiser_co];
int base_quantiser_cg = QLUT[ctx->quantiser_cg]; int base_quantiser_cg = QLUT[ctx->quantiser_cg];
if (ctx->perceptual_tuning) { // Allocate preprocess buffer for all tiles
tav_quantise_perceptual(ctx->compat_enc, work_y, quant_y, num_pixels, // For tiled mode: num_tiles * (4-byte header + max_tile_coeff_size * 3 * sizeof(int16_t))
base_quantiser_y, (float)ctx->dead_zone_threshold, width, height, ctx->decomp_levels, 0, 0); // For monoblock: just the frame
tav_quantise_perceptual(ctx->compat_enc, work_co, quant_co, num_pixels, const int tile_coeff_count = ctx->monoblock ? num_pixels : (TAV_PADDED_TILE_SIZE_X * TAV_PADDED_TILE_SIZE_Y);
base_quantiser_co, (float)ctx->dead_zone_threshold, width, height, ctx->decomp_levels, 1, 0); const int num_tiles = ctx->tiles_x * ctx->tiles_y;
tav_quantise_perceptual(ctx->compat_enc, work_cg, quant_cg, num_pixels, size_t preprocess_capacity = num_tiles * (4 + tile_coeff_count * 3 * sizeof(int16_t) * 2); // Conservative with EZBC overhead
base_quantiser_cg, (float)ctx->dead_zone_threshold, width, height, ctx->decomp_levels, 1, 0); uint8_t *preprocess_buffer = tav_malloc(preprocess_capacity);
size_t preprocess_offset = 0;
if (ctx->monoblock) {
// ======================================================================
// Monoblock mode: process entire frame as single tile
// ======================================================================
int16_t *quant_y = tav_calloc(num_pixels, sizeof(int16_t));
int16_t *quant_co = tav_calloc(num_pixels, sizeof(int16_t));
int16_t *quant_cg = tav_calloc(num_pixels, sizeof(int16_t));
// Apply 2D DWT to full frame
tav_dwt_2d_forward(frame_y, width, height, ctx->decomp_levels, ctx->wavelet_type);
tav_dwt_2d_forward(frame_co, width, height, ctx->decomp_levels, ctx->wavelet_type);
tav_dwt_2d_forward(frame_cg, width, height, ctx->decomp_levels, ctx->wavelet_type);
// Quantize
if (ctx->perceptual_tuning) {
tav_quantise_perceptual(ctx->compat_enc, frame_y, quant_y, num_pixels,
base_quantiser_y, (float)ctx->dead_zone_threshold, width, height, ctx->decomp_levels, 0, 0);
tav_quantise_perceptual(ctx->compat_enc, frame_co, quant_co, num_pixels,
base_quantiser_co, (float)ctx->dead_zone_threshold, width, height, ctx->decomp_levels, 1, 0);
tav_quantise_perceptual(ctx->compat_enc, frame_cg, quant_cg, num_pixels,
base_quantiser_cg, (float)ctx->dead_zone_threshold, width, height, ctx->decomp_levels, 1, 0);
} else {
tav_quantise_uniform(frame_y, quant_y, num_pixels, base_quantiser_y,
(float)ctx->dead_zone_threshold, width, height,
ctx->decomp_levels, 0);
tav_quantise_uniform(frame_co, quant_co, num_pixels, base_quantiser_co,
(float)ctx->dead_zone_threshold, width, height,
ctx->decomp_levels, 1);
tav_quantise_uniform(frame_cg, quant_cg, num_pixels, base_quantiser_cg,
(float)ctx->dead_zone_threshold, width, height,
ctx->decomp_levels, 1);
}
// EZBC encode
preprocess_offset = preprocess_coefficients_ezbc(
quant_y, quant_co, quant_cg, NULL,
num_pixels, width, height, ctx->channel_layout,
preprocess_buffer
);
free(quant_y); free(quant_co); free(quant_cg);
} else { } else {
tav_quantise_uniform(work_y, quant_y, num_pixels, base_quantiser_y, // ======================================================================
(float)ctx->dead_zone_threshold, width, height, // Tiled mode: process each tile independently
ctx->decomp_levels, 0); // ======================================================================
tav_quantise_uniform(work_co, quant_co, num_pixels, base_quantiser_co, const int padded_pixels = TAV_PADDED_TILE_SIZE_X * TAV_PADDED_TILE_SIZE_Y;
(float)ctx->dead_zone_threshold, width, height,
ctx->decomp_levels, 1); // Allocate reusable tile buffers
tav_quantise_uniform(work_cg, quant_cg, num_pixels, base_quantiser_cg, float *tile_y = tav_calloc(padded_pixels, sizeof(float));
(float)ctx->dead_zone_threshold, width, height, float *tile_co = tav_calloc(padded_pixels, sizeof(float));
ctx->decomp_levels, 1); float *tile_cg = tav_calloc(padded_pixels, sizeof(float));
int16_t *quant_y = tav_calloc(padded_pixels, sizeof(int16_t));
int16_t *quant_co = tav_calloc(padded_pixels, sizeof(int16_t));
int16_t *quant_cg = tav_calloc(padded_pixels, sizeof(int16_t));
for (int tile_y_idx = 0; tile_y_idx < ctx->tiles_y; tile_y_idx++) {
for (int tile_x_idx = 0; tile_x_idx < ctx->tiles_x; tile_x_idx++) {
// Write tile header: [mode(1)][qY_override(1)][qCo_override(1)][qCg_override(1)]
preprocess_buffer[preprocess_offset++] = 0x01; // TAV_MODE_INTRA
preprocess_buffer[preprocess_offset++] = 0; // qY override (0 = use header)
preprocess_buffer[preprocess_offset++] = 0; // qCo override
preprocess_buffer[preprocess_offset++] = 0; // qCg override
// Extract padded tile from full frame
tav_extract_padded_tile(frame_y, frame_co, frame_cg,
width, height,
tile_x_idx, tile_y_idx,
tile_y, tile_co, tile_cg);
// Apply 2D DWT to padded tile
tav_dwt_2d_forward_padded_tile(tile_y, ctx->decomp_levels, ctx->wavelet_type);
tav_dwt_2d_forward_padded_tile(tile_co, ctx->decomp_levels, ctx->wavelet_type);
tav_dwt_2d_forward_padded_tile(tile_cg, ctx->decomp_levels, ctx->wavelet_type);
// Quantize tile coefficients
if (ctx->perceptual_tuning) {
tav_quantise_perceptual(ctx->compat_enc, tile_y, quant_y, padded_pixels,
base_quantiser_y, (float)ctx->dead_zone_threshold,
TAV_PADDED_TILE_SIZE_X, TAV_PADDED_TILE_SIZE_Y,
ctx->decomp_levels, 0, 0);
tav_quantise_perceptual(ctx->compat_enc, tile_co, quant_co, padded_pixels,
base_quantiser_co, (float)ctx->dead_zone_threshold,
TAV_PADDED_TILE_SIZE_X, TAV_PADDED_TILE_SIZE_Y,
ctx->decomp_levels, 1, 0);
tav_quantise_perceptual(ctx->compat_enc, tile_cg, quant_cg, padded_pixels,
base_quantiser_cg, (float)ctx->dead_zone_threshold,
TAV_PADDED_TILE_SIZE_X, TAV_PADDED_TILE_SIZE_Y,
ctx->decomp_levels, 1, 0);
} else {
tav_quantise_uniform(tile_y, quant_y, padded_pixels, base_quantiser_y,
(float)ctx->dead_zone_threshold,
TAV_PADDED_TILE_SIZE_X, TAV_PADDED_TILE_SIZE_Y,
ctx->decomp_levels, 0);
tav_quantise_uniform(tile_co, quant_co, padded_pixels, base_quantiser_co,
(float)ctx->dead_zone_threshold,
TAV_PADDED_TILE_SIZE_X, TAV_PADDED_TILE_SIZE_Y,
ctx->decomp_levels, 1);
tav_quantise_uniform(tile_cg, quant_cg, padded_pixels, base_quantiser_cg,
(float)ctx->dead_zone_threshold,
TAV_PADDED_TILE_SIZE_X, TAV_PADDED_TILE_SIZE_Y,
ctx->decomp_levels, 1);
}
// EZBC encode tile
size_t tile_size = preprocess_coefficients_ezbc(
quant_y, quant_co, quant_cg, NULL,
padded_pixels, TAV_PADDED_TILE_SIZE_X, TAV_PADDED_TILE_SIZE_Y,
ctx->channel_layout,
preprocess_buffer + preprocess_offset
);
preprocess_offset += tile_size;
// Clear tile buffers for next iteration
memset(tile_y, 0, padded_pixels * sizeof(float));
memset(tile_co, 0, padded_pixels * sizeof(float));
memset(tile_cg, 0, padded_pixels * sizeof(float));
}
}
free(tile_y); free(tile_co); free(tile_cg);
free(quant_y); free(quant_co); free(quant_cg);
} }
// Step 4: Preprocess coefficients // Free full-frame YCoCg buffers
size_t preprocess_capacity = num_pixels * 3 * sizeof(int16_t) + 65536; // Conservative free(frame_y); free(frame_co); free(frame_cg);
uint8_t *preprocess_buffer = tav_malloc(preprocess_capacity);
// Use EZBC preprocessing (Twobitmap is deprecated) // Step 5: Zstd compress all tile data
size_t preprocessed_size = preprocess_coefficients_ezbc( size_t compressed_bound = ZSTD_compressBound(preprocess_offset);
quant_y, quant_co, quant_cg, NULL,
num_pixels, width, height, ctx->channel_layout,
preprocess_buffer
);
// Step 5: Zstd compress
size_t compressed_bound = ZSTD_compressBound(preprocessed_size);
uint8_t *compression_buffer = tav_malloc(compressed_bound); uint8_t *compression_buffer = tav_malloc(compressed_bound);
size_t compressed_size = ZSTD_compress( size_t compressed_size = ZSTD_compress(
compression_buffer, compressed_bound, compression_buffer, compressed_bound,
preprocess_buffer, preprocessed_size, preprocess_buffer, preprocess_offset,
ctx->zstd_level ctx->zstd_level
); );
free(preprocess_buffer);
if (ZSTD_isError(compressed_size)) { if (ZSTD_isError(compressed_size)) {
free(work_y); free(work_co); free(work_cg);
free(quant_y); free(quant_co); free(quant_cg);
free(preprocess_buffer);
free(compression_buffer); free(compression_buffer);
snprintf(slot->error_message, MAX_ERROR_MESSAGE, snprintf(slot->error_message, MAX_ERROR_MESSAGE,
"Zstd compression failed: %s", ZSTD_getErrorName(compressed_size)); "Zstd compression failed: %s", ZSTD_getErrorName(compressed_size));
@@ -1452,10 +1565,6 @@ static int encode_gop_intra_only(tav_encoder_context_t *ctx, gop_slot_t *slot) {
slot->packets = pkt; slot->packets = pkt;
slot->num_packets = 1; slot->num_packets = 1;
// Cleanup
free(work_y); free(work_co); free(work_cg);
free(quant_y); free(quant_co); free(quant_cg);
free(preprocess_buffer);
free(compression_buffer); free(compression_buffer);
return 0; // Success return 0; // Success

View File

@@ -91,6 +91,11 @@ typedef struct {
size_t total_bytes; size_t total_bytes;
time_t start_time; time_t start_time;
// GOP frame buffer (for tav_encoder_encode_gop())
uint8_t **gop_frames; // Array of frame pointers [gop_size]
int gop_frame_count; // Number of frames in current GOP
int *gop_frame_numbers; // Frame numbers for timecodes [gop_size]
// CLI options // CLI options
int verbose; int verbose;
int encode_limit; // Max frames to encode (0=all) int encode_limit; // Max frames to encode (0=all)
@@ -254,7 +259,7 @@ static int get_video_info(const char *input_file, int *width, int *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) {
char cmd[MAX_PATH * 2]; char cmd[MAX_PATH * 2];
snprintf(cmd, sizeof(cmd), snprintf(cmd, sizeof(cmd),
"ffmpeg -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\" -",
input_file, width, height, width, height); input_file, width, height, width, height);
FILE *fp = popen(cmd, "r"); FILE *fp = popen(cmd, "r");
@@ -305,12 +310,22 @@ static int write_tav_header(FILE *fp, const tav_encoder_params_t *params, int ha
// 6 = ICtCp monoblock perceptual // 6 = ICtCp monoblock perceptual
// Add 8 if using CDF 5/3 temporal wavelet // Add 8 if using CDF 5/3 temporal wavelet
uint8_t version; uint8_t version;
if (params->perceptual_tuning) { if (params->monoblock) {
// Monoblock perceptual: version 5 (YCoCg-R) or 6 (ICtCp) if (params->perceptual_tuning) {
version = params->channel_layout ? 6 : 5; // Monoblock perceptual: version 5 (YCoCg-R) or 6 (ICtCp)
version = params->channel_layout ? 6 : 5;
} else {
// Monoblock uniform: version 3 (YCoCg-R) or 4 (ICtCp)
version = params->channel_layout ? 4 : 3;
}
} else { } else {
// Monoblock uniform: version 3 (YCoCg-R) or 4 (ICtCp) if (params->perceptual_tuning) {
version = params->channel_layout ? 4 : 3; // Tiled perceptual: version 7 (YCoCg-R) or 8 (ICtCp)
version = params->channel_layout ? 7 : 8;
} else {
// Tiled uniform: version 1 (YCoCg-R) or 2 (ICtCp)
version = params->channel_layout ? 1 : 2;
}
} }
// Add 8 if using CDF 5/3 temporal wavelet // Add 8 if using CDF 5/3 temporal wavelet
if (params->enable_temporal_dwt && params->temporal_wavelet == 0) { if (params->enable_temporal_dwt && params->temporal_wavelet == 0) {
@@ -468,7 +483,7 @@ static int write_gop_sync_packet(FILE *fp, int frame_count) {
static int extract_audio_to_file(const char *input_file, const char *output_file) { static int extract_audio_to_file(const char *input_file, const char *output_file) {
char cmd[MAX_PATH * 2]; char cmd[MAX_PATH * 2];
snprintf(cmd, sizeof(cmd), snprintf(cmd, sizeof(cmd),
"ffmpeg -v quiet -i \"%s\" -f f32le -acodec pcm_f32le -ar %d -ac 2 " "ffmpeg -hide_banner -v quiet -i \"%s\" -f f32le -acodec pcm_f32le -ar %d -ac 2 "
"-af \"aresample=resampler=soxr:precision=28:cutoff=0.99:dither_scale=0,highpass=f=16\" " "-af \"aresample=resampler=soxr:precision=28:cutoff=0.99:dither_scale=0,highpass=f=16\" "
"-y \"%s\" 2>/dev/null", "-y \"%s\" 2>/dev/null",
input_file, AUDIO_SAMPLE_RATE, output_file); input_file, AUDIO_SAMPLE_RATE, output_file);
@@ -952,11 +967,51 @@ static int encode_video(cli_context_t *cli) {
} }
} }
// Allocate frame buffer // Allocate GOP frame buffer for tav_encoder_encode_gop()
size_t frame_size = cli->enc_params.width * cli->enc_params.height * 3; size_t frame_size = cli->enc_params.width * cli->enc_params.height * 3;
int gop_size = cli->enc_params.gop_size;
// In intra-only mode, encode each frame immediately (GOP size = 1)
if (!cli->enc_params.enable_temporal_dwt) {
gop_size = 1;
}
cli->gop_frames = malloc(gop_size * sizeof(uint8_t*));
cli->gop_frame_numbers = malloc(gop_size * sizeof(int));
cli->gop_frame_count = 0;
if (!cli->gop_frames || !cli->gop_frame_numbers) {
fprintf(stderr, "Error: Failed to allocate GOP frame buffer\n");
tav_encoder_free(ctx);
pclose(cli->ffmpeg_pipe);
return -1;
}
for (int i = 0; i < gop_size; i++) {
cli->gop_frames[i] = malloc(frame_size);
if (!cli->gop_frames[i]) {
fprintf(stderr, "Error: Failed to allocate GOP frame %d\n", i);
for (int j = 0; j < i; j++) free(cli->gop_frames[j]);
free(cli->gop_frames);
free(cli->gop_frame_numbers);
tav_encoder_free(ctx);
pclose(cli->ffmpeg_pipe);
return -1;
}
}
if (cli->verbose) {
printf(" GOP frame buffer: %d frames x %zu bytes = %zu KB\n",
gop_size, frame_size, (gop_size * frame_size) / 1024);
}
// Temporary frame buffer for reading from FFmpeg
uint8_t *rgb_frame = malloc(frame_size); uint8_t *rgb_frame = malloc(frame_size);
if (!rgb_frame) { if (!rgb_frame) {
fprintf(stderr, "Error: Failed to allocate frame buffer\n"); fprintf(stderr, "Error: Failed to allocate frame buffer\n");
for (int i = 0; i < gop_size; i++) free(cli->gop_frames[i]);
free(cli->gop_frames);
free(cli->gop_frame_numbers);
tav_encoder_free(ctx); tav_encoder_free(ctx);
pclose(cli->ffmpeg_pipe); pclose(cli->ffmpeg_pipe);
return -1; return -1;
@@ -981,12 +1036,12 @@ static int encode_video(cli_context_t *cli) {
write_fontrom_packet(cli->output_fp, cli->fontrom_high, FONTROM_OPCODE_HIGH, cli->verbose); write_fontrom_packet(cli->output_fp, cli->fontrom_high, FONTROM_OPCODE_HIGH, cli->verbose);
} }
// Encoding loop // Encoding loop using tav_encoder_encode_gop()
printf("Encoding frames...\n"); printf("Encoding frames...\n");
cli->start_time = time(NULL); cli->start_time = time(NULL);
int64_t frame_pts = 0;
tav_encoder_packet_t *packet = NULL; tav_encoder_packet_t *packet = NULL;
int encoding_error = 0;
while (1) { while (1) {
// Check encode limit // Check encode limit
@@ -1000,9 +1055,15 @@ static int encode_video(cli_context_t *cli) {
break; // EOF break; // EOF
} else if (result < 0) { } else if (result < 0) {
fprintf(stderr, "Error reading frame\n"); fprintf(stderr, "Error reading frame\n");
encoding_error = 1;
break; break;
} }
// Copy frame to GOP buffer
memcpy(cli->gop_frames[cli->gop_frame_count], rgb_frame, frame_size);
cli->gop_frame_numbers[cli->gop_frame_count] = (int)cli->frame_count;
cli->gop_frame_count++;
// Accumulate audio samples for this frame (will write when GOP completes) // Accumulate audio samples for this frame (will write when GOP completes)
if (cli->has_audio && cli->audio_buffer && cli->gop_audio_buffer) { if (cli->has_audio && cli->audio_buffer && cli->gop_audio_buffer) {
size_t samples_read = read_audio_samples(cli, cli->audio_buffer, cli->samples_per_frame); size_t samples_read = read_audio_samples(cli, cli->audio_buffer, cli->samples_per_frame);
@@ -1015,40 +1076,55 @@ static int encode_video(cli_context_t *cli) {
} }
} }
// Encode frame cli->frame_count++;
result = tav_encoder_encode_frame(ctx, rgb_frame, frame_pts, &packet);
if (result < 0) { // Check if GOP is full
fprintf(stderr, "Error: %s\n", tav_encoder_get_error(ctx)); if (cli->gop_frame_count >= gop_size) {
break; // Encode complete GOP
} result = tav_encoder_encode_gop(ctx,
(const uint8_t**)cli->gop_frames,
cli->gop_frame_count,
cli->gop_frame_numbers,
&packet);
if (result > 0 && packet) { if (result < 0) {
// GOP is complete - write in correct order: TIMECODE, AUDIO, VIDEO, GOP_SYNC fprintf(stderr, "Error: %s\n", tav_encoder_get_error(ctx));
encoding_error = 1;
// 1. Write timecode before GOP break;
write_timecode_packet(cli->output_fp, cli->frame_count - (cli->frame_count % cli->enc_params.gop_size),
cli->enc_params.fps_num, cli->enc_params.fps_den);
// 2. Write accumulated audio for this GOP as single TAD packet
if (cli->has_audio && cli->gop_audio_samples > 0) {
write_audio_packet(cli->output_fp, cli, cli->gop_audio_buffer, cli->gop_audio_samples);
cli->gop_audio_samples = 0; // Reset for next GOP
} }
// 3. Write video GOP packet if (packet) {
write_tav_packet(cli->output_fp, packet); // GOP is complete - write in correct order: TIMECODE, AUDIO, VIDEO, GOP_SYNC
cli->total_bytes += packet->size;
cli->gop_count++;
// 4. Write GOP_SYNC after GOP packets (0x12 = GOP unified) // 1. Write timecode before GOP (use first frame number in GOP)
if (packet->packet_type == TAV_PACKET_GOP_UNIFIED) { write_timecode_packet(cli->output_fp, cli->gop_frame_numbers[0],
// Extract GOP size from packet (byte 1) cli->enc_params.fps_num, cli->enc_params.fps_den);
int gop_size = packet->data[1];
write_gop_sync_packet(cli->output_fp, gop_size); // 2. Write accumulated audio for this GOP as single TAD packet
if (cli->has_audio && cli->gop_audio_samples > 0) {
write_audio_packet(cli->output_fp, cli, cli->gop_audio_buffer, cli->gop_audio_samples);
cli->gop_audio_samples = 0; // Reset for next GOP
}
// 3. Write video GOP packet
write_tav_packet(cli->output_fp, packet);
cli->total_bytes += packet->size;
cli->gop_count++;
// 4. Write GOP_SYNC after GOP packets
if (packet->packet_type == TAV_PACKET_GOP_UNIFIED) {
int frames_in_gop = packet->data[1];
write_gop_sync_packet(cli->output_fp, frames_in_gop);
} else if (packet->packet_type == TAV_PACKET_IFRAME) {
write_gop_sync_packet(cli->output_fp, 1);
}
tav_encoder_free_packet(packet);
packet = NULL;
} }
tav_encoder_free_packet(packet); // Reset GOP buffer
cli->gop_frame_count = 0;
// Progress // Progress
if (cli->verbose || cli->frame_count % 60 == 0) { if (cli->verbose || cli->frame_count % 60 == 0) {
@@ -1065,21 +1141,27 @@ static int encode_video(cli_context_t *cli) {
fflush(stdout); fflush(stdout);
} }
} }
cli->frame_count++;
frame_pts++;
} }
printf("\n"); printf("\n");
// Flush encoder // Encode remaining frames in GOP buffer (partial GOP)
printf("Flushing encoder...\n"); if (!encoding_error && cli->gop_frame_count > 0) {
while (tav_encoder_flush(ctx, &packet) > 0) { printf("Encoding final partial GOP (%d frames)...\n", cli->gop_frame_count);
if (packet) {
int result = tav_encoder_encode_gop(ctx,
(const uint8_t**)cli->gop_frames,
cli->gop_frame_count,
cli->gop_frame_numbers,
&packet);
if (result < 0) {
fprintf(stderr, "Error encoding final GOP: %s\n", tav_encoder_get_error(ctx));
} else if (packet) {
// Write remaining packets in correct order: TIMECODE, AUDIO, VIDEO, GOP_SYNC // Write remaining packets in correct order: TIMECODE, AUDIO, VIDEO, GOP_SYNC
// 1. Write timecode // 1. Write timecode
write_timecode_packet(cli->output_fp, cli->frame_count - (cli->frame_count % cli->enc_params.gop_size), write_timecode_packet(cli->output_fp, cli->gop_frame_numbers[0],
cli->enc_params.fps_num, cli->enc_params.fps_den); cli->enc_params.fps_num, cli->enc_params.fps_den);
// 2. Write any remaining accumulated audio for this GOP // 2. Write any remaining accumulated audio for this GOP
@@ -1095,8 +1177,8 @@ static int encode_video(cli_context_t *cli) {
// 4. Write GOP_SYNC after GOP packets // 4. Write GOP_SYNC after GOP packets
if (packet->packet_type == TAV_PACKET_GOP_UNIFIED) { if (packet->packet_type == TAV_PACKET_GOP_UNIFIED) {
int gop_size = packet->data[1]; int frames_in_gop = packet->data[1];
write_gop_sync_packet(cli->output_fp, gop_size); write_gop_sync_packet(cli->output_fp, frames_in_gop);
} else if (packet->packet_type == TAV_PACKET_IFRAME) { } else if (packet->packet_type == TAV_PACKET_IFRAME) {
write_gop_sync_packet(cli->output_fp, 1); write_gop_sync_packet(cli->output_fp, 1);
} }
@@ -1113,6 +1195,19 @@ static int encode_video(cli_context_t *cli) {
tav_encoder_free(ctx); tav_encoder_free(ctx);
pclose(cli->ffmpeg_pipe); pclose(cli->ffmpeg_pipe);
// Cleanup GOP frame buffer
if (cli->gop_frames) {
for (int i = 0; i < gop_size; i++) {
free(cli->gop_frames[i]);
}
free(cli->gop_frames);
cli->gop_frames = NULL;
}
if (cli->gop_frame_numbers) {
free(cli->gop_frame_numbers);
cli->gop_frame_numbers = NULL;
}
// Cleanup audio resources // Cleanup audio resources
if (cli->audio_buffer) { if (cli->audio_buffer) {
free(cli->audio_buffer); free(cli->audio_buffer);