UCF reading and writing

This commit is contained in:
minjaesong
2025-10-02 23:49:57 +09:00
parent d4fae0071b
commit 27ad3361ea
3 changed files with 272 additions and 10 deletions

View File

@@ -7,7 +7,11 @@
const WIDTH = 560
const HEIGHT = 448
const TAV_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x54, 0x41, 0x56] // "\x1FTSVM TAV"
const UCF_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x55, 0x43, 0x46] // "\x1FTSVM UCF"
const TAV_VERSION = 1 // Initial DWT version
const UCF_VERSION = 1
const ADDRESSING_EXTERNAL = 0x01
const ADDRESSING_INTERNAL = 0x02
const SND_BASE_ADDR = audio.getBaseAddr()
const pcm = require("pcm")
const MP2_FRAME_SIZE = [144,216,252,288,360,432,504,576,720,864,1008,1152,1440,1728]
@@ -400,9 +404,13 @@ let stopPlay = false
let akku = FRAME_TIME
let akku2 = 0.0
let currentFileIndex = 1 // Track which file we're playing in concatenated stream
let totalFilesProcessed = 0
let decoderDbgInfo = {}
let blockDataPtr = sys.malloc(2377744)
let cueElements = []
// Function to try reading next TAV file header at current position
function tryReadNextTAVHeader() {
// Save current position
@@ -422,13 +430,23 @@ function tryReadNextTAVHeader() {
// Check if it matches TAV magic
let isValidTAV = true
let isValidUCF = true
for (let i = 0; i < newMagic.length; i++) {
if (newMagic[i] !== TAV_MAGIC[i+1]) {
isValidTAV = false
serial.printerr("Header mismatch: got "+newMagic.join())
break
}
}
for (let i = 0; i < newMagic.length; i++) {
if (newMagic[i] !== UCF_MAGIC[i+1]) {
isValidUCF = false
}
}
if (!isValidTAV && !isValidUCF) {
serial.printerr("Header mismatch: got "+newMagic.join())
return 1
}
if (isValidTAV) {
serial.println("Got next video file")
@@ -463,6 +481,67 @@ function tryReadNextTAVHeader() {
return newHeader
}
else if (isValidUCF) {
serial.println("Got Universal Cue Format")
// TODO read and store the cue, then proceed to read next TAV packet (should be 0x1F)
let version = seqread.readOneByte()
if (version !== UCF_VERSION) {
serial.println(`Error: Unsupported UCF version: ${version} (expected ${UCF_VERSION})`)
return 2
}
let numElements = seqread.readShort()
let cueSize = seqread.readInt()
seqread.skip(1)
serial.println(`UCF Version: ${version}, Elements: ${numElements}`)
// Parse cue elements
for (let i = 0; i < numElements; i++) {
let element = {}
element.addressingModeAndIntent = seqread.readOneByte()
element.addressingMode = element.addressingModeAndIntent & 15
let nameLength = seqread.readShort()
element.name = seqread.readString(nameLength)
if (element.addressingMode === ADDRESSING_EXTERNAL) {
let pathLength = seqread.readShort()
element.path = seqread.readString(pathLength)
serial.println(`Element ${i + 1}: ${element.name} -> ${element.path} (external)`)
} else if (element.addressingMode === ADDRESSING_INTERNAL) {
// Read 48-bit offset (6 bytes, little endian)
let offsetBytes = []
for (let j = 0; j < 6; j++) {
offsetBytes.push(seqread.readOneByte())
}
element.offset = 0
for (let j = 0; j < 6; j++) {
element.offset |= (offsetBytes[j] << (j * 8))
}
serial.println(`Element ${i + 1}: ${element.name} -> offset ${element.offset} (internal)`)
} else {
serial.println(`Error: Unknown addressing mode: ${element.addressingMode}`)
return 5
}
cueElements.push(element)
}
// skip zeros
let readCount = seqread.getReadCount()
serial.println(`Skip to first video (${readCount} -> ${cueSize})`)
seqread.skip(cueSize - readCount + 1)
currentFileIndex -= 1
return tryReadNextTAVHeader()
}
else {
serial.printerr("File not TAV/UCF. Magic: " + newMagic.join())
return 7
}
} catch (e) {
serial.printerr(e)
@@ -477,8 +556,6 @@ function tryReadNextTAVHeader() {
// Playback loop - properly adapted from TEV with multi-file support
try {
let t1 = sys.nanoTime()
let totalFilesProcessed = 0
let decoderDbgInfo = {}
while (!stopPlay && seqread.getReadCount() < FILE_LENGTH) {
@@ -519,8 +596,10 @@ try {
// Continue with new file
packetType = seqread.readOneByte()
}
else
else {
serial.printerr("Header read failed: " + JSON.stringify(nextHeader))
break
}
}
if (packetType === TAV_PACKET_SYNC || packetType == TAV_PACKET_SYNC_NTSC) {

View File

@@ -12,7 +12,7 @@ if (!exec_args[1]) {
const interactive = exec_args[2] && exec_args[2].toLowerCase() == "-i"
const fullFilePath = _G.shell.resolvePathInput(exec_args[1])
if (!files.exists(fullFilePath.full)) {
if (!files.open(fullFilePath.full).exists) {
serial.println(`Error: File not found: ${fullFilePath.full}`)
return 2
}
@@ -147,7 +147,8 @@ let cueElements = []
for (let i = 0; i < numElements; i++) {
let element = {}
element.addressingMode = reader.readOneByte()
element.addressingModeAndIntent = reader.readOneByte()
element.addressingMode = element.addressingModeAndIntent & 15
let nameLength = reader.readShort()
element.name = reader.readString(nameLength)
@@ -234,7 +235,7 @@ for (let i = 0; i < cueElements.length; i++) {
targetPath = elementPath
}
if (!files.exists(targetPath)) {
if (!files.open(targetPath).exists) {
serial.println(`Warning: External file not found: ${targetPath}`)
continue
}
@@ -307,7 +308,7 @@ for (let i = 0; i < cueElements.length; i++) {
exec_args[1] = targetPath
if (playerFile) {
let playerPath = `A:\\tvdos\\bin\\${playerFile}.js`
if (files.exists(playerPath)) {
if (files.open(playerPath).exists) {
eval(files.readText(playerPath))
} else {
serial.println(`Warning: Player not found: ${playerFile}`)
@@ -334,7 +335,7 @@ for (let i = 0; i < cueElements.length; i++) {
// Execute the appropriate player
let playerPath = `A:\\tvdos\\bin\\${playerFile}.js`
if (!files.exists(playerPath)) {
if (!files.open(playerPath).exists) {
serial.println(`Warning: Player script not found: ${playerPath}`)
continue
}

View File

@@ -0,0 +1,182 @@
/**
* UCF Payload Writer for TAV Files
* Creates a 4KB UCF cue file for concatenated TAV files
* Usage: ./create_ucf_payload input.tav output.ucf
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#define UCF_SIZE 4096
#define TAV_OFFSET_BIAS UCF_SIZE
#define TAV_MAGIC "\x1FTSVMTA" // Matches both TAV and TAP
typedef struct {
uint8_t magic[8];
uint8_t version;
uint16_t width;
uint16_t height;
uint8_t fps;
uint32_t total_frames;
// ... rest of header fields
} __attribute__((packed)) TAVHeader;
// Write UCF header
static void write_ucf_header(FILE *out, uint16_t num_cues) {
uint8_t magic[8] = {0x1F, 'T', 'S', 'V', 'M', 'U', 'C', 'F'};
uint8_t version = 1;
uint32_t cue_file_size = UCF_SIZE;
uint8_t reserved = 0;
fwrite(magic, 1, 8, out);
fwrite(&version, 1, 1, out);
fwrite(&num_cues, 2, 1, out);
fwrite(&cue_file_size, 4, 1, out);
fwrite(&reserved, 1, 1, out);
}
// Write UCF cue element (internal addressing, human+machine interactable)
static void write_cue_element(FILE *out, uint64_t offset, uint16_t track_num) {
uint8_t addressing_mode = 0x21; // 0x20 (human) | 0x01 (machine) | 0x02 (internal)
char name[16];
snprintf(name, sizeof(name), "Track %d", track_num);
uint16_t name_len = strlen(name);
// Offset with 4KB bias
uint64_t biased_offset = offset + TAV_OFFSET_BIAS;
fwrite(&addressing_mode, 1, 1, out);
fwrite(&name_len, 2, 1, out);
fwrite(name, 1, name_len, out);
// Write 48-bit (6-byte) offset
fwrite(&biased_offset, 6, 1, out);
}
// Find all TAV headers in the file
static int find_tav_headers(FILE *in, uint64_t **offsets_out) {
uint64_t *offsets = NULL;
int count = 0;
int capacity = 16;
offsets = malloc(capacity * sizeof(uint64_t));
if (!offsets) {
fprintf(stderr, "Error: Memory allocation failed\n");
return -1;
}
// Seek to beginning
fseek(in, 0, SEEK_SET);
uint64_t pos = 0;
uint8_t magic[8];
while (fread(magic, 1, 8, in) == 8) {
// Check for TAV magic signature
if (memcmp(magic, TAV_MAGIC, 7) == 0 && (magic[7] == 'V' || magic[7] == 'P')) {
// Found TAV header
if (count >= capacity) {
capacity *= 2;
uint64_t *new_offsets = realloc(offsets, capacity * sizeof(uint64_t));
if (!new_offsets) {
fprintf(stderr, "Error: Memory reallocation failed\n");
free(offsets);
return -1;
}
offsets = new_offsets;
}
offsets[count++] = pos;
printf("Found TAV header at offset: 0x%lX (%lu)\n", pos, pos);
// Skip past this header (32 bytes total)
fseek(in, pos + 32, SEEK_SET);
pos += 32;
} else {
// Move forward by 1 byte for next search
fseek(in, pos + 1, SEEK_SET);
pos++;
}
}
*offsets_out = offsets;
return count;
}
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <input.tav> <output.ucf>\n", argv[0]);
fprintf(stderr, "Creates a 4KB UCF payload for concatenated TAV file\n");
return 1;
}
const char *input_path = argv[1];
const char *output_path = argv[2];
// Open input file
FILE *in = fopen(input_path, "rb");
if (!in) {
fprintf(stderr, "Error: Cannot open input file '%s'\n", input_path);
return 1;
}
// Find all TAV headers
uint64_t *offsets = NULL;
int num_tracks = find_tav_headers(in, &offsets);
fclose(in);
if (num_tracks < 0) {
fprintf(stderr, "Error: Failed to scan input file\n");
return 1;
}
if (num_tracks == 0) {
fprintf(stderr, "Error: No TAV headers found in input file\n");
return 1;
}
printf("\nFound %d TAV header(s)\n", num_tracks);
// Create output UCF file
FILE *out = fopen(output_path, "wb");
if (!out) {
fprintf(stderr, "Error: Cannot create output file '%s'\n", output_path);
free(offsets);
return 1;
}
// Write UCF header
write_ucf_header(out, num_tracks);
// Write cue elements
for (int i = 0; i < num_tracks; i++) {
write_cue_element(out, offsets[i], i + 1);
printf("Written cue element: Track %d at offset 0x%lX (biased: 0x%lX)\n",
i + 1, offsets[i], offsets[i] + TAV_OFFSET_BIAS);
}
// Get current file position
long current_pos = ftell(out);
// Fill remaining space with zeros to reach 4KB
if (current_pos < UCF_SIZE) {
size_t remaining = UCF_SIZE - current_pos;
uint8_t *zeros = calloc(remaining, 1);
if (zeros) {
fwrite(zeros, 1, remaining, out);
free(zeros);
}
}
fclose(out);
free(offsets);
printf("\nUCF payload created successfully: %s\n", output_path);
printf("File size: %d bytes (4KB)\n", UCF_SIZE);
printf("\nTo create seekable TAV file, prepend this UCF to your TAV file:\n");
printf(" cat %s input.tav > output_seekable.tav\n", output_path);
return 0;
}