mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-03-07 11:51:49 +09:00
tav: support for fractional framerate
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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`),
|
||||
|
||||
@@ -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) {
|
||||
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");
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -360,15 +360,18 @@ 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';
|
||||
}*/
|
||||
// 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 {
|
||||
if (verbose) printf("Unknown type");
|
||||
@@ -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);
|
||||
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",
|
||||
|
||||
Reference in New Issue
Block a user