tav: support for fractional framerate

This commit is contained in:
minjaesong
2025-12-25 18:26:56 +09:00
parent 4afe3816c7
commit 54b61fb436
5 changed files with 167 additions and 21 deletions

View File

@@ -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()
}

View File

@@ -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`),

View File

@@ -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");

View File

@@ -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

View File

@@ -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",