/** * TAV-DT Encoder - Digital Tape Format Encoder * * Encodes video to TAV-DT format with forward error correction. * * TAV-DT is a packetised streaming format designed for digital tape/broadcast: * - Fixed dimensions: 720x480 (NTSC) or 720x576 (PAL) * - 16-frame GOPs with 9/7 spatial wavelet, Haar temporal * - Mandatory TAD audio * - LDPC rate 1/2 for headers, Reed-Solomon (255,223) for payloads * * Packet structure (revised 2025-12-17): * - Main header: 32 bytes raw (256 bits) -> 64 bytes LDPC encoded (512 bits, rate 256/512) * Layout: sync(4) + fps(1) + flags(1) + reserved(2) + size(4) + timecode(8) + offset(4) + reserved(4) + crc(4) * CRC covers bytes 0-27 (everything except CRC itself) * - TAD subpacket: header 16 bytes raw (128 bits) -> 32 bytes LDPC (256 bits, rate 128/256), + RS-encoded payload * Layout: sample_count(2) + quant_bits(1) + reserved(2) + compressed_size(4) + rs_block_count(3) + crc(4) * - TAV subpacket: header 16 bytes raw (128 bits) -> 32 bytes LDPC (256 bits, rate 128/256), + RS-encoded payload * Layout: sync(4) + gop_size(1) + compressed_size(4) + rs_block_count(3) + crc(4) * - No packet type bytes - always audio then video * * Created by CuriousTorvald and Claude on 2025-12-09. * Revised 2025-12-17 for power-of-two header sizes, subpacket CRCs, and TAV subpacket sync. */ #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #include #include "tav_encoder_lib.h" #include "encoder_tad.h" #include "reed_solomon.h" #include "ldpc.h" #include "ldpc_payload.h" // FEC mode for payloads (stored in flags byte bit 2) #define FEC_MODE_RS 0 // Reed-Solomon (255,223) - default #define FEC_MODE_LDPC 1 // LDPC (255,223) - experimental // ============================================================================= // Constants // ============================================================================= // TAV-DT sync patterns (big endian) #define TAV_DT_SYNC_NTSC 0xE3537A1F #define TAV_DT_SYNC_PAL 0xD193A745 // TAV-DT dimensions #define DT_WIDTH 720 #define DT_HEIGHT_NTSC 480 #define DT_HEIGHT_PAL 576 // Fixed parameters #define DT_GOP_SIZE 16 #define DT_SPATIAL_LEVELS 4 #define DT_TEMPORAL_LEVELS 2 #define DT_MAIN_HEADER_SIZE 28 // fps(1) + flags(1) + reserved(2) + size(4) + timecode(8) + offset(4) + reserved(4) + crc(4) #define DT_TAD_HEADER_SIZE 14 // sample_count(2) + quant_bits(1) + compressed_size(4) + rs_block_count(3) + crc(4) #define DT_TAV_HEADER_SIZE 14 // gop_size(1) + reserved(2) + compressed_size(4) + rs_block_count(3) + crc(4) // TAV subpacket sync pattern (big endian) #define TAV_SUBPACKET_SYNC 0xA3F7C91E // Quality level to quantiser mapping static const int QUALITY_Y[] = {79, 47, 23, 11, 5, 2}; static const int QUALITY_CO[] = {123, 108, 91, 76, 59, 29}; static const int QUALITY_CG[] = {148, 133, 113, 99, 76, 39}; // Audio samples per GOP (32kHz / framerate * gop_size) #define AUDIO_SAMPLE_RATE 32000 // ============================================================================= // Multithreading Structures // ============================================================================= #define GOP_SLOT_EMPTY 0 #define GOP_SLOT_READY 1 #define GOP_SLOT_ENCODING 2 #define GOP_SLOT_COMPLETE 3 typedef struct { // Input frames (copied from main thread) uint8_t **rgb_frames; // Frame data pointers [gop_size] int *frame_numbers; // Frame number array [gop_size] int num_frames; // Actual number of frames in this GOP int gop_index; // Sequential GOP index for ordering output // Audio samples for this GOP float *audio_samples; // Interleaved stereo samples size_t audio_sample_count; // Output tav_encoder_packet_t *packet; // Encoded video packet uint8_t *tad_output; // Encoded audio data size_t tad_size; // Encoded audio size int success; // 1 if encoding succeeded // Encoder params (copy for thread safety) tav_encoder_params_t params; // Slot status volatile int status; } gop_job_t; /** * Get number of available CPUs. */ static int get_available_cpus(void) { #ifdef _SC_NPROCESSORS_ONLN long nproc = sysconf(_SC_NPROCESSORS_ONLN); if (nproc > 0) { return (int)nproc; } #endif return 1; // Fallback to single core } /** * Get default thread count (cap at 8) */ static int get_default_thread_count(void) { int available = get_available_cpus(); return available < 8 ? available : 8; } // ============================================================================= // CRC-32 // ============================================================================= static uint32_t crc32_table[256]; static int crc32_initialized = 0; static void init_crc32_table(void) { if (crc32_initialized) return; for (uint32_t i = 0; i < 256; i++) { uint32_t crc = i; for (int j = 0; j < 8; j++) { if (crc & 1) { crc = (crc >> 1) ^ 0xEDB88320; } else { crc >>= 1; } } crc32_table[i] = crc; } crc32_initialized = 1; } static uint32_t calculate_crc32(const uint8_t *data, size_t length) { init_crc32_table(); uint32_t crc = 0xFFFFFFFF; for (size_t i = 0; i < length; i++) { crc = (crc >> 8) ^ crc32_table[(crc ^ data[i]) & 0xFF]; } return crc ^ 0xFFFFFFFF; } // ============================================================================= // Encoder Context // ============================================================================= typedef struct { // Input/output char *input_file; char *output_file; FILE *output_fp; // Video encoder context tav_encoder_context_t *video_ctx; // Video parameters int width; int height; int fps_num; int fps_den; int target_fps_num; // Target output framerate numerator (0 = no conversion) int target_fps_den; // Target output framerate denominator int original_fps_num; // Source framerate (always probed) int original_fps_den; int is_interlaced; int is_pal; int quality_index; // Frame buffers uint8_t **gop_frames; int gop_frame_count; // Audio buffer float *audio_buffer; size_t audio_buffer_samples; size_t audio_buffer_capacity; // Timecode uint64_t current_timecode_ns; int frame_number; // Statistics uint64_t packets_written; uint64_t bytes_written; uint64_t frames_encoded; // Options int verbose; int encode_limit; int fec_mode; // FEC_MODE_RS or FEC_MODE_LDPC for payloads // Multithreading int num_threads; // 0 = single-threaded, 1+ = num worker threads gop_job_t *gop_jobs; // Array of GOP job slots [num_threads] pthread_t *worker_threads; // Array of worker thread handles [num_threads] pthread_mutex_t job_mutex; // Mutex for job slot access pthread_cond_t job_ready; // Signal when a job slot is ready for encoding pthread_cond_t job_complete; // Signal when a job slot is complete volatile int shutdown_workers; // 1 when workers should exit // Encoder params (template for worker threads) tav_encoder_params_t enc_params; } dt_encoder_t; // ============================================================================= // Utility Functions // ============================================================================= static void print_usage(const char *program) { printf("TAV-DT Encoder - Digital Tape Format with FEC\n"); printf("\nUsage: %s -i input.mp4 -o output.tavdt [options]\n\n", program); printf("Required:\n"); printf(" -i, --input FILE Input video file (via FFmpeg)\n"); printf(" -o, --output FILE Output TAV-DT file\n"); printf("\nOptions:\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(" --pal Force PAL format (720x576)\n"); printf(" --interlaced Interlaced output\n"); printf(" --ldpc-payload Use LDPC(255,223) instead of RS(255,223) for payloads\n"); printf(" (experimental: better at high error rates)\n"); printf(" --encode-limit N Encode only N frames (for testing)\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(" -v, --verbose Verbose output\n"); printf(" -h, --help Show this help\n"); } // ============================================================================= // FEC Block Encoding (RS or LDPC based on mode) // ============================================================================= static size_t encode_fec_blocks(const uint8_t *data, size_t data_len, uint8_t *output, int fec_mode) { if (fec_mode == FEC_MODE_LDPC) { // Use LDPC(255,223) encoding return ldpc_p_encode_blocks(data, data_len, output); } else { // Use RS(255,223) encoding (default) size_t output_len = 0; size_t remaining = data_len; const uint8_t *src = data; uint8_t *dst = output; while (remaining > 0) { size_t block_data = (remaining > RS_DATA_SIZE) ? RS_DATA_SIZE : remaining; size_t encoded_len = rs_encode(src, block_data, dst); // Pad to full block size for consistent block boundaries if (encoded_len < RS_BLOCK_SIZE) { memset(dst + encoded_len, 0, RS_BLOCK_SIZE - encoded_len); } src += block_data; dst += RS_BLOCK_SIZE; output_len += RS_BLOCK_SIZE; remaining -= block_data; } return output_len; } } // ============================================================================= // Packet Writing // ============================================================================= static int write_packet(dt_encoder_t *enc, uint64_t timecode_ns, const uint8_t *tad_data, size_t tad_size, const uint8_t *tav_data, size_t tav_size, int gop_size, uint16_t audio_samples, uint8_t audio_quant_bits) { // Calculate RS block counts uint32_t tad_rs_blocks = (tad_size + RS_DATA_SIZE - 1) / RS_DATA_SIZE; uint32_t tav_rs_blocks = (tav_size + RS_DATA_SIZE - 1) / RS_DATA_SIZE; // Calculate sizes size_t tad_rs_size = tad_rs_blocks * RS_BLOCK_SIZE; size_t tav_rs_size = tav_rs_blocks * RS_BLOCK_SIZE; // Subpacket sizes: LDPC header + RS payload (TAV includes sync) size_t tad_subpacket_size = DT_TAD_HEADER_SIZE * 2 + tad_rs_size; // 28 + RS size_t tav_subpacket_size = 4 + DT_TAV_HEADER_SIZE * 2 + tav_rs_size; // sync(4) + 28 + RS uint32_t offset_to_video = tad_subpacket_size; // Offset from after main header to TAV sync uint32_t packet_size = tad_subpacket_size + tav_subpacket_size; // Build main header (28 bytes raw = 224 bits, sync written separately) // Layout: fps(1) + flags(1) + reserved(2) + size(4) + timecode(8) + offset(4) + reserved(4) + crc(4) // CRC is calculated over bytes 0-23 (everything except CRC itself) uint8_t master_sync[4]; uint8_t header[DT_MAIN_HEADER_SIZE]; // 28 bytes memset(header, 0, DT_MAIN_HEADER_SIZE); // Write sync pattern in big-endian (network byte order) uint32_t sync = enc->is_pal ? TAV_DT_SYNC_PAL : TAV_DT_SYNC_NTSC; master_sync[0] = (sync >> 24) & 0xFF; master_sync[1] = (sync >> 16) & 0xFF; master_sync[2] = (sync >> 8) & 0xFF; master_sync[3] = sync & 0xFF; // FPS byte: encode framerate uint8_t fps_byte; if (enc->fps_den == 1) fps_byte = enc->fps_num; else if (enc->fps_den == 1001) fps_byte = enc->fps_num / 1000; else fps_byte = enc->fps_num / enc->fps_den; header[0] = fps_byte; // Flags byte uint8_t flags = 0; flags |= (enc->is_interlaced ? 0x01 : 0x00); flags |= (enc->fps_den == 1001 ? 0x02 : 0x00); flags |= ((enc->fec_mode & 0x01) << 2); // FEC mode in bit 2 flags |= (enc->quality_index & 0x0F) << 4; header[1] = flags; // Reserved (2 bytes) at offset 2-3 header[2] = 0; header[3] = 0; // Packet size (4 bytes) at offset 4-7 memcpy(header + 4, &packet_size, 4); // Timecode (8 bytes) at offset 8-15 memcpy(header + 8, &timecode_ns, 8); // Offset to video (4 bytes) at offset 16-20 memcpy(header + 16, &offset_to_video, 4); // Reserved (4 bytes) at offset 20-24 // Already zero from memset // CRC-32 (4 bytes) at offset 24-27, calculated over bytes 0-23 uint32_t crc = calculate_crc32(header, 24); memcpy(header + 24, &crc, 4); // LDPC encode main header (28 -> 56 bytes, rate 224/448 bits) uint8_t ldpc_header[DT_MAIN_HEADER_SIZE * 2]; ldpc_encode(header, DT_MAIN_HEADER_SIZE, ldpc_header); // Build TAD subpacket header (14 bytes raw = 112 bits) // Layout: sample_count(2) + quant_bits(1) + compressed_size(4) + rs_block_count(3) + crc(4) uint8_t tad_header[DT_TAD_HEADER_SIZE]; // 14 bytes memset(tad_header, 0, DT_TAD_HEADER_SIZE); memcpy(tad_header + 0, &audio_samples, 2); tad_header[2] = audio_quant_bits; uint32_t tad_compressed_size = tad_size; memcpy(tad_header + 3, &tad_compressed_size, 4); // RS block count as uint24 at offset 7-9 tad_header[7] = tad_rs_blocks & 0xFF; tad_header[8] = (tad_rs_blocks >> 8) & 0xFF; tad_header[9] = (tad_rs_blocks >> 16) & 0xFF; // CRC-32 (4 bytes) at offset 12-15, calculated over bytes 0-9 uint32_t tad_crc = calculate_crc32(tad_header, 10); memcpy(tad_header + 10, &tad_crc, 4); // LDPC encode TAD header (14 -> 28 bytes, rate 112/224 bits) uint8_t ldpc_tad_header[DT_TAD_HEADER_SIZE * 2]; ldpc_encode(tad_header, DT_TAD_HEADER_SIZE, ldpc_tad_header); // Build TAV subpacket header (14 bytes raw = 112 bits) // Layout: sync(4) + gop_size(1) + compressed_size(4) + rs_block_count(3) + crc(4) uint8_t tav_sync[4]; uint8_t tav_header[DT_TAV_HEADER_SIZE]; // 14 bytes memset(tav_header, 0, DT_TAV_HEADER_SIZE); // Write TAV subpacket sync pattern in big-endian tav_sync[0] = (TAV_SUBPACKET_SYNC >> 24) & 0xFF; tav_sync[1] = (TAV_SUBPACKET_SYNC >> 16) & 0xFF; tav_sync[2] = (TAV_SUBPACKET_SYNC >> 8) & 0xFF; tav_sync[3] = TAV_SUBPACKET_SYNC & 0xFF; tav_header[0] = gop_size; uint32_t tav_compressed_size = tav_size; memcpy(tav_header + 3, &tav_compressed_size, 4); // RS block count as uint24 at offset 7-9 tav_header[7] = tav_rs_blocks & 0xFF; tav_header[8] = (tav_rs_blocks >> 8) & 0xFF; tav_header[9] = (tav_rs_blocks >> 16) & 0xFF; // CRC-32 (4 bytes) at offset 12-15, calculated over bytes 0-11 uint32_t tav_crc = calculate_crc32(tav_header, 10); memcpy(tav_header + 10, &tav_crc, 4); // LDPC encode TAV header (14 -> 28 bytes, rate 112/224 bits) uint8_t ldpc_tav_header[DT_TAV_HEADER_SIZE * 2]; ldpc_encode(tav_header, DT_TAV_HEADER_SIZE, ldpc_tav_header); // FEC encode payloads (RS or LDPC based on mode) uint8_t *tad_rs_data = malloc(tad_rs_size); uint8_t *tav_rs_data = malloc(tav_rs_size); encode_fec_blocks(tad_data, tad_size, tad_rs_data, enc->fec_mode); encode_fec_blocks(tav_data, tav_size, tav_rs_data, enc->fec_mode); // Write everything // Sync patterns are written separately (not LDPC-coded) per spec fwrite(master_sync, 1, 4, enc->output_fp); // Main sync (4 bytes) fwrite(ldpc_header, 1, DT_MAIN_HEADER_SIZE * 2, enc->output_fp); // LDPC header (56 bytes) fwrite(ldpc_tad_header, 1, DT_TAD_HEADER_SIZE * 2, enc->output_fp); // TAD LDPC header (28 bytes) fwrite(tad_rs_data, 1, tad_rs_size, enc->output_fp); // TAD RS payload fwrite(tav_sync, 1, 4, enc->output_fp); // TAV sync (4 bytes) fwrite(ldpc_tav_header, 1, DT_TAV_HEADER_SIZE * 2, enc->output_fp); // TAV LDPC header (28 bytes) fwrite(tav_rs_data, 1, tav_rs_size, enc->output_fp); // TAV RS payload size_t total_written = 4 + DT_MAIN_HEADER_SIZE * 2 + tad_subpacket_size + 4 + tav_subpacket_size; if (enc->verbose) { printf("GOP %lu: %d frames, header=%zu tad=%zu tav=%zu total=%zu bytes\n", enc->packets_written + 1, gop_size, (size_t)(DT_MAIN_HEADER_SIZE * 2), tad_subpacket_size, tav_subpacket_size, total_written); } free(tad_rs_data); free(tav_rs_data); enc->packets_written++; enc->bytes_written += total_written; return 0; } // ============================================================================= // FFmpeg Integration // ============================================================================= static FILE *spawn_ffmpeg_video(dt_encoder_t *enc, pid_t *pid) { int pipefd[2]; if (pipe(pipefd) < 0) { fprintf(stderr, "Error: Failed to create pipe\n"); return NULL; } *pid = fork(); if (*pid < 0) { fprintf(stderr, "Error: Failed to fork\n"); close(pipefd[0]); close(pipefd[1]); return NULL; } if (*pid == 0) { // Child process close(pipefd[0]); dup2(pipefd[1], STDOUT_FILENO); close(pipefd[1]); char video_size[32]; snprintf(video_size, sizeof(video_size), "%dx%d", enc->width, enc->height); // Build fps filter prefix if conversion is requested char fps_filter[128] = ""; if (enc->target_fps_num > 0 && enc->target_fps_den > 0 && enc->original_fps_num > 0 && enc->original_fps_den > 0) { // Compare framerates long long target_rate = (long long)enc->target_fps_num * enc->original_fps_den; long long source_rate = (long long)enc->original_fps_num * enc->target_fps_den; if (target_rate > source_rate) { // Upsampling: use motion interpolation snprintf(fps_filter, sizeof(fps_filter), "minterpolate=fps=%d/%d,", enc->target_fps_num, enc->target_fps_den); } else if (target_rate < source_rate) { // Downsampling: use fps filter snprintf(fps_filter, sizeof(fps_filter), "fps=%d/%d,", enc->target_fps_num, enc->target_fps_den); } // If equal, fps_filter remains empty } // Use same filtergraph as reference TAV encoder char vf[320]; snprintf(vf, sizeof(vf), "%sscale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d%s", fps_filter, enc->width, enc->height, enc->width, enc->height, enc->is_interlaced ? ",setfield=tff" : ""); execlp("ffmpeg", "ffmpeg", "-hide_banner", "-i", enc->input_file, "-vf", vf, "-pix_fmt", "rgb24", "-f", "rawvideo", "-an", "-v", "warning", "-", (char*)NULL); fprintf(stderr, "Error: Failed to execute FFmpeg\n"); exit(1); } close(pipefd[1]); return fdopen(pipefd[0], "rb"); } static FILE *spawn_ffmpeg_audio(dt_encoder_t *enc, pid_t *pid) { int pipefd[2]; if (pipe(pipefd) < 0) { fprintf(stderr, "Error: Failed to create pipe\n"); return NULL; } *pid = fork(); if (*pid < 0) { fprintf(stderr, "Error: Failed to fork\n"); close(pipefd[0]); close(pipefd[1]); return NULL; } if (*pid == 0) { // Child process close(pipefd[0]); dup2(pipefd[1], STDOUT_FILENO); close(pipefd[1]); execlp("ffmpeg", "ffmpeg", "-i", enc->input_file, "-f", "f32le", "-acodec", "pcm_f32le", "-ar", "32000", "-ac", "2", "-vn", "-v", "warning", "-", (char*)NULL); fprintf(stderr, "Error: Failed to execute FFmpeg\n"); exit(1); } close(pipefd[1]); return fdopen(pipefd[0], "rb"); } // ============================================================================= // Multithreading Support // ============================================================================= /** * Worker thread context - passed to worker_thread_main. */ typedef struct { dt_encoder_t *enc; int thread_id; } worker_context_t; /** * Worker thread main function. * Continuously picks up jobs from the job pool and encodes them. */ static void *worker_thread_main(void *arg) { worker_context_t *wctx = (worker_context_t *)arg; dt_encoder_t *enc = wctx->enc; (void)wctx->thread_id; // Unused but kept for debugging while (1) { pthread_mutex_lock(&enc->job_mutex); // Wait for a job or shutdown signal while (!enc->shutdown_workers) { // Look for a job slot that is ready to encode int found_job = -1; for (int i = 0; i < enc->num_threads; i++) { if (enc->gop_jobs[i].status == GOP_SLOT_READY) { enc->gop_jobs[i].status = GOP_SLOT_ENCODING; found_job = i; break; } } if (found_job >= 0) { pthread_mutex_unlock(&enc->job_mutex); // Encode this GOP gop_job_t *job = &enc->gop_jobs[found_job]; // Create thread-local encoder context tav_encoder_context_t *ctx = tav_encoder_create(&job->params); if (!ctx) { fprintf(stderr, "Failed to create encoder for GOP %d\n", job->gop_index); job->success = 0; } else { // Encode video GOP int result = tav_encoder_encode_gop(ctx, (const uint8_t **)job->rgb_frames, job->num_frames, job->frame_numbers, &job->packet); job->success = (result >= 0 && job->packet != NULL); // Encode audio if (job->success && job->audio_sample_count > 0) { int max_index = tad32_quality_to_max_index(enc->quality_index); job->tad_size = tad32_encode_chunk(job->audio_samples, job->audio_sample_count, max_index, 1.0f, enc->enc_params.zstd_level, job->tad_output); } tav_encoder_free(ctx); } // Mark job as complete (reacquire lock for next iteration) pthread_mutex_lock(&enc->job_mutex); job->status = GOP_SLOT_COMPLETE; pthread_cond_broadcast(&enc->job_complete); // Keep lock held for next iteration of inner while loop continue; // Look for more jobs } // No job found, wait for signal pthread_cond_wait(&enc->job_ready, &enc->job_mutex); } pthread_mutex_unlock(&enc->job_mutex); break; // Shutdown } free(wctx); return NULL; } /** * Initialize multithreading resources. * Returns 0 on success, -1 on failure. */ static int init_threading(dt_encoder_t *enc) { if (enc->num_threads <= 0) { return 0; // Single-threaded mode } // Initialize mutex and condition variables if (pthread_mutex_init(&enc->job_mutex, NULL) != 0) { fprintf(stderr, "Error: Failed to initialize job mutex\n"); return -1; } if (pthread_cond_init(&enc->job_ready, NULL) != 0) { fprintf(stderr, "Error: Failed to initialize job_ready cond\n"); pthread_mutex_destroy(&enc->job_mutex); return -1; } if (pthread_cond_init(&enc->job_complete, NULL) != 0) { fprintf(stderr, "Error: Failed to initialize job_complete cond\n"); pthread_cond_destroy(&enc->job_ready); pthread_mutex_destroy(&enc->job_mutex); return -1; } // Allocate job slots (one per thread) enc->gop_jobs = calloc(enc->num_threads, sizeof(gop_job_t)); if (!enc->gop_jobs) { fprintf(stderr, "Error: Failed to allocate job slots\n"); pthread_cond_destroy(&enc->job_complete); pthread_cond_destroy(&enc->job_ready); pthread_mutex_destroy(&enc->job_mutex); return -1; } // Allocate worker thread handles enc->worker_threads = malloc(enc->num_threads * sizeof(pthread_t)); if (!enc->worker_threads) { fprintf(stderr, "Error: Failed to allocate thread handles\n"); free(enc->gop_jobs); pthread_cond_destroy(&enc->job_complete); pthread_cond_destroy(&enc->job_ready); pthread_mutex_destroy(&enc->job_mutex); return -1; } // Start worker threads enc->shutdown_workers = 0; for (int i = 0; i < enc->num_threads; i++) { worker_context_t *wctx = malloc(sizeof(worker_context_t)); if (!wctx) { fprintf(stderr, "Error: Failed to allocate worker context\n"); enc->shutdown_workers = 1; pthread_cond_broadcast(&enc->job_ready); for (int j = 0; j < i; j++) { pthread_join(enc->worker_threads[j], NULL); } free(enc->worker_threads); free(enc->gop_jobs); pthread_cond_destroy(&enc->job_complete); pthread_cond_destroy(&enc->job_ready); pthread_mutex_destroy(&enc->job_mutex); return -1; } wctx->enc = enc; wctx->thread_id = i; if (pthread_create(&enc->worker_threads[i], NULL, worker_thread_main, wctx) != 0) { fprintf(stderr, "Error: Failed to create worker thread %d\n", i); free(wctx); enc->shutdown_workers = 1; pthread_cond_broadcast(&enc->job_ready); for (int j = 0; j < i; j++) { pthread_join(enc->worker_threads[j], NULL); } free(enc->worker_threads); free(enc->gop_jobs); pthread_cond_destroy(&enc->job_complete); pthread_cond_destroy(&enc->job_ready); pthread_mutex_destroy(&enc->job_mutex); return -1; } } printf("Started %d worker threads for parallel GOP encoding\n", enc->num_threads); return 0; } /** * Shutdown multithreading resources. */ static void shutdown_threading(dt_encoder_t *enc) { if (enc->num_threads <= 0) { return; } // Signal workers to shutdown pthread_mutex_lock(&enc->job_mutex); enc->shutdown_workers = 1; pthread_cond_broadcast(&enc->job_ready); pthread_mutex_unlock(&enc->job_mutex); // Wait for all workers to finish for (int i = 0; i < enc->num_threads; i++) { pthread_join(enc->worker_threads[i], NULL); } // Free job slots (and any remaining resources) if (enc->gop_jobs) { for (int i = 0; i < enc->num_threads; i++) { if (enc->gop_jobs[i].packet) { tav_encoder_free_packet(enc->gop_jobs[i].packet); } } free(enc->gop_jobs); enc->gop_jobs = NULL; } if (enc->worker_threads) { free(enc->worker_threads); enc->worker_threads = NULL; } pthread_cond_destroy(&enc->job_complete); pthread_cond_destroy(&enc->job_ready); pthread_mutex_destroy(&enc->job_mutex); } // ============================================================================= // Main Encoding Loop // ============================================================================= // Single-threaded encoding loop static int run_encoder_st(dt_encoder_t *enc, FILE *video_pipe, FILE *audio_pipe, pid_t video_pid __attribute__((unused)), pid_t audio_pid __attribute__((unused))) { size_t frame_size = enc->width * enc->height * 3; double gop_duration = (double)DT_GOP_SIZE * enc->fps_den / enc->fps_num; size_t audio_samples_per_gop = (size_t)(AUDIO_SAMPLE_RATE * gop_duration) + 1024; // TAD output buffer size_t tad_buffer_size = audio_samples_per_gop * 2; uint8_t *tad_output = malloc(tad_buffer_size); enc->frame_number = 0; enc->gop_frame_count = 0; enc->current_timecode_ns = 0; clock_t start_time = clock(); while (1) { if (enc->encode_limit > 0 && enc->frame_number >= enc->encode_limit) { break; } size_t bytes_read = fread(enc->gop_frames[enc->gop_frame_count], 1, frame_size, video_pipe); if (bytes_read < frame_size) { break; } enc->gop_frame_count++; enc->frame_number++; // Read corresponding audio double frame_duration = (double)enc->fps_den / enc->fps_num; size_t audio_samples_per_frame = (size_t)(AUDIO_SAMPLE_RATE * frame_duration); size_t audio_bytes = audio_samples_per_frame * 2 * sizeof(float); if (enc->audio_buffer_samples + audio_samples_per_frame > enc->audio_buffer_capacity) { size_t new_capacity = enc->audio_buffer_capacity * 2; float *new_buffer = realloc(enc->audio_buffer, new_capacity * 2 * sizeof(float)); if (new_buffer) { enc->audio_buffer = new_buffer; enc->audio_buffer_capacity = new_capacity; } } size_t audio_read = fread(enc->audio_buffer + enc->audio_buffer_samples * 2, 1, audio_bytes, audio_pipe); enc->audio_buffer_samples += audio_read / (2 * sizeof(float)); // Encode GOP when full if (enc->gop_frame_count >= DT_GOP_SIZE) { tav_encoder_packet_t *video_packet = NULL; int frame_numbers[DT_GOP_SIZE]; for (int i = 0; i < DT_GOP_SIZE; i++) { frame_numbers[i] = enc->frame_number - DT_GOP_SIZE + i; } int result = tav_encoder_encode_gop(enc->video_ctx, (const uint8_t **)enc->gop_frames, DT_GOP_SIZE, frame_numbers, &video_packet); if (result < 0 || !video_packet) { fprintf(stderr, "Error: Video encoding failed\n"); break; } int max_index = tad32_quality_to_max_index(enc->quality_index); size_t tad_size = tad32_encode_chunk(enc->audio_buffer, enc->audio_buffer_samples, max_index, 1.0f, enc->enc_params.zstd_level, tad_output); write_packet(enc, enc->current_timecode_ns, tad_output, tad_size, video_packet->data, video_packet->size, DT_GOP_SIZE, (uint16_t)enc->audio_buffer_samples, max_index); enc->current_timecode_ns += (uint64_t)(gop_duration * 1e9); enc->frames_encoded += DT_GOP_SIZE; enc->gop_frame_count = 0; enc->audio_buffer_samples = 0; tav_encoder_free_packet(video_packet); // Display progress clock_t now = clock(); double elapsed = (double)(now - start_time) / CLOCKS_PER_SEC; double fps = elapsed > 0 ? (double)enc->frame_number / elapsed : 0.0; double duration = (double)enc->frame_number * enc->fps_den / enc->fps_num; double bitrate = duration > 0 ? (ftell(enc->output_fp) * 8.0) / duration / 1000.0 : 0.0; long gop_count = enc->frame_number / DT_GOP_SIZE; size_t total_kb = ftell(enc->output_fp) / 1024; printf("\rFrame %d | GOPs: %ld | %.1f fps | %.1f kbps | %zu KB ", enc->frame_number, gop_count, fps, bitrate, total_kb); fflush(stdout); } } // Handle partial final GOP if (enc->gop_frame_count > 0) { tav_encoder_packet_t *video_packet = NULL; int *frame_numbers = malloc(enc->gop_frame_count * sizeof(int)); for (int i = 0; i < enc->gop_frame_count; i++) { frame_numbers[i] = enc->frame_number - enc->gop_frame_count + i; } int result = tav_encoder_encode_gop(enc->video_ctx, (const uint8_t **)enc->gop_frames, enc->gop_frame_count, frame_numbers, &video_packet); if (result >= 0 && video_packet) { int max_index = tad32_quality_to_max_index(enc->quality_index); size_t tad_size = tad32_encode_chunk(enc->audio_buffer, enc->audio_buffer_samples, max_index, 1.0f, enc->enc_params.zstd_level, tad_output); write_packet(enc, enc->current_timecode_ns, tad_output, tad_size, video_packet->data, video_packet->size, enc->gop_frame_count, (uint16_t)enc->audio_buffer_samples, max_index); enc->frames_encoded += enc->gop_frame_count; tav_encoder_free_packet(video_packet); } free(frame_numbers); } free(tad_output); return 0; } // Multithreaded encoding loop static int run_encoder_mt(dt_encoder_t *enc, FILE *video_pipe, FILE *audio_pipe, pid_t video_pid __attribute__((unused)), pid_t audio_pid __attribute__((unused))) { size_t frame_size = enc->width * enc->height * 3; double gop_duration = (double)DT_GOP_SIZE * enc->fps_den / enc->fps_num; // Calculate audio buffer size with generous padding to handle FFmpeg's audio delivery // FFmpeg may deliver all audio for a GOP in the first read, so we need space for: // 1. The expected GOP audio: AUDIO_SAMPLE_RATE * gop_duration // 2. Worst-case per-frame variations: DT_GOP_SIZE * samples_per_frame size_t expected_samples = (size_t)(AUDIO_SAMPLE_RATE * gop_duration); size_t samples_per_frame = (size_t)(AUDIO_SAMPLE_RATE * enc->fps_den / enc->fps_num) + 1; size_t audio_samples_per_gop = expected_samples + (DT_GOP_SIZE * samples_per_frame); size_t tad_buffer_size = audio_samples_per_gop * 2; // Initialize threading if (init_threading(enc) < 0) { return -1; } // Allocate per-slot frame buffers and audio buffers for (int slot = 0; slot < enc->num_threads; slot++) { enc->gop_jobs[slot].rgb_frames = malloc(DT_GOP_SIZE * sizeof(uint8_t*)); enc->gop_jobs[slot].frame_numbers = malloc(DT_GOP_SIZE * sizeof(int)); enc->gop_jobs[slot].audio_samples = malloc(audio_samples_per_gop * 2 * sizeof(float)); enc->gop_jobs[slot].tad_output = malloc(tad_buffer_size); if (!enc->gop_jobs[slot].rgb_frames || !enc->gop_jobs[slot].frame_numbers || !enc->gop_jobs[slot].audio_samples || !enc->gop_jobs[slot].tad_output) { fprintf(stderr, "Error: Failed to allocate job slot %d buffers\n", slot); shutdown_threading(enc); return -1; } for (int f = 0; f < DT_GOP_SIZE; f++) { enc->gop_jobs[slot].rgb_frames[f] = malloc(frame_size); if (!enc->gop_jobs[slot].rgb_frames[f]) { fprintf(stderr, "Error: Failed to allocate frame buffer for slot %d\n", slot); shutdown_threading(enc); return -1; } } // Copy encoder params for thread safety enc->gop_jobs[slot].params = enc->enc_params; enc->gop_jobs[slot].status = GOP_SLOT_EMPTY; enc->gop_jobs[slot].num_frames = 0; enc->gop_jobs[slot].audio_sample_count = 0; enc->gop_jobs[slot].tad_size = 0; enc->gop_jobs[slot].packet = NULL; enc->gop_jobs[slot].success = 0; } printf("Encoding frames with %d threads...\n", enc->num_threads); clock_t start_time = clock(); int current_slot = 0; int next_gop_to_write = 0; int current_gop_index = 0; int frames_in_current_gop = 0; int encoding_error = 0; int eof_reached = 0; enc->current_timecode_ns = 0; while (!encoding_error && !eof_reached) { // Step 1: Try to write any completed GOPs in order pthread_mutex_lock(&enc->job_mutex); while (!encoding_error) { int found = -1; for (int i = 0; i < enc->num_threads; i++) { if (enc->gop_jobs[i].status == GOP_SLOT_COMPLETE && enc->gop_jobs[i].gop_index == next_gop_to_write) { found = i; break; } } if (found < 0) break; gop_job_t *job = &enc->gop_jobs[found]; pthread_mutex_unlock(&enc->job_mutex); // Write this GOP if (job->success && job->packet) { int max_index = tad32_quality_to_max_index(enc->quality_index); write_packet(enc, enc->current_timecode_ns, job->tad_output, job->tad_size, job->packet->data, job->packet->size, job->num_frames, (uint16_t)job->audio_sample_count, max_index); enc->current_timecode_ns += (uint64_t)(gop_duration * 1e9); enc->frames_encoded += job->num_frames; tav_encoder_free_packet(job->packet); job->packet = NULL; // Display progress clock_t now = clock(); double elapsed = (double)(now - start_time) / CLOCKS_PER_SEC; double fps = elapsed > 0 ? (double)enc->frames_encoded / elapsed : 0.0; double duration = (double)enc->frames_encoded * enc->fps_den / enc->fps_num; double bitrate = duration > 0 ? (ftell(enc->output_fp) * 8.0) / duration / 1000.0 : 0.0; long gop_count = enc->frames_encoded / DT_GOP_SIZE; size_t total_kb = ftell(enc->output_fp) / 1024; printf("\rFrame %lu | GOPs: %ld | %.1f fps | %.1f kbps | %zu KB ", enc->frames_encoded, gop_count, fps, bitrate, total_kb); fflush(stdout); } pthread_mutex_lock(&enc->job_mutex); job->status = GOP_SLOT_EMPTY; job->num_frames = 0; job->audio_sample_count = 0; job->tad_size = 0; next_gop_to_write++; } pthread_mutex_unlock(&enc->job_mutex); if (encoding_error || eof_reached) break; // Step 2: Fill current slot with frames gop_job_t *slot = &enc->gop_jobs[current_slot]; // Wait for slot to be empty pthread_mutex_lock(&enc->job_mutex); while (slot->status != GOP_SLOT_EMPTY && !enc->shutdown_workers) { // While waiting, check if we can write any completed GOPs int wrote_something = 0; for (int i = 0; i < enc->num_threads; i++) { if (enc->gop_jobs[i].status == GOP_SLOT_COMPLETE && enc->gop_jobs[i].gop_index == next_gop_to_write) { gop_job_t *job = &enc->gop_jobs[i]; pthread_mutex_unlock(&enc->job_mutex); if (job->success && job->packet) { int max_index = tad32_quality_to_max_index(enc->quality_index); write_packet(enc, enc->current_timecode_ns, job->tad_output, job->tad_size, job->packet->data, job->packet->size, job->num_frames, (uint16_t)job->audio_sample_count, max_index); enc->current_timecode_ns += (uint64_t)(gop_duration * 1e9); enc->frames_encoded += job->num_frames; tav_encoder_free_packet(job->packet); job->packet = NULL; } pthread_mutex_lock(&enc->job_mutex); job->status = GOP_SLOT_EMPTY; job->num_frames = 0; job->audio_sample_count = 0; job->tad_size = 0; next_gop_to_write++; wrote_something = 1; break; } } if (!wrote_something) { pthread_cond_wait(&enc->job_complete, &enc->job_mutex); } } pthread_mutex_unlock(&enc->job_mutex); // Reset audio accumulator only when starting a fresh GOP if (frames_in_current_gop == 0) { slot->audio_sample_count = 0; } // Read frames into the slot while (frames_in_current_gop < DT_GOP_SIZE && !eof_reached) { if (enc->encode_limit > 0 && enc->frame_number >= enc->encode_limit) { eof_reached = 1; break; } size_t bytes_read = fread(slot->rgb_frames[frames_in_current_gop], 1, frame_size, video_pipe); if (bytes_read < frame_size) { eof_reached = 1; break; } slot->frame_numbers[frames_in_current_gop] = enc->frame_number; enc->frame_number++; frames_in_current_gop++; // Read corresponding audio - read whatever is available up to buffer capacity // Note: FFmpeg may buffer audio, so the first read might get multiple frames worth size_t audio_buffer_capacity_samples = audio_samples_per_gop; size_t audio_space_remaining = audio_buffer_capacity_samples - slot->audio_sample_count; if (audio_space_remaining > 0) { // Read up to the remaining buffer space size_t max_read_bytes = audio_space_remaining * 2 * sizeof(float); size_t audio_read = fread(slot->audio_samples + slot->audio_sample_count * 2, 1, max_read_bytes, audio_pipe); slot->audio_sample_count += audio_read / (2 * sizeof(float)); } // Submit GOP when full if (frames_in_current_gop >= DT_GOP_SIZE) { slot->num_frames = frames_in_current_gop; slot->gop_index = current_gop_index; pthread_mutex_lock(&enc->job_mutex); slot->status = GOP_SLOT_READY; pthread_cond_broadcast(&enc->job_ready); pthread_mutex_unlock(&enc->job_mutex); current_slot = (current_slot + 1) % enc->num_threads; current_gop_index++; frames_in_current_gop = 0; break; // Exit frame-reading loop to wait for next available slot } } } // Submit any partial GOP at EOF if (frames_in_current_gop > 0) { gop_job_t *slot = &enc->gop_jobs[current_slot]; slot->num_frames = frames_in_current_gop; slot->gop_index = current_gop_index; pthread_mutex_lock(&enc->job_mutex); slot->status = GOP_SLOT_READY; pthread_cond_broadcast(&enc->job_ready); pthread_mutex_unlock(&enc->job_mutex); current_gop_index++; } // Wait for all remaining GOPs to complete and write them while (!encoding_error && next_gop_to_write < current_gop_index) { pthread_mutex_lock(&enc->job_mutex); int found = -1; while (found < 0 && !encoding_error) { for (int i = 0; i < enc->num_threads; i++) { if (enc->gop_jobs[i].status == GOP_SLOT_COMPLETE && enc->gop_jobs[i].gop_index == next_gop_to_write) { found = i; break; } } if (found < 0) { pthread_cond_wait(&enc->job_complete, &enc->job_mutex); } } if (found >= 0) { gop_job_t *job = &enc->gop_jobs[found]; pthread_mutex_unlock(&enc->job_mutex); if (job->success && job->packet) { int max_index = tad32_quality_to_max_index(enc->quality_index); write_packet(enc, enc->current_timecode_ns, job->tad_output, job->tad_size, job->packet->data, job->packet->size, job->num_frames, (uint16_t)job->audio_sample_count, max_index); enc->current_timecode_ns += (uint64_t)(gop_duration * 1e9); enc->frames_encoded += job->num_frames; tav_encoder_free_packet(job->packet); job->packet = NULL; } pthread_mutex_lock(&enc->job_mutex); job->status = GOP_SLOT_EMPTY; job->num_frames = 0; job->audio_sample_count = 0; job->tad_size = 0; next_gop_to_write++; pthread_mutex_unlock(&enc->job_mutex); } else { pthread_mutex_unlock(&enc->job_mutex); } } // Free per-slot buffers before shutdown for (int slot = 0; slot < enc->num_threads; slot++) { if (enc->gop_jobs[slot].rgb_frames) { for (int f = 0; f < DT_GOP_SIZE; f++) { free(enc->gop_jobs[slot].rgb_frames[f]); } free(enc->gop_jobs[slot].rgb_frames); } free(enc->gop_jobs[slot].frame_numbers); free(enc->gop_jobs[slot].audio_samples); free(enc->gop_jobs[slot].tad_output); } shutdown_threading(enc); return encoding_error ? -1 : 0; } static int run_encoder(dt_encoder_t *enc) { // Open output file enc->output_fp = fopen(enc->output_file, "wb"); if (!enc->output_fp) { fprintf(stderr, "Error: Cannot create output file: %s\n", enc->output_file); return -1; } // Set up video encoder params tav_encoder_params_init(&enc->enc_params, enc->width, enc->height); enc->enc_params.fps_num = enc->fps_num; enc->enc_params.fps_den = enc->fps_den; enc->enc_params.wavelet_type = 1; // CDF 9/7 enc->enc_params.temporal_wavelet = 255; // Haar enc->enc_params.decomp_levels = DT_SPATIAL_LEVELS; enc->enc_params.temporal_levels = DT_TEMPORAL_LEVELS; enc->enc_params.enable_temporal_dwt = 1; enc->enc_params.gop_size = DT_GOP_SIZE; enc->enc_params.quality_level = enc->quality_index; enc->enc_params.quantiser_y = QUALITY_Y[enc->quality_index]; enc->enc_params.quantiser_co = QUALITY_CO[enc->quality_index]; enc->enc_params.quantiser_cg = QUALITY_CG[enc->quality_index]; enc->enc_params.entropy_coder = 1; // EZBC enc->enc_params.encoder_preset = 0x01; // Sports mode enc->enc_params.monoblock = 1; // Force monoblock enc->enc_params.verbose = enc->verbose; enc->enc_params.zstd_level = -1; // disable Zstd // For single-threaded mode, create a context to validate params enc->video_ctx = tav_encoder_create(&enc->enc_params); if (!enc->video_ctx) { fprintf(stderr, "Error: Cannot create video encoder\n"); fclose(enc->output_fp); return -1; } printf("Forced Monoblock mode (--monoblock)\n"); // Get actual parameters (may have been adjusted) tav_encoder_get_params(enc->video_ctx, &enc->enc_params); if (enc->verbose) { printf("Auto-selected Haar temporal wavelet with sports mode (resolution: %dx%d = %d pixels, quantiser_y = %d)\n", enc->width, enc->height, enc->width * enc->height, enc->enc_params.quantiser_y); } // Spawn FFmpeg for video pid_t video_pid; FILE *video_pipe = spawn_ffmpeg_video(enc, &video_pid); if (!video_pipe) { tav_encoder_free(enc->video_ctx); fclose(enc->output_fp); return -1; } // Spawn FFmpeg for audio pid_t audio_pid; FILE *audio_pipe = spawn_ffmpeg_audio(enc, &audio_pid); if (!audio_pipe) { fclose(video_pipe); waitpid(video_pid, NULL, 0); tav_encoder_free(enc->video_ctx); fclose(enc->output_fp); return -1; } // Allocate frame buffers for single-threaded mode size_t frame_size = enc->width * enc->height * 3; enc->gop_frames = malloc(DT_GOP_SIZE * sizeof(uint8_t *)); for (int i = 0; i < DT_GOP_SIZE; i++) { enc->gop_frames[i] = malloc(frame_size); } // Audio buffer (enough for one GOP worth of audio) double gop_duration = (double)DT_GOP_SIZE * enc->fps_den / enc->fps_num; size_t audio_samples_per_gop = (size_t)(AUDIO_SAMPLE_RATE * gop_duration) + 1024; enc->audio_buffer = malloc(audio_samples_per_gop * 2 * sizeof(float)); enc->audio_buffer_capacity = audio_samples_per_gop; enc->audio_buffer_samples = 0; clock_t start_time = clock(); // Run encoding if (enc->num_threads > 0) { printf("Multithreaded mode: %d threads\n", enc->num_threads); run_encoder_mt(enc, video_pipe, audio_pipe, video_pid, audio_pid); } else { printf("Single-threaded mode\n"); run_encoder_st(enc, video_pipe, audio_pipe, video_pid, audio_pid); } clock_t end_time = clock(); double elapsed = (double)(end_time - start_time) / CLOCKS_PER_SEC; // Print statistics printf("\nEncoding complete%s:\n", enc->num_threads > 0 ? " (multithreaded)" : ""); printf(" Frames: %lu\n", enc->frames_encoded); printf(" GOPs: %lu\n", enc->packets_written); printf(" Output size: %lu bytes (%.2f MB)\n", enc->bytes_written, enc->bytes_written / 1048576.0); printf(" Encoding speed: %.1f fps\n", enc->frames_encoded / elapsed); if (enc->frames_encoded > 0) { printf(" Bitrate: %.1f kbps\n", enc->bytes_written * 8.0 / (enc->frames_encoded * enc->fps_den / enc->fps_num) / 1000.0); } // Cleanup free(enc->audio_buffer); for (int i = 0; i < DT_GOP_SIZE; i++) { free(enc->gop_frames[i]); } free(enc->gop_frames); fclose(video_pipe); fclose(audio_pipe); waitpid(video_pid, NULL, 0); waitpid(audio_pid, NULL, 0); tav_encoder_free(enc->video_ctx); fclose(enc->output_fp); return 0; } // ============================================================================= // Main // ============================================================================= int main(int argc, char **argv) { dt_encoder_t enc; memset(&enc, 0, sizeof(enc)); // Defaults enc.width = DT_WIDTH; enc.height = DT_HEIGHT_NTSC; enc.fps_num = 24; enc.fps_den = 1; enc.quality_index = 3; enc.is_pal = 0; enc.is_interlaced = 0; enc.num_threads = get_default_thread_count(); // Default: min(8, available CPUs) // Initialize FEC libraries rs_init(); ldpc_init(); ldpc_p_init(); // LDPC payload codec static struct option long_options[] = { {"input", required_argument, 0, 'i'}, {"output", required_argument, 0, 'o'}, {"quality", required_argument, 0, 'q'}, {"fps", required_argument, 0, 'f'}, {"threads", required_argument, 0, 't'}, {"ntsc", no_argument, 0, 'N'}, {"pal", no_argument, 0, 'P'}, {"interlaced", no_argument, 0, 'I'}, {"ldpc-payload", no_argument, 0, 'D'}, {"encode-limit", required_argument, 0, 'L'}, {"verbose", no_argument, 0, 'v'}, {"help", no_argument, 0, 'h'}, {0, 0, 0, 0} }; int opt; while ((opt = getopt_long(argc, argv, "i:o:q:f:t:vhNPI", long_options, NULL)) != -1) { switch (opt) { case 'i': enc.input_file = optarg; break; case 'o': enc.output_file = optarg; break; case 'q': enc.quality_index = atoi(optarg); if (enc.quality_index < 0) enc.quality_index = 0; if (enc.quality_index > 5) enc.quality_index = 5; 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': { int threads = atoi(optarg); if (threads < 0) { fprintf(stderr, "Error: Thread count must be positive\n"); return 1; } // Both 0 and 1 mean single-threaded (use value 0 internally) enc.num_threads = (threads <= 1) ? 0 : threads; if (enc.num_threads > 16) enc.num_threads = 16; // Cap at 16 break; } case 'N': enc.is_pal = 0; enc.height = DT_HEIGHT_NTSC; break; case 'P': enc.is_pal = 1; enc.height = DT_HEIGHT_PAL; break; case 'I': enc.is_interlaced = 1; break; case 'D': enc.fec_mode = FEC_MODE_LDPC; break; case 'L': enc.encode_limit = atoi(optarg); break; case 'v': enc.verbose = 1; break; case 'h': print_usage(argv[0]); return 0; default: print_usage(argv[0]); return 1; } } if (!enc.input_file || !enc.output_file) { fprintf(stderr, "Error: Input and output files are required\n"); print_usage(argv[0]); return 1; } // Probe input file for framerate (always probe to get original fps) enc.original_fps_num = 24; enc.original_fps_den = 1; char probe_cmd[4096]; 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'", enc.input_file); FILE *probe = popen(probe_cmd, "r"); if (probe) { char line[256]; if (fgets(line, sizeof(line), probe)) { if (sscanf(line, "%d/%d", &enc.original_fps_num, &enc.original_fps_den) != 2) { enc.original_fps_num = 24; enc.original_fps_den = 1; } } pclose(probe); } // If user didn't specify target fps, use probed fps if (enc.target_fps_num == 0) { enc.fps_num = enc.original_fps_num; enc.fps_den = enc.original_fps_den; } printf("\nTAV-DT Encoder (Revised Spec 2025-12-11)\n"); printf(" Format: %s %s\n", enc.is_pal ? "PAL" : "NTSC", enc.is_interlaced ? "interlaced" : "progressive"); printf(" Resolution: %dx%d (internal: %dx%d)\n", enc.width, enc.height, enc.width, enc.is_interlaced ? enc.height / 2 : enc.height); printf(" Source framerate: %d/%d\n", enc.original_fps_num, enc.original_fps_den); // Report fps conversion if enabled if (enc.target_fps_num > 0) { long long target_rate = (long long)enc.target_fps_num * enc.original_fps_den; long long source_rate = (long long)enc.original_fps_num * enc.target_fps_den; if (target_rate > source_rate) { printf(" Framerate conversion: %d/%d -> %d/%d (minterpolate)\n", enc.original_fps_num, enc.original_fps_den, enc.target_fps_num, enc.target_fps_den); } else if (target_rate < source_rate) { printf(" Framerate conversion: %d/%d -> %d/%d (fps)\n", enc.original_fps_num, enc.original_fps_den, enc.target_fps_num, enc.target_fps_den); } // If equal, no conversion message needed } printf(" Quality: %d\n", enc.quality_index); 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(" Threads: %d%s\n", enc.num_threads > 0 ? enc.num_threads : 1, enc.num_threads > 0 ? " (multithreaded)" : " (single-threaded)"); printf(" Header sizes: main=%dB tad=%dB tav=%dB (after LDPC)\n", DT_MAIN_HEADER_SIZE * 2, DT_TAD_HEADER_SIZE * 2, DT_TAV_HEADER_SIZE * 2); return run_encoder(&enc); }