diff --git a/assets/disk0/tvdos/moviedev/tav_inspector.js b/assets/disk0/tvdos/moviedev/tav_inspector.js new file mode 100644 index 0000000..e323909 --- /dev/null +++ b/assets/disk0/tvdos/moviedev/tav_inspector.js @@ -0,0 +1,721 @@ +// TAV Packet Inspector - JavaScript port for TSVM +// Ported from tav_inspector.c by CuriousTorvald and Claude +// Usage: tav_inspector [options] + +const seqread = require('seqread') + +// Frame mode constants +const FRAME_MODE_SKIP = 0x00 +const FRAME_MODE_INTRA = 0x01 +const FRAME_MODE_DELTA = 0x02 + +// Packet type constants +const TAV_PACKET_IFRAME = 0x10 +const TAV_PACKET_PFRAME = 0x11 +const TAV_PACKET_GOP_UNIFIED = 0x12 +const TAV_PACKET_GOP_UNIFIED_MOTION = 0x13 +const TAV_PACKET_PFRAME_RESIDUAL = 0x14 +const TAV_PACKET_BFRAME_RESIDUAL = 0x15 +const TAV_PACKET_PFRAME_ADAPTIVE = 0x16 +const TAV_PACKET_BFRAME_ADAPTIVE = 0x17 +const TAV_PACKET_AUDIO_MP2 = 0x20 +const TAV_PACKET_AUDIO_PCM8 = 0x21 +const TAV_PACKET_AUDIO_TAD = 0x24 +const TAV_PACKET_SUBTITLE = 0x30 +const TAV_PACKET_SUBTITLE_TC = 0x31 +const TAV_PACKET_VIDEOTEX = 0x3F +const TAV_PACKET_AUDIO_TRACK = 0x40 +const TAV_PACKET_VIDEO_CH2_I = 0x70 +const TAV_PACKET_VIDEO_CH2_P = 0x71 +const TAV_PACKET_VIDEO_CH3_I = 0x72 +const TAV_PACKET_VIDEO_CH3_P = 0x73 +const TAV_PACKET_VIDEO_CH4_I = 0x74 +const TAV_PACKET_VIDEO_CH4_P = 0x75 +const TAV_PACKET_VIDEO_CH5_I = 0x76 +const TAV_PACKET_VIDEO_CH5_P = 0x77 +const TAV_PACKET_VIDEO_CH6_I = 0x78 +const TAV_PACKET_VIDEO_CH6_P = 0x79 +const TAV_PACKET_VIDEO_CH7_I = 0x7A +const TAV_PACKET_VIDEO_CH7_P = 0x7B +const TAV_PACKET_VIDEO_CH8_I = 0x7C +const TAV_PACKET_VIDEO_CH8_P = 0x7D +const TAV_PACKET_VIDEO_CH9_I = 0x7E +const TAV_PACKET_VIDEO_CH9_P = 0x7F +const TAV_PACKET_EXIF = 0xE0 +const TAV_PACKET_ID3V1 = 0xE1 +const TAV_PACKET_ID3V2 = 0xE2 +const TAV_PACKET_VORBIS_COMMENT = 0xE3 +const TAV_PACKET_CD_TEXT = 0xE4 +const TAV_PACKET_EXTENDED_HDR = 0xEF +const TAV_PACKET_LOOP_START = 0xF0 +const TAV_PACKET_LOOP_END = 0xF1 +const TAV_PACKET_SCREEN_MASK = 0xF2 +const TAV_PACKET_GOP_SYNC = 0xFC +const TAV_PACKET_TIMECODE = 0xFD +const TAV_PACKET_SYNC_NTSC = 0xFE +const TAV_PACKET_SYNC = 0xFF +const TAV_PACKET_NOOP = 0x00 + +const QLUT = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,256,264,272,280,288,296,304,312,320,328,336,344,352,360,368,376,384,392,400,408,416,424,432,440,448,456,464,472,480,488,496,504,512,528,544,560,576,592,608,624,640,656,672,688,704,720,736,752,768,784,800,816,832,848,864,880,896,912,928,944,960,976,992,1008,1024,1056,1088,1120,1152,1184,1216,1248,1280,1312,1344,1376,1408,1440,1472,1504,1536,1568,1600,1632,1664,1696,1728,1760,1792,1824,1856,1888,1920,1952,1984,2016,2048,2112,2176,2240,2304,2368,2432,2496,2560,2624,2688,2752,2816,2880,2944,3008,3072,3136,3200,3264,3328,3392,3456,3520,3584,3648,3712,3776,3840,3904,3968,4032,4096] +const CLAYOUT = ["Luma-Chroma", "Luma-Chroma-Alpha", "Luma", "Luma-Alpha", "Chroma", "Chroma-Alpha"] +const VERDESC = ["null", "YCoCg tiled, uniform", "ICtCp tiled, uniform", "YCoCg monoblock, uniform", "ICtCp monoblock, uniform", "YCoCg monoblock, perceptual", "ICtCp monoblock, perceptual", "YCoCg tiled, perceptual", "ICtCp tiled, perceptual"] +const TEMPORAL_WAVELET = ["Haar", "CDF 5/3"] + +function getPacketTypeName(type) { + switch (type) { + case TAV_PACKET_IFRAME: return "I-FRAME" + case TAV_PACKET_PFRAME: return "P-FRAME" + case TAV_PACKET_GOP_UNIFIED: return "GOP (3D DWT Unified)" + case TAV_PACKET_GOP_UNIFIED_MOTION: return "GOP (3D DWT Unified with Motion Data)" + case TAV_PACKET_PFRAME_RESIDUAL: return "P-FRAME (residual)" + case TAV_PACKET_BFRAME_RESIDUAL: return "B-FRAME (residual)" + case TAV_PACKET_PFRAME_ADAPTIVE: return "P-FRAME (quadtree)" + case TAV_PACKET_BFRAME_ADAPTIVE: return "B-FRAME (quadtree)" + case TAV_PACKET_AUDIO_MP2: return "AUDIO MP2" + case TAV_PACKET_AUDIO_PCM8: return "AUDIO PCM8 (zstd)" + case TAV_PACKET_AUDIO_TAD: return "AUDIO TAD (zstd)" + case TAV_PACKET_SUBTITLE: return "SUBTITLE (SSF frame-locked)" + case TAV_PACKET_SUBTITLE_TC: return "SUBTITLE (SSF-TC timecoded)" + case TAV_PACKET_VIDEOTEX: return "VIDEOTEX (text-mode video)" + case TAV_PACKET_AUDIO_TRACK: return "AUDIO TRACK (Separate MP2)" + case TAV_PACKET_EXIF: return "METADATA (EXIF)" + case TAV_PACKET_ID3V1: return "METADATA (ID3v1)" + case TAV_PACKET_ID3V2: return "METADATA (ID3v2)" + case TAV_PACKET_VORBIS_COMMENT: return "METADATA (Vorbis)" + case TAV_PACKET_CD_TEXT: return "METADATA (CD-Text)" + case TAV_PACKET_EXTENDED_HDR: return "EXTENDED HEADER" + case TAV_PACKET_LOOP_START: return "LOOP START" + case TAV_PACKET_LOOP_END: return "LOOP END" + case TAV_PACKET_SCREEN_MASK: return "SCREEN MASK" + case TAV_PACKET_GOP_SYNC: return "GOP SYNC" + case TAV_PACKET_TIMECODE: return "TIMECODE" + case TAV_PACKET_SYNC_NTSC: return "SYNC (NTSC)" + case TAV_PACKET_SYNC: return "SYNC" + case TAV_PACKET_NOOP: return "NO-OP" + default: + if (type >= 0x70 && type <= 0x7F) { + return "MUX VIDEO" + } + return "UNKNOWN" + } +} + +// Read int64 (little-endian) +function readInt64() { + let lo = seqread.readInt() >>> 0 + let hi = seqread.readInt() >>> 0 + return lo + hi * 4294967296 +} + +// Read uint24 (little-endian) +function readUint24() { + let b0 = seqread.readOneByte() + let b1 = seqread.readOneByte() + let b2 = seqread.readOneByte() + return b0 | (b1 << 8) | (b2 << 16) +} + +// Get frame info from compressed data +function getFrameInfo(compressedSize) { + let info = { mode: -1, quantiser: 0xFF } + + if (compressedSize === 0) return info + + // Read compressed data into memory + let compressedPtr = sys.malloc(compressedSize) + if (compressedPtr === 0) { + seqread.skip(compressedSize) + return info + } + + seqread.readBytes(compressedSize, compressedPtr) + + // Decompress (max 2MB buffer) + let decompressedSize = 2 * 1024 * 1024 + let decompressedPtr = sys.malloc(decompressedSize) + if (decompressedPtr === 0) { + sys.free(compressedPtr) + return info + } + + try { + let actualSize = gzip.decompFromTo(compressedPtr, compressedSize, decompressedPtr) + + if (actualSize >= 1) { + info.mode = sys.peek(decompressedPtr) & 0xFF + } + if (info.mode !== FRAME_MODE_SKIP && actualSize >= 2) { + info.quantiser = sys.peek(decompressedPtr + 1) & 0xFF + } + } catch (e) { + // Decompression failed, keep default values + } + + sys.free(decompressedPtr) + sys.free(compressedPtr) + + return info +} + +// Parse extended header +function parseExtendedHeader(output) { + let numPairs = seqread.readShort() + output.push(` - ${numPairs} key-value pairs:\n`) + + for (let i = 0; i < numPairs; i++) { + let key = seqread.readFourCC() + let valueType = seqread.readOneByte() + + let valueTypeStr = "Unknown" + switch (valueType) { + case 0x00: valueTypeStr = "Int16"; break + case 0x01: valueTypeStr = "Int24"; break + case 0x02: valueTypeStr = "Int32"; break + case 0x03: valueTypeStr = "Int48"; break + case 0x04: valueTypeStr = "Int64"; break + case 0x10: valueTypeStr = "Bytes"; break + } + + output.push(` ${key} (type: ${valueTypeStr} (0x${valueType.toString(16).padStart(2,'0')})): `) + + if (valueType === 0x04) { // Int64 + let value = readInt64() + + if (key === "CDAT") { + let timeSec = Math.floor(value / 1000000) + let date = new Date(timeSec * 1000) + output.push(date.toUTCString()) + } else { + output.push((value / 1000000000).toFixed(6) + " seconds") + } + } else if (valueType === 0x10) { // Bytes + let length = seqread.readShort() + let data = seqread.readString(length) + output.push(`"${data}"`) + } else { + output.push("Unknown type") + } + + if (i < numPairs - 1) { + output.push("\n") + } + } +} + +// Parse subtitle packet +function parseSubtitlePacket(size, isTimecoded, output) { + let index = readUint24() + + let timecodeNs = 0 + let headerSize = 4 // 3 bytes index + 1 byte opcode + if (isTimecoded) { + timecodeNs = readInt64() + headerSize += 8 + } + + let opcode = seqread.readOneByte() + + output.push(` [Index=${index}`) + if (isTimecoded) { + output.push(`, Time=${(timecodeNs / 1000000000).toFixed(3)}s`) + } + output.push(`, Opcode=0x${opcode.toString(16).padStart(2,'0')}`) + + switch (opcode) { + case 0x01: output.push(" (SHOW)"); break + case 0x02: output.push(" (HIDE)"); break + case 0x03: output.push(" (MOVE)"); break + case 0x80: output.push(" (UPLOAD LOW FONT)"); break + case 0x81: output.push(" (UPLOAD HIGH FONT)"); break + default: + if (opcode >= 0x10 && opcode <= 0x2F) output.push(" (SHOW LANG)") + else if (opcode >= 0x30 && opcode <= 0x41) output.push(" (REVEAL)") + break + } + output.push("]") + + // Read text content for SHOW commands + let remaining = size - headerSize + if ((opcode === 0x01 || (opcode >= 0x10 && opcode <= 0x2F) || (opcode >= 0x30 && opcode <= 0x41)) && remaining > 0) { + let text = seqread.readString(remaining) + // Clean up control characters + text = text.replace(/[\n\r\t]/g, ' ') + output.push(` Text: "${text}"`) + } else { + seqread.skip(remaining) + } +} + +// Parse videotex packet +function parseVideotexPacket(size, output) { + let compressedPtr = sys.malloc(size) + if (compressedPtr === 0) { + seqread.skip(size) + output.push(` - size=${size} bytes`) + return + } + + seqread.readBytes(size, compressedPtr) + + let decompressSize = 8192 + let decompressedPtr = sys.malloc(decompressSize) + if (decompressedPtr === 0) { + sys.free(compressedPtr) + output.push(` - size=${size} bytes`) + return + } + + try { + let actualSize = gzip.decompFromTo(compressedPtr, size, decompressedPtr) + + if (actualSize >= 2) { + let rows = sys.peek(decompressedPtr) & 0xFF + let cols = sys.peek(decompressedPtr + 1) & 0xFF + let ratio = (actualSize / size).toFixed(2) + output.push(` - size=${size} bytes (decompressed: ${actualSize} bytes, grid: ${cols}x${rows}, ratio: ${ratio}:1)`) + } else { + output.push(` - size=${size} bytes (decompression failed)`) + } + } catch (e) { + output.push(` - size=${size} bytes (decompression failed)`) + } + + sys.free(decompressedPtr) + sys.free(compressedPtr) +} + +// Main function +function main() { + if (exec_args.length < 3) { + println("Usage: tav_inspector ") + println(" Analyzes TAV file packets and writes report to output file") + return 1 + } + + let inputPath = _G.shell.resolvePathInput(exec_args[1]).full + let outputPath = _G.shell.resolvePathInput(exec_args[2]).full + const FILE_LENGTH = files.open(inputPath).size + + // Prepare sequential reader + try { + seqread.prepare(inputPath) + } catch (e) { + println(`Error: Cannot open file ${inputPath}`) + println(e.toString()) + return 1 + } + + let output = [] + + // Read and verify TAV header (32 bytes) + let magic = seqread.readString(8) + let expectedMagic = "\x1FTSVMTAV" + if (magic !== expectedMagic) { + println("Error: Invalid TAV magic number") + return 1 + } + + // Parse header fields + let version = seqread.readOneByte() + let baseVersion = (version > 8) ? (version - 8) : version + let temporalMotionCoder = (version > 8) ? 1 : 0 + let width = seqread.readShort() + let height = seqread.readShort() + let fps = seqread.readOneByte() + let totalFrames = seqread.readInt() + let wavelet = seqread.readOneByte() + let decompLevels = seqread.readOneByte() + let quantY = seqread.readOneByte() + let quantCo = seqread.readOneByte() + let quantCg = seqread.readOneByte() + let extraFlags = seqread.readOneByte() + let videoFlags = seqread.readOneByte() + let quality = seqread.readOneByte() + let channelLayout = seqread.readOneByte() + let entropyCoder = seqread.readOneByte() + let encoderPreset = seqread.readOneByte() + seqread.skip(3) // Reserved bytes + + let waveletNames = ["LGT 5/3", "CDF 9/7", "CDF 13/7", "Reserved", "Reserved", + "Reserved", "Reserved", "Reserved", "Reserved", + "Reserved", "Reserved", "Reserved", "Reserved", + "Reserved", "Reserved", "Reserved", "DD-4"] + + // Write header information + output.push("TAV Packet Inspector\n") + output.push(`File: ${inputPath}\n`) + output.push("==================================================\n\n") + + output.push("TAV Header:\n") + output.push(` Version: ${version} (base: ${baseVersion} - ${VERDESC[baseVersion]}, temporal: ${TEMPORAL_WAVELET[temporalMotionCoder]})\n`) + output.push(` Resolution: ${width}x${height}\n`) + output.push(` Frame rate: ${fps} fps`) + if (videoFlags & 0x02) output.push(" (NTSC)") + output.push("\n") + output.push(` Total frames: ${totalFrames}\n`) + output.push(` Wavelet: ${wavelet}`) + if (wavelet < 17) output.push(` (${waveletNames[wavelet === 16 ? 16 : wavelet]})`) + if (wavelet === 255) output.push(" (Haar)") + output.push("\n") + output.push(` Decomp levels: ${decompLevels}\n`) + output.push(` Quantisers: Y=${QLUT[quantY]}, Co=${QLUT[quantCo]}, Cg=${QLUT[quantCg]} (Index=${quantY},${quantCo},${quantCg})\n`) + if (quality > 0) + output.push(` Quality: ${quality - 1}\n`) + else + output.push(" Quality: n/a\n") + output.push(` Channel layout: ${CLAYOUT[channelLayout]}\n`) + output.push(` Entropy coder: ${entropyCoder === 0 ? "Twobit-map" : "EZBC"}\n`) + output.push(" Encoder preset: ") + if (encoderPreset === 0) { + output.push("Default\n") + } else { + let presets = [] + if (encoderPreset & 0x01) presets.push("Sports") + if (encoderPreset & 0x02) presets.push("Anime") + output.push(presets.join(", ") + "\n") + } + output.push(" Flags:\n") + output.push(` Has audio: ${(extraFlags & 0x01) ? "Yes" : "No"}\n`) + output.push(` Has subtitles: ${(extraFlags & 0x02) ? "Yes" : "No"}\n`) + output.push(` Progressive: ${(videoFlags & 0x01) ? "No (interlaced)" : "Yes"}\n`) + output.push(` Lossless: ${(videoFlags & 0x04) ? "Yes" : "No"}\n`) + if (extraFlags & 0x04) output.push(" Progressive TX: Enabled\n") + if (extraFlags & 0x08) output.push(" ROI encoding: Enabled\n") + output.push("\nPackets:\n") + output.push("==================================================\n") + + // Statistics + let stats = { + iframeCount: 0, + pframeCount: 0, + pframeIntraCount: 0, + pframeDeltaCount: 0, + pframeSkipCount: 0, + gopUnifiedCount: 0, + gopUnifiedMotionCount: 0, + gopSyncCount: 0, + totalGopFrames: 0, + audioCount: 0, + audioMp2Count: 0, + audioPcm8Count: 0, + audioTadCount: 0, + audioTrackCount: 0, + subtitleCount: 0, + videotexCount: 0, + timecodeCount: 0, + syncCount: 0, + syncNtscCount: 0, + extendedHeaderCount: 0, + metadataCount: 0, + loopPointCount: 0, + muxVideoCount: 0, + unknownCount: 0, + totalVideoBytes: 0, + totalAudioBytes: 0, + audioMp2Bytes: 0, + audioPcm8Bytes: 0, + audioTadBytes: 0, + audioTrackBytes: 0, + videotexBytes: 0 + } + + let packetNum = 0 + let currentFrame = 0 + + // Parse packets + try { + while (seqread.getReadCount() < FILE_LENGTH) { + let packetOffset = seqread.getReadCount() + let packetType = seqread.readOneByte() + + output.push(`Packet ${packetNum} (offset 0x${packetOffset.toString(16).toUpperCase()}): Type 0x${packetType.toString(16).padStart(2,'0').toUpperCase()} (${getPacketTypeName(packetType)})`) + + switch (packetType) { + case TAV_PACKET_EXTENDED_HDR: + stats.extendedHeaderCount++ + parseExtendedHeader(output) + break + + case TAV_PACKET_TIMECODE: + stats.timecodeCount++ + let timecodeNs = readInt64() + let timecodeSec = (timecodeNs / 1000000000).toFixed(6) + output.push(` - ${timecodeSec} seconds (Frame ${currentFrame})`) + break + + case TAV_PACKET_GOP_UNIFIED: + case TAV_PACKET_GOP_UNIFIED_MOTION: + let gopSize = seqread.readOneByte() + + let size0 = 0 + if (packetType === TAV_PACKET_GOP_UNIFIED_MOTION) { + size0 = seqread.readInt() + stats.totalVideoBytes += size0 + stats.gopUnifiedMotionCount++ + seqread.skip(size0) + } + + let size1 = seqread.readInt() + stats.totalVideoBytes += size1 + seqread.skip(size1) + + stats.totalGopFrames += gopSize + if (packetType === TAV_PACKET_GOP_UNIFIED) { + stats.gopUnifiedCount++ + } + + let totalSize = size0 + size1 + let bytesPerFrame = (totalSize / gopSize).toFixed(2) + output.push(` - GOP size=${gopSize}, data size=${totalSize} bytes (${bytesPerFrame} bytes/frame)`) + break + + case TAV_PACKET_GOP_SYNC: + let frameCount = seqread.readOneByte() + stats.gopSyncCount++ + currentFrame += frameCount + output.push(` - ${frameCount} frames decoded from GOP block`) + break + + case TAV_PACKET_IFRAME: + case TAV_PACKET_PFRAME: + case TAV_PACKET_VIDEO_CH2_I: + case TAV_PACKET_VIDEO_CH2_P: + case TAV_PACKET_VIDEO_CH3_I: + case TAV_PACKET_VIDEO_CH3_P: + case TAV_PACKET_VIDEO_CH4_I: + case TAV_PACKET_VIDEO_CH4_P: + case TAV_PACKET_VIDEO_CH5_I: + case TAV_PACKET_VIDEO_CH5_P: + case TAV_PACKET_VIDEO_CH6_I: + case TAV_PACKET_VIDEO_CH6_P: + case TAV_PACKET_VIDEO_CH7_I: + case TAV_PACKET_VIDEO_CH7_P: + case TAV_PACKET_VIDEO_CH8_I: + case TAV_PACKET_VIDEO_CH8_P: + case TAV_PACKET_VIDEO_CH9_I: + case TAV_PACKET_VIDEO_CH9_P: + let size = seqread.readInt() + stats.totalVideoBytes += size + + let frameInfo = getFrameInfo(size) + + if (packetType === TAV_PACKET_PFRAME || + (packetType >= 0x71 && packetType <= 0x7F && (packetType & 1))) { + // P-frame + if (packetType === TAV_PACKET_PFRAME) { + stats.pframeCount++ + if (frameInfo.mode === FRAME_MODE_INTRA) stats.pframeIntraCount++ + else if (frameInfo.mode === FRAME_MODE_DELTA) stats.pframeDeltaCount++ + else if (frameInfo.mode === FRAME_MODE_SKIP) stats.pframeSkipCount++ + currentFrame++ + } else { + stats.muxVideoCount++ + } + } else { + // I-frame + if (packetType === TAV_PACKET_IFRAME) { + stats.iframeCount++ + currentFrame++ + } else { + stats.muxVideoCount++ + } + } + + output.push(` - size=${size} bytes`) + + if (frameInfo.mode >= 0) { + if (frameInfo.mode === FRAME_MODE_SKIP) output.push(" [SKIP]") + else if (frameInfo.mode === FRAME_MODE_DELTA) output.push(" [DELTA]") + else if (frameInfo.mode === FRAME_MODE_INTRA) output.push(" [INTRA]") + + if (frameInfo.mode !== FRAME_MODE_SKIP) { + if (frameInfo.quantiser !== 0xFF) { + output.push(` [Q=${frameInfo.quantiser}]`) + } + } + } + + if (packetType >= 0x70 && packetType <= 0x7F) { + let channel = Math.floor((packetType - 0x70) / 2) + 2 + output.push(` (Channel ${channel})`) + } + break + + case TAV_PACKET_AUDIO_MP2: + stats.audioCount++ + stats.audioMp2Count++ + let mp2Size = seqread.readInt() + stats.totalAudioBytes += mp2Size + stats.audioMp2Bytes += mp2Size + output.push(` - size=${mp2Size} bytes`) + seqread.skip(mp2Size) + break + + case TAV_PACKET_AUDIO_PCM8: + stats.audioCount++ + stats.audioPcm8Count++ + let pcm8Size = seqread.readInt() + stats.totalAudioBytes += pcm8Size + stats.audioPcm8Bytes += pcm8Size + output.push(` - size=${pcm8Size} bytes (zstd compressed)`) + seqread.skip(pcm8Size) + break + + case TAV_PACKET_AUDIO_TAD: + stats.audioCount++ + stats.audioTadCount++ + + let sampleCount0 = seqread.readShort() + let payloadSizePlus7 = seqread.readInt() + let sampleCount = seqread.readShort() + let quantiser = seqread.readOneByte() + let compressedSize = seqread.readInt() + + stats.totalAudioBytes += compressedSize + stats.audioTadBytes += compressedSize + + output.push(` - samples=${sampleCount}, size=${compressedSize} bytes, quantiser=${quantiser * 2 + 1} steps (index ${quantiser})`) + seqread.skip(compressedSize) + break + + case TAV_PACKET_AUDIO_TRACK: + stats.audioCount++ + stats.audioTrackCount++ + let trackSize = seqread.readInt() + stats.totalAudioBytes += trackSize + stats.audioTrackBytes += trackSize + output.push(` - size=${trackSize} bytes (separate track)`) + seqread.skip(trackSize) + break + + case TAV_PACKET_SUBTITLE: + case TAV_PACKET_SUBTITLE_TC: + stats.subtitleCount++ + let subSize = seqread.readInt() + output.push(` - size=${subSize} bytes`) + parseSubtitlePacket(subSize, packetType === TAV_PACKET_SUBTITLE_TC, output) + break + + case TAV_PACKET_VIDEOTEX: + stats.videotexCount++ + let vtSize = seqread.readInt() + stats.videotexBytes += vtSize + parseVideotexPacket(vtSize, output) + break + + case TAV_PACKET_EXIF: + case TAV_PACKET_ID3V1: + case TAV_PACKET_ID3V2: + case TAV_PACKET_VORBIS_COMMENT: + case TAV_PACKET_CD_TEXT: + stats.metadataCount++ + let metaSize = seqread.readInt() + output.push(` - size=${metaSize} bytes`) + seqread.skip(metaSize) + break + + case TAV_PACKET_LOOP_START: + case TAV_PACKET_LOOP_END: + stats.loopPointCount++ + output.push(" (no payload)") + break + + case TAV_PACKET_SCREEN_MASK: + let frameNumber = seqread.readInt() + let top = seqread.readShort() + let right = seqread.readShort() + let bottom = seqread.readShort() + let left = seqread.readShort() + output.push(` - Frame=${frameNumber} [top=${top}, right=${right}, bottom=${bottom}, left=${left}]`) + break + + case TAV_PACKET_SYNC: + stats.syncCount++ + break + + case TAV_PACKET_SYNC_NTSC: + stats.syncNtscCount++ + break + + case TAV_PACKET_NOOP: + // Silent no-op + break + + default: + stats.unknownCount++ + output.push(" (UNKNOWN)") + break + } + + output.push("\n") + packetNum++ + } + } catch (e) { + output.push(`\nError during packet parsing: ${e}\n`) + } + + // Print summary + output.push("\n==================================================\n") + output.push("Summary Statistics:\n") + output.push("==================================================\n") + output.push(`Total packets: ${packetNum}\n`) + output.push("\nVideo:\n") + output.push(` I-frames: ${stats.iframeCount}\n`) + output.push(` P-frames: ${stats.pframeCount}`) + if (stats.pframeCount > 0) { + output.push(` (INTRA: ${stats.pframeIntraCount}, DELTA: ${stats.pframeDeltaCount}, SKIP: ${stats.pframeSkipCount}`) + let knownModes = stats.pframeIntraCount + stats.pframeDeltaCount + stats.pframeSkipCount + if (knownModes < stats.pframeCount) { + output.push(`, Unknown: ${stats.pframeCount - knownModes}`) + } + output.push(")") + } + output.push("\n") + if (stats.gopUnifiedCount + stats.gopUnifiedMotionCount > 0) { + let avgFramesPerGop = (stats.totalGopFrames / (stats.gopUnifiedCount + stats.gopUnifiedMotionCount)).toFixed(1) + output.push(` 3D GOP packets: ${stats.gopUnifiedCount + stats.gopUnifiedMotionCount} (total frames: ${stats.totalGopFrames}, avg ${avgFramesPerGop} frames/GOP)\n`) + output.push(` GOP sync packets: ${stats.gopSyncCount}\n`) + } + output.push(` Mux video: ${stats.muxVideoCount}\n`) + output.push(` Total video bytes: ${stats.totalVideoBytes} (${(stats.totalVideoBytes / 1024 / 1024).toFixed(2)} MB)\n`) + output.push("\nAudio:\n") + output.push(` Total packets: ${stats.audioCount}\n`) + if (stats.audioMp2Count > 0) { + output.push(` MP2: ${stats.audioMp2Count} packets, ${stats.audioMp2Bytes} bytes (${(stats.audioMp2Bytes / 1024 / 1024).toFixed(2)} MB)\n`) + } + if (stats.audioPcm8Count > 0) { + output.push(` PCM8 (zstd): ${stats.audioPcm8Count} packets, ${stats.audioPcm8Bytes} bytes (${(stats.audioPcm8Bytes / 1024 / 1024).toFixed(2)} MB)\n`) + } + if (stats.audioTadCount > 0) { + output.push(` TAD32 (zstd): ${stats.audioTadCount} packets, ${stats.audioTadBytes} bytes (${(stats.audioTadBytes / 1024 / 1024).toFixed(2)} MB)\n`) + } + if (stats.audioTrackCount > 0) { + output.push(` Separate track: ${stats.audioTrackCount} packets, ${stats.audioTrackBytes} bytes (${(stats.audioTrackBytes / 1024 / 1024).toFixed(2)} MB)\n`) + } + output.push(` Total audio bytes: ${stats.totalAudioBytes} (${(stats.totalAudioBytes / 1024 / 1024).toFixed(2)} MB)\n`) + output.push("\nOther:\n") + output.push(` Timecodes: ${stats.timecodeCount}\n`) + output.push(` Subtitles: ${stats.subtitleCount}\n`) + if (stats.videotexCount > 0) { + output.push(` Videotex frames: ${stats.videotexCount} (${stats.videotexBytes} bytes, ${(stats.videotexBytes / 1024 / 1024).toFixed(2)} MB)\n`) + } + output.push(` Extended headers: ${stats.extendedHeaderCount}\n`) + output.push(` Metadata packets: ${stats.metadataCount}\n`) + output.push(` Loop points: ${stats.loopPointCount}\n`) + output.push(` Sync packets: ${stats.syncCount}\n`) + output.push(` NTSC sync packets: ${stats.syncNtscCount}\n`) + output.push(` Unknown packets: ${stats.unknownCount}\n`) + + // Write output to file + try { + let outputStr = output.join("") + files.open(outputPath).swrite(outputStr) + println(`Analysis complete. Report written to ${outputPath}`) + return 0 + } catch (e) { + println(`Error writing output file: ${e}`) + return 1 + } +} + +return main() diff --git a/terranmon.txt b/terranmon.txt index 32c1aaf..41f033f 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -697,7 +697,7 @@ DCT-based compression, optional motion compensation, and efficient temporal codi ... ## Header (24 bytes) - uint8 Magic[8]: "\x1F TSVM TEV" or "\x1F TSVM TEP" + uint8 Magic[8]: "\x1FTSVMTEV" or "\x1FTSVMTEP" uint8 Version: 2 (YCoCg-R) or 3 (ICtCp) uint16 Width: video width in pixels uint16 Height: video height in pixels @@ -903,7 +903,7 @@ transmission capability, and region-of-interest coding. ... ## Header (32 bytes) - uint8 Magic[8]: "\x1F TSVM TAV" or "\x1F TSVM TAP" + uint8 Magic[8]: "\x1FTSVMTAV" or "\x1FTSVMTAP" uint8 Version: Base version number: - 1 = YCoCg-R multi-tile uniform @@ -967,6 +967,7 @@ transmission capability, and region-of-interest coding. Preset "Default" -> 0x00 Preset "Sports" -> 0x01 Preset "Anime" -> 0x02 + NOTE: not all presets have preset flags. See Preset section for details. uint8 Reserved[1]: fill with zeros uint8 Device Orientation - 0 = No rotation @@ -983,6 +984,14 @@ transmission capability, and region-of-interest coding. When header-only file contain video packets, they should be presented as an Intro Movie before the user-interactable selector (served by the UCF payoad) +### Presets +The encoder supports following presets: +- Sports: use finer temporal quantisation, resulting in better-preserved motion. Less effective as resolution goes up +- Anime: instructs the decoder to disable grain synthensis +- D1/D1PAL: encode to D1-compatible video. This mode overrides quantiser setting, audio format is fixed to PCMu8, resolution is fixed to D1/D1PAL, interlacing is forced, and produces header-less file. All extra packets (subtitles, metadata, etc.) are dropped. +- D1P/D1PALP: same as D1/D1PAL but progressive mode is used. + + ## Packet Structure (some special packets have no payload. See Packet Types for details) uint8 Packet Type uint32 Payload Size @@ -1618,7 +1627,7 @@ is stored separately and quality index is shared with that of the video. ... ## Header (16 bytes) - uint8 Magic[8]: "\x1F TSVM TAD" + uint8 Magic[8]: "\x1FTSVMTAD" uint8 Version: 1 uint8 Quality Level: 0-5 (0=lowest quality/smallest, 5=highest quality/largest) uint8 Flags: @@ -1911,7 +1920,7 @@ A universal, simple cue designed to work as both playlist to cue up external fil ... ## Header (16 bytes) - uint8 Magic[8]: "\x1F TSVM UCF" + uint8 Magic[8]: "\x1FTSVMUCF" uint8 Version: 1 uint16 Number of cue elements uint32 (Optional) Size of the cue file, useful for allocating fixed length for future expansion; 0 when not used diff --git a/video_encoder/encoder_tav.c b/video_encoder/encoder_tav.c index f78b744..623f887 100644 --- a/video_encoder/encoder_tav.c +++ b/video_encoder/encoder_tav.c @@ -2395,6 +2395,11 @@ static int parse_resolution(const char *res_str, int *width, int *height, const *height = 144; return 1; } + if (strcmp(res_str, "vga") == 0 || strcmp(res_str, "VGA") == 0) { + *width = 640; + *height = 480; + return 1; + } if (strcmp(res_str, "d1") == 0 || strcmp(res_str, "D1") == 0) { *width = 720; *height = 480; @@ -2405,9 +2410,109 @@ static int parse_resolution(const char *res_str, int *width, int *height, const *height = 576; return 1; } + if (strcmp(res_str, "960h") == 0 || strcmp(res_str, "960H") == 0) { + *width = 960; + *height = 576; + return 1; + } + // HD-ish resolutions + if (strcmp(res_str, "540p") == 0 || strcmp(res_str, "540P") == 0 || strcmp(res_str, "qHD") == 0) { + *width = 960; + *height = 540; + return 1; + } + if (strcmp(res_str, "720p") == 0 || strcmp(res_str, "720P") == 0 || strcmp(res_str, "wxga") == 0 || strcmp(res_str, "WXGA") == 0) { + *width = 1280; + *height = 720; + return 1; + } + if (strcmp(res_str, "800p") == 0 || strcmp(res_str, "800P") == 0) { + *width = 1280; + *height = 800; + return 1; + } + if (strcmp(res_str, "900p") == 0 || strcmp(res_str, "900P") == 0) { + *width = 1600; + *height = 900; + return 1; + } + if (strcmp(res_str, "960p") == 0 || strcmp(res_str, "960P") == 0 || strcmp(res_str, "wsxga") == 0 || strcmp(res_str, "WSXGA") == 0) { + *width = 1706; + *height = 960; + return 1; + } + if (strcmp(res_str, "1080p") == 0 || strcmp(res_str, "1080P") == 0 || strcmp(res_str, "fhd") == 0 || strcmp(res_str, "FHD") == 0 || strcmp(res_str, "wuxga") == 0 || strcmp(res_str, "WUXGA") == 0) { + *width = 1920; + *height = 1080; + return 1; + } + if (strcmp(res_str, "1440p") == 0 || strcmp(res_str, "1440P") == 0 || strcmp(res_str, "wqhd") == 0 || strcmp(res_str, "WQHD") == 0) { + *width = 2560; + *height = 1440; + return 1; + } + if (strcmp(res_str, "4k") == 0 || strcmp(res_str, "4K") == 0 || strcmp(res_str, "2160p") == 0 || strcmp(res_str, "2160p") == 0 || strcmp(res_str, "uhd") == 0 || strcmp(res_str, "UHD") == 0) { + *width = 3840; + *height = 2160; + return 1; + } + // 4K Univisium + if (strcmp(res_str, "4ku") == 0 || strcmp(res_str, "4KU") == 0) { + *width = 4096; + *height = 2048; + return 1; + } + // 3K Univisium + if (strcmp(res_str, "3ku") == 0 || strcmp(res_str, "3KU") == 0) { + *width = 3072; + *height = 1536; + return 1; + } + // 2K Univisium + if (strcmp(res_str, "2ku") == 0 || strcmp(res_str, "2KU") == 0) { + *width = 2048; + *height = 1024; + return 1; + } + // 1K Univisium + if (strcmp(res_str, "1ku") == 0 || strcmp(res_str, "1KU") == 0) { + *width = 1024; + *height = 512; + return 1; + } + // 4K DCI + if (strcmp(res_str, "4kdci") == 0 || strcmp(res_str, "4KDCI") == 0 || strcmp(res_str, "4k_dci") == 0 || strcmp(res_str, "4K_DCI") == 0 || strcmp(res_str, "4k-dci") == 0 || strcmp(res_str, "4K-DCI") == 0) { + *width = 4096; + *height = 2160; + return 1; + } + // 2.5K DCI + if (strcmp(res_str, "2.5kdci") == 0 || strcmp(res_str, "2.5KDCI") == 0 || strcmp(res_str, "2.5k_dci") == 0 || strcmp(res_str, "2.5K_DCI") == 0 || strcmp(res_str, "2.5k-dci") == 0 || strcmp(res_str, "2.5K-DCI") == 0 || + strcmp(res_str, "2,5kdci") == 0 || strcmp(res_str, "2,5KDCI") == 0 || strcmp(res_str, "2,5k_dci") == 0 || strcmp(res_str, "2,5K_DCI") == 0 || strcmp(res_str, "2,5k-dci") == 0 || strcmp(res_str, "2,5K-DCI") == 0) { + *width = 2560; + *height = 1350; + return 1; + } + // 2K DCI + if (strcmp(res_str, "2kdci") == 0 || strcmp(res_str, "2KDCI") == 0 || strcmp(res_str, "2k_dci") == 0 || strcmp(res_str, "2K_DCI") == 0 || strcmp(res_str, "2k-dci") == 0 || strcmp(res_str, "2K-DCI") == 0) { + *width = 2048; + *height = 1080; + return 1; + } + // 1K DCI + if (strcmp(res_str, "1kdci") == 0 || strcmp(res_str, "1KDCI") == 0 || strcmp(res_str, "1k_dci") == 0 || strcmp(res_str, "1K_DCI") == 0 || strcmp(res_str, "1k-dci") == 0 || strcmp(res_str, "1K-DCI") == 0) { + *width = 1024; + *height = 540; + return 1; + } if (strcmp(res_str, "half") == 0 || strcmp(res_str, "HALF") == 0) { - *width = DEFAULT_WIDTH >> 1; - *height = DEFAULT_HEIGHT >> 1; + *width = 280; + *height = 224; + return 1; + } + if (strcmp(res_str, "full") == 0 || strcmp(res_str, "FULL") == 0 || strcmp(res_str, "tsvm") == 0 || strcmp(res_str, "TSVM") == 0) { + *width = 560; + *height = 448; return 1; } if (strcmp(res_str, "default") == 0 || strcmp(res_str, "DEFAULT") == 0) {