From 27ad3361ea2c285827692a18c167645f3e2c2290 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Thu, 2 Oct 2025 23:49:57 +0900 Subject: [PATCH] UCF reading and writing --- assets/disk0/tvdos/bin/playtav.js | 89 +++++++++++++- assets/disk0/tvdos/bin/playucf.js | 11 +- video_encoder/create_ucf_payload.c | 182 +++++++++++++++++++++++++++++ 3 files changed, 272 insertions(+), 10 deletions(-) create mode 100644 video_encoder/create_ucf_payload.c diff --git a/assets/disk0/tvdos/bin/playtav.js b/assets/disk0/tvdos/bin/playtav.js index 5d6fcb1..259c4b4 100644 --- a/assets/disk0/tvdos/bin/playtav.js +++ b/assets/disk0/tvdos/bin/playtav.js @@ -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) { diff --git a/assets/disk0/tvdos/bin/playucf.js b/assets/disk0/tvdos/bin/playucf.js index ba96080..dec18fa 100644 --- a/assets/disk0/tvdos/bin/playucf.js +++ b/assets/disk0/tvdos/bin/playucf.js @@ -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 } diff --git a/video_encoder/create_ucf_payload.c b/video_encoder/create_ucf_payload.c new file mode 100644 index 0000000..0cb6b3e --- /dev/null +++ b/video_encoder/create_ucf_payload.c @@ -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 +#include +#include +#include + +#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 \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; +}