/* encoder_tav_text.c Text-based video encoder for TSVM using custom font ROMs Outputs Videotex files with custom header and packet type 0x3F (text mode) File structure: - Videotex header (32 bytes): magic "\x1FTSVM-VT", version, grid dims, fps, total_frames - Extended header packet (0xEF): BGNT, ENDT, CDAT, VNDR, FMPG - Font ROM packets (0x30): lowrom and highrom (1920 bytes each) - Per-frame sequence: [audio 0x20], [timecode 0xFD], [videotex 0x3F], [sync 0xFF] Videotex packet structure (0x3F): Zstd([rows][cols][fg-array][bg-array][char-array]) - rows: uint8 (32) - cols: uint8 (80) - fg-array: rows*cols bytes (foreground colors, 0xF0=black, 0xFE=white) - bg-array: rows*cols bytes (background colors, 0xF0=black, 0xFE=white) - char-array: rows*cols bytes (glyph indices 0-255) Total uncompressed size: 2 + (80*32*3) = 7682 bytes Separated arrays compress much better (fg/bg are just 0xF0/0xFE runs) Video size: 80×32 characters (560×448 pixels with 7×14 font) Audio: MP2 encoding at 96 kbps, 32 KHz stereo (packet 0x20) Each text frame is treated as an I-frame with sync packet Usage: gcc -O3 -std=c11 -Wall encoder_tav_text.c -o encoder_tav_text -lm -lzstd ./encoder_tav_text -i video.mp4 -f font.chr -o output.vtx */ #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #define ENCODER_VENDOR_STRING "Encoder-TAV-Text 20251121 (videotex)" #define CHAR_W 7 #define CHAR_H 14 #define GRID_W 80 #define GRID_H 32 #define PIXEL_W (GRID_W * CHAR_W) // 560 #define PIXEL_H (GRID_H * CHAR_H) // 448 #define PATCH_SZ (CHAR_W * CHAR_H) #define SAMPLE_RATE 32000 // TAV packet types #define PACKET_TIMECODE 0xFD #define PACKET_SYNC 0xFF #define PACKET_AUDIO_MP2 0x20 #define PACKET_SSF 0x30 #define PACKET_TEXT 0x3F #define PACKET_EXTENDED_HDR 0xEF // SSF opcodes for font ROM #define SSF_OPCODE_LOWROM 0x80 #define SSF_OPCODE_HIGHROM 0x81 // Font ROM size constants #define FONTROM_PADDED_SIZE 1920 #define GLYPHS_PER_ROM 128 // Color mapping (4-bit RGB to TSVM palette) #define COLOR_BLACK 0xF0 #define COLOR_WHITE 0xFE typedef struct { uint8_t *data; // Binary glyph data (PATCH_SZ bytes per glyph) int count; // Number of glyphs } FontROM; // Get FFmpeg version string char *get_ffmpeg_version(void) { FILE *pipe = popen("ffmpeg -version 2>&1 | head -1", "r"); if (!pipe) return NULL; char *version = malloc(256); if (!version) { pclose(pipe); return NULL; } if (fgets(version, 256, pipe)) { // Remove trailing newline size_t len = strlen(version); if (len > 0 && version[len - 1] == '\n') { version[len - 1] = '\0'; } pclose(pipe); return version; } free(version); pclose(pipe); return NULL; } // Detect video FPS using ffprobe float detect_fps(const char *video_path) { char cmd[1024]; snprintf(cmd, sizeof(cmd), "ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate " "-of default=noprint_wrappers=1:nokey=1 \"%s\" 2>/dev/null", video_path); FILE *pipe = popen(cmd, "r"); if (!pipe) return 30.0f; // fallback char fps_str[64] = {0}; if (fgets(fps_str, sizeof(fps_str), pipe)) { // Parse fraction like "30/1" or "24000/1001" int num = 0, den = 1; if (sscanf(fps_str, "%d/%d", &num, &den) == 2 && den > 0) { pclose(pipe); return (float)num / (float)den; } } pclose(pipe); return 30.0f; // fallback } // Load font ROM (14 bytes per glyph, no header) FontROM *load_font_rom(const char *path) { FILE *f = fopen(path, "rb"); if (!f) return NULL; fseek(f, 0, SEEK_END); long size = ftell(f); fseek(f, 0, SEEK_SET); if (size % 14 != 0) { fprintf(stderr, "Warning: ROM size not divisible by 14 (got %ld bytes)\n", size); } int glyph_count = size / 14; FontROM *rom = malloc(sizeof(FontROM)); rom->count = glyph_count; rom->data = malloc(glyph_count * PATCH_SZ); // Read and unpack glyphs for (int g = 0; g < glyph_count; g++) { uint8_t row_bytes[14]; if (fread(row_bytes, 14, 1, f) != 1) { free(rom->data); free(rom); fclose(f); return NULL; } // Unpack bits to binary pixels for (int row = 0; row < CHAR_H; row++) { for (int col = 0; col < CHAR_W; col++) { // Bit 6 = leftmost, bit 0 = rightmost int bit = (row_bytes[row] >> (6 - col)) & 1; rom->data[g * PATCH_SZ + row * CHAR_W + col] = bit; } } } fclose(f); fprintf(stderr, "Loaded font ROM: %d glyphs\n", glyph_count); return rom; } // Find best matching glyph for a grayscale patch int find_best_glyph(const uint8_t *patch, const FontROM *rom, uint8_t *out_bg, uint8_t *out_fg) { // Try both normal and inverted matching int best_glyph = 0; float best_error = INFINITY; uint8_t best_bg = COLOR_BLACK, best_fg = COLOR_WHITE; for (int g = 0; g < rom->count; g++) { const uint8_t *glyph = &rom->data[g * PATCH_SZ]; // Try normal: glyph 1 = fg, glyph 0 = bg float err_normal = 0; for (int i = 0; i < PATCH_SZ; i++) { int expected = glyph[i] ? 255 : 0; int diff = patch[i] - expected; err_normal += diff * diff; } // Try inverted: glyph 0 = fg, glyph 1 = bg float err_inverted = 0; for (int i = 0; i < PATCH_SZ; i++) { int expected = glyph[i] ? 0 : 255; int diff = patch[i] - expected; err_inverted += diff * diff; } if (err_normal < best_error) { best_error = err_normal; best_glyph = g; best_bg = COLOR_BLACK; best_fg = COLOR_WHITE; } if (err_inverted < best_error) { best_error = err_inverted; best_glyph = g; best_bg = COLOR_WHITE; best_fg = COLOR_BLACK; } } *out_bg = best_bg; *out_fg = best_fg; return best_glyph; } // Convert frame to text mode void frame_to_text(const uint8_t *pixels, const FontROM *rom, uint8_t *bg_col, uint8_t *fg_col, uint8_t *chars) { uint8_t patch[PATCH_SZ]; for (int gr = 0; gr < GRID_H; gr++) { for (int gc = 0; gc < GRID_W; gc++) { int idx = gr * GRID_W + gc; // Extract patch for (int y = 0; y < CHAR_H; y++) { for (int x = 0; x < CHAR_W; x++) { int px = gc * CHAR_W + x; int py = gr * CHAR_H + y; patch[y * CHAR_W + x] = pixels[py * PIXEL_W + px]; } } // Find best match chars[idx] = find_best_glyph(patch, rom, &bg_col[idx], &fg_col[idx]); } } } // Get current time in nanoseconds since UNIX epoch uint64_t get_current_time_ns(void) { struct timeval tv; gettimeofday(&tv, NULL); return (uint64_t)tv.tv_sec * 1000000000ULL + (uint64_t)tv.tv_usec * 1000ULL; } // Write Videotex header (32 bytes, similar to TAV but simpler) void write_videotex_header(FILE *f, uint8_t fps, uint32_t total_frames) { fwrite("\x1FTSVMTAV", 8, 1, f); // Version: 1 (uint8) fputc(1, f); // Grid dimensions (uint8 each) fputc(GRID_W, f); // cols = 80 fputc(0, f); fputc(GRID_H, f); // rows = 32 fputc(0, f); // FPS (uint8) fputc(fps, f); // Total frames (uint32, little-endian) fwrite(&total_frames, sizeof(uint32_t), 1, f); fputc(0, f); // wavelet filter type fputc(0, f); // decomposition levels fputc(0, f); // quantiser Y fputc(0, f); // quantiser Co fputc(0, f); // quantiser Cg // Feature Flags fputc(0x03, f); // bit 0 = has audio; bit 1 = has subtitle (Videotex is classified as subtitles) // Video Flags fputc(0x80, f); // bit 7 = has no video (Videotex is classified as subtitles) fputc(0, f); // encoder quality level fputc(0x02, f); // channel layout: Y only fputc(0, f); // entropy coder fputc(0, f); // reserved fputc(0, f); // reserved fputc(0, f); // device orientation: no rotation fputc(0, f); // file role: generic } // Write extended header packet with metadata // Returns the file offset where ENDT value is written (for later update) long write_extended_header(FILE *f, uint64_t creation_time_ns, const char *ffmpeg_version) { fputc(PACKET_EXTENDED_HDR, f); // Helper macros for key-value pairs #define WRITE_KV_UINT64(key_str, value) do { \ fwrite(key_str, 1, 4, f); \ uint8_t value_type = 0x04; /* Uint64 */ \ fwrite(&value_type, 1, 1, f); \ uint64_t val = (value); \ fwrite(&val, sizeof(uint64_t), 1, f); \ } while(0) #define WRITE_KV_BYTES(key_str, data, len) do { \ fwrite(key_str, 1, 4, f); \ uint8_t value_type = 0x10; /* Bytes */ \ fwrite(&value_type, 1, 1, f); \ uint16_t length = (len); \ fwrite(&length, sizeof(uint16_t), 1, f); \ fwrite((data), 1, (len), f); \ } while(0) // Count key-value pairs (BGNT, ENDT, CDAT, VNDR, FMPG) uint16_t num_pairs = ffmpeg_version ? 5 : 4; // FMPG is optional fwrite(&num_pairs, sizeof(uint16_t), 1, f); // BGNT: Video begin time (0 for frame 0) WRITE_KV_UINT64("BGNT", 0ULL); // ENDT: Video end time (placeholder, will be updated at end) long endt_offset = ftell(f); WRITE_KV_UINT64("ENDT", 0ULL); // CDAT: Creation time in nanoseconds since UNIX epoch WRITE_KV_UINT64("CDAT", creation_time_ns); // VNDR: Encoder name and version const char *vendor_str = ENCODER_VENDOR_STRING; WRITE_KV_BYTES("VNDR", vendor_str, strlen(vendor_str)); // FMPG: FFmpeg version (if available) if (ffmpeg_version) { WRITE_KV_BYTES("FMPG", ffmpeg_version, strlen(ffmpeg_version)); } #undef WRITE_KV_UINT64 #undef WRITE_KV_BYTES // Return offset of ENDT value (skip key, type byte) return endt_offset + 4 + 1; // 4 bytes for "ENDT", 1 byte for type } // Write font ROM packet (SSF packet type 0x30) void write_fontrom_packet(FILE *f, const uint8_t *rom_data, size_t data_size, uint8_t opcode) { // Prepare padded ROM data (pad to FONTROM_PADDED_SIZE with zeros) uint8_t *padded_data = calloc(1, FONTROM_PADDED_SIZE); memcpy(padded_data, rom_data, data_size); // Packet structure: // [type:0x30][size:uint32][index:uint24][opcode:uint8][length:uint16][data][terminator:0x00] uint32_t packet_size = 3 + 1 + 2 + FONTROM_PADDED_SIZE + 1; // Write packet type and size fputc(PACKET_SSF, f); fwrite(&packet_size, sizeof(uint32_t), 1, f); // Write SSF payload // Index (3 bytes, always 0 for font ROM) fputc(0, f); fputc(0, f); fputc(0, f); // Opcode (0x80=lowrom, 0x81=highrom) fputc(opcode, f); // Payload length (uint16, little-endian) uint16_t payload_len = FONTROM_PADDED_SIZE; fwrite(&payload_len, sizeof(uint16_t), 1, f); // Font data (padded to 1920 bytes) fwrite(padded_data, 1, FONTROM_PADDED_SIZE, f); // Terminator fputc(0x00, f); free(padded_data); fprintf(stderr, "Font ROM uploaded: %zu bytes (padded to %d), opcode 0x%02X\n", data_size, FONTROM_PADDED_SIZE, opcode); } // Write timecode packet (nanoseconds) void write_timecode(FILE *f, uint64_t timecode_ns) { fputc(PACKET_TIMECODE, f); fwrite(&timecode_ns, sizeof(uint64_t), 1, f); } // Write sync packet void write_sync(FILE *f) { fputc(PACKET_SYNC, f); } // Write MP2 audio packet void write_audio_mp2(FILE *f, const uint8_t *data, uint32_t size) { fputc(PACKET_AUDIO_MP2, f); fwrite(&size, sizeof(uint32_t), 1, f); fwrite(data, 1, size, f); } // Write text packet with separated arrays (better compression) void write_text_packet(FILE *f, const uint8_t *bg_col, const uint8_t *fg_col, const uint8_t *chars, int rows, int cols) { int grid_size = rows * cols; // Prepare uncompressed data: [rows][cols][fg-array][bg-array][char-array] // Separated arrays compress much better (fg/bg are just 0xF0/0xFE runs) size_t uncompressed_size = 2 + grid_size * 3; uint8_t *uncompressed = malloc(uncompressed_size); uncompressed[0] = rows; uncompressed[1] = cols; // Copy arrays in order: foreground, background, characters memcpy(&uncompressed[2], fg_col, grid_size); // Foreground first memcpy(&uncompressed[2 + grid_size], bg_col, grid_size); // Background second memcpy(&uncompressed[2 + grid_size * 2], chars, grid_size); // Characters third // Compress with Zstd size_t max_compressed = ZSTD_compressBound(uncompressed_size); uint8_t *compressed = malloc(max_compressed); size_t compressed_size = ZSTD_compress(compressed, max_compressed, uncompressed, uncompressed_size, 3); if (ZSTD_isError(compressed_size)) { fprintf(stderr, "Zstd compression error\n"); exit(1); } // Write packet: [type][size][data] fputc(PACKET_TEXT, f); uint32_t size32 = compressed_size; fwrite(&size32, 4, 1, f); fwrite(compressed, compressed_size, 1, f); free(compressed); free(uncompressed); } int main(int argc, char **argv) { if (argc < 7) { fprintf(stderr, "Usage: %s -i