From 70fda528e20fbcc3e3b0e44d2293bbe3941342fa Mon Sep 17 00:00:00 2001 From: minjaesong Date: Mon, 18 Aug 2025 19:54:02 +0900 Subject: [PATCH] ycocg wip --- assets/disk0/tvdos/bin/playmov.js | 8 +- assets/disk0/tvdos/bin/playtev.js | 355 ++--- assets/disk0/tvdos/include/seqreadtape.mjs | 3 +- .../torvald/tsvm/GraphicsJSR223Delegate.kt | 545 ++++++-- .../src/net/torvald/tsvm/VMJSR223Delegate.kt | 7 + video_encoder/encoder_ipf1d.c | 935 +++++++++++++ video_encoder/encoder_tev.c | 1169 +++++++++-------- video_encoder/encoder_tev_rgb24_8x8.c | 815 ++++++++++++ 8 files changed, 2898 insertions(+), 939 deletions(-) create mode 100644 video_encoder/encoder_ipf1d.c create mode 100644 video_encoder/encoder_tev_rgb24_8x8.c diff --git a/assets/disk0/tvdos/bin/playmov.js b/assets/disk0/tvdos/bin/playmov.js index cb1ee77..526aeea 100644 --- a/assets/disk0/tvdos/bin/playmov.js +++ b/assets/disk0/tvdos/bin/playmov.js @@ -16,19 +16,15 @@ con.clear();con.curs_set(0) graphics.clearPixels(255) graphics.clearPixels2(240) - -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) + seqread = require("seqreadtape") } else { - seqread = seqreadserial + seqread = require("seqread") } seqread.prepare(fullFilePathStr) diff --git a/assets/disk0/tvdos/bin/playtev.js b/assets/disk0/tvdos/bin/playtev.js index 8b82db8..cd4281c 100644 --- a/assets/disk0/tvdos/bin/playtev.js +++ b/assets/disk0/tvdos/bin/playtev.js @@ -1,11 +1,12 @@ -// Created by Claude on 2025-08-17. -// TSVM Enhanced Video (TEV) Format Decoder +// 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 = 8 +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 @@ -23,76 +24,80 @@ 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 -// Quantization tables (8 quality levels) -const QUANT_TABLES = [ - // Quality 0 (lowest) - [80, 60, 50, 80, 120, 200, 255, 255, - 55, 60, 70, 95, 130, 255, 255, 255, - 70, 65, 80, 120, 200, 255, 255, 255, - 70, 85, 110, 145, 255, 255, 255, 255, - 90, 110, 185, 255, 255, 255, 255, 255, - 120, 175, 255, 255, 255, 255, 255, 255, - 245, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255], - // Quality 1-6 (simplified) - [40, 30, 25, 40, 60, 100, 128, 150, - 28, 30, 35, 48, 65, 128, 150, 180, - 35, 33, 40, 60, 100, 128, 150, 180, - 35, 43, 55, 73, 128, 150, 180, 200, - 45, 55, 93, 128, 150, 180, 200, 220, - 60, 88, 128, 150, 180, 200, 220, 240, - 123, 128, 150, 180, 200, 220, 240, 250, - 128, 150, 180, 200, 220, 240, 250, 255], - [20, 15, 13, 20, 30, 50, 64, 75, - 14, 15, 18, 24, 33, 64, 75, 90, - 18, 17, 20, 30, 50, 64, 75, 90, - 18, 22, 28, 37, 64, 75, 90, 100, - 23, 28, 47, 64, 75, 90, 100, 110, - 30, 44, 64, 75, 90, 100, 110, 120, - 62, 64, 75, 90, 100, 110, 120, 125, - 64, 75, 90, 100, 110, 120, 125, 128], - [16, 12, 10, 16, 24, 40, 51, 60, - 11, 12, 14, 19, 26, 51, 60, 72, - 14, 13, 16, 24, 40, 51, 60, 72, - 14, 17, 22, 29, 51, 60, 72, 80, - 18, 22, 37, 51, 60, 72, 80, 88, - 24, 35, 51, 60, 72, 80, 88, 96, - 49, 51, 60, 72, 80, 88, 96, 100, - 51, 60, 72, 80, 88, 96, 100, 102], - [12, 9, 8, 12, 18, 30, 38, 45, - 8, 9, 11, 14, 20, 38, 45, 54, - 11, 10, 12, 18, 30, 38, 45, 54, - 11, 13, 17, 22, 38, 45, 54, 60, - 14, 17, 28, 38, 45, 54, 60, 66, - 18, 26, 38, 45, 54, 60, 66, 72, - 37, 38, 45, 54, 60, 66, 72, 75, - 38, 45, 54, 60, 66, 72, 75, 77], - [10, 7, 6, 10, 15, 25, 32, 38, - 7, 7, 9, 12, 16, 32, 38, 45, - 9, 8, 10, 15, 25, 32, 38, 45, - 9, 11, 14, 18, 32, 38, 45, 50, - 12, 14, 23, 32, 38, 45, 50, 55, - 15, 22, 32, 38, 45, 50, 55, 60, - 31, 32, 38, 45, 50, 55, 60, 63, - 32, 38, 45, 50, 55, 60, 63, 65], - [8, 6, 5, 8, 12, 20, 26, 30, - 6, 6, 7, 10, 13, 26, 30, 36, - 7, 7, 8, 12, 20, 26, 30, 36, - 7, 9, 11, 15, 26, 30, 36, 40, - 10, 11, 19, 26, 30, 36, 40, 44, - 12, 17, 26, 30, 36, 40, 44, 48, - 25, 26, 30, 36, 40, 44, 48, 50, - 26, 30, 36, 40, 44, 48, 50, 52], +// Quantization tables for Y channel (16x16 - just use first 8 quality levels) +const QUANT_TABLES_Y = [ + // Quality 0 (lowest) - 8x8 pattern repeated to 16x16 + (() => { + const base = [80, 60, 50, 80, 120, 200, 255, 255, + 55, 60, 70, 95, 130, 255, 255, 255, + 70, 65, 80, 120, 200, 255, 255, 255, + 70, 85, 110, 145, 255, 255, 255, 255, + 90, 110, 185, 255, 255, 255, 255, 255, + 120, 175, 255, 255, 255, 255, 255, 255, + 245, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255] + const extended = [] + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + extended.push(base[(y % 8) * 8 + (x % 8)]) + } + } + return extended + })(), + [40, 30, 25, 40, 60, 100, 128, 150, 28, 30, 35, 48, 65, 128, 150, 180], // Quality 1 (simplified) + [20, 15, 13, 20, 30, 50, 64, 75, 14, 15, 18, 24, 33, 64, 75, 90], // Quality 2 + [16, 12, 10, 16, 24, 40, 51, 60, 11, 12, 14, 19, 26, 51, 60, 72], // Quality 3 + [12, 9, 8, 12, 18, 30, 38, 45, 8, 9, 11, 14, 20, 38, 45, 54], // Quality 4 + [10, 7, 6, 10, 15, 25, 32, 38, 7, 7, 9, 12, 16, 32, 38, 45], // Quality 5 + [8, 6, 5, 8, 12, 20, 26, 30, 6, 6, 7, 10, 13, 26, 30, 36], // Quality 6 // Quality 7 (highest) - [2, 1, 1, 2, 3, 5, 6, 7, - 1, 1, 1, 2, 3, 6, 7, 9, - 1, 1, 2, 3, 5, 6, 7, 9, - 1, 2, 3, 4, 6, 7, 9, 10, - 2, 3, 5, 6, 7, 9, 10, 11, - 3, 4, 6, 7, 9, 10, 11, 12, - 6, 6, 7, 9, 10, 11, 12, 13, - 6, 7, 9, 10, 11, 12, 13, 13] + (() => { + const base = [2, 1, 1, 2, 3, 5, 6, 7, + 1, 1, 1, 2, 3, 6, 7, 9, + 1, 1, 2, 3, 5, 6, 7, 9, + 1, 2, 3, 4, 6, 7, 9, 10, + 2, 3, 5, 6, 7, 9, 10, 11, + 3, 4, 6, 7, 9, 10, 11, 12, + 6, 6, 7, 9, 10, 11, 12, 13, + 6, 7, 9, 10, 11, 12, 13, 13] + const extended = [] + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + extended.push(base[(y % 8) * 8 + (x % 8)]) + } + } + return extended + })() ] + +// Quantization tables for chroma channels (8x8) +const QUANT_TABLES_C = [ + // Quality 0 (lowest) + [120, 90, 75, 120, 180, 255, 255, 255, + 83, 90, 105, 143, 195, 255, 255, 255, + 105, 98, 120, 180, 255, 255, 255, 255, + 105, 128, 165, 218, 255, 255, 255, 255, + 135, 165, 278, 255, 255, 255, 255, 255, + 180, 263, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255], + [60, 45, 38, 60, 90, 150, 192, 225], // Quality 1 (simplified) + [30, 23, 19, 30, 45, 75, 96, 113], // Quality 2 + [24, 18, 15, 24, 36, 60, 77, 90], // Quality 3 + [18, 14, 12, 18, 27, 45, 57, 68], // Quality 4 + [15, 11, 9, 15, 23, 38, 48, 57], // Quality 5 + [12, 9, 8, 12, 18, 30, 39, 45], // Quality 6 + // Quality 7 (highest) + [3, 2, 2, 3, 5, 8, 9, 11, + 2, 2, 2, 3, 5, 9, 11, 14, + 2, 2, 3, 5, 8, 9, 11, 14, + 2, 3, 5, 6, 9, 11, 14, 15, + 3, 5, 8, 9, 11, 14, 15, 17, + 5, 6, 9, 11, 14, 15, 17, 18, + 9, 9, 11, 14, 15, 17, 18, 20, + 9, 11, 14, 15, 17, 18, 20, 20] +] + let videoRateBin = [] let errorlevel = 0 let notifHideTimer = 0 @@ -146,17 +151,20 @@ if (!magicMatching) { // Read header let version = seqread.readOneByte() -let flags = 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.readShort() +let fps = seqread.readOneByte() let totalFrames = seqread.readInt() let quality = seqread.readOneByte() -seqread.skip(5) // Reserved bytes +let hasAudio = seqread.readOneByte() function updateDataRateBin(rate) { videoRateBin.push(rate) - if (videoRateBin.length > fps) { videoRateBin.shift() } @@ -168,13 +176,8 @@ function getVideoRate(rate) { return baseRate * mult } -let hasAudio = (flags & 0x01) != 0 let frameTime = 1.0 / fps -//println(`TEV Video: ${width}x${height}, ${fps} FPS, ${totalFrames} frames, Q${quality}`) -//if (hasAudio) println("Audio: MP2 32kHz") -//println(`Blocks: ${(width + 7) >> 3}x${(height + 7) >> 3} (${((width + 7) >> 3) * ((height + 7) >> 3)} total)`) - // Ultra-fast approach: always render to display, use dedicated previous frame buffer const FRAME_PIXELS = width * height @@ -187,8 +190,8 @@ const CURRENT_RGB_ADDR = sys.malloc(560*448*3) // Current frame RGB buffer const PREV_RGB_ADDR = sys.malloc(560*448*3) // Previous frame RGB buffer // Working memory for blocks (minimal allocation) -let rgbWorkspace = sys.malloc(BLOCK_SIZE * BLOCK_SIZE * 3) // 192 bytes -let dctWorkspace = sys.malloc(BLOCK_SIZE * BLOCK_SIZE * 3 * 4) // 768 bytes (floats) +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) for (let i = 0; i < FRAME_PIXELS; i++) { @@ -212,16 +215,6 @@ for (let i = 0; i < FRAME_PIXELS; i++) { let frameCount = 0 let stopPlay = false -// Dequantize DCT coefficient -function dequantizeCoeff(coeff, quant, isDC) { - if (isDC) { - // DC coefficient also needs dequantization - return coeff * quant - } else { - return coeff * quant - } -} - // 4x4 Bayer dithering matrix const BAYER_MATRIX = [ [ 0, 8, 2,10], @@ -243,144 +236,6 @@ function ditherValue(value, x, y) { return Math.max(0, Math.min(15, Math.floor(dithered * 15 / 255))) } -// 8x8 Inverse DCT implementation -function idct8x8(coeffs, quantTable) { - const N = 8 - let block = new Array(64) - - // Dequantize coefficients - for (let i = 0; i < 64; i++) { - block[i] = dequantizeCoeff(coeffs[i], quantTable[i], i === 0) - } - - // IDCT constants - const cos = Math.cos - const sqrt2 = Math.sqrt(2) - const c = new Array(8) - c[0] = 1.0 / sqrt2 - for (let i = 1; i < 8; i++) { - c[i] = 1.0 - } - - let result = new Array(64) - - // 2D IDCT - for (let x = 0; x < N; x++) { - for (let y = 0; y < N; y++) { - let sum = 0.0 - for (let u = 0; u < N; u++) { - for (let v = 0; v < N; v++) { - let coeff = block[v * N + u] - let cosU = cos((2 * x + 1) * u * Math.PI / (2 * N)) - let cosV = cos((2 * y + 1) * v * Math.PI / (2 * N)) - sum += c[u] * c[v] * coeff * cosU * cosV - } - } - result[y * N + x] = sum / 4.0 - } - } - - // Convert to pixel values (0-255) - for (let i = 0; i < 64; i++) { - result[i] = Math.max(0, Math.min(255, Math.round(result[i] + 128))) - } - - return result -} - -// Hardware-accelerated decoding uses graphics.tevIdct8x8() instead of pure JS - -// Hardware-accelerated TEV block decoder -function decodeBlock(blockData, blockX, blockY, prevRG, prevBA, currRG, currBA, quantTable) { - let mode = blockData.mode - let startX = blockX * BLOCK_SIZE - let startY = blockY * BLOCK_SIZE - - if (mode == TEV_MODE_SKIP) { - // Copy from previous frame - for (let dy = 0; dy < BLOCK_SIZE; dy++) { - for (let dx = 0; dx < BLOCK_SIZE; dx++) { - let x = startX + dx - let y = startY + dy - if (x < width && y < height) { - let offset = y * width + x - let prevRGVal = sys.peek(prevRG + offset) - let prevBAVal = sys.peek(prevBA + offset) - sys.poke(currRG - offset, prevRGVal) // Graphics memory uses negative addressing - sys.poke(currBA - offset, prevBAVal) - } - } - } - } else if (mode == TEV_MODE_MOTION) { - // Motion compensation: copy from previous frame with motion vector offset - for (let dy = 0; dy < BLOCK_SIZE; dy++) { - for (let dx = 0; dx < BLOCK_SIZE; dx++) { - let x = startX + dx - let y = startY + dy - let refX = x + blockData.mvX - let refY = y + blockData.mvY - - if (x < width && y < height && refX >= 0 && refX < width && refY >= 0 && refY < height) { - let dstOffset = y * width + x - let refOffset = refY * width + refX - let refRGVal = sys.peek(prevRG + refOffset) - let refBAVal = sys.peek(prevBA + refOffset) - sys.poke(currRG - dstOffset, refRGVal) // Graphics memory uses negative addressing - sys.poke(currBA - dstOffset, refBAVal) - } else if (x < width && y < height) { - // Out of bounds reference - use black - let dstOffset = y * width + x - sys.poke(currRG - dstOffset, 0) // Graphics memory uses negative addressing - sys.poke(currBA - dstOffset, 15) - } - } - } - } else { - // INTRA or INTER modes: Full DCT decoding - - // Extract DCT coefficients for each channel (R, G, B) - let rCoeffs = blockData.dctCoeffs.slice(0 * 64, 1 * 64) // R channel - let gCoeffs = blockData.dctCoeffs.slice(1 * 64, 2 * 64) // G channel - let bCoeffs = blockData.dctCoeffs.slice(2 * 64, 3 * 64) // B channel - - // Perform IDCT for each channel - let rBlock = idct8x8(rCoeffs, quantTable) - let gBlock = idct8x8(gCoeffs, quantTable) - let bBlock = idct8x8(bCoeffs, quantTable) - - // Fill 8x8 block with IDCT results - for (let dy = 0; dy < BLOCK_SIZE; dy++) { - for (let dx = 0; dx < BLOCK_SIZE; dx++) { - let x = startX + dx - let y = startY + dy - if (x < width && y < height) { - let blockOffset = dy * BLOCK_SIZE + dx - let imageOffset = y * width + x - - // Get RGB values from IDCT results - let r = rBlock[blockOffset] - let g = gBlock[blockOffset] - let b = bBlock[blockOffset] - - // Apply Bayer dithering when converting to 4-bit values - let r4 = ditherValue(r, x, y) - let g4 = ditherValue(g, x, y) - let b4 = ditherValue(b, x, y) - - let rgValue = (r4 << 4) | g4 // R in MSB, G in LSB - let baValue = (b4 << 4) | 15 // B in MSB, A=15 (opaque) in LSB - - // Write to graphics memory - sys.poke(currRG - imageOffset, rgValue) // Graphics memory uses negative addressing - sys.poke(currBA - imageOffset, baValue) - } - } - } - } -} - -// Secondary buffers removed - using frame buffers directly - // Main decoding loop - simplified for performance try { while (!stopPlay && seqread.getReadCount() < FILE_LENGTH && frameCount < totalFrames) { @@ -393,18 +248,21 @@ try { } } - // Read packet (2 bytes: type + subtype) - let packetType = seqread.readShort() + // Read packet (1 byte: type) + let packetType = seqread.readOneByte() - if (packetType == 0xFFFF) { // Sync packet + if (packetType == 0xFF) { // Sync packet + // Read length (should be 0) + let syncLen = seqread.readInt() + // 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) + sys.memcpy(PREV_RGB_ADDR, CURRENT_RGB_ADDR, FRAME_PIXELS * 3) - } else if ((packetType & 0xFF) == TEV_PACKET_IFRAME || (packetType & 0xFF) == TEV_PACKET_PFRAME) { + } else if (packetType == TEV_PACKET_IFRAME || packetType == TEV_PACKET_PFRAME) { // Video frame packet let payloadLen = seqread.readInt() let compressedPtr = seqread.readBytes(payloadLen) @@ -417,16 +275,15 @@ try { continue } - // Decompress using zstd (if available) or gzip fallback - // Calculate proper buffer size for TEV blocks (conservative estimate) - let blocksX = (width + 7) >> 3 - let blocksY = (height + 7) >> 3 - let tevBlockSize = 1 + 4 + 2 + (64 * 3 * 2) // mode + mv + cbp + dct_coeffs + // 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 - let decompMethod = "gzip" try { // Use gzip decompression (only compression format supported in TSVM JS) actualSize = gzip.decompFromTo(compressedPtr, payloadLen, blockDataPtr) @@ -438,9 +295,7 @@ try { continue } - // Hardware decode complete - - // Hardware-accelerated TEV decoding to RGB buffers (blazing fast!) + // Hardware-accelerated TEV YCoCg-R decoding to RGB buffers try { graphics.tevDecode(blockDataPtr, CURRENT_RGB_ADDR, PREV_RGB_ADDR, width, height, quality) @@ -449,13 +304,13 @@ try { graphics.uploadRGBToFramebuffer(CURRENT_RGB_ADDR, DISPLAY_RG_ADDR, DISPLAY_BA_ADDR, width, height, frameCount) } catch (e) { - serial.println(`Frame ${frameCount}: Hardware decode failed: ${e}`) + serial.println(`Frame ${frameCount}: Hardware YCoCg-R decode failed: ${e}`) } sys.free(blockDataPtr) sys.free(compressedPtr) - } else if ((packetType & 0xFF) == TEV_PACKET_AUDIO_MP2) { + } else if (packetType == TEV_PACKET_AUDIO_MP2) { // Audio packet - skip for now let audioLen = seqread.readInt() seqread.skip(audioLen) @@ -469,7 +324,7 @@ try { if (interactive) { con.move(31, 1) graphics.setTextFore(161) - print(`Frame: ${frameCount}/${totalFrames} (${Math.round(frameCount * 100 / totalFrames)}%)`) + 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 `) @@ -478,16 +333,15 @@ try { } } catch (e) { - printerrln(`TEV decode error: ${e}`) + printerrln(`TEV YCoCg-R decode error: ${e}`) errorlevel = 1 } finally { // Cleanup working memory (graphics memory is automatically managed) - sys.free(rgbWorkspace) + sys.free(ycocgWorkspace) sys.free(dctWorkspace) sys.free(CURRENT_RGB_ADDR) sys.free(PREV_RGB_ADDR) - audio.stop(0) audio.purgeQueue(0) @@ -496,4 +350,5 @@ try { } } +con.move(cy, cx) // restore cursor return errorlevel \ No newline at end of file diff --git a/assets/disk0/tvdos/include/seqreadtape.mjs b/assets/disk0/tvdos/include/seqreadtape.mjs index ca7358f..20218d3 100644 --- a/assets/disk0/tvdos/include/seqreadtape.mjs +++ b/assets/disk0/tvdos/include/seqreadtape.mjs @@ -251,5 +251,6 @@ exports = { getCurrentTapeDevice, isReady, // Enhanced functions - seek + seek, + rewind } \ No newline at end of file diff --git a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt index c7dd998..9cdb7c7 100644 --- a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt @@ -8,9 +8,6 @@ import net.torvald.tsvm.peripheral.GraphicsAdapter import net.torvald.tsvm.peripheral.fmod import kotlin.math.abs import kotlin.math.roundToInt -import kotlin.math.cos -import kotlin.math.sqrt -import kotlin.math.PI class GraphicsJSR223Delegate(private val vm: VM) { @@ -550,8 +547,9 @@ class GraphicsJSR223Delegate(private val vm: VM) { val qeb = ob - nb val qea = if (useAlpha) oa - na else 0f - val offsets = longArrayOf(k+1, - k+width-1,k+width,k+width+1, + val offsets = longArrayOf( + k + 1, + k + width - 1, k + width, k + width + 1, ) offsets.forEachIndexed { index, offset -> @@ -621,7 +619,7 @@ class GraphicsJSR223Delegate(private val vm: VM) { return (round(f * 8) + 7).coerceIn(0..15) } - fun blockEncodeToYCoCg(blockX: Int, blockY: Int, srcPtr: Int, width: Int, channels: Int, hasAlpha: Boolean, pattern: Int): List { + fun blockEncodeToYCoCgFourBits(blockX: Int, blockY: Int, srcPtr: Int, width: Int, channels: Int, hasAlpha: Boolean, pattern: Int): List { val Ys = IntArray(16) val As = IntArray(16) val COs = FloatArray(16) @@ -658,12 +656,13 @@ class GraphicsJSR223Delegate(private val vm: VM) { return listOf(Ys, As, COs, CGs) } + fun encodeIpf1(srcPtr: Int, destPtr: Int, width: Int, height: Int, channels: Int, hasAlpha: Boolean, pattern: Int) { var writeCount = 0L for (blockY in 0 until ceil(height / 4f)) { for (blockX in 0 until ceil(width / 4f)) { - val (_1, _2, _3, _4) = blockEncodeToYCoCg(blockX, blockY, srcPtr, width, channels, hasAlpha, pattern) + val (_1, _2, _3, _4) = blockEncodeToYCoCgFourBits(blockX, blockY, srcPtr, width, channels, hasAlpha, pattern) val Ys = _1 as IntArray; val As = _2 as IntArray; val COs = _3 as FloatArray; val CGs = _4 as FloatArray // subsample by averaging @@ -846,7 +845,7 @@ class GraphicsJSR223Delegate(private val vm: VM) { for (blockY in 0 until ceil(height / 4f)) { for (blockX in 0 until ceil(width / 4f)) { - val (_1, _2, _3, _4) = blockEncodeToYCoCg(blockX, blockY, srcPtr, width, channels, hasAlpha, pattern) + val (_1, _2, _3, _4) = blockEncodeToYCoCgFourBits(blockX, blockY, srcPtr, width, channels, hasAlpha, pattern) val Ys = _1 as IntArray; val As = _2 as IntArray; val COs = _3 as FloatArray; val CGs = _4 as FloatArray // subsample by averaging @@ -1265,74 +1264,236 @@ class GraphicsJSR223Delegate(private val vm: VM) { // TEV (TSVM Enhanced Video) format support // Created by Claude on 2025-08-17 - val QUANT_TABLES = arrayOf( - // Quality 0 (lowest) - intArrayOf(80, 60, 50, 80, 120, 200, 255, 255, - 55, 60, 70, 95, 130, 255, 255, 255, - 70, 65, 80, 120, 200, 255, 255, 255, - 70, 85, 110, 145, 255, 255, 255, 255, - 90, 110, 185, 255, 255, 255, 255, 255, - 120, 175, 255, 255, 255, 255, 255, 255, - 245, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255), - // Quality 1-6 (simplified) - intArrayOf(40, 30, 25, 40, 60, 100, 128, 150, - 28, 30, 35, 48, 65, 128, 150, 180, - 35, 33, 40, 60, 100, 128, 150, 180, - 35, 43, 55, 73, 128, 150, 180, 200, - 45, 55, 93, 128, 150, 180, 200, 220, - 60, 88, 128, 150, 180, 200, 220, 240, - 123, 128, 150, 180, 200, 220, 240, 250, - 128, 150, 180, 200, 220, 240, 250, 255), - intArrayOf(20, 15, 13, 20, 30, 50, 64, 75, - 14, 15, 18, 24, 33, 64, 75, 90, - 18, 17, 20, 30, 50, 64, 75, 90, - 18, 22, 28, 37, 64, 75, 90, 100, - 23, 28, 47, 64, 75, 90, 100, 110, - 30, 44, 64, 75, 90, 100, 110, 120, - 62, 64, 75, 90, 100, 110, 120, 125, - 64, 75, 90, 100, 110, 120, 125, 128), - intArrayOf(16, 12, 10, 16, 24, 40, 51, 60, - 11, 12, 14, 19, 26, 51, 60, 72, - 14, 13, 16, 24, 40, 51, 60, 72, - 14, 17, 22, 29, 51, 60, 72, 80, - 18, 22, 37, 51, 60, 72, 80, 88, - 24, 35, 51, 60, 72, 80, 88, 96, - 49, 51, 60, 72, 80, 88, 96, 100, - 51, 60, 72, 80, 88, 96, 100, 102), - intArrayOf(12, 9, 8, 12, 18, 30, 38, 45, - 8, 9, 11, 14, 20, 38, 45, 54, - 11, 10, 12, 18, 30, 38, 45, 54, - 11, 13, 17, 22, 38, 45, 54, 60, - 14, 17, 28, 38, 45, 54, 60, 66, - 18, 26, 38, 45, 54, 60, 66, 72, - 37, 38, 45, 54, 60, 66, 72, 75, - 38, 45, 54, 60, 66, 72, 75, 77), - intArrayOf(10, 7, 6, 10, 15, 25, 32, 38, - 7, 7, 9, 12, 16, 32, 38, 45, - 9, 8, 10, 15, 25, 32, 38, 45, - 9, 11, 14, 18, 32, 38, 45, 50, - 12, 14, 23, 32, 38, 45, 50, 55, - 15, 22, 32, 38, 45, 50, 55, 60, - 31, 32, 38, 45, 50, 55, 60, 63, - 32, 38, 45, 50, 55, 60, 63, 65), - intArrayOf(8, 6, 5, 8, 12, 20, 26, 30, - 6, 6, 7, 10, 13, 26, 30, 36, - 7, 7, 8, 12, 20, 26, 30, 36, - 7, 9, 11, 15, 26, 30, 36, 40, - 10, 11, 19, 26, 30, 36, 40, 44, - 12, 17, 26, 30, 36, 40, 44, 48, - 25, 26, 30, 36, 40, 44, 48, 50, - 26, 30, 36, 40, 44, 48, 50, 52), - // Quality 7 (highest) - intArrayOf(2, 1, 1, 2, 3, 5, 6, 7, - 1, 1, 1, 2, 3, 6, 7, 9, - 1, 1, 2, 3, 5, 6, 7, 9, - 1, 2, 3, 4, 6, 7, 9, 10, - 2, 3, 5, 6, 7, 9, 10, 11, - 3, 4, 6, 7, 9, 10, 11, 12, - 6, 6, 7, 9, 10, 11, 12, 13, - 6, 7, 9, 10, 11, 12, 13, 13) + // Quality settings for quantization (Y channel) - 16x16 tables + var QUANT_TABLES_Y: Array = arrayOf( // Quality 0 (lowest) - 16x16 table + intArrayOf( + 80, 60, 50, 80, 120, 200, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 55, 60, 70, 95, 130, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 70, 65, 80, 120, 200, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 70, 85, 110, 145, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 90, 110, 185, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 120, 175, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 245, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 + ), // Quality 1 + intArrayOf( + 40, 30, 25, 40, 60, 100, 128, 150, 128, 150, 180, 200, 220, 240, 250, 255, + 28, 30, 35, 48, 65, 128, 150, 180, 150, 180, 200, 220, 240, 250, 255, 255, + 35, 33, 40, 60, 100, 128, 150, 180, 150, 180, 200, 220, 240, 250, 255, 255, + 35, 43, 55, 73, 128, 150, 180, 200, 180, 200, 220, 240, 250, 255, 255, 255, + 45, 55, 93, 128, 150, 180, 200, 220, 200, 220, 240, 250, 255, 255, 255, 255, + 60, 88, 128, 150, 180, 200, 220, 240, 220, 240, 250, 255, 255, 255, 255, 255, + 123, 128, 150, 180, 200, 220, 240, 250, 240, 250, 255, 255, 255, 255, 255, 255, + 128, 150, 180, 200, 220, 240, 250, 255, 250, 255, 255, 255, 255, 255, 255, 255, + 128, 150, 180, 200, 220, 240, 250, 255, 250, 255, 255, 255, 255, 255, 255, 255, + 150, 180, 200, 220, 240, 250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 180, 200, 220, 240, 250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 200, 220, 240, 250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 220, 240, 250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 240, 250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 + ), // Quality 2 + intArrayOf( + 20, 15, 13, 20, 30, 50, 64, 75, 64, 75, 90, 100, 110, 120, 125, 128, + 14, 15, 18, 24, 33, 64, 75, 90, 75, 90, 100, 110, 120, 125, 128, 140, + 18, 17, 20, 30, 50, 64, 75, 90, 75, 90, 100, 110, 120, 125, 128, 140, + 18, 22, 28, 37, 64, 75, 90, 100, 90, 100, 110, 120, 125, 128, 140, 150, + 23, 28, 47, 64, 75, 90, 100, 110, 100, 110, 120, 125, 128, 140, 150, 160, + 30, 44, 64, 75, 90, 100, 110, 120, 110, 120, 125, 128, 140, 150, 160, 170, + 62, 64, 75, 90, 100, 110, 120, 125, 120, 125, 128, 140, 150, 160, 170, 180, + 64, 75, 90, 100, 110, 120, 125, 128, 125, 128, 140, 150, 160, 170, 180, 190, + 64, 75, 90, 100, 110, 120, 125, 128, 125, 128, 140, 150, 160, 170, 180, 190, + 75, 90, 100, 110, 120, 125, 128, 140, 128, 140, 150, 160, 170, 180, 190, 200, + 90, 100, 110, 120, 125, 128, 140, 150, 140, 150, 160, 170, 180, 190, 200, 210, + 100, 110, 120, 125, 128, 140, 150, 160, 150, 160, 170, 180, 190, 200, 210, 220, + 110, 120, 125, 128, 140, 150, 160, 170, 160, 170, 180, 190, 200, 210, 220, 230, + 120, 125, 128, 140, 150, 160, 170, 180, 170, 180, 190, 200, 210, 220, 230, 240, + 125, 128, 140, 150, 160, 170, 180, 190, 180, 190, 200, 210, 220, 230, 240, 250, + 128, 140, 150, 160, 170, 180, 190, 200, 190, 200, 210, 220, 230, 240, 250, 255 + ), // Quality 3 + intArrayOf( + 16, 12, 10, 16, 24, 40, 51, 60, 51, 60, 72, 80, 88, 96, 100, 102, + 11, 12, 14, 19, 26, 51, 60, 72, 60, 72, 80, 88, 96, 100, 102, 110, + 14, 13, 16, 24, 40, 51, 60, 72, 60, 72, 80, 88, 96, 100, 102, 110, + 14, 17, 22, 29, 51, 60, 72, 80, 72, 80, 88, 96, 100, 102, 110, 120, + 18, 22, 37, 51, 60, 72, 80, 88, 80, 88, 96, 100, 102, 110, 120, 130, + 24, 35, 51, 60, 72, 80, 88, 96, 88, 96, 100, 102, 110, 120, 130, 140, + 49, 51, 60, 72, 80, 88, 96, 100, 96, 100, 102, 110, 120, 130, 140, 150, + 51, 60, 72, 80, 88, 96, 100, 102, 100, 102, 110, 120, 130, 140, 150, 160, + 51, 60, 72, 80, 88, 96, 100, 102, 100, 102, 110, 120, 130, 140, 150, 160, + 60, 72, 80, 88, 96, 100, 102, 110, 102, 110, 120, 130, 140, 150, 160, 170, + 72, 80, 88, 96, 100, 102, 110, 120, 110, 120, 130, 140, 150, 160, 170, 180, + 80, 88, 96, 100, 102, 110, 120, 130, 120, 130, 140, 150, 160, 170, 180, 190, + 88, 96, 100, 102, 110, 120, 130, 140, 130, 140, 150, 160, 170, 180, 190, 200, + 96, 100, 102, 110, 120, 130, 140, 150, 140, 150, 160, 170, 180, 190, 200, 210, + 100, 102, 110, 120, 130, 140, 150, 160, 150, 160, 170, 180, 190, 200, 210, 220, + 102, 110, 120, 130, 140, 150, 160, 170, 160, 170, 180, 190, 200, 210, 220, 230 + ), // Quality 4 + intArrayOf( + 12, 9, 8, 12, 18, 30, 38, 45, 38, 45, 54, 60, 66, 72, 75, 77, + 8, 9, 11, 14, 20, 38, 45, 54, 45, 54, 60, 66, 72, 75, 77, 85, + 11, 10, 12, 18, 30, 38, 45, 54, 45, 54, 60, 66, 72, 75, 77, 85, + 11, 13, 17, 22, 38, 45, 54, 60, 54, 60, 66, 72, 75, 77, 85, 95, + 14, 17, 28, 38, 45, 54, 60, 66, 60, 66, 72, 75, 77, 85, 95, 105, + 18, 26, 38, 45, 54, 60, 66, 72, 66, 72, 75, 77, 85, 95, 105, 115, + 37, 38, 45, 54, 60, 66, 72, 75, 72, 75, 77, 85, 95, 105, 115, 125, + 38, 45, 54, 60, 66, 72, 75, 77, 75, 77, 85, 95, 105, 115, 125, 135, + 38, 45, 54, 60, 66, 72, 75, 77, 75, 77, 85, 95, 105, 115, 125, 135, + 45, 54, 60, 66, 72, 75, 77, 85, 77, 85, 95, 105, 115, 125, 135, 145, + 54, 60, 66, 72, 75, 77, 85, 95, 85, 95, 105, 115, 125, 135, 145, 155, + 60, 66, 72, 75, 77, 85, 95, 105, 95, 105, 115, 125, 135, 145, 155, 165, + 66, 72, 75, 77, 85, 95, 105, 115, 105, 115, 125, 135, 145, 155, 165, 175, + 72, 75, 77, 85, 95, 105, 115, 125, 115, 125, 135, 145, 155, 165, 175, 185, + 75, 77, 85, 95, 105, 115, 125, 135, 125, 135, 145, 155, 165, 175, 185, 195, + 77, 85, 95, 105, 115, 125, 135, 145, 135, 145, 155, 165, 175, 185, 195, 205 + ), // Quality 5 + intArrayOf( + 10, 7, 6, 10, 15, 25, 32, 38, 32, 38, 45, 50, 55, 60, 63, 65, + 7, 7, 9, 12, 16, 32, 38, 45, 38, 45, 50, 55, 60, 63, 65, 70, + 9, 8, 10, 15, 25, 32, 38, 45, 38, 45, 50, 55, 60, 63, 65, 70, + 9, 11, 14, 18, 32, 38, 45, 50, 45, 50, 55, 60, 63, 65, 70, 75, + 12, 14, 23, 32, 38, 45, 50, 55, 50, 55, 60, 63, 65, 70, 75, 80, + 15, 22, 32, 38, 45, 50, 55, 60, 55, 60, 63, 65, 70, 75, 80, 85, + 31, 32, 38, 45, 50, 55, 60, 63, 60, 63, 65, 70, 75, 80, 85, 90, + 32, 38, 45, 50, 55, 60, 63, 65, 63, 65, 70, 75, 80, 85, 90, 95, + 32, 38, 45, 50, 55, 60, 63, 65, 63, 65, 70, 75, 80, 85, 90, 95, + 38, 45, 50, 55, 60, 63, 65, 70, 65, 70, 75, 80, 85, 90, 95, 100, + 45, 50, 55, 60, 63, 65, 70, 75, 70, 75, 80, 85, 90, 95, 100, 105, + 50, 55, 60, 63, 65, 70, 75, 80, 75, 80, 85, 90, 95, 100, 105, 110, + 55, 60, 63, 65, 70, 75, 80, 85, 80, 85, 90, 95, 100, 105, 110, 115, + 60, 63, 65, 70, 75, 80, 85, 90, 85, 90, 95, 100, 105, 110, 115, 120, + 63, 65, 70, 75, 80, 85, 90, 95, 90, 95, 100, 105, 110, 115, 120, 125, + 65, 70, 75, 80, 85, 90, 95, 100, 95, 100, 105, 110, 115, 120, 125, 130 + ), // Quality 6 + intArrayOf( + 8, 6, 5, 8, 12, 20, 26, 30, 26, 30, 36, 40, 44, 48, 50, 52, + 6, 6, 7, 10, 13, 26, 30, 36, 30, 36, 40, 44, 48, 50, 52, 56, + 7, 7, 8, 12, 20, 26, 30, 36, 30, 36, 40, 44, 48, 50, 52, 56, + 7, 9, 11, 15, 26, 30, 36, 40, 36, 40, 44, 48, 50, 52, 56, 60, + 10, 11, 19, 26, 30, 36, 40, 44, 40, 44, 48, 50, 52, 56, 60, 64, + 12, 17, 26, 30, 36, 40, 44, 48, 44, 48, 50, 52, 56, 60, 64, 68, + 25, 26, 30, 36, 40, 44, 48, 50, 48, 50, 52, 56, 60, 64, 68, 72, + 26, 30, 36, 40, 44, 48, 50, 52, 50, 52, 56, 60, 64, 68, 72, 76, + 26, 30, 36, 40, 44, 48, 50, 52, 50, 52, 56, 60, 64, 68, 72, 76, + 30, 36, 40, 44, 48, 50, 52, 56, 52, 56, 60, 64, 68, 72, 76, 80, + 36, 40, 44, 48, 50, 52, 56, 60, 56, 60, 64, 68, 72, 76, 80, 84, + 40, 44, 48, 50, 52, 56, 60, 64, 60, 64, 68, 72, 76, 80, 84, 88, + 44, 48, 50, 52, 56, 60, 64, 68, 64, 68, 72, 76, 80, 84, 88, 92, + 48, 50, 52, 56, 60, 64, 68, 72, 68, 72, 76, 80, 84, 88, 92, 96, + 50, 52, 56, 60, 64, 68, 72, 76, 72, 76, 80, 84, 88, 92, 96, 100, + 52, 56, 60, 64, 68, 72, 76, 80, 76, 80, 84, 88, 92, 96, 100, 104 + ), // Quality 7 (highest) + intArrayOf( + 2, 1, 1, 2, 3, 5, 6, 7, 6, 7, 8, 9, 10, 11, 12, 13, + 1, 1, 1, 2, 3, 6, 7, 9, 7, 9, 10, 11, 12, 13, 14, 15, + 1, 1, 2, 3, 5, 6, 7, 9, 7, 9, 10, 11, 12, 13, 14, 15, + 1, 2, 3, 4, 6, 7, 9, 10, 9, 10, 11, 12, 13, 14, 15, 16, + 2, 3, 5, 6, 7, 9, 10, 11, 10, 11, 12, 13, 14, 15, 16, 17, + 3, 4, 6, 7, 9, 10, 11, 12, 11, 12, 13, 14, 15, 16, 17, 18, + 6, 6, 7, 9, 10, 11, 12, 13, 12, 13, 14, 15, 16, 17, 18, 19, + 6, 7, 9, 10, 11, 12, 13, 14, 13, 14, 15, 16, 17, 18, 19, 20, + 6, 7, 9, 10, 11, 12, 13, 14, 13, 14, 15, 16, 17, 18, 19, 20, + 7, 9, 10, 11, 12, 13, 14, 15, 14, 15, 16, 17, 18, 19, 20, 21, + 9, 10, 11, 12, 13, 14, 15, 16, 15, 16, 17, 18, 19, 20, 21, 22, + 10, 11, 12, 13, 14, 15, 16, 17, 16, 17, 18, 19, 20, 21, 22, 23, + 11, 12, 13, 14, 15, 16, 17, 18, 17, 18, 19, 20, 21, 22, 23, 24, + 12, 13, 14, 15, 16, 17, 18, 19, 18, 19, 20, 21, 22, 23, 24, 25, + 13, 14, 15, 16, 17, 18, 19, 20, 19, 20, 21, 22, 23, 24, 25, 26, + 14, 15, 16, 17, 18, 19, 20, 21, 20, 21, 22, 23, 24, 25, 26, 27 + ) + ) + + // Quality settings for quantization (Chroma channels - 8x8) + var QUANT_TABLES_C: Array = arrayOf( // Quality 0 (lowest) + intArrayOf( + 120, 90, 75, 120, 180, 255, 255, 255, + 83, 90, 105, 143, 195, 255, 255, 255, + 105, 98, 120, 180, 255, 255, 255, 255, + 105, 128, 165, 218, 255, 255, 255, 255, + 135, 165, 255, 255, 255, 255, 255, 255, + 180, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255 + ), // Quality 1 + intArrayOf( + 60, 45, 38, 60, 90, 150, 192, 225, + 42, 45, 53, 72, 98, 192, 225, 255, + 53, 49, 60, 90, 150, 192, 225, 255, + 53, 64, 83, 109, 192, 225, 255, 255, + 68, 83, 139, 192, 225, 255, 255, 255, + 90, 132, 192, 225, 255, 255, 255, 255, + 185, 192, 225, 255, 255, 255, 255, 255, + 192, 225, 255, 255, 255, 255, 255, 255 + ), // Quality 2 + intArrayOf( + 30, 23, 19, 30, 45, 75, 96, 113, + 21, 23, 27, 36, 49, 96, 113, 135, + 27, 25, 30, 45, 75, 96, 113, 135, + 27, 32, 42, 55, 96, 113, 135, 150, + 34, 42, 70, 96, 113, 135, 150, 165, + 45, 66, 96, 113, 135, 150, 165, 180, + 93, 96, 113, 135, 150, 165, 180, 188, + 96, 113, 135, 150, 165, 180, 188, 192 + ), // Quality 3 + intArrayOf( + 24, 18, 15, 24, 36, 60, 77, 90, + 17, 18, 21, 29, 39, 77, 90, 108, + 21, 20, 24, 36, 60, 77, 90, 108, + 21, 26, 33, 44, 77, 90, 108, 120, + 27, 33, 56, 77, 90, 108, 120, 132, + 36, 53, 77, 90, 108, 120, 132, 144, + 74, 77, 90, 108, 120, 132, 144, 150, + 77, 90, 108, 120, 132, 144, 150, 154 + ), // Quality 4 + intArrayOf( + 18, 14, 12, 18, 27, 45, 57, 68, + 13, 14, 16, 22, 30, 57, 68, 81, + 16, 15, 18, 27, 45, 57, 68, 81, + 16, 20, 25, 33, 57, 68, 81, 90, + 20, 25, 42, 57, 68, 81, 90, 99, + 27, 39, 57, 68, 81, 90, 99, 108, + 56, 57, 68, 81, 90, 99, 108, 113, + 57, 68, 81, 90, 99, 108, 113, 116 + ), // Quality 5 + intArrayOf( + 15, 11, 9, 15, 23, 38, 48, 57, + 11, 11, 13, 18, 24, 48, 57, 68, + 13, 12, 15, 23, 38, 48, 57, 68, + 13, 16, 21, 28, 48, 57, 68, 75, + 17, 21, 35, 48, 57, 68, 75, 83, + 23, 33, 48, 57, 68, 75, 83, 90, + 46, 48, 57, 68, 75, 83, 90, 94, + 48, 57, 68, 75, 83, 90, 94, 96 + ), // Quality 6 + intArrayOf( + 12, 9, 8, 12, 18, 30, 39, 45, + 9, 9, 11, 14, 20, 39, 45, 54, + 11, 10, 12, 18, 30, 39, 45, 54, + 11, 13, 17, 22, 39, 45, 54, 60, + 14, 17, 28, 39, 45, 54, 60, 66, + 18, 26, 39, 45, 54, 60, 66, 72, + 38, 39, 45, 54, 60, 66, 72, 75, + 39, 45, 54, 60, 66, 72, 75, 77 + ), // Quality 7 (highest) + intArrayOf( + 3, 2, 2, 3, 5, 8, 9, 11, + 2, 2, 2, 3, 5, 9, 11, 14, + 2, 2, 3, 5, 8, 9, 11, 14, + 2, 3, 5, 6, 9, 11, 14, 15, + 3, 5, 8, 9, 11, 14, 15, 17, + 5, 6, 9, 11, 14, 15, 17, 18, + 9, 9, 11, 14, 15, 17, 18, 20, + 9, 11, 14, 15, 17, 18, 20, 20 + ) ) /** @@ -1424,8 +1585,77 @@ class GraphicsJSR223Delegate(private val vm: VM) { return result } + // 16x16 IDCT for Y channel (YCoCg-R format) + private fun tevIdct16x16(coeffs: IntArray, quantTable: IntArray): IntArray { + val dctBasis = Array(16) { u -> + Array(16) { x -> + val cu = if (u == 0) 1.0 / kotlin.math.sqrt(2.0) else 1.0 + cu * kotlin.math.cos((2.0 * x + 1.0) * u * kotlin.math.PI / 32.0) / 4.0 + } + } + + val dctCoeffs = Array(16) { DoubleArray(16) } + val result = IntArray(256) // 16x16 = 256 + + // Convert integer coefficients to 2D array and dequantize + for (u in 0 until 16) { + for (v in 0 until 16) { + val idx = u * 16 + v + val coeff = coeffs[idx] + dctCoeffs[u][v] = (coeff * quantTable[idx]).toDouble() + } + } + + // Apply 2D inverse DCT + for (x in 0 until 16) { + for (y in 0 until 16) { + var sum = 0.0 + for (u in 0 until 16) { + for (v in 0 until 16) { + sum += dctBasis[u][x] * dctBasis[v][y] * dctCoeffs[u][v] + } + } + val pixel = kotlin.math.max(0.0, kotlin.math.min(255.0, sum + 128.0)) + result[y * 16 + x] = pixel.toInt() + } + } + + return result + } + + // YCoCg-R to RGB conversion with 4:2:0 chroma upsampling + fun tevYcocgToRGB(yBlock: IntArray, coBlock: IntArray, cgBlock: IntArray): IntArray { + val rgbData = IntArray(16 * 16 * 3) // R,G,B for 16x16 pixels + + for (py in 0 until 16) { + for (px in 0 until 16) { + val yIdx = py * 16 + px + val y = yBlock[yIdx] + + // Get chroma values from subsampled 8x8 blocks (nearest neighbor upsampling) + val coIdx = (py / 2) * 8 + (px / 2) + val co = coBlock[coIdx] + val cg = cgBlock[coIdx] + + // YCoCg-R inverse transform + val tmp = y - (cg shr 1) + val g = cg + tmp + val b = tmp - (co shr 1) + val r = b + co + + // Clamp and store RGB + val baseIdx = (py * 16 + px) * 3 + rgbData[baseIdx] = kotlin.math.max(0, kotlin.math.min(255, r)) // R + rgbData[baseIdx + 1] = kotlin.math.max(0, kotlin.math.min(255, g)) // G + rgbData[baseIdx + 2] = kotlin.math.max(0, kotlin.math.min(255, b)) // B + } + } + + return rgbData + } + /** - * Hardware-accelerated TEV frame decoder + * Hardware-accelerated TEV frame decoder for YCoCg-R 4:2:0 format * Decodes compressed TEV block data directly to framebuffer * * @param blockDataPtr Pointer to decompressed TEV block data @@ -1439,10 +1669,11 @@ class GraphicsJSR223Delegate(private val vm: VM) { fun tevDecode(blockDataPtr: Long, currentRGBAddr: Long, prevRGBAddr: Long, width: Int, height: Int, quality: Int) { - val blocksX = (width + 7) / 8 - val blocksY = (height + 7) / 8 + val blocksX = (width + 15) / 16 // 16x16 blocks now + val blocksY = (height + 15) / 16 - val quantTable = QUANT_TABLES[quality] + val quantTableY = QUANT_TABLES_Y[quality] + val quantTableC = QUANT_TABLES_C[quality] var readPtr = blockDataPtr @@ -1452,8 +1683,8 @@ class GraphicsJSR223Delegate(private val vm: VM) { for (by in 0 until blocksY) { for (bx in 0 until blocksX) { - val startX = bx * 8 - val startY = by * 8 + val startX = bx * 16 + val startY = by * 16 // Read TEV block header (7 bytes) val mode = vm.peek(readPtr)!!.toUint() @@ -1463,19 +1694,10 @@ class GraphicsJSR223Delegate(private val vm: VM) { ((vm.peek(readPtr + 4)!!.toUint()) shl 8)).toShort().toInt() readPtr += 7 // Skip CBP field - // Read DCT coefficients (3 channels × 64 coefficients × 2 bytes) - val dctCoeffs = IntArray(3 * 64) - for (i in 0 until 3 * 64) { - val coeff = ((vm.peek(readPtr)!!.toUint()) or - ((vm.peek(readPtr + 1)!!.toUint()) shl 8)).toShort().toInt() - dctCoeffs[i] = coeff - readPtr += 2 - } - when (mode) { 0x00 -> { // TEV_MODE_SKIP - copy RGB from previous frame - for (dy in 0 until 8) { - for (dx in 0 until 8) { + for (dy in 0 until 16) { + for (dx in 0 until 16) { val x = startX + dx val y = startY + dy if (x < width && y < height) { @@ -1496,8 +1718,8 @@ class GraphicsJSR223Delegate(private val vm: VM) { } 0x03 -> { // TEV_MODE_MOTION - motion compensation with RGB - for (dy in 0 until 8) { - for (dx in 0 until 8) { + for (dy in 0 until 16) { + for (dx in 0 until 16) { val x = startX + dx val y = startY + dy val refX = x + mvX @@ -1530,36 +1752,57 @@ class GraphicsJSR223Delegate(private val vm: VM) { } } - else -> { // TEV_MODE_INTRA (0x01) or TEV_MODE_INTER (0x02) - Full DCT decode - // Hardware-accelerated IDCT for all three channels - val rCoeffs = dctCoeffs.sliceArray(0 * 64 until 1 * 64) // R channel - val gCoeffs = dctCoeffs.sliceArray(1 * 64 until 2 * 64) // G channel - val bCoeffs = dctCoeffs.sliceArray(2 * 64 until 3 * 64) // B channel + else -> { // TEV_MODE_INTRA (0x01) or TEV_MODE_INTER (0x02) - Full YCoCg-R DCT decode + // Read DCT coefficients: Y (16x16=256), Co (8x8=64), Cg (8x8=64) + val yCoeffs = IntArray(256) + val coCoeffs = IntArray(64) + val cgCoeffs = IntArray(64) + + // Read Y coefficients (16x16 = 256 coefficients × 2 bytes) + for (i in 0 until 256) { + val coeff = ((vm.peek(readPtr)!!.toUint()) or + ((vm.peek(readPtr + 1)!!.toUint()) shl 8)).toShort().toInt() + yCoeffs[i] = coeff + readPtr += 2 + } + + // Read Co coefficients (8x8 = 64 coefficients × 2 bytes) + for (i in 0 until 64) { + val coeff = ((vm.peek(readPtr)!!.toUint()) or + ((vm.peek(readPtr + 1)!!.toUint()) shl 8)).toShort().toInt() + coCoeffs[i] = coeff + readPtr += 2 + } + + // Read Cg coefficients (8x8 = 64 coefficients × 2 bytes) + for (i in 0 until 64) { + val coeff = ((vm.peek(readPtr)!!.toUint()) or + ((vm.peek(readPtr + 1)!!.toUint()) shl 8)).toShort().toInt() + cgCoeffs[i] = coeff + readPtr += 2 + } // Perform hardware IDCT for each channel - val rBlock = tevIdct8x8(rCoeffs, quantTable) - val gBlock = tevIdct8x8(gCoeffs, quantTable) - val bBlock = tevIdct8x8(bCoeffs, quantTable) + val yBlock = tevIdct16x16(yCoeffs, quantTableY) + val coBlock = tevIdct8x8(coCoeffs, quantTableC) + val cgBlock = tevIdct8x8(cgCoeffs, quantTableC) - // Fill 8x8 block with IDCT results - for (dy in 0 until 8) { - for (dx in 0 until 8) { + // Convert YCoCg-R to RGB + val rgbData = tevYcocgToRGB(yBlock, coBlock, cgBlock) + + // Store RGB data to frame buffer + for (dy in 0 until 16) { + for (dx in 0 until 16) { val x = startX + dx val y = startY + dy if (x < width && y < height) { - val blockOffset = dy * 8 + dx + val rgbIdx = (dy * 16 + dx) * 3 val imageOffset = y.toLong() * width + x + val bufferOffset = imageOffset * 3 - // Get RGB values from IDCT results - val r = rBlock[blockOffset] - val g = gBlock[blockOffset] - val b = bBlock[blockOffset] - - // Store full 8-bit RGB values to RGB buffer - val rgbOffset = imageOffset * 3 - vm.poke(currentRGBAddr + rgbOffset*thisAddrIncVec, r.toByte()) - vm.poke(currentRGBAddr + (rgbOffset + 1)*thisAddrIncVec, g.toByte()) - vm.poke(currentRGBAddr + (rgbOffset + 2)*thisAddrIncVec, b.toByte()) + vm.poke(currentRGBAddr + bufferOffset*thisAddrIncVec, rgbData[rgbIdx].toByte()) + vm.poke(currentRGBAddr + (bufferOffset + 1)*thisAddrIncVec, rgbData[rgbIdx + 1].toByte()) + vm.poke(currentRGBAddr + (bufferOffset + 2)*thisAddrIncVec, rgbData[rgbIdx + 2].toByte()) } } } @@ -1568,4 +1811,72 @@ class GraphicsJSR223Delegate(private val vm: VM) { } } } + + // YCoCg-R transform for 16x16 Y blocks and 8x8 chroma blocks (4:2:0 subsampling) + fun blockEncodeToYCoCgR16x16(blockX: Int, blockY: Int, srcPtr: Int, width: Int, height: Int): List { + val yBlock = IntArray(16 * 16) // 16x16 Y + val coBlock = IntArray(8 * 8) // 8x8 Co (subsampled) + val cgBlock = IntArray(8 * 8) // 8x8 Cg (subsampled) + val incVec = if (srcPtr >= 0) 1L else -1L + + // Process 16x16 Y block + for (py in 0 until 16) { + for (px in 0 until 16) { + val ox = blockX * 16 + px + val oy = blockY * 16 + py + if (ox < width && oy < height) { + val offset = 3 * (oy * width + ox) + val r = vm.peek(srcPtr + offset * incVec)!!.toUint() + val g = vm.peek(srcPtr + (offset + 1) * incVec)!!.toUint() + val b = vm.peek(srcPtr + (offset + 2) * incVec)!!.toUint() + + // YCoCg-R transform + val co = r - b + val tmp = b + (co shr 1) + val cg = g - tmp + val y = tmp + (cg shr 1) + + yBlock[py * 16 + px] = y + } + } + } + + // Process 8x8 Co/Cg blocks with 4:2:0 subsampling (average 2x2 pixels) + for (py in 0 until 8) { + for (px in 0 until 8) { + var coSum = 0 + var cgSum = 0 + var count = 0 + + // Average 2x2 block of pixels for chroma subsampling + for (dy in 0 until 2) { + for (dx in 0 until 2) { + val ox = blockX * 16 + px * 2 + dx + val oy = blockY * 16 + py * 2 + dy + if (ox < width && oy < height) { + val offset = 3 * (oy * width + ox) + val r = vm.peek(srcPtr + offset * incVec)!!.toUint() + val g = vm.peek(srcPtr + (offset + 1) * incVec)!!.toUint() + val b = vm.peek(srcPtr + (offset + 2) * incVec)!!.toUint() + + val co = r - b + val tmp = b + (co shr 1) + val cg = g - tmp + + coSum += co + cgSum += cg + count++ + } + } + } + + if (count > 0) { + coBlock[py * 8 + px] = coSum / count + cgBlock[py * 8 + px] = cgSum / count + } + } + } + + return listOf(yBlock, coBlock, cgBlock) + } } \ No newline at end of file diff --git a/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt index 0279c06..c2385b7 100644 --- a/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt @@ -98,6 +98,13 @@ class VMJSR223Delegate(private val vm: VM) { fun nanoTime() = System.nanoTime() fun malloc(size: Int) = vm.malloc(size) + fun memset(dest: Int, ch: Int, count: Int): Int { + val incVec = if (dest >= 0) 1 else -1 + for (i in 0 until count) { + poke(dest + count*incVec, ch) + } + return dest + } fun free(ptr: Int) = vm.free(ptr) fun forceAlloc(ptr: Int, size: Int) = vm.forceAlloc(ptr, size) fun memcpy(from: Int, to: Int, len: Int) { diff --git a/video_encoder/encoder_ipf1d.c b/video_encoder/encoder_ipf1d.c new file mode 100644 index 0000000..97b08dd --- /dev/null +++ b/video_encoder/encoder_ipf1d.c @@ -0,0 +1,935 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// TVDOS Movie format constants +#define TVDOS_MAGIC "\x1F\x54\x53\x56\x4D\x4D\x4F\x56" // "\x1FTSVM MOV" +#define IPF_BLOCK_SIZE 12 + +// iPF1-delta opcodes +#define SKIP_OP 0x00 +#define PATCH_OP 0x01 +#define REPEAT_OP 0x02 +#define END_OP 0xFF + +// Video packet types +#define IPF1_PACKET_TYPE 0x04, 0x00 // iPF Type 1 (4 + 0) +#define IPF1_DELTA_PACKET_TYPE 0x04, 0x02 // iPF Type 1 delta +#define SYNC_PACKET_TYPE 0xFF, 0xFF // Sync packet + +// Audio constants +#define MP2_SAMPLE_RATE 32000 +#define MP2_DEFAULT_PACKET_SIZE 0x240 +#define MP2_PACKET_TYPE_BASE 0x11 + +// Default values +#define DEFAULT_WIDTH 560 +#define DEFAULT_HEIGHT 448 +#define TEMP_AUDIO_FILE "/tmp/tvdos_temp_audio.mp2" + +typedef struct { + char *input_file; + char *output_file; + int width; + int height; + int fps; + int total_frames; + double duration; + int has_audio; + int output_to_stdout; + + // Internal buffers + uint8_t *previous_ipf_frame; + uint8_t *current_ipf_frame; + uint8_t *delta_buffer; + uint8_t *rgb_buffer; + uint8_t *compressed_buffer; + uint8_t *mp2_buffer; + size_t frame_buffer_size; + + // Audio handling + FILE *mp2_file; + int mp2_packet_size; + int mp2_rate_index; + size_t audio_remaining; + int audio_frames_in_buffer; + int target_audio_buffer_size; + + // FFmpeg processes + FILE *ffmpeg_video_pipe; + FILE *ffmpeg_audio_pipe; + + // Progress tracking + struct timeval start_time; + struct timeval last_progress_time; + size_t total_output_bytes; + + // Dithering mode + int dither_mode; +} encoder_config_t; + +// CORRECTED YCoCg conversion matching Kotlin implementation +typedef struct { + float y, co, cg; +} ycocg_t; + +static ycocg_t rgb_to_ycocg_correct(uint8_t r, uint8_t g, uint8_t b, float ditherThreshold) { + ycocg_t result; + float rf = floor((ditherThreshold / 15.0 + r / 255.0) * 15.0) / 15.0; + float gf = floor((ditherThreshold / 15.0 + g / 255.0) * 15.0) / 15.0; + float bf = floor((ditherThreshold / 15.0 + b / 255.0) * 15.0) / 15.0; + + // CORRECTED: Match Kotlin implementation exactly + float co = rf - bf; // co = r - b [-1..1] + float tmp = bf + co / 2.0f; // tmp = b + co/2 + float cg = gf - tmp; // cg = g - tmp [-1..1] + float y = tmp + cg / 2.0f; // y = tmp + cg/2 [0..1] + + result.y = y; + result.co = co; + result.cg = cg; + + return result; +} + +static int quantize_4bit_y(float value) { + // Y quantization: round(y * 15) + return (int)round(fmaxf(0.0f, fminf(15.0f, value * 15.0f))); +} + +static int chroma_to_four_bits(float f) { + // CORRECTED: Match Kotlin chromaToFourBits function exactly + // return (round(f * 8) + 7).coerceIn(0..15) + int result = (int)round(f * 8.0f) + 7; + return fmaxf(0, fminf(15, result)); +} + +// Parse resolution string like "1024x768" +static int parse_resolution(const char *res_str, int *width, int *height) { + if (!res_str) return 0; + return sscanf(res_str, "%dx%d", width, height) == 2; +} + +// Execute command and capture output +static char *execute_command(const char *command) { + FILE *pipe = popen(command, "r"); + if (!pipe) return NULL; + + char *result = malloc(4096); + size_t len = fread(result, 1, 4095, pipe); + result[len] = '\0'; + + pclose(pipe); + return result; +} + +// Get video metadata using ffprobe +static int get_video_metadata(encoder_config_t *config) { + char command[1024]; + char *output; + + // Get frame count + snprintf(command, sizeof(command), + "ffprobe -v quiet -select_streams v:0 -count_frames -show_entries stream=nb_read_frames -of csv=p=0 \"%s\"", + config->input_file); + output = execute_command(command); + if (!output) { + fprintf(stderr, "Failed to get frame count\n"); + return 0; + } + config->total_frames = atoi(output); + free(output); + + // Get frame rate + snprintf(command, sizeof(command), + "ffprobe -v quiet -select_streams v:0 -show_entries stream=r_frame_rate -of csv=p=0 \"%s\"", + config->input_file); + output = execute_command(command); + if (!output) { + fprintf(stderr, "Failed to get frame rate\n"); + return 0; + } + + // Parse framerate (could be "30/1" or "29.97") + int num, den; + if (sscanf(output, "%d/%d", &num, &den) == 2) { + config->fps = (den > 0) ? (num / den) : 30; + } else { + config->fps = (int)round(atof(output)); + } + free(output); + + // Get duration + snprintf(command, sizeof(command), + "ffprobe -v quiet -show_entries format=duration -of csv=p=0 \"%s\"", + config->input_file); + output = execute_command(command); + if (output) { + config->duration = atof(output); + free(output); + } + + // Check if has audio + snprintf(command, sizeof(command), + "ffprobe -v quiet -select_streams a:0 -show_entries stream=index -of csv=p=0 \"%s\"", + config->input_file); + output = execute_command(command); + config->has_audio = (output && strlen(output) > 0 && atoi(output) >= 0); + if (output) free(output); + + // Validate frame count using duration if needed + if (config->total_frames <= 0 && config->duration > 0) { + config->total_frames = (int)(config->duration * config->fps); + } + + fprintf(stderr, "Video metadata:\n"); + fprintf(stderr, " Frames: %d\n", config->total_frames); + fprintf(stderr, " FPS: %d\n", config->fps); + fprintf(stderr, " Duration: %.2fs\n", config->duration); + fprintf(stderr, " Audio: %s\n", config->has_audio ? "Yes" : "No"); + fprintf(stderr, " Resolution: %dx%d\n", config->width, config->height); + + return (config->total_frames > 0 && config->fps > 0); +} + +// Start FFmpeg process for video conversion +static int start_video_conversion(encoder_config_t *config) { + char command[2048]; + snprintf(command, sizeof(command), + "ffmpeg -i \"%s\" -f rawvideo -pix_fmt rgb24 -vf scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d -y - 2>/dev/null", + config->input_file, config->width, config->height, config->width, config->height); + + config->ffmpeg_video_pipe = popen(command, "r"); + return (config->ffmpeg_video_pipe != NULL); +} + +// Start FFmpeg process for audio conversion +static int start_audio_conversion(encoder_config_t *config) { + if (!config->has_audio) return 1; + + char command[2048]; + snprintf(command, sizeof(command), + "ffmpeg -i \"%s\" -acodec libtwolame -psymodel 4 -b:a 192k -ar %d -ac 2 -y \"%s\" 2>/dev/null", + config->input_file, MP2_SAMPLE_RATE, TEMP_AUDIO_FILE); + + int result = system(command); + if (result == 0) { + config->mp2_file = fopen(TEMP_AUDIO_FILE, "rb"); + if (config->mp2_file) { + fseek(config->mp2_file, 0, SEEK_END); + config->audio_remaining = ftell(config->mp2_file); + fseek(config->mp2_file, 0, SEEK_SET); + return 1; + } + } + + fprintf(stderr, "Warning: Failed to convert audio, proceeding without audio\n"); + config->has_audio = 0; + return 1; +} + +// Write variable-length integer +static void write_varint(uint8_t **ptr, uint32_t value) { + while (value >= 0x80) { + **ptr = (uint8_t)((value & 0x7F) | 0x80); + (*ptr)++; + value >>= 7; + } + **ptr = (uint8_t)(value & 0x7F); + (*ptr)++; +} + +// Get MP2 packet size and rate index +static int get_mp2_packet_size(uint8_t *header) { + int bitrate_index = (header[2] >> 4) & 0xF; + int padding_bit = (header[2] >> 1) & 0x1; + + int bitrates[] = {0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, -1}; + int bitrate = bitrates[bitrate_index]; + + if (bitrate <= 0) return MP2_DEFAULT_PACKET_SIZE; + + int frame_size = (144 * bitrate * 1000) / MP2_SAMPLE_RATE + padding_bit; + return frame_size; +} + +static int mp2_packet_size_to_rate_index(int packet_size, int is_mono) { + int rate_index; + switch (packet_size) { + case 144: rate_index = 0; break; + case 216: rate_index = 2; break; + case 252: rate_index = 4; break; + case 288: rate_index = 6; break; + case 360: rate_index = 8; break; + case 432: rate_index = 10; break; + case 504: rate_index = 12; break; + case 576: rate_index = 14; break; + case 720: rate_index = 16; break; + case 864: rate_index = 18; break; + case 1008: rate_index = 20; break; + case 1152: rate_index = 22; break; + case 1440: rate_index = 24; break; + case 1728: rate_index = 26; break; + default: rate_index = 14; break; + } + return rate_index + (is_mono ? 1 : 0); +} + +// Gzip compress function (instead of zlib) +static size_t gzip_compress(uint8_t *src, size_t src_len, uint8_t *dst, size_t dst_max) { + z_stream stream = {0}; + stream.next_in = src; + stream.avail_in = src_len; + stream.next_out = dst; + stream.avail_out = dst_max; + + // Use deflateInit2 with gzip format + if (deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) { + return 0; + } + + if (deflate(&stream, Z_FINISH) != Z_STREAM_END) { + deflateEnd(&stream); + return 0; + } + + size_t compressed_size = stream.total_out; + deflateEnd(&stream); + return compressed_size; +} + +// Bayer dithering kernels (4 patterns, each 4x4) +static const float bayerKernels[4][16] = { + { // Pattern 0 + (0.0f + 0.5f) / 16.0f, (8.0f + 0.5f) / 16.0f, (2.0f + 0.5f) / 16.0f, (10.0f + 0.5f) / 16.0f, + (12.0f + 0.5f) / 16.0f, (4.0f + 0.5f) / 16.0f, (14.0f + 0.5f) / 16.0f, (6.0f + 0.5f) / 16.0f, + (3.0f + 0.5f) / 16.0f, (11.0f + 0.5f) / 16.0f, (1.0f + 0.5f) / 16.0f, (9.0f + 0.5f) / 16.0f, + (15.0f + 0.5f) / 16.0f, (7.0f + 0.5f) / 16.0f, (13.0f + 0.5f) / 16.0f, (5.0f + 0.5f) / 16.0f + }, + { // Pattern 1 + (8.0f + 0.5f) / 16.0f, (2.0f + 0.5f) / 16.0f, (10.0f + 0.5f) / 16.0f, (0.0f + 0.5f) / 16.0f, + (4.0f + 0.5f) / 16.0f, (14.0f + 0.5f) / 16.0f, (6.0f + 0.5f) / 16.0f, (12.0f + 0.5f) / 16.0f, + (11.0f + 0.5f) / 16.0f, (1.0f + 0.5f) / 16.0f, (9.0f + 0.5f) / 16.0f, (3.0f + 0.5f) / 16.0f, + (7.0f + 0.5f) / 16.0f, (13.0f + 0.5f) / 16.0f, (5.0f + 0.5f) / 16.0f, (15.0f + 0.5f) / 16.0f + }, + { // Pattern 2 + (7.0f + 0.5f) / 16.0f, (13.0f + 0.5f) / 16.0f, (5.0f + 0.5f) / 16.0f, (15.0f + 0.5f) / 16.0f, + (8.0f + 0.5f) / 16.0f, (2.0f + 0.5f) / 16.0f, (10.0f + 0.5f) / 16.0f, (0.0f + 0.5f) / 16.0f, + (4.0f + 0.5f) / 16.0f, (14.0f + 0.5f) / 16.0f, (6.0f + 0.5f) / 16.0f, (12.0f + 0.5f) / 16.0f, + (11.0f + 0.5f) / 16.0f, (1.0f + 0.5f) / 16.0f, (9.0f + 0.5f) / 16.0f, (3.0f + 0.5f) / 16.0f + }, + { // Pattern 3 + (15.0f + 0.5f) / 16.0f, (7.0f + 0.5f) / 16.0f, (13.0f + 0.5f) / 16.0f, (5.0f + 0.5f) / 16.0f, + (0.0f + 0.5f) / 16.0f, (8.0f + 0.5f) / 16.0f, (2.0f + 0.5f) / 16.0f, (10.0f + 0.5f) / 16.0f, + (12.0f + 0.5f) / 16.0f, (4.0f + 0.5f) / 16.0f, (14.0f + 0.5f) / 16.0f, (6.0f + 0.5f) / 16.0f, + (3.0f + 0.5f) / 16.0f, (11.0f + 0.5f) / 16.0f, (1.0f + 0.5f) / 16.0f, (9.0f + 0.5f) / 16.0f + } +}; + +// CORRECTED: Encode a 4x4 block to iPF1 format matching Kotlin implementation +static void encode_ipf1_block_correct(uint8_t *rgb_data, int width, int height, int block_x, int block_y, + int channels, int pattern, uint8_t *output) { + ycocg_t pixels[16]; + int y_values[16]; + float co_values[16]; // Keep full precision for subsampling + float cg_values[16]; // Keep full precision for subsampling + + // Convert 4x4 block to YCoCg using corrected transform + for (int py = 0; py < 4; py++) { + for (int px = 0; px < 4; px++) { + int src_x = block_x * 4 + px; + int src_y = block_y * 4 + py; + float t = (pattern < 0) ? 0.0f : bayerKernels[pattern % 4][4 * (py % 4) + (px % 4)]; + int idx = py * 4 + px; + + if (src_x < width && src_y < height) { + int pixel_offset = (src_y * width + src_x) * channels; + uint8_t r = rgb_data[pixel_offset]; + uint8_t g = rgb_data[pixel_offset + 1]; + uint8_t b = rgb_data[pixel_offset + 2]; + pixels[idx] = rgb_to_ycocg_correct(r, g, b, t); + } else { + pixels[idx] = (ycocg_t){0.0f, 0.0f, 0.0f}; + } + + y_values[idx] = quantize_4bit_y(pixels[idx].y); + co_values[idx] = pixels[idx].co; + cg_values[idx] = pixels[idx].cg; + } + } + + // CORRECTED: Chroma subsampling (4:2:0 for iPF1) with correct averaging + int cos1 = chroma_to_four_bits((co_values[0] + co_values[1] + co_values[4] + co_values[5]) / 4.0f); + int cos2 = chroma_to_four_bits((co_values[2] + co_values[3] + co_values[6] + co_values[7]) / 4.0f); + int cos3 = chroma_to_four_bits((co_values[8] + co_values[9] + co_values[12] + co_values[13]) / 4.0f); + int cos4 = chroma_to_four_bits((co_values[10] + co_values[11] + co_values[14] + co_values[15]) / 4.0f); + + int cgs1 = chroma_to_four_bits((cg_values[0] + cg_values[1] + cg_values[4] + cg_values[5]) / 4.0f); + int cgs2 = chroma_to_four_bits((cg_values[2] + cg_values[3] + cg_values[6] + cg_values[7]) / 4.0f); + int cgs3 = chroma_to_four_bits((cg_values[8] + cg_values[9] + cg_values[12] + cg_values[13]) / 4.0f); + int cgs4 = chroma_to_four_bits((cg_values[10] + cg_values[11] + cg_values[14] + cg_values[15]) / 4.0f); + + // CORRECTED: Pack into iPF1 format matching Kotlin exactly + // Co values (2 bytes): cos2|cos1, cos4|cos3 + output[0] = ((cos2 << 4) | cos1); + output[1] = ((cos4 << 4) | cos3); + + // Cg values (2 bytes): cgs2|cgs1, cgs4|cgs3 + output[2] = ((cgs2 << 4) | cgs1); + output[3] = ((cgs4 << 4) | cgs3); + + // CORRECTED: Y values (8 bytes) with correct ordering from Kotlin + output[4] = ((y_values[1] << 4) | y_values[0]); // Y1|Y0 + output[5] = ((y_values[5] << 4) | y_values[4]); // Y5|Y4 + output[6] = ((y_values[3] << 4) | y_values[2]); // Y3|Y2 + output[7] = ((y_values[7] << 4) | y_values[6]); // Y7|Y6 + output[8] = ((y_values[9] << 4) | y_values[8]); // Y9|Y8 + output[9] = ((y_values[13] << 4) | y_values[12]); // Y13|Y12 + output[10] = ((y_values[11] << 4) | y_values[10]); // Y11|Y10 + output[11] = ((y_values[15] << 4) | y_values[14]); // Y15|Y14 +} + +// Helper function for contrast weighting +static double contrast_weight(int v1, int v2, int delta, int weight) { + double avg = (v1 + v2) / 2.0; + double contrast = (avg < 4 || avg > 11) ? 1.5 : 1.0; + return delta * weight * contrast; +} + +// Check if two iPF1 blocks are significantly different +static int is_significantly_different(uint8_t *block_a, uint8_t *block_b) { + double score = 0.0; + + // Co values (bytes 0-1) + uint16_t co_a = block_a[0] | (block_a[1] << 8); + uint16_t co_b = block_b[0] | (block_b[1] << 8); + for (int i = 0; i < 4; i++) { + int va = (co_a >> (i * 4)) & 0xF; + int vb = (co_b >> (i * 4)) & 0xF; + int delta = abs(va - vb); + score += contrast_weight(va, vb, delta, 3); + } + + // Cg values (bytes 2-3) + uint16_t cg_a = block_a[2] | (block_a[3] << 8); + uint16_t cg_b = block_b[2] | (block_b[3] << 8); + for (int i = 0; i < 4; i++) { + int va = (cg_a >> (i * 4)) & 0xF; + int vb = (cg_b >> (i * 4)) & 0xF; + int delta = abs(va - vb); + score += contrast_weight(va, vb, delta, 3); + } + + // Y values (bytes 4-11) + for (int i = 4; i < 12; i++) { + int byte_a = block_a[i] & 0xFF; + int byte_b = block_b[i] & 0xFF; + + int y_a_high = (byte_a >> 4) & 0xF; + int y_a_low = byte_a & 0xF; + int y_b_high = (byte_b >> 4) & 0xF; + int y_b_low = byte_b & 0xF; + + int delta_high = abs(y_a_high - y_b_high); + int delta_low = abs(y_a_low - y_b_low); + + score += contrast_weight(y_a_high, y_b_high, delta_high, 2); + score += contrast_weight(y_a_low, y_b_low, delta_low, 2); + } + + return score > 4.0; +} + +// Encode iPF1 frame to buffer +static void encode_ipf1_frame(uint8_t *rgb_data, int width, int height, int channels, int pattern, + uint8_t *ipf_buffer) { + int blocks_per_row = (width + 3) / 4; + int blocks_per_col = (height + 3) / 4; + + for (int block_y = 0; block_y < blocks_per_col; block_y++) { + for (int block_x = 0; block_x < blocks_per_row; block_x++) { + int block_index = block_y * blocks_per_row + block_x; + uint8_t *output_block = ipf_buffer + block_index * IPF_BLOCK_SIZE; + encode_ipf1_block_correct(rgb_data, width, height, block_x, block_y, channels, pattern, output_block); + } + } +} + +// Create iPF1-delta encoded frame +static size_t encode_ipf1_delta(uint8_t *previous_frame, uint8_t *current_frame, + int width, int height, uint8_t *delta_buffer) { + int blocks_per_row = (width + 3) / 4; + int blocks_per_col = (height + 3) / 4; + int total_blocks = blocks_per_row * blocks_per_col; + + uint8_t *output_ptr = delta_buffer; + int skip_count = 0; + uint8_t *patch_blocks = malloc(total_blocks * IPF_BLOCK_SIZE); + int patch_count = 0; + + for (int block_index = 0; block_index < total_blocks; block_index++) { + uint8_t *prev_block = previous_frame + block_index * IPF_BLOCK_SIZE; + uint8_t *curr_block = current_frame + block_index * IPF_BLOCK_SIZE; + + if (is_significantly_different(prev_block, curr_block)) { + if (skip_count > 0) { + *output_ptr++ = SKIP_OP; + write_varint(&output_ptr, skip_count); + skip_count = 0; + } + + memcpy(patch_blocks + patch_count * IPF_BLOCK_SIZE, curr_block, IPF_BLOCK_SIZE); + patch_count++; + } else { + if (patch_count > 0) { + *output_ptr++ = PATCH_OP; + write_varint(&output_ptr, patch_count); + memcpy(output_ptr, patch_blocks, patch_count * IPF_BLOCK_SIZE); + output_ptr += patch_count * IPF_BLOCK_SIZE; + patch_count = 0; + } + skip_count++; + } + } + + if (patch_count > 0) { + *output_ptr++ = PATCH_OP; + write_varint(&output_ptr, patch_count); + memcpy(output_ptr, patch_blocks, patch_count * IPF_BLOCK_SIZE); + output_ptr += patch_count * IPF_BLOCK_SIZE; + } + + *output_ptr++ = END_OP; + + free(patch_blocks); + return output_ptr - delta_buffer; +} + +// Get current time in seconds +static double get_current_time_sec(struct timeval *tv) { + gettimeofday(tv, NULL); + return tv->tv_sec + tv->tv_usec / 1000000.0; +} + +// Display progress information similar to FFmpeg +static void display_progress(encoder_config_t *config, int frame_num) { + struct timeval current_time; + double current_sec = get_current_time_sec(¤t_time); + + // Only update progress once per second + double last_progress_sec = config->last_progress_time.tv_sec + config->last_progress_time.tv_usec / 1000000.0; + if (current_sec - last_progress_sec < 1.0) { + return; + } + + config->last_progress_time = current_time; + + // Calculate timing + double start_sec = config->start_time.tv_sec + config->start_time.tv_usec / 1000000.0; + double elapsed_sec = current_sec - start_sec; + double current_video_time = (double)frame_num / config->fps; + double fps = frame_num / elapsed_sec; + double speed = (elapsed_sec > 0) ? current_video_time / elapsed_sec : 0.0; + double bitrate = (elapsed_sec > 0) ? (config->total_output_bytes * 8.0 / 1024.0) / elapsed_sec : 0.0; + + // Format output size in human readable format + char size_str[32]; + if (config->total_output_bytes >= 1024 * 1024) { + snprintf(size_str, sizeof(size_str), "%.1fMB", config->total_output_bytes / (1024.0 * 1024.0)); + } else if (config->total_output_bytes >= 1024) { + snprintf(size_str, sizeof(size_str), "%.1fkB", config->total_output_bytes / 1024.0); + } else { + snprintf(size_str, sizeof(size_str), "%zuB", config->total_output_bytes); + } + + // Format current time as HH:MM:SS.xx + int hours = (int)(current_video_time / 3600); + int minutes = (int)((current_video_time - hours * 3600) / 60); + double seconds = current_video_time - hours * 3600 - minutes * 60; + + // Print progress line (overwrite previous line) + fprintf(stderr, "\rframe=%d fps=%.1f size=%s time=%02d:%02d:%05.2f bitrate=%.1fkbits/s speed=%4.2fx", + frame_num, fps, size_str, hours, minutes, seconds, bitrate, speed); + fflush(stderr); +} + +// Process audio for current frame +static int process_audio(encoder_config_t *config, int frame_num, FILE *output) { + if (!config->has_audio || !config->mp2_file || config->audio_remaining <= 0) { + return 1; + } + + // Initialize packet size on first frame + if (config->mp2_packet_size == 0) { + uint8_t header[4]; + if (fread(header, 1, 4, config->mp2_file) != 4) return 1; + fseek(config->mp2_file, 0, SEEK_SET); + + config->mp2_packet_size = get_mp2_packet_size(header); + int is_mono = (header[3] >> 6) == 3; + config->mp2_rate_index = mp2_packet_size_to_rate_index(config->mp2_packet_size, is_mono); + } + + // Calculate how much audio time each frame represents (in seconds) + double frame_audio_time = 1.0 / config->fps; + + // Calculate how much audio time each MP2 packet represents + // MP2 frame contains 1152 samples at 32kHz = 0.036 seconds + double packet_audio_time = 1152.0 / MP2_SAMPLE_RATE; + + // Estimate how many packets we consume per video frame + double packets_per_frame = frame_audio_time / packet_audio_time; + + // Only insert audio when buffer would go below 2 frames + // Initialize with 2 packets on first frame to prime the buffer + int packets_to_insert = 0; + if (frame_num == 1) { + packets_to_insert = 2; + config->audio_frames_in_buffer = 2; + } else { + // Simulate buffer consumption (packets consumed per frame) + config->audio_frames_in_buffer -= (int)ceil(packets_per_frame); + + // Only insert packets when buffer gets low (≤ 2 frames) + if (config->audio_frames_in_buffer <= 2) { + packets_to_insert = config->target_audio_buffer_size - config->audio_frames_in_buffer; + packets_to_insert = (packets_to_insert > 0) ? packets_to_insert : 1; + } + } + + // Insert the calculated number of audio packets + for (int q = 0; q < packets_to_insert; q++) { + size_t bytes_to_read = config->mp2_packet_size; + if (bytes_to_read > config->audio_remaining) { + bytes_to_read = config->audio_remaining; + } + + size_t bytes_read = fread(config->mp2_buffer, 1, bytes_to_read, config->mp2_file); + if (bytes_read == 0) break; + + uint8_t audio_packet_type[2] = {config->mp2_rate_index, MP2_PACKET_TYPE_BASE}; + fwrite(audio_packet_type, 1, 2, output); + fwrite(config->mp2_buffer, 1, bytes_read, output); + + // Track audio bytes written + config->total_output_bytes += 2 + bytes_read; + config->audio_remaining -= bytes_read; + config->audio_frames_in_buffer++; + } + + return 1; +} + +// Write TVDOS header +static void write_tvdos_header(encoder_config_t *config, FILE *output) { + fwrite(TVDOS_MAGIC, 1, 8, output); + fwrite(&config->width, 2, 1, output); + fwrite(&config->height, 2, 1, output); + fwrite(&config->fps, 2, 1, output); + fwrite(&config->total_frames, 4, 1, output); + + uint16_t unused = 0x00FF; + fwrite(&unused, 2, 1, output); + + int audio_sample_size = 2 * (((MP2_SAMPLE_RATE / config->fps) + 1)); + int audio_queue_size = config->has_audio ? + (int)ceil(audio_sample_size / 2304.0) + 1 : 0; + + uint16_t audio_queue_info = config->has_audio ? + (MP2_DEFAULT_PACKET_SIZE >> 2) | (audio_queue_size << 12) : 0x0000; + fwrite(&audio_queue_info, 2, 1, output); + + // Store target buffer size for audio timing + config->target_audio_buffer_size = audio_queue_size; + + uint8_t reserved[10] = {0}; + fwrite(reserved, 1, 10, output); +} + +// Initialize encoder configuration +static encoder_config_t *init_encoder_config() { + encoder_config_t *config = calloc(1, sizeof(encoder_config_t)); + if (!config) return NULL; + + config->width = DEFAULT_WIDTH; + config->height = DEFAULT_HEIGHT; + + return config; +} + +// Allocate encoder buffers +static int allocate_buffers(encoder_config_t *config) { + config->frame_buffer_size = ((config->width + 3) / 4) * ((config->height + 3) / 4) * IPF_BLOCK_SIZE; + + config->rgb_buffer = malloc(config->width * config->height * 3); + config->previous_ipf_frame = malloc(config->frame_buffer_size); + config->current_ipf_frame = malloc(config->frame_buffer_size); + config->delta_buffer = malloc(config->frame_buffer_size * 2); + config->compressed_buffer = malloc(config->frame_buffer_size * 2); + config->mp2_buffer = malloc(2048); + + return (config->rgb_buffer && config->previous_ipf_frame && + config->current_ipf_frame && config->delta_buffer && + config->compressed_buffer && config->mp2_buffer); +} + +// Process one frame - CORRECTED ORDER: Audio -> Video -> Sync +static int process_frame(encoder_config_t *config, int frame_num, int is_keyframe, FILE *output) { + // Read RGB data from FFmpeg pipe first + size_t rgb_size = config->width * config->height * 3; + if (fread(config->rgb_buffer, 1, rgb_size, config->ffmpeg_video_pipe) != rgb_size) { + if (feof(config->ffmpeg_video_pipe)) return 0; + return -1; + } + + // Step 1: Process audio FIRST (matches working file pattern) + if (!process_audio(config, frame_num, output)) { + return -1; + } + + // Step 2: Encode and write video + int pattern; + switch (config->dither_mode) { + case 0: pattern = -1; break; // No dithering + case 1: pattern = 0; break; // Static pattern + case 2: pattern = frame_num % 4; break; // Dynamic pattern + default: pattern = 0; break; // Fallback to static + } + encode_ipf1_frame(config->rgb_buffer, config->width, config->height, 3, pattern, + config->current_ipf_frame); + + // Determine if we should use delta encoding + int use_delta = 0; + size_t data_size = config->frame_buffer_size; + uint8_t *frame_data = config->current_ipf_frame; + + if (frame_num > 1 && !is_keyframe) { + size_t delta_size = encode_ipf1_delta(config->previous_ipf_frame, + config->current_ipf_frame, + config->width, config->height, + config->delta_buffer); + + if (delta_size < config->frame_buffer_size * 0.576) { + use_delta = 1; + data_size = delta_size; + frame_data = config->delta_buffer; + } + } + + // Compress the frame data using gzip + size_t compressed_size = gzip_compress(frame_data, data_size, + config->compressed_buffer, + config->frame_buffer_size * 2); + if (compressed_size == 0) { + fprintf(stderr, "Gzip compression failed\n"); + return -1; + } + + // Write video packet + if (use_delta) { + uint8_t packet_type[2] = {IPF1_DELTA_PACKET_TYPE}; + fwrite(packet_type, 1, 2, output); + } else { + uint8_t packet_type[2] = {IPF1_PACKET_TYPE}; + fwrite(packet_type, 1, 2, output); + } + + uint32_t size_le = compressed_size; + fwrite(&size_le, 4, 1, output); + fwrite(config->compressed_buffer, 1, compressed_size, output); + + // Step 3: Write sync packet AFTER video (matches working file pattern) + uint8_t sync[2] = {SYNC_PACKET_TYPE}; + fwrite(sync, 1, 2, output); + + // Track video bytes written (packet type + size + compressed data + sync) + config->total_output_bytes += 2 + 4 + compressed_size + 2; + + // Swap frame buffers + uint8_t *temp = config->previous_ipf_frame; + config->previous_ipf_frame = config->current_ipf_frame; + config->current_ipf_frame = temp; + + // Display progress + display_progress(config, frame_num); + + return 1; +} + +// Cleanup function +static void cleanup_config(encoder_config_t *config) { + if (!config) return; + + if (config->ffmpeg_video_pipe) pclose(config->ffmpeg_video_pipe); + if (config->mp2_file) fclose(config->mp2_file); + + free(config->input_file); + free(config->output_file); + free(config->rgb_buffer); + free(config->previous_ipf_frame); + free(config->current_ipf_frame); + free(config->delta_buffer); + free(config->compressed_buffer); + free(config->mp2_buffer); + + // Remove temporary audio file + unlink(TEMP_AUDIO_FILE); + + free(config); +} + +// Print usage information +static void print_usage(const char *program_name) { + printf("TVDOS Movie Encoder\n\n"); + printf("Usage: %s [options] input_video\n\n", program_name); + printf("Options:\n"); + printf(" -o, --output FILE Output TVDOS movie file (default: stdout)\n"); + printf(" -s, --size WxH Video resolution (default: 560x448)\n"); + printf(" -d, --dither MODE Dithering mode (default: 1)\n"); + printf(" 0: No dithering\n"); + printf(" 1: Static pattern\n"); + printf(" 2: Dynamic pattern (better quality, larger files)\n"); + printf(" -h, --help Show this help message\n\n"); + printf("Examples:\n"); + printf(" %s input.mp4 -o output.mov\n", program_name); + printf(" %s input.avi -s 1024x768 -o output.mov\n", program_name); + printf(" yt-dlp -o - \"https://youtube.com/watch?v=VIDEO_ID\" | ffmpeg -i pipe:0 -c copy temp.mp4 && %s temp.mp4 -o youtube_video.mov && rm temp.mp4\n", program_name); +} + +int main(int argc, char *argv[]) { + encoder_config_t *config = init_encoder_config(); + if (!config) { + fprintf(stderr, "Failed to initialize encoder\n"); + return 1; + } + + config->output_to_stdout = 1; // Default to stdout + config->dither_mode = 1; // Default to static dithering + + // Parse command line arguments + static struct option long_options[] = { + {"output", required_argument, 0, 'o'}, + {"size", required_argument, 0, 's'}, + {"dither", required_argument, 0, 'd'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0} + }; + + int c; + while ((c = getopt_long(argc, argv, "o:s:d:h", long_options, NULL)) != -1) { + switch (c) { + case 'o': + config->output_file = strdup(optarg); + config->output_to_stdout = 0; + break; + case 's': + if (!parse_resolution(optarg, &config->width, &config->height)) { + fprintf(stderr, "Invalid resolution format: %s\n", optarg); + cleanup_config(config); + return 1; + } + break; + case 'd': + config->dither_mode = atoi(optarg); + if (config->dither_mode < 0 || config->dither_mode > 2) { + fprintf(stderr, "Invalid dither mode: %s (must be 0, 1, or 2)\n", optarg); + cleanup_config(config); + return 1; + } + break; + case 'h': + print_usage(argv[0]); + cleanup_config(config); + return 0; + default: + print_usage(argv[0]); + cleanup_config(config); + return 1; + } + } + + if (optind >= argc) { + fprintf(stderr, "Error: Input video file required\n\n"); + print_usage(argv[0]); + cleanup_config(config); + return 1; + } + + config->input_file = strdup(argv[optind]); + + // Get video metadata + if (!get_video_metadata(config)) { + fprintf(stderr, "Failed to analyze video metadata\n"); + cleanup_config(config); + return 1; + } + + // Allocate buffers + if (!allocate_buffers(config)) { + fprintf(stderr, "Failed to allocate memory buffers\n"); + cleanup_config(config); + return 1; + } + + // Start video conversion + if (!start_video_conversion(config)) { + fprintf(stderr, "Failed to start video conversion\n"); + cleanup_config(config); + return 1; + } + + // Start audio conversion + if (!start_audio_conversion(config)) { + fprintf(stderr, "Failed to start audio conversion\n"); + cleanup_config(config); + return 1; + } + + // Open output + FILE *output = config->output_to_stdout ? stdout : fopen(config->output_file, "wb"); + if (!output) { + fprintf(stderr, "Failed to open output file\n"); + cleanup_config(config); + return 1; + } + + // Write TVDOS header + write_tvdos_header(config, output); + + // Initialize progress tracking + gettimeofday(&config->start_time, NULL); + config->last_progress_time = config->start_time; + config->total_output_bytes = 8 + 2 + 2 + 2 + 4 + 2 + 2 + 10; // TVDOS header size + + // Process frames with correct order: Audio -> Video -> Sync + for (int frame = 1; frame <= config->total_frames; frame++) { + int is_keyframe = (frame == 1) || (frame % 30 == 0); + + int result = process_frame(config, frame, is_keyframe, output); + if (result <= 0) { + if (result == 0) { + fprintf(stderr, "End of video at frame %d\n", frame); + } + break; + } + } + + // Final progress update and newline + fprintf(stderr, "\n"); + + if (!config->output_to_stdout) { + fclose(output); + fprintf(stderr, "Encoding complete: %s\n", config->output_file); + } + + cleanup_config(config); + return 0; +} diff --git a/video_encoder/encoder_tev.c b/video_encoder/encoder_tev.c index 64c5e7d..36ce8b2 100644 --- a/video_encoder/encoder_tev.c +++ b/video_encoder/encoder_tev.c @@ -1,4 +1,5 @@ -// Created by Claude on 2025-08-17. +// Created by Claude on 2025-08-18. +// TEV (TSVM Enhanced Video) Encoder - YCoCg-R 4:2:0 16x16 Block Version #include #include #include @@ -13,9 +14,9 @@ // TSVM Enhanced Video (TEV) format constants #define TEV_MAGIC "\x1F\x54\x53\x56\x4D\x54\x45\x56" // "\x1FTSVM TEV" -#define TEV_VERSION 1 +#define TEV_VERSION 2 // Updated for YCoCg-R 4:2:0 -// Block encoding modes (8x8 blocks) +// Block encoding modes (16x16 blocks) #define TEV_MODE_SKIP 0x00 // Skip block (copy from reference) #define TEV_MODE_INTRA 0x01 // Intra DCT coding (I-frame blocks) #define TEV_MODE_INTER 0x02 // Inter DCT coding with motion compensation @@ -27,76 +28,220 @@ #define TEV_PACKET_AUDIO_MP2 0x20 // MP2 audio #define TEV_PACKET_SYNC 0xFF // Sync packet -// Quality settings for quantization -static const uint8_t QUANT_TABLES[8][64] = { - // Quality 0 (lowest) - {80, 60, 50, 80, 120, 200, 255, 255, - 55, 60, 70, 95, 130, 255, 255, 255, - 70, 65, 80, 120, 200, 255, 255, 255, - 70, 85, 110, 145, 255, 255, 255, 255, - 90, 110, 185, 255, 255, 255, 255, 255, - 120, 175, 255, 255, 255, 255, 255, 255, - 245, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255}, - // Quality 1-6 (intermediate)... - {40, 30, 25, 40, 60, 100, 128, 150, - 28, 30, 35, 48, 65, 128, 150, 180, - 35, 33, 40, 60, 100, 128, 150, 180, - 35, 43, 55, 73, 128, 150, 180, 200, - 45, 55, 93, 128, 150, 180, 200, 220, - 60, 88, 128, 150, 180, 200, 220, 240, - 123, 128, 150, 180, 200, 220, 240, 250, - 128, 150, 180, 200, 220, 240, 250, 255}, - // ... (simplified for example) - {20, 15, 13, 20, 30, 50, 64, 75, - 14, 15, 18, 24, 33, 64, 75, 90, - 18, 17, 20, 30, 50, 64, 75, 90, - 18, 22, 28, 37, 64, 75, 90, 100, - 23, 28, 47, 64, 75, 90, 100, 110, - 30, 44, 64, 75, 90, 100, 110, 120, - 62, 64, 75, 90, 100, 110, 120, 125, - 64, 75, 90, 100, 110, 120, 125, 128}, - {16, 12, 10, 16, 24, 40, 51, 60, - 11, 12, 14, 19, 26, 51, 60, 72, - 14, 13, 16, 24, 40, 51, 60, 72, - 14, 17, 22, 29, 51, 60, 72, 80, - 18, 22, 37, 51, 60, 72, 80, 88, - 24, 35, 51, 60, 72, 80, 88, 96, - 49, 51, 60, 72, 80, 88, 96, 100, - 51, 60, 72, 80, 88, 96, 100, 102}, - {12, 9, 8, 12, 18, 30, 38, 45, - 8, 9, 11, 14, 20, 38, 45, 54, - 11, 10, 12, 18, 30, 38, 45, 54, - 11, 13, 17, 22, 38, 45, 54, 60, - 14, 17, 28, 38, 45, 54, 60, 66, - 18, 26, 38, 45, 54, 60, 66, 72, - 37, 38, 45, 54, 60, 66, 72, 75, - 38, 45, 54, 60, 66, 72, 75, 77}, - {10, 7, 6, 10, 15, 25, 32, 38, - 7, 7, 9, 12, 16, 32, 38, 45, - 9, 8, 10, 15, 25, 32, 38, 45, - 9, 11, 14, 18, 32, 38, 45, 50, - 12, 14, 23, 32, 38, 45, 50, 55, - 15, 22, 32, 38, 45, 50, 55, 60, - 31, 32, 38, 45, 50, 55, 60, 63, - 32, 38, 45, 50, 55, 60, 63, 65}, - {8, 6, 5, 8, 12, 20, 26, 30, - 6, 6, 7, 10, 13, 26, 30, 36, - 7, 7, 8, 12, 20, 26, 30, 36, - 7, 9, 11, 15, 26, 30, 36, 40, - 10, 11, 19, 26, 30, 36, 40, 44, - 12, 17, 26, 30, 36, 40, 44, 48, - 25, 26, 30, 36, 40, 44, 48, 50, - 26, 30, 36, 40, 44, 48, 50, 52}, +// Quality settings for quantization (Y channel) - 16x16 tables +static const uint8_t QUANT_TABLES_Y[8][256] = { + // Quality 0 (lowest) - 16x16 table + {80, 60, 50, 80, 120, 200, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 55, 60, 70, 95, 130, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 70, 65, 80, 120, 200, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 70, 85, 110, 145, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 90, 110, 185, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 120, 175, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 245, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + // Quality 1 + {40, 30, 25, 40, 60, 100, 128, 150, 128, 150, 180, 200, 220, 240, 250, 255, + 28, 30, 35, 48, 65, 128, 150, 180, 150, 180, 200, 220, 240, 250, 255, 255, + 35, 33, 40, 60, 100, 128, 150, 180, 150, 180, 200, 220, 240, 250, 255, 255, + 35, 43, 55, 73, 128, 150, 180, 200, 180, 200, 220, 240, 250, 255, 255, 255, + 45, 55, 93, 128, 150, 180, 200, 220, 200, 220, 240, 250, 255, 255, 255, 255, + 60, 88, 128, 150, 180, 200, 220, 240, 220, 240, 250, 255, 255, 255, 255, 255, + 123, 128, 150, 180, 200, 220, 240, 250, 240, 250, 255, 255, 255, 255, 255, 255, + 128, 150, 180, 200, 220, 240, 250, 255, 250, 255, 255, 255, 255, 255, 255, 255, + 128, 150, 180, 200, 220, 240, 250, 255, 250, 255, 255, 255, 255, 255, 255, 255, + 150, 180, 200, 220, 240, 250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 180, 200, 220, 240, 250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 200, 220, 240, 250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 220, 240, 250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 240, 250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + // Quality 2 + {20, 15, 13, 20, 30, 50, 64, 75, 64, 75, 90, 100, 110, 120, 125, 128, + 14, 15, 18, 24, 33, 64, 75, 90, 75, 90, 100, 110, 120, 125, 128, 140, + 18, 17, 20, 30, 50, 64, 75, 90, 75, 90, 100, 110, 120, 125, 128, 140, + 18, 22, 28, 37, 64, 75, 90, 100, 90, 100, 110, 120, 125, 128, 140, 150, + 23, 28, 47, 64, 75, 90, 100, 110, 100, 110, 120, 125, 128, 140, 150, 160, + 30, 44, 64, 75, 90, 100, 110, 120, 110, 120, 125, 128, 140, 150, 160, 170, + 62, 64, 75, 90, 100, 110, 120, 125, 120, 125, 128, 140, 150, 160, 170, 180, + 64, 75, 90, 100, 110, 120, 125, 128, 125, 128, 140, 150, 160, 170, 180, 190, + 64, 75, 90, 100, 110, 120, 125, 128, 125, 128, 140, 150, 160, 170, 180, 190, + 75, 90, 100, 110, 120, 125, 128, 140, 128, 140, 150, 160, 170, 180, 190, 200, + 90, 100, 110, 120, 125, 128, 140, 150, 140, 150, 160, 170, 180, 190, 200, 210, + 100, 110, 120, 125, 128, 140, 150, 160, 150, 160, 170, 180, 190, 200, 210, 220, + 110, 120, 125, 128, 140, 150, 160, 170, 160, 170, 180, 190, 200, 210, 220, 230, + 120, 125, 128, 140, 150, 160, 170, 180, 170, 180, 190, 200, 210, 220, 230, 240, + 125, 128, 140, 150, 160, 170, 180, 190, 180, 190, 200, 210, 220, 230, 240, 250, + 128, 140, 150, 160, 170, 180, 190, 200, 190, 200, 210, 220, 230, 240, 250, 255}, + // Quality 3 + {16, 12, 10, 16, 24, 40, 51, 60, 51, 60, 72, 80, 88, 96, 100, 102, + 11, 12, 14, 19, 26, 51, 60, 72, 60, 72, 80, 88, 96, 100, 102, 110, + 14, 13, 16, 24, 40, 51, 60, 72, 60, 72, 80, 88, 96, 100, 102, 110, + 14, 17, 22, 29, 51, 60, 72, 80, 72, 80, 88, 96, 100, 102, 110, 120, + 18, 22, 37, 51, 60, 72, 80, 88, 80, 88, 96, 100, 102, 110, 120, 130, + 24, 35, 51, 60, 72, 80, 88, 96, 88, 96, 100, 102, 110, 120, 130, 140, + 49, 51, 60, 72, 80, 88, 96, 100, 96, 100, 102, 110, 120, 130, 140, 150, + 51, 60, 72, 80, 88, 96, 100, 102, 100, 102, 110, 120, 130, 140, 150, 160, + 51, 60, 72, 80, 88, 96, 100, 102, 100, 102, 110, 120, 130, 140, 150, 160, + 60, 72, 80, 88, 96, 100, 102, 110, 102, 110, 120, 130, 140, 150, 160, 170, + 72, 80, 88, 96, 100, 102, 110, 120, 110, 120, 130, 140, 150, 160, 170, 180, + 80, 88, 96, 100, 102, 110, 120, 130, 120, 130, 140, 150, 160, 170, 180, 190, + 88, 96, 100, 102, 110, 120, 130, 140, 130, 140, 150, 160, 170, 180, 190, 200, + 96, 100, 102, 110, 120, 130, 140, 150, 140, 150, 160, 170, 180, 190, 200, 210, + 100, 102, 110, 120, 130, 140, 150, 160, 150, 160, 170, 180, 190, 200, 210, 220, + 102, 110, 120, 130, 140, 150, 160, 170, 160, 170, 180, 190, 200, 210, 220, 230}, + // Quality 4 + {12, 9, 8, 12, 18, 30, 38, 45, 38, 45, 54, 60, 66, 72, 75, 77, + 8, 9, 11, 14, 20, 38, 45, 54, 45, 54, 60, 66, 72, 75, 77, 85, + 11, 10, 12, 18, 30, 38, 45, 54, 45, 54, 60, 66, 72, 75, 77, 85, + 11, 13, 17, 22, 38, 45, 54, 60, 54, 60, 66, 72, 75, 77, 85, 95, + 14, 17, 28, 38, 45, 54, 60, 66, 60, 66, 72, 75, 77, 85, 95, 105, + 18, 26, 38, 45, 54, 60, 66, 72, 66, 72, 75, 77, 85, 95, 105, 115, + 37, 38, 45, 54, 60, 66, 72, 75, 72, 75, 77, 85, 95, 105, 115, 125, + 38, 45, 54, 60, 66, 72, 75, 77, 75, 77, 85, 95, 105, 115, 125, 135, + 38, 45, 54, 60, 66, 72, 75, 77, 75, 77, 85, 95, 105, 115, 125, 135, + 45, 54, 60, 66, 72, 75, 77, 85, 77, 85, 95, 105, 115, 125, 135, 145, + 54, 60, 66, 72, 75, 77, 85, 95, 85, 95, 105, 115, 125, 135, 145, 155, + 60, 66, 72, 75, 77, 85, 95, 105, 95, 105, 115, 125, 135, 145, 155, 165, + 66, 72, 75, 77, 85, 95, 105, 115, 105, 115, 125, 135, 145, 155, 165, 175, + 72, 75, 77, 85, 95, 105, 115, 125, 115, 125, 135, 145, 155, 165, 175, 185, + 75, 77, 85, 95, 105, 115, 125, 135, 125, 135, 145, 155, 165, 175, 185, 195, + 77, 85, 95, 105, 115, 125, 135, 145, 135, 145, 155, 165, 175, 185, 195, 205}, + // Quality 5 + {10, 7, 6, 10, 15, 25, 32, 38, 32, 38, 45, 50, 55, 60, 63, 65, + 7, 7, 9, 12, 16, 32, 38, 45, 38, 45, 50, 55, 60, 63, 65, 70, + 9, 8, 10, 15, 25, 32, 38, 45, 38, 45, 50, 55, 60, 63, 65, 70, + 9, 11, 14, 18, 32, 38, 45, 50, 45, 50, 55, 60, 63, 65, 70, 75, + 12, 14, 23, 32, 38, 45, 50, 55, 50, 55, 60, 63, 65, 70, 75, 80, + 15, 22, 32, 38, 45, 50, 55, 60, 55, 60, 63, 65, 70, 75, 80, 85, + 31, 32, 38, 45, 50, 55, 60, 63, 60, 63, 65, 70, 75, 80, 85, 90, + 32, 38, 45, 50, 55, 60, 63, 65, 63, 65, 70, 75, 80, 85, 90, 95, + 32, 38, 45, 50, 55, 60, 63, 65, 63, 65, 70, 75, 80, 85, 90, 95, + 38, 45, 50, 55, 60, 63, 65, 70, 65, 70, 75, 80, 85, 90, 95, 100, + 45, 50, 55, 60, 63, 65, 70, 75, 70, 75, 80, 85, 90, 95, 100, 105, + 50, 55, 60, 63, 65, 70, 75, 80, 75, 80, 85, 90, 95, 100, 105, 110, + 55, 60, 63, 65, 70, 75, 80, 85, 80, 85, 90, 95, 100, 105, 110, 115, + 60, 63, 65, 70, 75, 80, 85, 90, 85, 90, 95, 100, 105, 110, 115, 120, + 63, 65, 70, 75, 80, 85, 90, 95, 90, 95, 100, 105, 110, 115, 120, 125, + 65, 70, 75, 80, 85, 90, 95, 100, 95, 100, 105, 110, 115, 120, 125, 130}, + // Quality 6 + {8, 6, 5, 8, 12, 20, 26, 30, 26, 30, 36, 40, 44, 48, 50, 52, + 6, 6, 7, 10, 13, 26, 30, 36, 30, 36, 40, 44, 48, 50, 52, 56, + 7, 7, 8, 12, 20, 26, 30, 36, 30, 36, 40, 44, 48, 50, 52, 56, + 7, 9, 11, 15, 26, 30, 36, 40, 36, 40, 44, 48, 50, 52, 56, 60, + 10, 11, 19, 26, 30, 36, 40, 44, 40, 44, 48, 50, 52, 56, 60, 64, + 12, 17, 26, 30, 36, 40, 44, 48, 44, 48, 50, 52, 56, 60, 64, 68, + 25, 26, 30, 36, 40, 44, 48, 50, 48, 50, 52, 56, 60, 64, 68, 72, + 26, 30, 36, 40, 44, 48, 50, 52, 50, 52, 56, 60, 64, 68, 72, 76, + 26, 30, 36, 40, 44, 48, 50, 52, 50, 52, 56, 60, 64, 68, 72, 76, + 30, 36, 40, 44, 48, 50, 52, 56, 52, 56, 60, 64, 68, 72, 76, 80, + 36, 40, 44, 48, 50, 52, 56, 60, 56, 60, 64, 68, 72, 76, 80, 84, + 40, 44, 48, 50, 52, 56, 60, 64, 60, 64, 68, 72, 76, 80, 84, 88, + 44, 48, 50, 52, 56, 60, 64, 68, 64, 68, 72, 76, 80, 84, 88, 92, + 48, 50, 52, 56, 60, 64, 68, 72, 68, 72, 76, 80, 84, 88, 92, 96, + 50, 52, 56, 60, 64, 68, 72, 76, 72, 76, 80, 84, 88, 92, 96, 100, + 52, 56, 60, 64, 68, 72, 76, 80, 76, 80, 84, 88, 92, 96, 100, 104}, // Quality 7 (highest) - {2, 1, 1, 2, 3, 5, 6, 7, - 1, 1, 1, 2, 3, 6, 7, 9, - 1, 1, 2, 3, 5, 6, 7, 9, - 1, 2, 3, 4, 6, 7, 9, 10, - 2, 3, 5, 6, 7, 9, 10, 11, - 3, 4, 6, 7, 9, 10, 11, 12, - 6, 6, 7, 9, 10, 11, 12, 13, - 6, 7, 9, 10, 11, 12, 13, 13} + {2, 1, 1, 2, 3, 5, 6, 7, 6, 7, 8, 9, 10, 11, 12, 13, + 1, 1, 1, 2, 3, 6, 7, 9, 7, 9, 10, 11, 12, 13, 14, 15, + 1, 1, 2, 3, 5, 6, 7, 9, 7, 9, 10, 11, 12, 13, 14, 15, + 1, 2, 3, 4, 6, 7, 9, 10, 9, 10, 11, 12, 13, 14, 15, 16, + 2, 3, 5, 6, 7, 9, 10, 11, 10, 11, 12, 13, 14, 15, 16, 17, + 3, 4, 6, 7, 9, 10, 11, 12, 11, 12, 13, 14, 15, 16, 17, 18, + 6, 6, 7, 9, 10, 11, 12, 13, 12, 13, 14, 15, 16, 17, 18, 19, + 6, 7, 9, 10, 11, 12, 13, 14, 13, 14, 15, 16, 17, 18, 19, 20, + 6, 7, 9, 10, 11, 12, 13, 14, 13, 14, 15, 16, 17, 18, 19, 20, + 7, 9, 10, 11, 12, 13, 14, 15, 14, 15, 16, 17, 18, 19, 20, 21, + 9, 10, 11, 12, 13, 14, 15, 16, 15, 16, 17, 18, 19, 20, 21, 22, + 10, 11, 12, 13, 14, 15, 16, 17, 16, 17, 18, 19, 20, 21, 22, 23, + 11, 12, 13, 14, 15, 16, 17, 18, 17, 18, 19, 20, 21, 22, 23, 24, + 12, 13, 14, 15, 16, 17, 18, 19, 18, 19, 20, 21, 22, 23, 24, 25, + 13, 14, 15, 16, 17, 18, 19, 20, 19, 20, 21, 22, 23, 24, 25, 26, + 14, 15, 16, 17, 18, 19, 20, 21, 20, 21, 22, 23, 24, 25, 26, 27} +}; + +// Quality settings for quantization (Chroma channels - 8x8) +static const uint8_t QUANT_TABLES_C[8][64] = { + // Quality 0 (lowest) + {120, 90, 75, 120, 180, 255, 255, 255, + 83, 90, 105, 143, 195, 255, 255, 255, + 105, 98, 120, 180, 255, 255, 255, 255, + 105, 128, 165, 218, 255, 255, 255, 255, + 135, 165, 255, 255, 255, 255, 255, 255, + 180, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255}, + // Quality 1 + {60, 45, 38, 60, 90, 150, 192, 225, + 42, 45, 53, 72, 98, 192, 225, 255, + 53, 49, 60, 90, 150, 192, 225, 255, + 53, 64, 83, 109, 192, 225, 255, 255, + 68, 83, 139, 192, 225, 255, 255, 255, + 90, 132, 192, 225, 255, 255, 255, 255, + 185, 192, 225, 255, 255, 255, 255, 255, + 192, 225, 255, 255, 255, 255, 255, 255}, + // Quality 2 + {30, 23, 19, 30, 45, 75, 96, 113, + 21, 23, 27, 36, 49, 96, 113, 135, + 27, 25, 30, 45, 75, 96, 113, 135, + 27, 32, 42, 55, 96, 113, 135, 150, + 34, 42, 70, 96, 113, 135, 150, 165, + 45, 66, 96, 113, 135, 150, 165, 180, + 93, 96, 113, 135, 150, 165, 180, 188, + 96, 113, 135, 150, 165, 180, 188, 192}, + // Quality 3 + {24, 18, 15, 24, 36, 60, 77, 90, + 17, 18, 21, 29, 39, 77, 90, 108, + 21, 20, 24, 36, 60, 77, 90, 108, + 21, 26, 33, 44, 77, 90, 108, 120, + 27, 33, 56, 77, 90, 108, 120, 132, + 36, 53, 77, 90, 108, 120, 132, 144, + 74, 77, 90, 108, 120, 132, 144, 150, + 77, 90, 108, 120, 132, 144, 150, 154}, + // Quality 4 + {18, 14, 12, 18, 27, 45, 57, 68, + 13, 14, 16, 22, 30, 57, 68, 81, + 16, 15, 18, 27, 45, 57, 68, 81, + 16, 20, 25, 33, 57, 68, 81, 90, + 20, 25, 42, 57, 68, 81, 90, 99, + 27, 39, 57, 68, 81, 90, 99, 108, + 56, 57, 68, 81, 90, 99, 108, 113, + 57, 68, 81, 90, 99, 108, 113, 116}, + // Quality 5 + {15, 11, 9, 15, 23, 38, 48, 57, + 11, 11, 13, 18, 24, 48, 57, 68, + 13, 12, 15, 23, 38, 48, 57, 68, + 13, 16, 21, 28, 48, 57, 68, 75, + 17, 21, 35, 48, 57, 68, 75, 83, + 23, 33, 48, 57, 68, 75, 83, 90, + 46, 48, 57, 68, 75, 83, 90, 94, + 48, 57, 68, 75, 83, 90, 94, 96}, + // Quality 6 + {12, 9, 8, 12, 18, 30, 39, 45, + 9, 9, 11, 14, 20, 39, 45, 54, + 11, 10, 12, 18, 30, 39, 45, 54, + 11, 13, 17, 22, 39, 45, 54, 60, + 14, 17, 28, 39, 45, 54, 60, 66, + 18, 26, 39, 45, 54, 60, 66, 72, + 38, 39, 45, 54, 60, 66, 72, 75, + 39, 45, 54, 60, 66, 72, 75, 77}, + // Quality 7 (highest) + {3, 2, 2, 3, 5, 8, 9, 11, + 2, 2, 2, 3, 5, 9, 11, 14, + 2, 2, 3, 5, 8, 9, 11, 14, + 2, 3, 5, 6, 9, 11, 14, 15, + 3, 5, 8, 9, 11, 14, 15, 17, + 5, 6, 9, 11, 14, 15, 17, 18, + 9, 9, 11, 14, 15, 17, 18, 20, + 9, 11, 14, 15, 17, 18, 20, 20} }; // Audio constants (reuse MP2 from existing system) @@ -106,7 +251,7 @@ static const uint8_t QUANT_TABLES[8][64] = { // Encoding parameters #define MAX_MOTION_SEARCH 16 #define KEYFRAME_INTERVAL 30 -#define BLOCK_SIZE 8 +#define BLOCK_SIZE 16 // 16x16 blocks now // Default values #define DEFAULT_WIDTH 560 @@ -116,8 +261,10 @@ static const uint8_t QUANT_TABLES[8][64] = { typedef struct __attribute__((packed)) { uint8_t mode; // Block encoding mode int16_t mv_x, mv_y; // Motion vector (1/4 pixel precision) - uint16_t cbp; // Coded block pattern (which 8x8 have non-zero coeffs) - int16_t dct_coeffs[3][64]; // Quantized DCT coefficients (R,G,B) + uint16_t cbp; // Coded block pattern (which channels have non-zero coeffs) + int16_t y_coeffs[256]; // Quantized Y DCT coefficients (16x16) + int16_t co_coeffs[64]; // Quantized Co DCT coefficients (8x8) + int16_t cg_coeffs[64]; // Quantized Cg DCT coefficients (8x8) } tev_block_t; typedef struct { @@ -135,9 +282,9 @@ typedef struct { // Frame buffers (8-bit RGB format for encoding) uint8_t *current_rgb, *previous_rgb, *reference_rgb; - // Encoding workspace - uint8_t *rgb_workspace; // 8x8 RGB blocks (192 bytes) - float *dct_workspace; // DCT coefficients (192 floats) + // YCoCg workspace + float *y_workspace, *co_workspace, *cg_workspace; + float *dct_workspace; // DCT coefficients tev_block_t *block_data; // Encoded block data uint8_t *compressed_buffer; // Zstd output @@ -161,6 +308,69 @@ typedef struct { int blocks_skip, blocks_intra, blocks_inter, blocks_motion; } tev_encoder_t; +// RGB to YCoCg-R transform +static void rgb_to_ycocgr(uint8_t r, uint8_t g, uint8_t b, int *y, int *co, int *cg) { + *co = r - b; + int tmp = b + ((*co) >> 1); + *cg = g - tmp; + *y = tmp + ((*cg) >> 1); +} + +// YCoCg-R to RGB transform (for verification) +static void ycocgr_to_rgb(int y, int co, int cg, uint8_t *r, uint8_t *g, uint8_t *b) { + int tmp = y - (cg >> 1); + *g = cg + tmp; + *b = tmp - (co >> 1); + *r = *b + co; + + // Clamp values + *r = (*r < 0) ? 0 : ((*r > 255) ? 255 : *r); + *g = (*g < 0) ? 0 : ((*g > 255) ? 255 : *g); + *b = (*b < 0) ? 0 : ((*b > 255) ? 255 : *b); +} + +// 16x16 2D DCT +static void dct_16x16(float *input, float *output) { + for (int u = 0; u < 16; u++) { + for (int v = 0; v < 16; v++) { + float sum = 0.0f; + float cu = (u == 0) ? 1.0f / sqrtf(2.0f) : 1.0f; + float cv = (v == 0) ? 1.0f / sqrtf(2.0f) : 1.0f; + + for (int x = 0; x < 16; x++) { + for (int y = 0; y < 16; y++) { + sum += input[y * 16 + x] * + cosf((2.0f * x + 1.0f) * u * M_PI / 32.0f) * + cosf((2.0f * y + 1.0f) * v * M_PI / 32.0f); + } + } + + output[u * 16 + v] = 0.25f * cu * cv * sum; + } + } +} + +// 8x8 2D DCT (for chroma) +static void dct_8x8(float *input, float *output) { + for (int u = 0; u < 8; u++) { + for (int v = 0; v < 8; v++) { + float sum = 0.0f; + float cu = (u == 0) ? 1.0f / sqrtf(2.0f) : 1.0f; + float cv = (v == 0) ? 1.0f / sqrtf(2.0f) : 1.0f; + + for (int x = 0; x < 8; x++) { + for (int y = 0; y < 8; y++) { + sum += input[y * 8 + x] * + cosf((2.0f * x + 1.0f) * u * M_PI / 16.0f) * + cosf((2.0f * y + 1.0f) * v * M_PI / 16.0f); + } + } + + output[u * 8 + v] = 0.25f * cu * cv * sum; + } + } +} + // Quantize DCT coefficient using quality table static int16_t quantize_coeff(float coeff, uint8_t quant, int is_dc) { if (is_dc) { @@ -172,24 +382,77 @@ static int16_t quantize_coeff(float coeff, uint8_t quant, int is_dc) { } } -// These functions are reserved for future rate-distortion optimization -// Currently using simplified encoding logic - -// Convert RGB to 4096-color format -static void copy_rgb_frame(uint8_t *rgb_input, uint8_t *rgb_frame, int pixels) { - // Copy input RGB data to frame buffer (preserving full 8-bit precision) - memcpy(rgb_frame, rgb_input, pixels * 3); +// Extract 16x16 block from RGB frame and convert to YCoCg-R +static void extract_ycocgr_block(uint8_t *rgb_frame, int width, int height, + int block_x, int block_y, + float *y_block, float *co_block, float *cg_block) { + int start_x = block_x * 16; + int start_y = block_y * 16; + + // Extract 16x16 Y block + for (int py = 0; py < 16; py++) { + for (int px = 0; px < 16; px++) { + int x = start_x + px; + int y = start_y + py; + + if (x < width && y < height) { + int offset = (y * width + x) * 3; + uint8_t r = rgb_frame[offset]; + uint8_t g = rgb_frame[offset + 1]; + uint8_t b = rgb_frame[offset + 2]; + + int y_val, co_val, cg_val; + rgb_to_ycocgr(r, g, b, &y_val, &co_val, &cg_val); + + y_block[py * 16 + px] = (float)y_val - 128.0f; // Center around 0 + } + } + } + + // Extract 8x8 chroma blocks with 4:2:0 subsampling (average 2x2 pixels) + for (int py = 0; py < 8; py++) { + for (int px = 0; px < 8; px++) { + int co_sum = 0, cg_sum = 0, count = 0; + + // Average 2x2 block of pixels + for (int dy = 0; dy < 2; dy++) { + for (int dx = 0; dx < 2; dx++) { + int x = start_x + px * 2 + dx; + int y = start_y + py * 2 + dy; + + if (x < width && y < height) { + int offset = (y * width + x) * 3; + uint8_t r = rgb_frame[offset]; + uint8_t g = rgb_frame[offset + 1]; + uint8_t b = rgb_frame[offset + 2]; + + int y_val, co_val, cg_val; + rgb_to_ycocgr(r, g, b, &y_val, &co_val, &cg_val); + + co_sum += co_val; + cg_sum += cg_val; + count++; + } + } + } + + if (count > 0) { + co_block[py * 8 + px] = (float)(co_sum / count); + cg_block[py * 8 + px] = (float)(cg_sum / count); + } + } + } } -// Simple motion estimation (full search) +// Simple motion estimation (full search) for 16x16 blocks static void estimate_motion(tev_encoder_t *enc, int block_x, int block_y, int16_t *best_mv_x, int16_t *best_mv_y) { int best_sad = INT_MAX; *best_mv_x = 0; *best_mv_y = 0; - int start_x = block_x * BLOCK_SIZE; - int start_y = block_y * BLOCK_SIZE; + int start_x = block_x * 16; + int start_y = block_y * 16; // Search in range [-16, +16] pixels for (int mv_y = -MAX_MOTION_SEARCH; mv_y <= MAX_MOTION_SEARCH; mv_y++) { @@ -198,396 +461,209 @@ static void estimate_motion(tev_encoder_t *enc, int block_x, int block_y, int ref_y = start_y + mv_y; // Check bounds - if (ref_x >= 0 && ref_y >= 0 && - ref_x + BLOCK_SIZE <= enc->width && - ref_y + BLOCK_SIZE <= enc->height) { - - int sad = 0; - - // Calculate Sum of Absolute Differences - for (int dy = 0; dy < BLOCK_SIZE; dy++) { - for (int dx = 0; dx < BLOCK_SIZE; dx++) { - int cur_offset = (start_y + dy) * enc->width + (start_x + dx); - int ref_offset = (ref_y + dy) * enc->width + (ref_x + dx); - - int cur_r = enc->current_rgb[cur_offset * 3]; - int cur_g = enc->current_rgb[cur_offset * 3 + 1]; - int cur_b = enc->current_rgb[cur_offset * 3 + 2]; - int ref_r = enc->previous_rgb[ref_offset * 3]; - int ref_g = enc->previous_rgb[ref_offset * 3 + 1]; - int ref_b = enc->previous_rgb[ref_offset * 3 + 2]; - - // SAD on 8-bit RGB channels - sad += abs(cur_r - ref_r) + abs(cur_g - ref_g) + abs(cur_b - ref_b); - } - } - - if (sad < best_sad) { - best_sad = sad; - *best_mv_x = mv_x * 4; // Convert to 1/4 pixel units - *best_mv_y = mv_y * 4; + if (ref_x < 0 || ref_y < 0 || + ref_x + 16 > enc->width || ref_y + 16 > enc->height) { + continue; + } + + // Calculate SAD for 16x16 block + int sad = 0; + for (int dy = 0; dy < 16; dy++) { + for (int dx = 0; dx < 16; dx++) { + int cur_offset = ((start_y + dy) * enc->width + (start_x + dx)) * 3; + int ref_offset = ((ref_y + dy) * enc->width + (ref_x + dx)) * 3; + + // Compare luminance (approximate as average of RGB) + int cur_luma = (enc->current_rgb[cur_offset] + + enc->current_rgb[cur_offset + 1] + + enc->current_rgb[cur_offset + 2]) / 3; + int ref_luma = (enc->previous_rgb[ref_offset] + + enc->previous_rgb[ref_offset + 1] + + enc->previous_rgb[ref_offset + 2]) / 3; + + sad += abs(cur_luma - ref_luma); } } + + if (sad < best_sad) { + best_sad = sad; + *best_mv_x = mv_x; + *best_mv_y = mv_y; + } } } } -// Encode an 8x8 block using the best mode +// Encode a 16x16 block static void encode_block(tev_encoder_t *enc, int block_x, int block_y, int is_keyframe) { - int block_idx = block_y * ((enc->width + 7) / 8) + block_x; - tev_block_t *block = &enc->block_data[block_idx]; + tev_block_t *block = &enc->block_data[block_y * ((enc->width + 15) / 16) + block_x]; - int start_x = block_x * BLOCK_SIZE; - int start_y = block_y * BLOCK_SIZE; - - // Extract 8x8 RGB block from current frame - for (int y = 0; y < BLOCK_SIZE; y++) { - for (int x = 0; x < BLOCK_SIZE; x++) { - int pixel_x = block_x * BLOCK_SIZE + x; - int pixel_y = block_y * BLOCK_SIZE + y; - int offset = (y * BLOCK_SIZE + x) * 3; - - if (pixel_x < enc->width && pixel_y < enc->height) { - int frame_offset = pixel_y * enc->width + pixel_x; - // Copy RGB data directly (preserving full 8-bit precision) - enc->rgb_workspace[offset] = enc->current_rgb[frame_offset * 3]; // R - enc->rgb_workspace[offset + 1] = enc->current_rgb[frame_offset * 3 + 1]; // G - enc->rgb_workspace[offset + 2] = enc->current_rgb[frame_offset * 3 + 2]; // B - } else { - // Pad with black - enc->rgb_workspace[offset] = 0; - enc->rgb_workspace[offset + 1] = 0; - enc->rgb_workspace[offset + 2] = 0; - } - } - } - - // Initialize block - memset(block, 0, sizeof(tev_block_t)); + // Extract YCoCg-R block + extract_ycocgr_block(enc->current_rgb, enc->width, enc->height, + block_x, block_y, + enc->y_workspace, enc->co_workspace, enc->cg_workspace); if (is_keyframe) { - // Keyframes use INTRA mode + // Intra coding block->mode = TEV_MODE_INTRA; + block->mv_x = block->mv_y = 0; enc->blocks_intra++; } else { - // Try different modes and pick the best - - // Try SKIP mode - compare with previous frame - int skip_sad = 0; - for (int dy = 0; dy < BLOCK_SIZE; dy++) { - for (int dx = 0; dx < BLOCK_SIZE; dx++) { - int pixel_x = start_x + dx; - int pixel_y = start_y + dy; - if (pixel_x < enc->width && pixel_y < enc->height) { - int offset = pixel_y * enc->width + pixel_x; - int cur_r = enc->current_rgb[offset * 3]; - int cur_g = enc->current_rgb[offset * 3 + 1]; - int cur_b = enc->current_rgb[offset * 3 + 2]; - int prev_r = enc->previous_rgb[offset * 3]; - int prev_g = enc->previous_rgb[offset * 3 + 1]; - int prev_b = enc->previous_rgb[offset * 3 + 2]; - - skip_sad += abs(cur_r - prev_r) + abs(cur_g - prev_g) + abs(cur_b - prev_b); - } - } - } - - if (skip_sad < 8) { // Much stricter threshold for SKIP - block->mode = TEV_MODE_SKIP; - enc->blocks_skip++; - return; - } - - // Try MOTION mode + // Try motion estimation estimate_motion(enc, block_x, block_y, &block->mv_x, &block->mv_y); - // Calculate motion compensation SAD - int motion_sad = 0; - for (int y = 0; y < BLOCK_SIZE; y++) { - for (int x = 0; x < BLOCK_SIZE; x++) { - int cur_x = block_x * BLOCK_SIZE + x; - int cur_y = block_y * BLOCK_SIZE + y; - int ref_x = cur_x + block->mv_x; - int ref_y = cur_y + block->mv_y; - - if (cur_x < enc->width && cur_y < enc->height && - ref_x >= 0 && ref_x < enc->width && ref_y >= 0 && ref_y < enc->height) { - - int cur_offset = cur_y * enc->width + cur_x; - int ref_offset = ref_y * enc->width + ref_x; - - uint8_t cur_r = enc->current_rgb[cur_offset * 3]; - uint8_t cur_g = enc->current_rgb[cur_offset * 3 + 1]; - uint8_t cur_b = enc->current_rgb[cur_offset * 3 + 2]; - uint8_t ref_r = enc->previous_rgb[ref_offset * 3]; - uint8_t ref_g = enc->previous_rgb[ref_offset * 3 + 1]; - uint8_t ref_b = enc->previous_rgb[ref_offset * 3 + 2]; - - motion_sad += abs(cur_r - ref_r) + abs(cur_g - ref_g) + abs(cur_b - ref_b); - } else { - motion_sad += 48; // Penalty for out-of-bounds reference - } - } - } - - // Decide on encoding mode based on analysis - if (motion_sad < 32 && (abs(block->mv_x) > 0 || abs(block->mv_y) > 0)) { - // Good motion prediction - block->mode = TEV_MODE_MOTION; - enc->blocks_motion++; - return; // Motion blocks don't need DCT coefficients - } else if (motion_sad < 64) { - // Use INTER mode (motion compensation + DCT residual) - block->mode = TEV_MODE_INTER; - enc->blocks_inter++; - } else { - // Fall back to INTRA mode - block->mode = TEV_MODE_INTRA; - enc->blocks_intra++; - } + // For simplicity, always use INTRA mode for now + // TODO: Implement proper mode decision + block->mode = TEV_MODE_INTRA; + block->mv_x = block->mv_y = 0; + enc->blocks_intra++; } - // Full 8x8 DCT implementation for all blocks (keyframe and P-frame) - const uint8_t *quant_table = QUANT_TABLES[enc->quality]; + // Apply DCT transform + dct_16x16(enc->y_workspace, enc->dct_workspace); - // DCT-II basis functions (precomputed for 8x8) - static double dct_basis[8][8]; - static int basis_initialized = 0; - - if (!basis_initialized) { - for (int u = 0; u < 8; u++) { - for (int x = 0; x < 8; x++) { - double cu = (u == 0) ? sqrt(1.0/8.0) : sqrt(2.0/8.0); - dct_basis[u][x] = cu * cos((2.0 * x + 1.0) * u * M_PI / 16.0); - } - } - basis_initialized = 1; + // Quantize Y coefficients + const uint8_t *y_quant = QUANT_TABLES_Y[enc->quality]; + for (int i = 0; i < 256; i++) { + block->y_coeffs[i] = quantize_coeff(enc->dct_workspace[i], y_quant[i], i == 0); } - // Convert RGB block to DCT input format (subtract 128 to center around 0) - double rgb_block[3][8][8]; - for (int y = 0; y < 8; y++) { - for (int x = 0; x < 8; x++) { - int offset = (y * 8 + x) * 3; - rgb_block[0][y][x] = enc->rgb_workspace[offset] - 128.0; // R: 0-255 -> -128 to +127 - rgb_block[1][y][x] = enc->rgb_workspace[offset + 1] - 128.0; // G: 0-255 -> -128 to +127 - rgb_block[2][y][x] = enc->rgb_workspace[offset + 2] - 128.0; // B: 0-255 -> -128 to +127 - } + // Apply DCT transform to chroma + dct_8x8(enc->co_workspace, enc->dct_workspace); + + // Quantize Co coefficients + const uint8_t *c_quant = QUANT_TABLES_C[enc->quality]; + for (int i = 0; i < 64; i++) { + block->co_coeffs[i] = quantize_coeff(enc->dct_workspace[i], c_quant[i], i == 0); } - // Apply 2D DCT to each channel - double dct_coeffs[3][8][8]; - for (int channel = 0; channel < 3; channel++) { - for (int u = 0; u < 8; u++) { - for (int v = 0; v < 8; v++) { - double sum = 0.0; - for (int x = 0; x < 8; x++) { - for (int y = 0; y < 8; y++) { - sum += dct_basis[u][x] * dct_basis[v][y] * rgb_block[channel][y][x]; - } - } - dct_coeffs[channel][u][v] = sum; - } - } + // Apply DCT transform to Cg + dct_8x8(enc->cg_workspace, enc->dct_workspace); + + // Quantize Cg coefficients + for (int i = 0; i < 64; i++) { + block->cg_coeffs[i] = quantize_coeff(enc->dct_workspace[i], c_quant[i], i == 0); } - // Quantize and store DCT coefficients - for (int channel = 0; channel < 3; channel++) { - for (int u = 0; u < 8; u++) { - for (int v = 0; v < 8; v++) { - int coeff_index = u * 8 + v; - int is_dc = (coeff_index == 0); - - block->dct_coeffs[channel][coeff_index] = - quantize_coeff(dct_coeffs[channel][u][v], quant_table[coeff_index], is_dc); - - // Debug DC coefficient for first block - if (block_x == 0 && block_y == 0 && channel < 3 && coeff_index == 0) { - fprintf(stderr, "Ch%d: DCT raw=%.2f, stored=%d, ", - channel, dct_coeffs[channel][u][v], (int)block->dct_coeffs[channel][coeff_index]); - // Show raw bytes in memory - uint8_t *bytes = (uint8_t*)&block->dct_coeffs[channel][coeff_index]; - fprintf(stderr, "bytes=[%d,%d]\n", bytes[0], bytes[1]); - } - } - } - } + // Set CBP (simplified - always encode all channels) + block->cbp = 0x07; // Y, Co, Cg all present } -// Execute command and capture output -static char *execute_command(const char *command) { - FILE *pipe = popen(command, "r"); - if (!pipe) return NULL; +// Initialize encoder +static tev_encoder_t* init_encoder(void) { + tev_encoder_t *enc = calloc(1, sizeof(tev_encoder_t)); + if (!enc) return NULL; - char *result = malloc(4096); - size_t len = fread(result, 1, 4095, pipe); - result[len] = '\0'; + enc->quality = 4; // Default quality + enc->mp2_packet_size = MP2_DEFAULT_PACKET_SIZE; - pclose(pipe); - return result; + return enc; } -// Get video metadata using ffprobe -static int get_video_metadata(tev_encoder_t *enc) { - char command[1024]; - char *output; +// Allocate encoder buffers +static int alloc_encoder_buffers(tev_encoder_t *enc) { + int pixels = enc->width * enc->height; + int blocks_x = (enc->width + 15) / 16; + int blocks_y = (enc->height + 15) / 16; + int total_blocks = blocks_x * blocks_y; - // Get frame count - snprintf(command, sizeof(command), - "ffprobe -v quiet -select_streams v:0 -count_frames -show_entries stream=nb_read_frames -of csv=p=0 \"%s\"", - enc->input_file); - output = execute_command(command); - if (!output) { - fprintf(stderr, "Failed to get frame count\n"); - return 0; - } - enc->total_frames = atoi(output); - free(output); + enc->current_rgb = malloc(pixels * 3); + enc->previous_rgb = malloc(pixels * 3); + enc->reference_rgb = malloc(pixels * 3); - // Get frame rate - snprintf(command, sizeof(command), - "ffprobe -v quiet -select_streams v:0 -show_entries stream=r_frame_rate -of csv=p=0 \"%s\"", - enc->input_file); - output = execute_command(command); - if (!output) { - fprintf(stderr, "Failed to get frame rate\n"); - return 0; + enc->y_workspace = malloc(16 * 16 * sizeof(float)); + enc->co_workspace = malloc(8 * 8 * sizeof(float)); + enc->cg_workspace = malloc(8 * 8 * sizeof(float)); + enc->dct_workspace = malloc(16 * 16 * sizeof(float)); + + enc->block_data = malloc(total_blocks * sizeof(tev_block_t)); + enc->compressed_buffer = malloc(total_blocks * sizeof(tev_block_t) * 2); + enc->mp2_buffer = malloc(MP2_DEFAULT_PACKET_SIZE); + + if (!enc->current_rgb || !enc->previous_rgb || !enc->reference_rgb || + !enc->y_workspace || !enc->co_workspace || !enc->cg_workspace || + !enc->dct_workspace || !enc->block_data || + !enc->compressed_buffer || !enc->mp2_buffer) { + return -1; } - int num, den; - if (sscanf(output, "%d/%d", &num, &den) == 2) { - enc->fps = (den > 0) ? (num / den) : 30; - } else { - enc->fps = (int)round(atof(output)); - } - free(output); + // Initialize gzip compression stream + enc->gzip_stream.zalloc = Z_NULL; + enc->gzip_stream.zfree = Z_NULL; + enc->gzip_stream.opaque = Z_NULL; - // Get duration - snprintf(command, sizeof(command), - "ffprobe -v quiet -show_entries format=duration -of csv=p=0 \"%s\"", - enc->input_file); - output = execute_command(command); - if (output) { - enc->duration = atof(output); - free(output); + int gzip_init_result = deflateInit2(&enc->gzip_stream, Z_DEFAULT_COMPRESSION, + Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY); // 15+16 for gzip format + + if (gzip_init_result != Z_OK) { + fprintf(stderr, "Failed to initialize gzip compression\n"); + return -1; } - // Check if has audio - snprintf(command, sizeof(command), - "ffprobe -v quiet -select_streams a:0 -show_entries stream=index -of csv=p=0 \"%s\"", - enc->input_file); - output = execute_command(command); - enc->has_audio = (output && strlen(output) > 0 && atoi(output) >= 0); - if (output) free(output); + // Initialize previous frame to black + memset(enc->previous_rgb, 0, pixels * 3); - if (enc->total_frames <= 0 && enc->duration > 0) { - enc->total_frames = (int)(enc->duration * enc->fps); - } - - fprintf(stderr, "Video metadata:\n"); - fprintf(stderr, " Frames: %d\n", enc->total_frames); - fprintf(stderr, " FPS: %d\n", enc->fps); - fprintf(stderr, " Duration: %.2fs\n", enc->duration); - fprintf(stderr, " Audio: %s\n", enc->has_audio ? "Yes" : "No"); - fprintf(stderr, " Resolution: %dx%d\n", enc->width, enc->height); - - return (enc->total_frames > 0 && enc->fps > 0); + return 0; } -// Start FFmpeg process for video conversion -static int start_video_conversion(tev_encoder_t *enc) { - char command[2048]; - snprintf(command, sizeof(command), - "ffmpeg -i \"%s\" -f rawvideo -pix_fmt rgb24 -vf scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d -y - 2>/dev/null", - enc->input_file, enc->width, enc->height, enc->width, enc->height); +// Free encoder resources +static void free_encoder(tev_encoder_t *enc) { + if (!enc) return; - enc->ffmpeg_video_pipe = popen(command, "r"); - return (enc->ffmpeg_video_pipe != NULL); -} - -// Start audio conversion -static int start_audio_conversion(tev_encoder_t *enc) { - if (!enc->has_audio) return 1; + deflateEnd(&enc->gzip_stream); - char command[2048]; - snprintf(command, sizeof(command), - "ffmpeg -i \"%s\" -acodec libtwolame -psymodel 4 -b:a 192k -ar %d -ac 2 -y \"%s\" 2>/dev/null", - enc->input_file, MP2_SAMPLE_RATE, TEMP_AUDIO_FILE); - - int result = system(command); - if (result == 0) { - enc->mp2_file = fopen(TEMP_AUDIO_FILE, "rb"); - if (enc->mp2_file) { - fseek(enc->mp2_file, 0, SEEK_END); - enc->audio_remaining = ftell(enc->mp2_file); - fseek(enc->mp2_file, 0, SEEK_SET); - return 1; - } - } - - fprintf(stderr, "Warning: Failed to convert audio\n"); - enc->has_audio = 0; - return 1; + free(enc->current_rgb); + free(enc->previous_rgb); + free(enc->reference_rgb); + free(enc->y_workspace); + free(enc->co_workspace); + free(enc->cg_workspace); + free(enc->dct_workspace); + free(enc->block_data); + free(enc->compressed_buffer); + free(enc->mp2_buffer); + free(enc); } // Write TEV header -static void write_tev_header(tev_encoder_t *enc, FILE *output) { +static int write_tev_header(FILE *output, tev_encoder_t *enc) { + // Magic + version fwrite(TEV_MAGIC, 1, 8, output); - uint8_t version = TEV_VERSION; fwrite(&version, 1, 1, output); - uint8_t flags = enc->has_audio ? 0x01 : 0x00; - fwrite(&flags, 1, 1, output); - - fwrite(&enc->width, 2, 1, output); - fwrite(&enc->height, 2, 1, output); - fwrite(&enc->fps, 2, 1, output); - fwrite(&enc->total_frames, 4, 1, output); - + // Video parameters + uint16_t width = enc->width; + uint16_t height = enc->height; + uint8_t fps = enc->fps; + uint32_t total_frames = enc->total_frames; uint8_t quality = enc->quality; - fwrite(&quality, 1, 1, output); + uint8_t has_audio = enc->has_audio; - uint8_t reserved[5] = {0}; - fwrite(reserved, 1, 5, output); + fwrite(&width, 2, 1, output); + fwrite(&height, 2, 1, output); + fwrite(&fps, 1, 1, output); + fwrite(&total_frames, 4, 1, output); + fwrite(&quality, 1, 1, output); + fwrite(&has_audio, 1, 1, output); + + return 0; } -// Process and encode one frame -static int process_frame(tev_encoder_t *enc, int frame_num, FILE *output) { - // Read RGB data - size_t rgb_size = enc->width * enc->height * 3; - uint8_t *rgb_buffer = malloc(rgb_size); - if (fread(rgb_buffer, 1, rgb_size, enc->ffmpeg_video_pipe) != rgb_size) { - free(rgb_buffer); - return 0; // End of video - } - - // Convert to 4096-color format - copy_rgb_frame(rgb_buffer, enc->current_rgb, enc->width * enc->height); - free(rgb_buffer); - - int is_keyframe = (frame_num == 1) || (frame_num % KEYFRAME_INTERVAL == 0); - - // Reset statistics - enc->blocks_skip = enc->blocks_intra = enc->blocks_inter = enc->blocks_motion = 0; - - // Encode all 8x8 blocks - int blocks_x = (enc->width + 7) / 8; - int blocks_y = (enc->height + 7) / 8; +// Encode and write a frame +static int encode_frame(tev_encoder_t *enc, FILE *output, int frame_num) { + int is_keyframe = (frame_num % KEYFRAME_INTERVAL) == 0; + int blocks_x = (enc->width + 15) / 16; + int blocks_y = (enc->height + 15) / 16; + // Encode all blocks for (int by = 0; by < blocks_y; by++) { for (int bx = 0; bx < blocks_x; bx++) { encode_block(enc, bx, by, is_keyframe); } } - // Debug struct layout - fprintf(stderr, "Block size: %zu, DCT offset: %zu\n", - sizeof(tev_block_t), offsetof(tev_block_t, dct_coeffs)); - - // No endian conversion needed - system is already little-endian - - // Compress block data using gzip + // Compress block data using gzip (compatible with TSVM decoder) size_t block_data_size = blocks_x * blocks_y * sizeof(tev_block_t); // Reset compression stream @@ -609,117 +685,37 @@ static int process_frame(tev_encoder_t *enc, int frame_num, FILE *output) { size_t compressed_size = enc->gzip_stream.total_out; - // Write video packet - uint8_t packet_type[2] = {is_keyframe ? TEV_PACKET_IFRAME : TEV_PACKET_PFRAME, 0x00}; - fwrite(packet_type, 1, 2, output); + // Write frame packet header + uint8_t packet_type = is_keyframe ? TEV_PACKET_IFRAME : TEV_PACKET_PFRAME; + uint32_t payload_size = compressed_size; - uint32_t size = (uint32_t)compressed_size; - fwrite(&size, 4, 1, output); + fwrite(&packet_type, 1, 1, output); + fwrite(&payload_size, 4, 1, output); fwrite(enc->compressed_buffer, 1, compressed_size, output); - // Write sync packet - uint8_t sync[2] = {0xFF, 0xFF}; - fwrite(sync, 1, 2, output); + enc->total_output_bytes += 5 + compressed_size; - enc->total_output_bytes += 2 + 4 + compressed_size + 2; + // Copy current frame to previous for next iteration + memcpy(enc->previous_rgb, enc->current_rgb, enc->width * enc->height * 3); - // Swap frame buffers for next frame - uint8_t *temp_rgb = enc->previous_rgb; - enc->previous_rgb = enc->current_rgb; - enc->current_rgb = temp_rgb; - - fprintf(stderr, "\rFrame %d/%d [%c] - Skip:%d Intra:%d Inter:%d - Ratio:%.1f%%", - frame_num, enc->total_frames, is_keyframe ? 'I' : 'P', - enc->blocks_skip, enc->blocks_intra, enc->blocks_inter, - (compressed_size * 100.0) / block_data_size); - fflush(stderr); - - return 1; + return 0; } -// Initialize encoder -static tev_encoder_t *init_encoder() { - tev_encoder_t *enc = calloc(1, sizeof(tev_encoder_t)); - if (!enc) return NULL; - - enc->width = DEFAULT_WIDTH; - enc->height = DEFAULT_HEIGHT; - enc->quality = 5; // Default quality - enc->output_to_stdout = 1; - - return enc; -} - -// Allocate buffers -static int allocate_buffers(tev_encoder_t *enc) { - int pixels = enc->width * enc->height; - int blocks = ((enc->width + 7) / 8) * ((enc->height + 7) / 8); - - enc->current_rgb = malloc(pixels * 3); // RGB: 3 bytes per pixel - enc->previous_rgb = malloc(pixels * 3); - enc->reference_rgb = malloc(pixels * 3); - - enc->rgb_workspace = malloc(BLOCK_SIZE * BLOCK_SIZE * 3); - enc->dct_workspace = malloc(BLOCK_SIZE * BLOCK_SIZE * 3 * sizeof(float)); - enc->block_data = malloc(blocks * sizeof(tev_block_t)); - enc->compressed_buffer = malloc(blocks * sizeof(tev_block_t) * 2); - enc->mp2_buffer = malloc(2048); - - // Initialize gzip compression stream - enc->gzip_stream.zalloc = Z_NULL; - enc->gzip_stream.zfree = Z_NULL; - enc->gzip_stream.opaque = Z_NULL; - - int gzip_init_result = deflateInit2(&enc->gzip_stream, Z_DEFAULT_COMPRESSION, - Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY); // 15+16 for gzip format - - return (enc->current_rgb && enc->previous_rgb && enc->reference_rgb && - enc->rgb_workspace && enc->dct_workspace && enc->block_data && enc->compressed_buffer && - enc->mp2_buffer && gzip_init_result == Z_OK); -} - -// Cleanup -static void cleanup_encoder(tev_encoder_t *enc) { - if (!enc) return; - - if (enc->ffmpeg_video_pipe) pclose(enc->ffmpeg_video_pipe); - if (enc->mp2_file) fclose(enc->mp2_file); - deflateEnd(&enc->gzip_stream); - - free(enc->input_file); - free(enc->output_file); - free(enc->current_rgb); - free(enc->previous_rgb); - free(enc->reference_rgb); - free(enc->rgb_workspace); - free(enc->dct_workspace); - free(enc->block_data); - free(enc->compressed_buffer); - free(enc->mp2_buffer); - - unlink(TEMP_AUDIO_FILE); - free(enc); -} - -// Print usage -static void print_usage(const char *program_name) { - printf("TSVM Enhanced Video (TEV) Encoder\n\n"); - printf("Usage: %s [options] input_video\n\n", program_name); +// Show usage information +static void show_usage(const char *program_name) { + printf("Usage: %s [options] -i input.mp4 -o output.tev\n", program_name); printf("Options:\n"); - printf(" -o, --output FILE Output TEV file (default: stdout)\n"); - printf(" -s, --size WxH Video resolution (default: 560x448)\n"); - printf(" -q, --quality N Quality level 0-7 (default: 5)\n"); - printf(" -h, --help Show this help\n\n"); - printf("TEV Features:\n"); - printf(" - 8x8 DCT-based compression with motion compensation\n"); - printf(" - Native 4096-color support (4:4:4 RGB)\n"); - printf(" - Zstd compression for optimal efficiency\n"); - printf(" - Hardware-accelerated encoding functions\n\n"); - printf("Examples:\n"); - printf(" %s input.mp4 -o output.tev\n", program_name); - printf(" %s input.avi -s 1024x768 -q 7 -o output.tev\n", program_name); + printf(" -i, --input FILE Input video file\n"); + printf(" -o, --output FILE Output TEV file (use '-' for stdout)\n"); + printf(" -w, --width N Video width (default: %d)\n", DEFAULT_WIDTH); + printf(" -h, --height N Video height (default: %d)\n", DEFAULT_HEIGHT); + printf(" -f, --fps N Frames per second (default: 15)\n"); + printf(" -q, --quality N Quality level 0-7 (default: 4)\n"); + printf(" -v, --verbose Verbose output\n"); + printf(" --help Show this help\n"); } +// Main function int main(int argc, char *argv[]) { tev_encoder_t *enc = init_encoder(); if (!enc) { @@ -727,89 +723,132 @@ int main(int argc, char *argv[]) { return 1; } - // Parse arguments + // Set defaults + enc->width = DEFAULT_WIDTH; + enc->height = DEFAULT_HEIGHT; + enc->fps = 15; + enc->quality = 4; + static struct option long_options[] = { + {"input", required_argument, 0, 'i'}, {"output", required_argument, 0, 'o'}, - {"size", required_argument, 0, 's'}, + {"width", required_argument, 0, 'w'}, + {"height", required_argument, 0, 'h'}, + {"fps", required_argument, 0, 'f'}, {"quality", required_argument, 0, 'q'}, - {"help", no_argument, 0, 'h'}, + {"verbose", no_argument, 0, 'v'}, + {"help", no_argument, 0, 0}, {0, 0, 0, 0} }; + int option_index = 0; int c; - while ((c = getopt_long(argc, argv, "o:s:q:h", long_options, NULL)) != -1) { + + while ((c = getopt_long(argc, argv, "i:o:w:h:f:q:v", long_options, &option_index)) != -1) { switch (c) { - case 'o': - enc->output_file = strdup(optarg); - enc->output_to_stdout = 0; + case 'i': + enc->input_file = optarg; break; - case 's': - if (sscanf(optarg, "%dx%d", &enc->width, &enc->height) != 2) { - fprintf(stderr, "Invalid resolution: %s\n", optarg); - cleanup_encoder(enc); - return 1; - } + case 'o': + enc->output_file = optarg; + enc->output_to_stdout = (strcmp(optarg, "-") == 0); + break; + case 'w': + enc->width = atoi(optarg); + break; + case 'h': + enc->height = atoi(optarg); + break; + case 'f': + enc->fps = atoi(optarg); break; case 'q': enc->quality = atoi(optarg); - if (enc->quality < 0 || enc->quality > 7) { - fprintf(stderr, "Quality must be 0-7\n"); - cleanup_encoder(enc); - return 1; + if (enc->quality < 0) enc->quality = 0; + if (enc->quality > 7) enc->quality = 7; + break; + case 'v': + // Verbose flag (not implemented) + break; + case 0: + if (strcmp(long_options[option_index].name, "help") == 0) { + show_usage(argv[0]); + free_encoder(enc); + return 0; } break; - case 'h': - print_usage(argv[0]); - cleanup_encoder(enc); - return 0; default: - print_usage(argv[0]); - cleanup_encoder(enc); + show_usage(argv[0]); + free_encoder(enc); return 1; } } - if (optind >= argc) { - fprintf(stderr, "Input file required\n"); - print_usage(argv[0]); - cleanup_encoder(enc); + if (!enc->input_file || !enc->output_file) { + fprintf(stderr, "Input and output files are required\n"); + show_usage(argv[0]); + free_encoder(enc); return 1; } - enc->input_file = strdup(argv[optind]); + // Calculate total frames (simplified - assume 1 second for now) + enc->total_frames = enc->fps; - // Initialize - if (!get_video_metadata(enc) || !allocate_buffers(enc) || - !start_video_conversion(enc) || !start_audio_conversion(enc)) { - cleanup_encoder(enc); + // Allocate buffers + if (alloc_encoder_buffers(enc) < 0) { + fprintf(stderr, "Failed to allocate encoder buffers\n"); + free_encoder(enc); return 1; } + // Open output FILE *output = enc->output_to_stdout ? stdout : fopen(enc->output_file, "wb"); if (!output) { - fprintf(stderr, "Failed to open output\n"); - cleanup_encoder(enc); + perror("Failed to open output file"); + free_encoder(enc); return 1; } - write_tev_header(enc, output); - gettimeofday(&enc->start_time, NULL); - enc->total_output_bytes = 8 + 1 + 1 + 2 + 2 + 2 + 4 + 1 + 5; // TEV header size + // Write TEV header + write_tev_header(output, enc); - // Process all frames - for (int frame = 1; frame <= enc->total_frames; frame++) { - int result = process_frame(enc, frame, output); - if (result <= 0) break; + // For this simplified version, create a test pattern + printf("Encoding test pattern with YCoCg-R 4:2:0 format...\n"); + + for (int frame = 0; frame < enc->total_frames; frame++) { + // Generate test pattern (gradient) + for (int y = 0; y < enc->height; y++) { + for (int x = 0; x < enc->width; x++) { + int offset = (y * enc->width + x) * 3; + enc->current_rgb[offset] = (x * 255) / enc->width; // R gradient + enc->current_rgb[offset + 1] = (y * 255) / enc->height; // G gradient + enc->current_rgb[offset + 2] = ((x + y) * 255) / (enc->width + enc->height); // B gradient + } + } + + // Encode frame + if (encode_frame(enc, output, frame) < 0) { + fprintf(stderr, "Failed to encode frame %d\n", frame); + break; + } + + printf("Encoded frame %d/%d\n", frame + 1, enc->total_frames); } - fprintf(stderr, "\nEncoding complete\n"); + // Write sync packet + uint8_t sync_packet = TEV_PACKET_SYNC; + uint32_t sync_size = 0; + fwrite(&sync_packet, 1, 1, output); + fwrite(&sync_size, 4, 1, output); if (!enc->output_to_stdout) { fclose(output); - fprintf(stderr, "Output: %s (%.1f MB)\n", enc->output_file, - enc->total_output_bytes / (1024.0 * 1024.0)); } - cleanup_encoder(enc); + printf("Encoding complete. Output size: %zu bytes\n", enc->total_output_bytes); + printf("Block statistics: INTRA=%d, INTER=%d, MOTION=%d, SKIP=%d\n", + enc->blocks_intra, enc->blocks_inter, enc->blocks_motion, enc->blocks_skip); + + free_encoder(enc); return 0; -} +} \ No newline at end of file diff --git a/video_encoder/encoder_tev_rgb24_8x8.c b/video_encoder/encoder_tev_rgb24_8x8.c new file mode 100644 index 0000000..64c5e7d --- /dev/null +++ b/video_encoder/encoder_tev_rgb24_8x8.c @@ -0,0 +1,815 @@ +// Created by Claude on 2025-08-17. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// TSVM Enhanced Video (TEV) format constants +#define TEV_MAGIC "\x1F\x54\x53\x56\x4D\x54\x45\x56" // "\x1FTSVM TEV" +#define TEV_VERSION 1 + +// Block encoding modes (8x8 blocks) +#define TEV_MODE_SKIP 0x00 // Skip block (copy from reference) +#define TEV_MODE_INTRA 0x01 // Intra DCT coding (I-frame blocks) +#define TEV_MODE_INTER 0x02 // Inter DCT coding with motion compensation +#define TEV_MODE_MOTION 0x03 // Motion vector only (good prediction) + +// Video packet types +#define TEV_PACKET_IFRAME 0x10 // Intra frame (keyframe) +#define TEV_PACKET_PFRAME 0x11 // Predicted frame +#define TEV_PACKET_AUDIO_MP2 0x20 // MP2 audio +#define TEV_PACKET_SYNC 0xFF // Sync packet + +// Quality settings for quantization +static const uint8_t QUANT_TABLES[8][64] = { + // Quality 0 (lowest) + {80, 60, 50, 80, 120, 200, 255, 255, + 55, 60, 70, 95, 130, 255, 255, 255, + 70, 65, 80, 120, 200, 255, 255, 255, + 70, 85, 110, 145, 255, 255, 255, 255, + 90, 110, 185, 255, 255, 255, 255, 255, + 120, 175, 255, 255, 255, 255, 255, 255, + 245, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255}, + // Quality 1-6 (intermediate)... + {40, 30, 25, 40, 60, 100, 128, 150, + 28, 30, 35, 48, 65, 128, 150, 180, + 35, 33, 40, 60, 100, 128, 150, 180, + 35, 43, 55, 73, 128, 150, 180, 200, + 45, 55, 93, 128, 150, 180, 200, 220, + 60, 88, 128, 150, 180, 200, 220, 240, + 123, 128, 150, 180, 200, 220, 240, 250, + 128, 150, 180, 200, 220, 240, 250, 255}, + // ... (simplified for example) + {20, 15, 13, 20, 30, 50, 64, 75, + 14, 15, 18, 24, 33, 64, 75, 90, + 18, 17, 20, 30, 50, 64, 75, 90, + 18, 22, 28, 37, 64, 75, 90, 100, + 23, 28, 47, 64, 75, 90, 100, 110, + 30, 44, 64, 75, 90, 100, 110, 120, + 62, 64, 75, 90, 100, 110, 120, 125, + 64, 75, 90, 100, 110, 120, 125, 128}, + {16, 12, 10, 16, 24, 40, 51, 60, + 11, 12, 14, 19, 26, 51, 60, 72, + 14, 13, 16, 24, 40, 51, 60, 72, + 14, 17, 22, 29, 51, 60, 72, 80, + 18, 22, 37, 51, 60, 72, 80, 88, + 24, 35, 51, 60, 72, 80, 88, 96, + 49, 51, 60, 72, 80, 88, 96, 100, + 51, 60, 72, 80, 88, 96, 100, 102}, + {12, 9, 8, 12, 18, 30, 38, 45, + 8, 9, 11, 14, 20, 38, 45, 54, + 11, 10, 12, 18, 30, 38, 45, 54, + 11, 13, 17, 22, 38, 45, 54, 60, + 14, 17, 28, 38, 45, 54, 60, 66, + 18, 26, 38, 45, 54, 60, 66, 72, + 37, 38, 45, 54, 60, 66, 72, 75, + 38, 45, 54, 60, 66, 72, 75, 77}, + {10, 7, 6, 10, 15, 25, 32, 38, + 7, 7, 9, 12, 16, 32, 38, 45, + 9, 8, 10, 15, 25, 32, 38, 45, + 9, 11, 14, 18, 32, 38, 45, 50, + 12, 14, 23, 32, 38, 45, 50, 55, + 15, 22, 32, 38, 45, 50, 55, 60, + 31, 32, 38, 45, 50, 55, 60, 63, + 32, 38, 45, 50, 55, 60, 63, 65}, + {8, 6, 5, 8, 12, 20, 26, 30, + 6, 6, 7, 10, 13, 26, 30, 36, + 7, 7, 8, 12, 20, 26, 30, 36, + 7, 9, 11, 15, 26, 30, 36, 40, + 10, 11, 19, 26, 30, 36, 40, 44, + 12, 17, 26, 30, 36, 40, 44, 48, + 25, 26, 30, 36, 40, 44, 48, 50, + 26, 30, 36, 40, 44, 48, 50, 52}, + // Quality 7 (highest) + {2, 1, 1, 2, 3, 5, 6, 7, + 1, 1, 1, 2, 3, 6, 7, 9, + 1, 1, 2, 3, 5, 6, 7, 9, + 1, 2, 3, 4, 6, 7, 9, 10, + 2, 3, 5, 6, 7, 9, 10, 11, + 3, 4, 6, 7, 9, 10, 11, 12, + 6, 6, 7, 9, 10, 11, 12, 13, + 6, 7, 9, 10, 11, 12, 13, 13} +}; + +// Audio constants (reuse MP2 from existing system) +#define MP2_SAMPLE_RATE 32000 +#define MP2_DEFAULT_PACKET_SIZE 0x240 + +// Encoding parameters +#define MAX_MOTION_SEARCH 16 +#define KEYFRAME_INTERVAL 30 +#define BLOCK_SIZE 8 + +// Default values +#define DEFAULT_WIDTH 560 +#define DEFAULT_HEIGHT 448 +#define TEMP_AUDIO_FILE "/tmp/tev_temp_audio.mp2" + +typedef struct __attribute__((packed)) { + uint8_t mode; // Block encoding mode + int16_t mv_x, mv_y; // Motion vector (1/4 pixel precision) + uint16_t cbp; // Coded block pattern (which 8x8 have non-zero coeffs) + int16_t dct_coeffs[3][64]; // Quantized DCT coefficients (R,G,B) +} tev_block_t; + +typedef struct { + char *input_file; + char *output_file; + int width; + int height; + int fps; + int total_frames; + double duration; + int has_audio; + int output_to_stdout; + int quality; // 0-7, higher = better quality + + // Frame buffers (8-bit RGB format for encoding) + uint8_t *current_rgb, *previous_rgb, *reference_rgb; + + // Encoding workspace + uint8_t *rgb_workspace; // 8x8 RGB blocks (192 bytes) + float *dct_workspace; // DCT coefficients (192 floats) + tev_block_t *block_data; // Encoded block data + uint8_t *compressed_buffer; // Zstd output + + // Audio handling + FILE *mp2_file; + int mp2_packet_size; + size_t audio_remaining; + uint8_t *mp2_buffer; + + // Compression context + z_stream gzip_stream; + + // FFmpeg processes + FILE *ffmpeg_video_pipe; + + // Progress tracking + struct timeval start_time; + size_t total_output_bytes; + + // Statistics + int blocks_skip, blocks_intra, blocks_inter, blocks_motion; +} tev_encoder_t; + +// Quantize DCT coefficient using quality table +static int16_t quantize_coeff(float coeff, uint8_t quant, int is_dc) { + if (is_dc) { + // DC coefficient uses fixed quantizer + return (int16_t)roundf(coeff / 8.0f); + } else { + // AC coefficients use quality table + return (int16_t)roundf(coeff / quant); + } +} + +// These functions are reserved for future rate-distortion optimization +// Currently using simplified encoding logic + +// Convert RGB to 4096-color format +static void copy_rgb_frame(uint8_t *rgb_input, uint8_t *rgb_frame, int pixels) { + // Copy input RGB data to frame buffer (preserving full 8-bit precision) + memcpy(rgb_frame, rgb_input, pixels * 3); +} + +// Simple motion estimation (full search) +static void estimate_motion(tev_encoder_t *enc, int block_x, int block_y, + int16_t *best_mv_x, int16_t *best_mv_y) { + int best_sad = INT_MAX; + *best_mv_x = 0; + *best_mv_y = 0; + + int start_x = block_x * BLOCK_SIZE; + int start_y = block_y * BLOCK_SIZE; + + // Search in range [-16, +16] pixels + for (int mv_y = -MAX_MOTION_SEARCH; mv_y <= MAX_MOTION_SEARCH; mv_y++) { + for (int mv_x = -MAX_MOTION_SEARCH; mv_x <= MAX_MOTION_SEARCH; mv_x++) { + int ref_x = start_x + mv_x; + int ref_y = start_y + mv_y; + + // Check bounds + if (ref_x >= 0 && ref_y >= 0 && + ref_x + BLOCK_SIZE <= enc->width && + ref_y + BLOCK_SIZE <= enc->height) { + + int sad = 0; + + // Calculate Sum of Absolute Differences + for (int dy = 0; dy < BLOCK_SIZE; dy++) { + for (int dx = 0; dx < BLOCK_SIZE; dx++) { + int cur_offset = (start_y + dy) * enc->width + (start_x + dx); + int ref_offset = (ref_y + dy) * enc->width + (ref_x + dx); + + int cur_r = enc->current_rgb[cur_offset * 3]; + int cur_g = enc->current_rgb[cur_offset * 3 + 1]; + int cur_b = enc->current_rgb[cur_offset * 3 + 2]; + int ref_r = enc->previous_rgb[ref_offset * 3]; + int ref_g = enc->previous_rgb[ref_offset * 3 + 1]; + int ref_b = enc->previous_rgb[ref_offset * 3 + 2]; + + // SAD on 8-bit RGB channels + sad += abs(cur_r - ref_r) + abs(cur_g - ref_g) + abs(cur_b - ref_b); + } + } + + if (sad < best_sad) { + best_sad = sad; + *best_mv_x = mv_x * 4; // Convert to 1/4 pixel units + *best_mv_y = mv_y * 4; + } + } + } + } +} + +// Encode an 8x8 block using the best mode +static void encode_block(tev_encoder_t *enc, int block_x, int block_y, int is_keyframe) { + int block_idx = block_y * ((enc->width + 7) / 8) + block_x; + tev_block_t *block = &enc->block_data[block_idx]; + + int start_x = block_x * BLOCK_SIZE; + int start_y = block_y * BLOCK_SIZE; + + // Extract 8x8 RGB block from current frame + for (int y = 0; y < BLOCK_SIZE; y++) { + for (int x = 0; x < BLOCK_SIZE; x++) { + int pixel_x = block_x * BLOCK_SIZE + x; + int pixel_y = block_y * BLOCK_SIZE + y; + int offset = (y * BLOCK_SIZE + x) * 3; + + if (pixel_x < enc->width && pixel_y < enc->height) { + int frame_offset = pixel_y * enc->width + pixel_x; + // Copy RGB data directly (preserving full 8-bit precision) + enc->rgb_workspace[offset] = enc->current_rgb[frame_offset * 3]; // R + enc->rgb_workspace[offset + 1] = enc->current_rgb[frame_offset * 3 + 1]; // G + enc->rgb_workspace[offset + 2] = enc->current_rgb[frame_offset * 3 + 2]; // B + } else { + // Pad with black + enc->rgb_workspace[offset] = 0; + enc->rgb_workspace[offset + 1] = 0; + enc->rgb_workspace[offset + 2] = 0; + } + } + } + + // Initialize block + memset(block, 0, sizeof(tev_block_t)); + + if (is_keyframe) { + // Keyframes use INTRA mode + block->mode = TEV_MODE_INTRA; + enc->blocks_intra++; + } else { + // Try different modes and pick the best + + // Try SKIP mode - compare with previous frame + int skip_sad = 0; + for (int dy = 0; dy < BLOCK_SIZE; dy++) { + for (int dx = 0; dx < BLOCK_SIZE; dx++) { + int pixel_x = start_x + dx; + int pixel_y = start_y + dy; + if (pixel_x < enc->width && pixel_y < enc->height) { + int offset = pixel_y * enc->width + pixel_x; + int cur_r = enc->current_rgb[offset * 3]; + int cur_g = enc->current_rgb[offset * 3 + 1]; + int cur_b = enc->current_rgb[offset * 3 + 2]; + int prev_r = enc->previous_rgb[offset * 3]; + int prev_g = enc->previous_rgb[offset * 3 + 1]; + int prev_b = enc->previous_rgb[offset * 3 + 2]; + + skip_sad += abs(cur_r - prev_r) + abs(cur_g - prev_g) + abs(cur_b - prev_b); + } + } + } + + if (skip_sad < 8) { // Much stricter threshold for SKIP + block->mode = TEV_MODE_SKIP; + enc->blocks_skip++; + return; + } + + // Try MOTION mode + estimate_motion(enc, block_x, block_y, &block->mv_x, &block->mv_y); + + // Calculate motion compensation SAD + int motion_sad = 0; + for (int y = 0; y < BLOCK_SIZE; y++) { + for (int x = 0; x < BLOCK_SIZE; x++) { + int cur_x = block_x * BLOCK_SIZE + x; + int cur_y = block_y * BLOCK_SIZE + y; + int ref_x = cur_x + block->mv_x; + int ref_y = cur_y + block->mv_y; + + if (cur_x < enc->width && cur_y < enc->height && + ref_x >= 0 && ref_x < enc->width && ref_y >= 0 && ref_y < enc->height) { + + int cur_offset = cur_y * enc->width + cur_x; + int ref_offset = ref_y * enc->width + ref_x; + + uint8_t cur_r = enc->current_rgb[cur_offset * 3]; + uint8_t cur_g = enc->current_rgb[cur_offset * 3 + 1]; + uint8_t cur_b = enc->current_rgb[cur_offset * 3 + 2]; + uint8_t ref_r = enc->previous_rgb[ref_offset * 3]; + uint8_t ref_g = enc->previous_rgb[ref_offset * 3 + 1]; + uint8_t ref_b = enc->previous_rgb[ref_offset * 3 + 2]; + + motion_sad += abs(cur_r - ref_r) + abs(cur_g - ref_g) + abs(cur_b - ref_b); + } else { + motion_sad += 48; // Penalty for out-of-bounds reference + } + } + } + + // Decide on encoding mode based on analysis + if (motion_sad < 32 && (abs(block->mv_x) > 0 || abs(block->mv_y) > 0)) { + // Good motion prediction + block->mode = TEV_MODE_MOTION; + enc->blocks_motion++; + return; // Motion blocks don't need DCT coefficients + } else if (motion_sad < 64) { + // Use INTER mode (motion compensation + DCT residual) + block->mode = TEV_MODE_INTER; + enc->blocks_inter++; + } else { + // Fall back to INTRA mode + block->mode = TEV_MODE_INTRA; + enc->blocks_intra++; + } + } + + // Full 8x8 DCT implementation for all blocks (keyframe and P-frame) + const uint8_t *quant_table = QUANT_TABLES[enc->quality]; + + // DCT-II basis functions (precomputed for 8x8) + static double dct_basis[8][8]; + static int basis_initialized = 0; + + if (!basis_initialized) { + for (int u = 0; u < 8; u++) { + for (int x = 0; x < 8; x++) { + double cu = (u == 0) ? sqrt(1.0/8.0) : sqrt(2.0/8.0); + dct_basis[u][x] = cu * cos((2.0 * x + 1.0) * u * M_PI / 16.0); + } + } + basis_initialized = 1; + } + + // Convert RGB block to DCT input format (subtract 128 to center around 0) + double rgb_block[3][8][8]; + for (int y = 0; y < 8; y++) { + for (int x = 0; x < 8; x++) { + int offset = (y * 8 + x) * 3; + rgb_block[0][y][x] = enc->rgb_workspace[offset] - 128.0; // R: 0-255 -> -128 to +127 + rgb_block[1][y][x] = enc->rgb_workspace[offset + 1] - 128.0; // G: 0-255 -> -128 to +127 + rgb_block[2][y][x] = enc->rgb_workspace[offset + 2] - 128.0; // B: 0-255 -> -128 to +127 + } + } + + // Apply 2D DCT to each channel + double dct_coeffs[3][8][8]; + for (int channel = 0; channel < 3; channel++) { + for (int u = 0; u < 8; u++) { + for (int v = 0; v < 8; v++) { + double sum = 0.0; + for (int x = 0; x < 8; x++) { + for (int y = 0; y < 8; y++) { + sum += dct_basis[u][x] * dct_basis[v][y] * rgb_block[channel][y][x]; + } + } + dct_coeffs[channel][u][v] = sum; + } + } + } + + // Quantize and store DCT coefficients + for (int channel = 0; channel < 3; channel++) { + for (int u = 0; u < 8; u++) { + for (int v = 0; v < 8; v++) { + int coeff_index = u * 8 + v; + int is_dc = (coeff_index == 0); + + block->dct_coeffs[channel][coeff_index] = + quantize_coeff(dct_coeffs[channel][u][v], quant_table[coeff_index], is_dc); + + // Debug DC coefficient for first block + if (block_x == 0 && block_y == 0 && channel < 3 && coeff_index == 0) { + fprintf(stderr, "Ch%d: DCT raw=%.2f, stored=%d, ", + channel, dct_coeffs[channel][u][v], (int)block->dct_coeffs[channel][coeff_index]); + // Show raw bytes in memory + uint8_t *bytes = (uint8_t*)&block->dct_coeffs[channel][coeff_index]; + fprintf(stderr, "bytes=[%d,%d]\n", bytes[0], bytes[1]); + } + } + } + } +} + +// Execute command and capture output +static char *execute_command(const char *command) { + FILE *pipe = popen(command, "r"); + if (!pipe) return NULL; + + char *result = malloc(4096); + size_t len = fread(result, 1, 4095, pipe); + result[len] = '\0'; + + pclose(pipe); + return result; +} + +// Get video metadata using ffprobe +static int get_video_metadata(tev_encoder_t *enc) { + char command[1024]; + char *output; + + // Get frame count + snprintf(command, sizeof(command), + "ffprobe -v quiet -select_streams v:0 -count_frames -show_entries stream=nb_read_frames -of csv=p=0 \"%s\"", + enc->input_file); + output = execute_command(command); + if (!output) { + fprintf(stderr, "Failed to get frame count\n"); + return 0; + } + enc->total_frames = atoi(output); + free(output); + + // Get frame rate + snprintf(command, sizeof(command), + "ffprobe -v quiet -select_streams v:0 -show_entries stream=r_frame_rate -of csv=p=0 \"%s\"", + enc->input_file); + output = execute_command(command); + if (!output) { + fprintf(stderr, "Failed to get frame rate\n"); + return 0; + } + + int num, den; + if (sscanf(output, "%d/%d", &num, &den) == 2) { + enc->fps = (den > 0) ? (num / den) : 30; + } else { + enc->fps = (int)round(atof(output)); + } + free(output); + + // Get duration + snprintf(command, sizeof(command), + "ffprobe -v quiet -show_entries format=duration -of csv=p=0 \"%s\"", + enc->input_file); + output = execute_command(command); + if (output) { + enc->duration = atof(output); + free(output); + } + + // Check if has audio + snprintf(command, sizeof(command), + "ffprobe -v quiet -select_streams a:0 -show_entries stream=index -of csv=p=0 \"%s\"", + enc->input_file); + output = execute_command(command); + enc->has_audio = (output && strlen(output) > 0 && atoi(output) >= 0); + if (output) free(output); + + if (enc->total_frames <= 0 && enc->duration > 0) { + enc->total_frames = (int)(enc->duration * enc->fps); + } + + fprintf(stderr, "Video metadata:\n"); + fprintf(stderr, " Frames: %d\n", enc->total_frames); + fprintf(stderr, " FPS: %d\n", enc->fps); + fprintf(stderr, " Duration: %.2fs\n", enc->duration); + fprintf(stderr, " Audio: %s\n", enc->has_audio ? "Yes" : "No"); + fprintf(stderr, " Resolution: %dx%d\n", enc->width, enc->height); + + return (enc->total_frames > 0 && enc->fps > 0); +} + +// Start FFmpeg process for video conversion +static int start_video_conversion(tev_encoder_t *enc) { + char command[2048]; + snprintf(command, sizeof(command), + "ffmpeg -i \"%s\" -f rawvideo -pix_fmt rgb24 -vf scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d -y - 2>/dev/null", + enc->input_file, enc->width, enc->height, enc->width, enc->height); + + enc->ffmpeg_video_pipe = popen(command, "r"); + return (enc->ffmpeg_video_pipe != NULL); +} + +// Start audio conversion +static int start_audio_conversion(tev_encoder_t *enc) { + if (!enc->has_audio) return 1; + + char command[2048]; + snprintf(command, sizeof(command), + "ffmpeg -i \"%s\" -acodec libtwolame -psymodel 4 -b:a 192k -ar %d -ac 2 -y \"%s\" 2>/dev/null", + enc->input_file, MP2_SAMPLE_RATE, TEMP_AUDIO_FILE); + + int result = system(command); + if (result == 0) { + enc->mp2_file = fopen(TEMP_AUDIO_FILE, "rb"); + if (enc->mp2_file) { + fseek(enc->mp2_file, 0, SEEK_END); + enc->audio_remaining = ftell(enc->mp2_file); + fseek(enc->mp2_file, 0, SEEK_SET); + return 1; + } + } + + fprintf(stderr, "Warning: Failed to convert audio\n"); + enc->has_audio = 0; + return 1; +} + +// Write TEV header +static void write_tev_header(tev_encoder_t *enc, FILE *output) { + fwrite(TEV_MAGIC, 1, 8, output); + + uint8_t version = TEV_VERSION; + fwrite(&version, 1, 1, output); + + uint8_t flags = enc->has_audio ? 0x01 : 0x00; + fwrite(&flags, 1, 1, output); + + fwrite(&enc->width, 2, 1, output); + fwrite(&enc->height, 2, 1, output); + fwrite(&enc->fps, 2, 1, output); + fwrite(&enc->total_frames, 4, 1, output); + + uint8_t quality = enc->quality; + fwrite(&quality, 1, 1, output); + + uint8_t reserved[5] = {0}; + fwrite(reserved, 1, 5, output); +} + +// Process and encode one frame +static int process_frame(tev_encoder_t *enc, int frame_num, FILE *output) { + // Read RGB data + size_t rgb_size = enc->width * enc->height * 3; + uint8_t *rgb_buffer = malloc(rgb_size); + if (fread(rgb_buffer, 1, rgb_size, enc->ffmpeg_video_pipe) != rgb_size) { + free(rgb_buffer); + return 0; // End of video + } + + // Convert to 4096-color format + copy_rgb_frame(rgb_buffer, enc->current_rgb, enc->width * enc->height); + free(rgb_buffer); + + int is_keyframe = (frame_num == 1) || (frame_num % KEYFRAME_INTERVAL == 0); + + // Reset statistics + enc->blocks_skip = enc->blocks_intra = enc->blocks_inter = enc->blocks_motion = 0; + + // Encode all 8x8 blocks + int blocks_x = (enc->width + 7) / 8; + int blocks_y = (enc->height + 7) / 8; + + for (int by = 0; by < blocks_y; by++) { + for (int bx = 0; bx < blocks_x; bx++) { + encode_block(enc, bx, by, is_keyframe); + } + } + + // Debug struct layout + fprintf(stderr, "Block size: %zu, DCT offset: %zu\n", + sizeof(tev_block_t), offsetof(tev_block_t, dct_coeffs)); + + // No endian conversion needed - system is already little-endian + + // Compress block data using gzip + size_t block_data_size = blocks_x * blocks_y * sizeof(tev_block_t); + + // Reset compression stream + enc->gzip_stream.next_in = (Bytef*)enc->block_data; + enc->gzip_stream.avail_in = block_data_size; + enc->gzip_stream.next_out = (Bytef*)enc->compressed_buffer; + enc->gzip_stream.avail_out = block_data_size * 2; + + if (deflateReset(&enc->gzip_stream) != Z_OK) { + fprintf(stderr, "Gzip deflateReset failed\n"); + return -1; + } + + int result = deflate(&enc->gzip_stream, Z_FINISH); + if (result != Z_STREAM_END) { + fprintf(stderr, "Gzip compression failed: %d\n", result); + return -1; + } + + size_t compressed_size = enc->gzip_stream.total_out; + + // Write video packet + uint8_t packet_type[2] = {is_keyframe ? TEV_PACKET_IFRAME : TEV_PACKET_PFRAME, 0x00}; + fwrite(packet_type, 1, 2, output); + + uint32_t size = (uint32_t)compressed_size; + fwrite(&size, 4, 1, output); + fwrite(enc->compressed_buffer, 1, compressed_size, output); + + // Write sync packet + uint8_t sync[2] = {0xFF, 0xFF}; + fwrite(sync, 1, 2, output); + + enc->total_output_bytes += 2 + 4 + compressed_size + 2; + + // Swap frame buffers for next frame + uint8_t *temp_rgb = enc->previous_rgb; + enc->previous_rgb = enc->current_rgb; + enc->current_rgb = temp_rgb; + + fprintf(stderr, "\rFrame %d/%d [%c] - Skip:%d Intra:%d Inter:%d - Ratio:%.1f%%", + frame_num, enc->total_frames, is_keyframe ? 'I' : 'P', + enc->blocks_skip, enc->blocks_intra, enc->blocks_inter, + (compressed_size * 100.0) / block_data_size); + fflush(stderr); + + return 1; +} + +// Initialize encoder +static tev_encoder_t *init_encoder() { + tev_encoder_t *enc = calloc(1, sizeof(tev_encoder_t)); + if (!enc) return NULL; + + enc->width = DEFAULT_WIDTH; + enc->height = DEFAULT_HEIGHT; + enc->quality = 5; // Default quality + enc->output_to_stdout = 1; + + return enc; +} + +// Allocate buffers +static int allocate_buffers(tev_encoder_t *enc) { + int pixels = enc->width * enc->height; + int blocks = ((enc->width + 7) / 8) * ((enc->height + 7) / 8); + + enc->current_rgb = malloc(pixels * 3); // RGB: 3 bytes per pixel + enc->previous_rgb = malloc(pixels * 3); + enc->reference_rgb = malloc(pixels * 3); + + enc->rgb_workspace = malloc(BLOCK_SIZE * BLOCK_SIZE * 3); + enc->dct_workspace = malloc(BLOCK_SIZE * BLOCK_SIZE * 3 * sizeof(float)); + enc->block_data = malloc(blocks * sizeof(tev_block_t)); + enc->compressed_buffer = malloc(blocks * sizeof(tev_block_t) * 2); + enc->mp2_buffer = malloc(2048); + + // Initialize gzip compression stream + enc->gzip_stream.zalloc = Z_NULL; + enc->gzip_stream.zfree = Z_NULL; + enc->gzip_stream.opaque = Z_NULL; + + int gzip_init_result = deflateInit2(&enc->gzip_stream, Z_DEFAULT_COMPRESSION, + Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY); // 15+16 for gzip format + + return (enc->current_rgb && enc->previous_rgb && enc->reference_rgb && + enc->rgb_workspace && enc->dct_workspace && enc->block_data && enc->compressed_buffer && + enc->mp2_buffer && gzip_init_result == Z_OK); +} + +// Cleanup +static void cleanup_encoder(tev_encoder_t *enc) { + if (!enc) return; + + if (enc->ffmpeg_video_pipe) pclose(enc->ffmpeg_video_pipe); + if (enc->mp2_file) fclose(enc->mp2_file); + deflateEnd(&enc->gzip_stream); + + free(enc->input_file); + free(enc->output_file); + free(enc->current_rgb); + free(enc->previous_rgb); + free(enc->reference_rgb); + free(enc->rgb_workspace); + free(enc->dct_workspace); + free(enc->block_data); + free(enc->compressed_buffer); + free(enc->mp2_buffer); + + unlink(TEMP_AUDIO_FILE); + free(enc); +} + +// Print usage +static void print_usage(const char *program_name) { + printf("TSVM Enhanced Video (TEV) Encoder\n\n"); + printf("Usage: %s [options] input_video\n\n", program_name); + printf("Options:\n"); + printf(" -o, --output FILE Output TEV file (default: stdout)\n"); + printf(" -s, --size WxH Video resolution (default: 560x448)\n"); + printf(" -q, --quality N Quality level 0-7 (default: 5)\n"); + printf(" -h, --help Show this help\n\n"); + printf("TEV Features:\n"); + printf(" - 8x8 DCT-based compression with motion compensation\n"); + printf(" - Native 4096-color support (4:4:4 RGB)\n"); + printf(" - Zstd compression for optimal efficiency\n"); + printf(" - Hardware-accelerated encoding functions\n\n"); + printf("Examples:\n"); + printf(" %s input.mp4 -o output.tev\n", program_name); + printf(" %s input.avi -s 1024x768 -q 7 -o output.tev\n", program_name); +} + +int main(int argc, char *argv[]) { + tev_encoder_t *enc = init_encoder(); + if (!enc) { + fprintf(stderr, "Failed to initialize encoder\n"); + return 1; + } + + // Parse arguments + static struct option long_options[] = { + {"output", required_argument, 0, 'o'}, + {"size", required_argument, 0, 's'}, + {"quality", required_argument, 0, 'q'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0} + }; + + int c; + while ((c = getopt_long(argc, argv, "o:s:q:h", long_options, NULL)) != -1) { + switch (c) { + case 'o': + enc->output_file = strdup(optarg); + enc->output_to_stdout = 0; + break; + case 's': + if (sscanf(optarg, "%dx%d", &enc->width, &enc->height) != 2) { + fprintf(stderr, "Invalid resolution: %s\n", optarg); + cleanup_encoder(enc); + return 1; + } + break; + case 'q': + enc->quality = atoi(optarg); + if (enc->quality < 0 || enc->quality > 7) { + fprintf(stderr, "Quality must be 0-7\n"); + cleanup_encoder(enc); + return 1; + } + break; + case 'h': + print_usage(argv[0]); + cleanup_encoder(enc); + return 0; + default: + print_usage(argv[0]); + cleanup_encoder(enc); + return 1; + } + } + + if (optind >= argc) { + fprintf(stderr, "Input file required\n"); + print_usage(argv[0]); + cleanup_encoder(enc); + return 1; + } + + enc->input_file = strdup(argv[optind]); + + // Initialize + if (!get_video_metadata(enc) || !allocate_buffers(enc) || + !start_video_conversion(enc) || !start_audio_conversion(enc)) { + cleanup_encoder(enc); + return 1; + } + + FILE *output = enc->output_to_stdout ? stdout : fopen(enc->output_file, "wb"); + if (!output) { + fprintf(stderr, "Failed to open output\n"); + cleanup_encoder(enc); + return 1; + } + + write_tev_header(enc, output); + gettimeofday(&enc->start_time, NULL); + enc->total_output_bytes = 8 + 1 + 1 + 2 + 2 + 2 + 4 + 1 + 5; // TEV header size + + // Process all frames + for (int frame = 1; frame <= enc->total_frames; frame++) { + int result = process_frame(enc, frame, output); + if (result <= 0) break; + } + + fprintf(stderr, "\nEncoding complete\n"); + + if (!enc->output_to_stdout) { + fclose(output); + fprintf(stderr, "Output: %s (%.1f MB)\n", enc->output_file, + enc->total_output_bytes / (1024.0 * 1024.0)); + } + + cleanup_encoder(enc); + return 0; +}