diff --git a/assets/disk0/tvdos/bin/playtav.js b/assets/disk0/tvdos/bin/playtav.js index 80604da..15acc5d 100644 --- a/assets/disk0/tvdos/bin/playtav.js +++ b/assets/disk0/tvdos/bin/playtav.js @@ -516,8 +516,8 @@ if (isTapFile) { seqread.skip(8) } else { console.log(`got unknown packet type 0x${packetType.toString(16)}`) - // Unknown packet, try to skip it safely - break + let size = seqread.readInt() + seqread.skip(size) } packetType = seqread.readOneByte() } diff --git a/assets/disk0/tvdos/bin/zfm.js b/assets/disk0/tvdos/bin/zfm.js index cd8a851..d694a68 100644 --- a/assets/disk0/tvdos/bin/zfm.js +++ b/assets/disk0/tvdos/bin/zfm.js @@ -26,7 +26,7 @@ const COL_HL_EXT = { "wav": 31, "adpcm": 31, "pcm": 32, - "mp3": 33, +// "mp3": 33, "tad": 33, "mp2": 34, "mv1": 213, @@ -36,6 +36,8 @@ const COL_HL_EXT = { "ipf": 190, "ipf1": 190, "ipf2": 190, + "im3": 190, + "tap": 190, "txt": 223, "md": 223, "log": 223 @@ -44,12 +46,14 @@ const COL_HL_EXT = { const EXEC_FUNS = { "wav": (f) => _G.shell.execute(`playwav "${f}" -i`), "adpcm": (f) => _G.shell.execute(`playwav "${f}" -i`), - "mp3": (f) => _G.shell.execute(`playmp3 "${f}" -i`), +// "mp3": (f) => _G.shell.execute(`playmp3 "${f}" -i`), "mp2": (f) => _G.shell.execute(`playmp2 "${f}" -i`), "mv1": (f) => _G.shell.execute(`playmv1 "${f}" -i`), "mv2": (f) => _G.shell.execute(`playtev "${f}" -i`), "mv3": (f) => _G.shell.execute(`playtav "${f}" -i`), "tav": (f) => _G.shell.execute(`playtav "${f}" -i`), + "im3": (f) => _G.shell.execute(`playtav "${f}" -i`), + "tap": (f) => _G.shell.execute(`playtav "${f}" -i`), "tad": (f) => _G.shell.execute(`playtad "${f}" -i`), "pcm": (f) => _G.shell.execute(`playpcm "${f}" -i`), "ipf": (f) => _G.shell.execute(`decodeipf "${f}" -i`), diff --git a/video_encoder/src/decoder_tav.c b/video_encoder/src/decoder_tav.c index 85b1611..ff1396d 100644 --- a/video_encoder/src/decoder_tav.c +++ b/video_encoder/src/decoder_tav.c @@ -172,6 +172,10 @@ typedef struct { int is_still_image; // 1 if input is a still picture (TAP format) int output_tga; // 1 for TGA output, 0 for PNG (default) + // Extended framerate support (XFPS) + int fps_num; // Framerate numerator (from header or XFPS extended header) + int fps_den; // Framerate denominator (1 for standard, 1001 for NTSC, or from XFPS) + // Threading support (video decoding) int num_threads; int num_slots; @@ -264,6 +268,23 @@ static int read_tav_header(decoder_context_t *ctx) { ctx->decode_height = ctx->header.height; } + // Initialize fps_num and fps_den from header + // If header.fps == 0xFF, the actual framerate is in the XFPS extended header entry + // If header.fps == 0x00, this is a still image + // Otherwise, fps_num = header.fps and fps_den is 1 (or 1001 for NTSC if video_flags bit 1 is set) + if (ctx->header.fps == 0xFF) { + // Will be set from XFPS extended header + ctx->fps_num = 0; + ctx->fps_den = 1; + } else if (ctx->header.fps == 0x00) { + // Still image + ctx->fps_num = 0; + ctx->fps_den = 1; + } else { + ctx->fps_num = ctx->header.fps; + ctx->fps_den = (ctx->header.video_flags & 0x02) ? 1001 : 1; + } + if (ctx->verbose) { printf("=== %s Header ===\n", ctx->is_still_image ? "TAP" : "TAV"); printf(" Format: %s\n", ctx->is_still_image ? "Still Picture" : "Video"); @@ -273,7 +294,11 @@ static int read_tav_header(decoder_context_t *ctx) { printf(" Interlaced: yes (decode height: %d)\n", ctx->decode_height); } if (!ctx->is_still_image) { - printf(" FPS: %d\n", ctx->header.fps); + if (ctx->header.fps == 0xFF) { + printf(" FPS: (extended - see XFPS)\n"); + } else { + 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); @@ -292,6 +317,84 @@ static int read_tav_header(decoder_context_t *ctx) { return 0; } +/** + * Scan for XFPS extended header entry if header.fps == 0xFF. + * Must be called after read_tav_header() while file position is at start of packets. + * Will restore file position after scanning. + */ +static void scan_for_xfps(decoder_context_t *ctx) { + if (ctx->header.fps != 0xFF) { + // No need to scan for XFPS + return; + } + + long start_pos = ftell(ctx->input_fp); + + // Scan packets looking for extended header + while (!feof(ctx->input_fp)) { + uint8_t packet_type; + if (fread(&packet_type, 1, 1, ctx->input_fp) != 1) break; + + if (packet_type == TAV_PACKET_EXTENDED_HDR) { + // Parse extended header looking for XFPS + uint16_t num_pairs; + if (fread(&num_pairs, 2, 1, ctx->input_fp) != 1) break; + + for (int i = 0; i < num_pairs; i++) { + char key[5] = {0}; + uint8_t value_type; + + if (fread(key, 1, 4, ctx->input_fp) != 4) break; + if (fread(&value_type, 1, 1, ctx->input_fp) != 1) break; + + if (value_type == 0x10) { // Bytes type + uint16_t length; + if (fread(&length, 2, 1, ctx->input_fp) != 1) break; + + if (strncmp(key, "XFPS", 4) == 0 && length < 32) { + // Found XFPS - parse it + char xfps_str[32] = {0}; + if (fread(xfps_str, 1, length, ctx->input_fp) != length) break; + xfps_str[length] = '\0'; + + int num, den; + if (sscanf(xfps_str, "%d/%d", &num, &den) == 2) { + ctx->fps_num = num; + ctx->fps_den = den; + if (ctx->verbose) { + printf(" XFPS: %d/%d (%.3f fps)\n", num, den, (double)num / den); + } + } + // Found XFPS, done scanning + goto done; + } else { + // Skip this value + fseek(ctx->input_fp, length, SEEK_CUR); + } + } else if (value_type == 0x04) { // Int64 + fseek(ctx->input_fp, 8, SEEK_CUR); + } else if (value_type <= 0x04) { // Other int types + int sizes[] = {2, 3, 4, 6, 8}; + fseek(ctx->input_fp, sizes[value_type], SEEK_CUR); + } + } + // Extended header parsed, done scanning (XFPS not found) + break; + } else if (packet_type == TAV_PACKET_TIMECODE) { + fseek(ctx->input_fp, 8, SEEK_CUR); + } else if (packet_type == TAV_PACKET_SYNC || packet_type == TAV_PACKET_SYNC_NTSC) { + // No payload + } else { + // Reached a non-metadata packet, stop scanning + break; + } + } + +done: + // Restore file position + fseek(ctx->input_fp, start_pos, SEEK_SET); +} + // ============================================================================= // FFmpeg Integration // ============================================================================= @@ -321,9 +424,14 @@ static int spawn_ffmpeg(decoder_context_t *ctx) { // 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]; + char framerate[32]; snprintf(video_size, sizeof(video_size), "%dx%d", ctx->header.width, ctx->decode_height); - snprintf(framerate, sizeof(framerate), "%d", ctx->header.fps); + // Use fps_num/fps_den for extended framerates (XFPS) + if (ctx->fps_den == 1) { + snprintf(framerate, sizeof(framerate), "%d", ctx->fps_num); + } else { + snprintf(framerate, sizeof(framerate), "%d/%d", ctx->fps_num, ctx->fps_den); + } // Redirect video pipe to fd 3 dup2(video_pipe_fd[0], 3); @@ -2071,6 +2179,9 @@ int main(int argc, char *argv[]) { return 1; } + // Scan for XFPS if header.fps == 0xFF + scan_for_xfps(&ctx); + // Handle still image (TAP) mode if (ctx.is_still_image) { printf("Detected still picture (TAP format)\n"); diff --git a/video_encoder/src/encoder_tav.c b/video_encoder/src/encoder_tav.c index e4882f7..05a3390 100644 --- a/video_encoder/src/encoder_tav.c +++ b/video_encoder/src/encoder_tav.c @@ -630,8 +630,19 @@ static int write_tav_header(FILE *fp, const tav_encoder_params_t *params, uint16_t height = (actual_height > 65535) ? 0 : (uint16_t)actual_height; fwrite(&height, sizeof(uint16_t), 1, fp); - // FPS (uint8_t, 1 byte) - 0 for still images, fps_num for video - uint8_t fps = is_still_image ? 0 : (uint8_t)params->fps_num; + // FPS (uint8_t, 1 byte) + // - 0x00 for still images + // - 0xFF if fps_num > 254 or fps_den is not 1 or 1001 (use XFPS extended header) + // - otherwise fps_num + uint8_t fps; + if (is_still_image) { + fps = 0; + } else if (params->fps_num > 254 || + (params->fps_den != 1 && params->fps_den != 1001)) { + fps = 0xFF; // Extended framerate in XFPS + } else { + fps = (uint8_t)params->fps_num; + } fputc(fps, fp); // Total frames (uint32_t, 4 bytes) @@ -699,11 +710,14 @@ static long write_extended_header(cli_context_t *cli, int width, int height) { uint8_t packet_type = TAV_PACKET_EXTENDED_HDR; if (fwrite(&packet_type, 1, 1, fp) != 1) return -1; - // Count key-value pairs: BGNT, ENDT, CDAT, VNDR, optionally FMPG, and optionally XDIM + // Count key-value pairs: BGNT, ENDT, CDAT, VNDR, optionally FMPG, XDIM, XFPS int has_xdim = (width > 65535 || height > 65535); + int has_xfps = (cli->enc_params.fps_num > 254 || + (cli->enc_params.fps_den != 1 && cli->enc_params.fps_den != 1001)); uint16_t num_pairs = 4; // BGNT, ENDT, CDAT, VNDR if (cli->ffmpeg_version) num_pairs++; // FMPG if (has_xdim) num_pairs++; // XDIM + if (has_xfps) num_pairs++; // XFPS if (fwrite(&num_pairs, sizeof(uint16_t), 1, fp) != 1) return -1; // Helper macros for writing key-value pairs @@ -751,6 +765,14 @@ static long write_extended_header(cli_context_t *cli, int width, int height) { WRITE_KV_BYTES("XDIM", xdim_str, strlen(xdim_str)); } + // XFPS: Extended framerate (if fps_num > 254 or fps_den is not 1 or 1001) + if (has_xfps) { + char xfps_str[32]; + snprintf(xfps_str, sizeof(xfps_str), "%d/%d", + cli->enc_params.fps_num, cli->enc_params.fps_den); + WRITE_KV_BYTES("XFPS", xfps_str, strlen(xfps_str)); + } + #undef WRITE_KV_UINT64 #undef WRITE_KV_BYTES diff --git a/video_encoder/tav_inspector.c b/video_encoder/tav_inspector.c index bf5b6e9..b51367b 100644 --- a/video_encoder/tav_inspector.c +++ b/video_encoder/tav_inspector.c @@ -360,14 +360,17 @@ void print_extended_header(FILE *fp, int verbose) { if (verbose) { data[length] = '\0'; - // Truncate long strings - /*if (length > 60) { - data[57] = '.'; - data[58] = '.'; - data[59] = '.'; - data[60] = '\0'; - }*/ - printf("\"%s\"", data); + // Special handling for XFPS: show parsed framerate + if (strncmp(key, "XFPS", 4) == 0) { + int num, den; + if (sscanf(data, "%d/%d", &num, &den) == 2) { + printf("%d/%d (%.3f fps)", num, den, (double)num / den); + } else { + printf("\"%s\"", data); + } + } else { + printf("\"%s\"", data); + } } free(data); } else { @@ -663,9 +666,15 @@ static const char* TEMPORAL_WAVELET[] = {"Haar", "CDF 5/3"}; printf(" Version: %d (base: %d - %s, temporal: %s)\n", version, base_version, VERDESC[base_version], TEMPORAL_WAVELET[temporal_motion_coder]); printf(" Resolution: %dx%d\n", width, height); - printf(" Frame rate: %d fps", fps); - if (video_flags & 0x02) printf(" (NTSC)"); - printf("\n"); + if (fps == 0xFF) { + printf(" Frame rate: (extended - see XFPS in extended header)\n"); + } else if (fps == 0) { + printf(" Frame rate: (still image)\n"); + } else { + printf(" Frame rate: %d fps", fps); + if (video_flags & 0x02) printf(" (NTSC)"); + printf("\n"); + } printf(" Total frames: %u\n", total_frames); printf(" Wavelet: %d", wavelet); const char *wavelet_names[] = {"LGT 5/3", "CDF 9/7", "CDF 13/7", "Reserved", "Reserved",