mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-03-07 19:51:51 +09:00
ycocg wip
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -251,5 +251,6 @@ exports = {
|
||||
getCurrentTapeDevice,
|
||||
isReady,
|
||||
// Enhanced functions
|
||||
seek
|
||||
seek,
|
||||
rewind
|
||||
}
|
||||
@@ -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<Any> {
|
||||
fun blockEncodeToYCoCgFourBits(blockX: Int, blockY: Int, srcPtr: Int, width: Int, channels: Int, hasAlpha: Boolean, pattern: Int): List<Any> {
|
||||
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<IntArray> = 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<IntArray> = 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<IntArray> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
935
video_encoder/encoder_ipf1d.c
Normal file
935
video_encoder/encoder_ipf1d.c
Normal file
@@ -0,0 +1,935 @@
|
||||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <zlib.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <getopt.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
// 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;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
815
video_encoder/encoder_tev_rgb24_8x8.c
Normal file
815
video_encoder/encoder_tev_rgb24_8x8.c
Normal file
@@ -0,0 +1,815 @@
|
||||
// Created by Claude on 2025-08-17.
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <zlib.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <getopt.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user