// Created by Claude on 2025-08-18. // TSVM Enhanced Video (TEV) Format Decoder - YCoCg-R 4:2:0 Version // Usage: playtev moviefile.tev [options] const WIDTH = 560 const HEIGHT = 448 const BLOCK_SIZE = 16 // 16x16 blocks for YCoCg-R const TEV_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x54, 0x45, 0x56] // "\x1FTSVM TEV" const TEV_VERSION = 2 // YCoCg-R version // Block encoding modes const TEV_MODE_SKIP = 0x00 const TEV_MODE_INTRA = 0x01 const TEV_MODE_INTER = 0x02 const TEV_MODE_MOTION = 0x03 // Packet types const TEV_PACKET_IFRAME = 0x10 const TEV_PACKET_PFRAME = 0x11 const TEV_PACKET_AUDIO_MP2 = 0x20 const TEV_PACKET_SYNC = 0xFF const interactive = exec_args[2] && exec_args[2].toLowerCase() == "-i" const fullFilePath = _G.shell.resolvePathInput(exec_args[1]) const FILE_LENGTH = files.open(fullFilePath.full).size let videoRateBin = [] let errorlevel = 0 let notifHideTimer = 0 const NOTIF_SHOWUPTIME = 3000000000 let [cy, cx] = con.getyx() if (interactive) { con.move(1,1) println("Push and hold Backspace to exit") } let seqreadserial = require("seqread") let seqreadtape = require("seqreadtape") let seqread = undefined let fullFilePathStr = fullFilePath.full // Select seqread driver to use if (fullFilePathStr.startsWith('$:/TAPE') || fullFilePathStr.startsWith('$:\\TAPE')) { seqread = seqreadtape seqread.seek(0) } else { seqread = seqreadserial } seqread.prepare(fullFilePathStr) con.clear() con.curs_set(0) graphics.setGraphicsMode(4) // 4096-color mode graphics.clearPixels(0) graphics.clearPixels2(0) // Check magic number let magic = seqread.readBytes(8) let magicMatching = true let actualMagic = [] TEV_MAGIC.forEach((b, i) => { let testb = sys.peek(magic + i) & 255 actualMagic.push(testb) if (testb != b) { magicMatching = false } }) sys.free(magic) if (!magicMatching) { println("Not a TEV file (MAGIC mismatch) -- got " + actualMagic.join()) return 1 } // Read header let version = seqread.readOneByte() if (version !== TEV_VERSION) { println(`Unsupported TEV version: ${version} (expected ${TEV_VERSION})`) return 1 } let width = seqread.readShort() let height = seqread.readShort() let fps = seqread.readOneByte() let totalFrames = seqread.readInt() let quality = seqread.readOneByte() let hasAudio = seqread.readOneByte() function updateDataRateBin(rate) { videoRateBin.push(rate) if (videoRateBin.length > fps) { videoRateBin.shift() } } function getVideoRate(rate) { let baseRate = videoRateBin.reduce((a, c) => a + c, 0) let mult = fps / videoRateBin.length return baseRate * mult } let frameTime = 1.0 / fps // Ultra-fast approach: always render to display, use dedicated previous frame buffer const FRAME_PIXELS = width * height // Frame buffer addresses for graphics display const DISPLAY_RG_ADDR = -1048577 // Main graphics RG plane (displayed) const DISPLAY_BA_ADDR = -1310721 // Main graphics BA plane (displayed) // RGB frame buffers (24-bit: R,G,B per pixel) const FRAME_SIZE = 560*448*3 // Total frame size = 752,640 bytes // Allocate frame buffers - malloc works correctly, addresses are start addresses const CURRENT_RGB_ADDR = sys.malloc(FRAME_SIZE) const PREV_RGB_ADDR = sys.malloc(FRAME_SIZE) // Working memory for blocks (minimal allocation) let ycocgWorkspace = sys.malloc(BLOCK_SIZE * BLOCK_SIZE * 3) // Y+Co+Cg workspace let dctWorkspace = sys.malloc(BLOCK_SIZE * BLOCK_SIZE * 4) // DCT coefficients (floats) // Initialize RGB frame buffers to black (0,0,0) sys.memset(CURRENT_RGB_ADDR, 0, FRAME_PIXELS * 3) sys.memset(PREV_RGB_ADDR, 0, FRAME_PIXELS * 3) // Initialize display framebuffer to black sys.memset(DISPLAY_RG_ADDR, 0, FRAME_PIXELS) // Black in RG plane sys.memset(DISPLAY_BA_ADDR, 15, FRAME_PIXELS) // Black with alpha=15 (opaque) in BA plane let frameCount = 0 let stopPlay = false // 4x4 Bayer dithering matrix const BAYER_MATRIX = [ [ 0, 8, 2,10], [12, 4,14, 6], [ 3,11, 1, 9], [15, 7,13, 5] ] // Apply Bayer dithering to reduce banding when quantizing to 4-bit function ditherValue(value, x, y) { // Get the dither threshold for this pixel position const threshold = BAYER_MATRIX[y & 3][x & 3] // Scale threshold from 0-15 to 0-15.9375 (16 steps over 16 values) const scaledThreshold = threshold / 16.0 // Add dither and quantize to 4-bit (0-15) const dithered = value + scaledThreshold return Math.max(0, Math.min(15, Math.floor(dithered * 15 / 255))) } // Main decoding loop - simplified for performance try { while (!stopPlay && seqread.getReadCount() < FILE_LENGTH && frameCount < totalFrames) { // Handle interactive controls if (interactive) { sys.poke(-40, 1) if (sys.peek(-41) == 67) { // Backspace stopPlay = true break } } // Read packet (1 byte: type) let packetType = seqread.readOneByte() if (packetType == 0xFF) { // Sync packet // Sync packet - frame complete frameCount++ // Copy current RGB frame to previous frame buffer for next frame reference // This is the only copying we need, and it happens once per frame after display sys.memcpy(CURRENT_RGB_ADDR, PREV_RGB_ADDR, FRAME_PIXELS * 3) } else if (packetType == TEV_PACKET_IFRAME || packetType == TEV_PACKET_PFRAME) { // Video frame packet let payloadLen = seqread.readInt() let compressedPtr = seqread.readBytes(payloadLen) updateDataRateBin(payloadLen) // Basic sanity check on compressed data if (payloadLen <= 0 || payloadLen > 1000000) { serial.println(`Frame ${frameCount}: Invalid payload length: ${payloadLen}`) sys.free(compressedPtr) continue } // Decompress using gzip // Calculate proper buffer size for TEV YCoCg-R blocks let blocksX = (width + 15) >> 4 // 16x16 blocks let blocksY = (height + 15) >> 4 let tevBlockSize = 1 + 4 + 2 + (256 * 2) + (64 * 2) + (64 * 2) // mode + mv + cbp + Y(16x16) + Co(8x8) + Cg(8x8) let decompressedSize = blocksX * blocksY * tevBlockSize * 2 // Double for safety let blockDataPtr = sys.malloc(decompressedSize) let actualSize try { // Use gzip decompression (only compression format supported in TSVM JS) actualSize = gzip.decompFromTo(compressedPtr, payloadLen, blockDataPtr) } catch (e) { // Decompression failed - skip this frame serial.println(`Frame ${frameCount}: Gzip decompression failed, skipping (compressed size: ${payloadLen}, error: ${e})`) sys.free(blockDataPtr) sys.free(compressedPtr) continue } // Hardware-accelerated TEV YCoCg-R decoding to RGB buffers try { graphics.tevDecode(blockDataPtr, CURRENT_RGB_ADDR, PREV_RGB_ADDR, width, height, quality) // graphics.tevDecode(blockDataPtr, CURRENT_RGB_ADDR, PREV_RGB_ADDR, width, height, 0) // force quality 0 for testing // Upload RGB buffer to display framebuffer with dithering graphics.uploadRGBToFramebuffer(CURRENT_RGB_ADDR, DISPLAY_RG_ADDR, DISPLAY_BA_ADDR, width, height, frameCount) } catch (e) { serial.println(`Frame ${frameCount}: Hardware YCoCg-R decode failed: ${e}`) } sys.free(blockDataPtr) sys.free(compressedPtr) } else if (packetType == TEV_PACKET_AUDIO_MP2) { // Audio packet - skip for now let audioLen = seqread.readInt() seqread.skip(audioLen) } else { println(`Unknown packet type: 0x${packetType.toString(16)}`) break } // Simple progress display if (interactive) { con.move(31, 1) graphics.setTextFore(161) print(`Frame: ${frameCount}/${totalFrames} (${Math.round(frameCount * 100 / totalFrames)}%) YCoCg-R`) con.move(32, 1) graphics.setTextFore(161) print(`VRate: ${(getVideoRate() / 1024 * 8)|0} kbps `) con.move(1, 1) } } } catch (e) { printerrln(`TEV YCoCg-R decode error: ${e}`) errorlevel = 1 } finally { // Cleanup working memory (graphics memory is automatically managed) sys.free(ycocgWorkspace) sys.free(dctWorkspace) if (CURRENT_RGB_ADDR > 0) sys.free(CURRENT_RGB_ADDR) if (PREV_RGB_ADDR > 0) sys.free(PREV_RGB_ADDR) audio.stop(0) audio.purgeQueue(0) if (interactive) { //con.clear() } } con.move(cy, cx) // restore cursor return errorlevel