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)
|
seqread.skip(8)
|
||||||
} else {
|
} else {
|
||||||
console.log(`got unknown packet type 0x${packetType.toString(16)}`)
|
console.log(`got unknown packet type 0x${packetType.toString(16)}`)
|
||||||
// Unknown packet, try to skip it safely
|
let size = seqread.readInt()
|
||||||
break
|
seqread.skip(size)
|
||||||
}
|
}
|
||||||
packetType = seqread.readOneByte()
|
packetType = seqread.readOneByte()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const COL_HL_EXT = {
|
|||||||
"wav": 31,
|
"wav": 31,
|
||||||
"adpcm": 31,
|
"adpcm": 31,
|
||||||
"pcm": 32,
|
"pcm": 32,
|
||||||
"mp3": 33,
|
// "mp3": 33,
|
||||||
"tad": 33,
|
"tad": 33,
|
||||||
"mp2": 34,
|
"mp2": 34,
|
||||||
"mv1": 213,
|
"mv1": 213,
|
||||||
@@ -36,6 +36,8 @@ const COL_HL_EXT = {
|
|||||||
"ipf": 190,
|
"ipf": 190,
|
||||||
"ipf1": 190,
|
"ipf1": 190,
|
||||||
"ipf2": 190,
|
"ipf2": 190,
|
||||||
|
"im3": 190,
|
||||||
|
"tap": 190,
|
||||||
"txt": 223,
|
"txt": 223,
|
||||||
"md": 223,
|
"md": 223,
|
||||||
"log": 223
|
"log": 223
|
||||||
@@ -44,12 +46,14 @@ const COL_HL_EXT = {
|
|||||||
const EXEC_FUNS = {
|
const EXEC_FUNS = {
|
||||||
"wav": (f) => _G.shell.execute(`playwav "${f}" -i`),
|
"wav": (f) => _G.shell.execute(`playwav "${f}" -i`),
|
||||||
"adpcm": (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`),
|
"mp2": (f) => _G.shell.execute(`playmp2 "${f}" -i`),
|
||||||
"mv1": (f) => _G.shell.execute(`playmv1 "${f}" -i`),
|
"mv1": (f) => _G.shell.execute(`playmv1 "${f}" -i`),
|
||||||
"mv2": (f) => _G.shell.execute(`playtev "${f}" -i`),
|
"mv2": (f) => _G.shell.execute(`playtev "${f}" -i`),
|
||||||
"mv3": (f) => _G.shell.execute(`playtav "${f}" -i`),
|
"mv3": (f) => _G.shell.execute(`playtav "${f}" -i`),
|
||||||
"tav": (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`),
|
"tad": (f) => _G.shell.execute(`playtad "${f}" -i`),
|
||||||
"pcm": (f) => _G.shell.execute(`playpcm "${f}" -i`),
|
"pcm": (f) => _G.shell.execute(`playpcm "${f}" -i`),
|
||||||
"ipf": (f) => _G.shell.execute(`decodeipf "${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 is_still_image; // 1 if input is a still picture (TAP format)
|
||||||
int output_tga; // 1 for TGA output, 0 for PNG (default)
|
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)
|
// Threading support (video decoding)
|
||||||
int num_threads;
|
int num_threads;
|
||||||
int num_slots;
|
int num_slots;
|
||||||
@@ -264,6 +268,23 @@ static int read_tav_header(decoder_context_t *ctx) {
|
|||||||
ctx->decode_height = ctx->header.height;
|
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) {
|
if (ctx->verbose) {
|
||||||
printf("=== %s Header ===\n", ctx->is_still_image ? "TAP" : "TAV");
|
printf("=== %s Header ===\n", ctx->is_still_image ? "TAP" : "TAV");
|
||||||
printf(" Format: %s\n", ctx->is_still_image ? "Still Picture" : "Video");
|
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);
|
printf(" Interlaced: yes (decode height: %d)\n", ctx->decode_height);
|
||||||
}
|
}
|
||||||
if (!ctx->is_still_image) {
|
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(" FPS: %d\n", ctx->header.fps);
|
||||||
|
}
|
||||||
printf(" Total frames: %u\n", ctx->header.total_frames);
|
printf(" Total frames: %u\n", ctx->header.total_frames);
|
||||||
}
|
}
|
||||||
printf(" Wavelet filter: %d\n", ctx->header.wavelet_filter);
|
printf(" Wavelet filter: %d\n", ctx->header.wavelet_filter);
|
||||||
@@ -292,6 +317,84 @@ static int read_tav_header(decoder_context_t *ctx) {
|
|||||||
return 0;
|
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
|
// 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 interlaced video: input is half-height fields, output is full-height interlaced
|
||||||
// For progressive video: input and output are both full-height
|
// For progressive video: input and output are both full-height
|
||||||
char video_size[32];
|
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(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
|
// Redirect video pipe to fd 3
|
||||||
dup2(video_pipe_fd[0], 3);
|
dup2(video_pipe_fd[0], 3);
|
||||||
@@ -2071,6 +2179,9 @@ int main(int argc, char *argv[]) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scan for XFPS if header.fps == 0xFF
|
||||||
|
scan_for_xfps(&ctx);
|
||||||
|
|
||||||
// Handle still image (TAP) mode
|
// Handle still image (TAP) mode
|
||||||
if (ctx.is_still_image) {
|
if (ctx.is_still_image) {
|
||||||
printf("Detected still picture (TAP format)\n");
|
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;
|
uint16_t height = (actual_height > 65535) ? 0 : (uint16_t)actual_height;
|
||||||
fwrite(&height, sizeof(uint16_t), 1, fp);
|
fwrite(&height, sizeof(uint16_t), 1, fp);
|
||||||
|
|
||||||
// FPS (uint8_t, 1 byte) - 0 for still images, fps_num for video
|
// FPS (uint8_t, 1 byte)
|
||||||
uint8_t fps = is_still_image ? 0 : (uint8_t)params->fps_num;
|
// - 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);
|
fputc(fps, fp);
|
||||||
|
|
||||||
// Total frames (uint32_t, 4 bytes)
|
// 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;
|
uint8_t packet_type = TAV_PACKET_EXTENDED_HDR;
|
||||||
if (fwrite(&packet_type, 1, 1, fp) != 1) return -1;
|
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_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
|
uint16_t num_pairs = 4; // BGNT, ENDT, CDAT, VNDR
|
||||||
if (cli->ffmpeg_version) num_pairs++; // FMPG
|
if (cli->ffmpeg_version) num_pairs++; // FMPG
|
||||||
if (has_xdim) num_pairs++; // XDIM
|
if (has_xdim) num_pairs++; // XDIM
|
||||||
|
if (has_xfps) num_pairs++; // XFPS
|
||||||
if (fwrite(&num_pairs, sizeof(uint16_t), 1, fp) != 1) return -1;
|
if (fwrite(&num_pairs, sizeof(uint16_t), 1, fp) != 1) return -1;
|
||||||
|
|
||||||
// Helper macros for writing key-value pairs
|
// 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));
|
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_UINT64
|
||||||
#undef WRITE_KV_BYTES
|
#undef WRITE_KV_BYTES
|
||||||
|
|
||||||
|
|||||||
@@ -360,15 +360,18 @@ void print_extended_header(FILE *fp, int verbose) {
|
|||||||
if (verbose) {
|
if (verbose) {
|
||||||
data[length] = '\0';
|
data[length] = '\0';
|
||||||
|
|
||||||
// Truncate long strings
|
// Special handling for XFPS: show parsed framerate
|
||||||
/*if (length > 60) {
|
if (strncmp(key, "XFPS", 4) == 0) {
|
||||||
data[57] = '.';
|
int num, den;
|
||||||
data[58] = '.';
|
if (sscanf(data, "%d/%d", &num, &den) == 2) {
|
||||||
data[59] = '.';
|
printf("%d/%d (%.3f fps)", num, den, (double)num / den);
|
||||||
data[60] = '\0';
|
} else {
|
||||||
}*/
|
|
||||||
printf("\"%s\"", data);
|
printf("\"%s\"", data);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
printf("\"%s\"", data);
|
||||||
|
}
|
||||||
|
}
|
||||||
free(data);
|
free(data);
|
||||||
} else {
|
} else {
|
||||||
if (verbose) printf("Unknown type");
|
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",
|
printf(" Version: %d (base: %d - %s, temporal: %s)\n",
|
||||||
version, base_version, VERDESC[base_version], TEMPORAL_WAVELET[temporal_motion_coder]);
|
version, base_version, VERDESC[base_version], TEMPORAL_WAVELET[temporal_motion_coder]);
|
||||||
printf(" Resolution: %dx%d\n", width, height);
|
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);
|
printf(" Frame rate: %d fps", fps);
|
||||||
if (video_flags & 0x02) printf(" (NTSC)");
|
if (video_flags & 0x02) printf(" (NTSC)");
|
||||||
printf("\n");
|
printf("\n");
|
||||||
|
}
|
||||||
printf(" Total frames: %u\n", total_frames);
|
printf(" Total frames: %u\n", total_frames);
|
||||||
printf(" Wavelet: %d", wavelet);
|
printf(" Wavelet: %d", wavelet);
|
||||||
const char *wavelet_names[] = {"LGT 5/3", "CDF 9/7", "CDF 13/7", "Reserved", "Reserved",
|
const char *wavelet_names[] = {"LGT 5/3", "CDF 9/7", "CDF 13/7", "Reserved", "Reserved",
|
||||||
|
|||||||
Reference in New Issue
Block a user