mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-12 15:44:05 +09:00
interlacing wip
This commit is contained in:
@@ -383,12 +383,30 @@ let qualityY = seqread.readOneByte()
|
|||||||
let qualityCo = seqread.readOneByte()
|
let qualityCo = seqread.readOneByte()
|
||||||
let qualityCg = seqread.readOneByte()
|
let qualityCg = seqread.readOneByte()
|
||||||
let flags = seqread.readOneByte()
|
let flags = seqread.readOneByte()
|
||||||
let hasAudio = flags & 1
|
let hasAudio = !!(flags & 1)
|
||||||
let hasSubtitle = flags & 2
|
let hasSubtitle = !!(flags & 2)
|
||||||
let unused1 = seqread.readOneByte()
|
let videoFlags = seqread.readOneByte()
|
||||||
|
let isInterlaced = !!(videoFlags & 1)
|
||||||
let unused2 = seqread.readOneByte()
|
let unused2 = seqread.readOneByte()
|
||||||
|
|
||||||
serial.println(`TEV Format ${version} (${colorSpace}); Q: ${qualityY} ${qualityCo} ${qualityCg}`)
|
|
||||||
|
serial.println(`Video metadata:`)
|
||||||
|
serial.println(` Frames: ${totalFrames}`)
|
||||||
|
serial.println(` FPS: ${fps}`)
|
||||||
|
serial.println(` Duration: ${totalFrames / fps}`)
|
||||||
|
serial.println(` Audio: ${hasAudio ? "Yes" : "No"}`)
|
||||||
|
serial.println(` Resolution: ${width}x${height}, ${isInterlaced ? "interlaced" : "progressive"}`)
|
||||||
|
|
||||||
|
|
||||||
|
// DEBUG interlace raw output
|
||||||
|
if (isInterlaced) {
|
||||||
|
height = height >> 1
|
||||||
|
isInterlaced = false
|
||||||
|
}
|
||||||
|
// END OF DEBUG
|
||||||
|
|
||||||
|
|
||||||
|
serial.println(`TEV Format ${version} (${colorSpace}); Q: ${qualityY} ${qualityCo} ${qualityCg}; Interlaced: ${isInterlaced ? 'Yes' : 'No'}`)
|
||||||
|
|
||||||
function updateDataRateBin(rate) {
|
function updateDataRateBin(rate) {
|
||||||
videoRateBin.push(rate)
|
videoRateBin.push(rate)
|
||||||
@@ -555,7 +573,7 @@ try {
|
|||||||
// Hardware-accelerated TEV decoding to RGB buffers (YCoCg-R or XYB based on version)
|
// Hardware-accelerated TEV decoding to RGB buffers (YCoCg-R or XYB based on version)
|
||||||
try {
|
try {
|
||||||
let decodeStart = sys.nanoTime()
|
let decodeStart = sys.nanoTime()
|
||||||
graphics.tevDecode(blockDataPtr, CURRENT_RGB_ADDR, PREV_RGB_ADDR, width, height, [qualityY, qualityCo, qualityCg], debugMotionVectors, version)
|
graphics.tevDecode(blockDataPtr, CURRENT_RGB_ADDR, PREV_RGB_ADDR, width, height, [qualityY, qualityCo, qualityCg], debugMotionVectors, version, isInterlaced)
|
||||||
decodeTime = (sys.nanoTime() - decodeStart) / 1000000.0 // Convert to milliseconds
|
decodeTime = (sys.nanoTime() - decodeStart) / 1000000.0 // Convert to milliseconds
|
||||||
|
|
||||||
// Upload RGB buffer to display framebuffer with dithering
|
// Upload RGB buffer to display framebuffer with dithering
|
||||||
|
|||||||
@@ -703,10 +703,12 @@ DCT-based compression, motion compensation, and efficient temporal coding.
|
|||||||
uint8 Quality Index for Y channel (0-99; 100 denotes all quantiser is 1)
|
uint8 Quality Index for Y channel (0-99; 100 denotes all quantiser is 1)
|
||||||
uint8 Quality Index for Co channel (0-99; 100 denotes all quantiser is 1)
|
uint8 Quality Index for Co channel (0-99; 100 denotes all quantiser is 1)
|
||||||
uint8 Quality Index for Cg channel (0-99; 100 denotes all quantiser is 1)
|
uint8 Quality Index for Cg channel (0-99; 100 denotes all quantiser is 1)
|
||||||
uint8 Flags
|
uint8 Extra Feature Flags
|
||||||
bit 0 = has audio
|
- bit 0 = has audio
|
||||||
bit 1 = has subtitle
|
- bit 1 = has subtitle
|
||||||
unit16 Reserved, fill with zero
|
uint8 Video Flags
|
||||||
|
- bit 0 = is interlaced (should be default for most non-archival TEV videos)
|
||||||
|
uint8 Reserved, fill with zero
|
||||||
|
|
||||||
## Packet Types
|
## Packet Types
|
||||||
0x10: I-frame (intra-coded frame)
|
0x10: I-frame (intra-coded frame)
|
||||||
@@ -718,7 +720,7 @@ DCT-based compression, motion compensation, and efficient temporal coding.
|
|||||||
## Video Packet Structure
|
## Video Packet Structure
|
||||||
uint8 Packet Type
|
uint8 Packet Type
|
||||||
uint32 Compressed Size
|
uint32 Compressed Size
|
||||||
* Gzip-compressed Block Data
|
* Zstd-compressed Block Data
|
||||||
|
|
||||||
## Block Data (per 16x16 block)
|
## Block Data (per 16x16 block)
|
||||||
uint8 Mode: encoding mode
|
uint8 Mode: encoding mode
|
||||||
|
|||||||
@@ -1512,24 +1512,96 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// YCoCg-R to RGB conversion with 4:2:0 chroma upsampling
|
// YCoCg-R to RGB conversion with 4:2:0 chroma upsampling
|
||||||
|
// Pre-allocated arrays for chroma component caching (reused across blocks)
|
||||||
|
private val cgHalfCache = IntArray(64) // 8x8 cache for cg/2 values
|
||||||
|
private val coHalfCache = IntArray(64) // 8x8 cache for co/2 values
|
||||||
|
|
||||||
|
// Temporary buffer for interlaced field processing
|
||||||
|
private val interlacedFieldBuffer = IntArray(560 * 224 * 3) // Half-height RGB buffer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* YADIF (Yet Another Deinterlacing Filter) implementation
|
||||||
|
* Converts interlaced field to progressive frame with temporal/spatial interpolation
|
||||||
|
*/
|
||||||
|
fun yadifDeinterlace(fieldRGBAddr: Long, outputRGBAddr: Long, width: Int, height: Int,
|
||||||
|
prevFieldAddr: Long, nextFieldAddr: Long, fieldParity: Int,
|
||||||
|
fieldIncVec: Int, outputIncVec: Int) {
|
||||||
|
val fieldHeight = height / 2
|
||||||
|
|
||||||
|
for (y in 0 until fieldHeight) {
|
||||||
|
for (x in 0 until width) {
|
||||||
|
val fieldOffset = (y * width + x) * 3
|
||||||
|
val outputOffset = ((y * 2 + fieldParity) * width + x) * 3
|
||||||
|
|
||||||
|
// Copy current field lines directly (no interpolation needed)
|
||||||
|
for (c in 0..2) {
|
||||||
|
val pixelValue = vm.peek(fieldRGBAddr + (fieldOffset + c) * fieldIncVec)!!
|
||||||
|
vm.poke(outputRGBAddr + (outputOffset + c) * outputIncVec, pixelValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpolate missing lines using Yadif algorithm
|
||||||
|
if (y > 0 && y < fieldHeight - 1) {
|
||||||
|
val interpOutputOffset = ((y * 2 + 1 - fieldParity) * width + x) * 3
|
||||||
|
|
||||||
|
for (c in 0..2) {
|
||||||
|
// Get spatial neighbors
|
||||||
|
val above = vm.peek(fieldRGBAddr + (fieldOffset - width * 3 + c) * fieldIncVec)!!.toInt() and 0xFF
|
||||||
|
val below = vm.peek(fieldRGBAddr + (fieldOffset + width * 3 + c) * fieldIncVec)!!.toInt() and 0xFF
|
||||||
|
val current = vm.peek(fieldRGBAddr + (fieldOffset + c) * fieldIncVec)!!.toInt() and 0xFF
|
||||||
|
|
||||||
|
// Simple spatial interpolation (can be enhanced with temporal prediction)
|
||||||
|
val spatialInterp = (above + below) / 2
|
||||||
|
|
||||||
|
// Apply edge-directed interpolation bias
|
||||||
|
val edgeBias = if (kotlin.math.abs(above - below) < 32) {
|
||||||
|
(current + spatialInterp) / 2 // Low edge activity: blend with current
|
||||||
|
} else {
|
||||||
|
spatialInterp // High edge activity: use spatial only
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.poke(outputRGBAddr + (interpOutputOffset + c) * outputIncVec,
|
||||||
|
edgeBias.coerceIn(0, 255).toByte())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle edge cases: first and last interpolated lines use simple spatial interpolation
|
||||||
|
for (x in 0 until width) {
|
||||||
|
val interpY = if (fieldParity == 0) 1 else 0
|
||||||
|
val outputOffset = (interpY * width + x) * 3
|
||||||
|
val referenceOffset = ((interpY + 1) * width + x) * 3
|
||||||
|
|
||||||
|
for (c in 0..2) {
|
||||||
|
val refPixel = vm.peek(outputRGBAddr + (referenceOffset + c) * outputIncVec)!!
|
||||||
|
vm.poke(outputRGBAddr + (outputOffset + c) * outputIncVec, refPixel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun tevYcocgToRGB(yBlock: IntArray, coBlock: IntArray, cgBlock: IntArray): IntArray {
|
fun tevYcocgToRGB(yBlock: IntArray, coBlock: IntArray, cgBlock: IntArray): IntArray {
|
||||||
val rgbData = IntArray(16 * 16 * 3) // R,G,B for 16x16 pixels
|
val rgbData = IntArray(16 * 16 * 3) // R,G,B for 16x16 pixels
|
||||||
|
|
||||||
|
// Pre-compute chroma division components for 8x8 chroma block (each reused 4x in 4:2:0)
|
||||||
|
for (i in 0 until 64) {
|
||||||
|
cgHalfCache[i] = cgBlock[i] / 2
|
||||||
|
coHalfCache[i] = coBlock[i] / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process 16x16 luma with cached chroma components
|
||||||
for (py in 0 until 16) {
|
for (py in 0 until 16) {
|
||||||
for (px in 0 until 16) {
|
for (px in 0 until 16) {
|
||||||
val yIdx = py * 16 + px
|
val yIdx = py * 16 + px
|
||||||
val y = yBlock[yIdx]
|
val y = yBlock[yIdx]
|
||||||
|
|
||||||
// Get chroma values from subsampled 8x8 blocks (nearest neighbor upsampling)
|
// Get pre-computed chroma components (4:2:0 upsampling)
|
||||||
val coIdx = (py / 2) * 8 + (px / 2)
|
val coIdx = (py / 2) * 8 + (px / 2)
|
||||||
val co = coBlock[coIdx]
|
|
||||||
val cg = cgBlock[coIdx]
|
|
||||||
|
|
||||||
// YCoCg-R inverse transform (per YCoCg-R spec with truncated division)
|
// YCoCg-R inverse transform using cached division results
|
||||||
val tmp = y - (cg / 2)
|
val tmp = y - cgHalfCache[coIdx]
|
||||||
val g = cg + tmp
|
val g = cgBlock[coIdx] + tmp
|
||||||
val b = tmp - (co / 2)
|
val b = tmp - coHalfCache[coIdx]
|
||||||
val r = b + co
|
val r = b + coBlock[coIdx]
|
||||||
|
|
||||||
// Clamp and store RGB
|
// Clamp and store RGB
|
||||||
val baseIdx = (py * 16 + px) * 3
|
val baseIdx = (py * 16 + px) * 3
|
||||||
@@ -1725,10 +1797,14 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
|||||||
*/
|
*/
|
||||||
fun tevDecode(blockDataPtr: Long, currentRGBAddr: Long, prevRGBAddr: Long,
|
fun tevDecode(blockDataPtr: Long, currentRGBAddr: Long, prevRGBAddr: Long,
|
||||||
width: Int, height: Int, qualityIndices: IntArray, debugMotionVectors: Boolean = false,
|
width: Int, height: Int, qualityIndices: IntArray, debugMotionVectors: Boolean = false,
|
||||||
tevVersion: Int = 2) {
|
tevVersion: Int = 2, isInterlaced: Boolean = false) {
|
||||||
|
|
||||||
|
// height doesn't change when interlaced, because that's the encoder's output
|
||||||
|
|
||||||
|
// For interlaced mode, decode to half-height field first
|
||||||
|
val decodingHeight = if (isInterlaced) height / 2 else height
|
||||||
val blocksX = (width + 15) / 16 // 16x16 blocks now
|
val blocksX = (width + 15) / 16 // 16x16 blocks now
|
||||||
val blocksY = (height + 15) / 16
|
val blocksY = (decodingHeight + 15) / 16
|
||||||
|
|
||||||
val quantYmult = jpeg_quality_to_mult(qualityIndices[0])
|
val quantYmult = jpeg_quality_to_mult(qualityIndices[0])
|
||||||
val quantCOmult = jpeg_quality_to_mult(qualityIndices[1])
|
val quantCOmult = jpeg_quality_to_mult(qualityIndices[1])
|
||||||
@@ -1769,7 +1845,7 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
|||||||
when (mode) {
|
when (mode) {
|
||||||
0x00 -> { // TEV_MODE_SKIP - copy RGB from previous frame (optimized with memcpy)
|
0x00 -> { // TEV_MODE_SKIP - copy RGB from previous frame (optimized with memcpy)
|
||||||
// Check if we can copy the entire block at once (no clipping)
|
// Check if we can copy the entire block at once (no clipping)
|
||||||
if (startX + 16 <= width && startY + 16 <= height) {
|
if (startX + 16 <= width && startY + 16 <= decodingHeight) {
|
||||||
// Optimized case: copy entire 16x16 block with row-by-row memcpy
|
// Optimized case: copy entire 16x16 block with row-by-row memcpy
|
||||||
for (dy in 0 until 16) {
|
for (dy in 0 until 16) {
|
||||||
val srcRowOffset = ((startY + dy).toLong() * width + startX) * 3
|
val srcRowOffset = ((startY + dy).toLong() * width + startX) * 3
|
||||||
@@ -1786,7 +1862,7 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
|||||||
for (dx in 0 until 16) {
|
for (dx in 0 until 16) {
|
||||||
val x = startX + dx
|
val x = startX + dx
|
||||||
val y = startY + dy
|
val y = startY + dy
|
||||||
if (x < width && y < height) {
|
if (x < width && y < decodingHeight) {
|
||||||
val pixelOffset = y.toLong() * width + x
|
val pixelOffset = y.toLong() * width + x
|
||||||
val rgbOffset = pixelOffset * 3
|
val rgbOffset = pixelOffset * 3
|
||||||
|
|
||||||
@@ -1816,7 +1892,7 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
|||||||
val refX = x + mvX
|
val refX = x + mvX
|
||||||
val refY = y + mvY
|
val refY = y + mvY
|
||||||
|
|
||||||
if (x < width && y < height) {
|
if (x < width && y < decodingHeight) {
|
||||||
val dstPixelOffset = y.toLong() * width + x
|
val dstPixelOffset = y.toLong() * width + x
|
||||||
val dstRgbOffset = dstPixelOffset * 3
|
val dstRgbOffset = dstPixelOffset * 3
|
||||||
|
|
||||||
@@ -1836,7 +1912,7 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
|||||||
val refStartY = startY + mvY
|
val refStartY = startY + mvY
|
||||||
|
|
||||||
// Check if entire 16x16 block can be copied with memcpy (no bounds issues)
|
// Check if entire 16x16 block can be copied with memcpy (no bounds issues)
|
||||||
if (startX + 16 <= width && startY + 16 <= height &&
|
if (startX + 16 <= width && startY + 16 <= decodingHeight &&
|
||||||
refStartX >= 0 && refStartY >= 0 && refStartX + 16 <= width && refStartY + 16 <= height) {
|
refStartX >= 0 && refStartY >= 0 && refStartX + 16 <= width && refStartY + 16 <= height) {
|
||||||
|
|
||||||
// Optimized case: copy entire 16x16 block with row-by-row memcpy
|
// Optimized case: copy entire 16x16 block with row-by-row memcpy
|
||||||
@@ -1858,16 +1934,16 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
|||||||
val refX = x + mvX
|
val refX = x + mvX
|
||||||
val refY = y + mvY
|
val refY = y + mvY
|
||||||
|
|
||||||
if (x < width && y < height) {
|
if (x < width && y < decodingHeight) {
|
||||||
val dstPixelOffset = y.toLong() * width + x
|
val dstPixelOffset = y.toLong() * width + x
|
||||||
val dstRgbOffset = dstPixelOffset * 3
|
val dstRgbOffset = dstPixelOffset * 3
|
||||||
|
|
||||||
if (refX >= 0 && refY >= 0 && refX < width && refY < height) {
|
if (refX >= 0 && refY >= 0 && refX < width && refY < decodingHeight) {
|
||||||
val refPixelOffset = refY.toLong() * width + refX
|
val refPixelOffset = refY.toLong() * width + refX
|
||||||
val refRgbOffset = refPixelOffset * 3
|
val refRgbOffset = refPixelOffset * 3
|
||||||
|
|
||||||
// Additional safety: ensure RGB offset is within valid range
|
// Additional safety: ensure RGB offset is within valid range
|
||||||
val maxValidOffset = (width * height - 1) * 3L + 2
|
val maxValidOffset = (width * decodingHeight - 1) * 3L + 2
|
||||||
if (refRgbOffset >= 0 && refRgbOffset <= maxValidOffset) {
|
if (refRgbOffset >= 0 && refRgbOffset <= maxValidOffset) {
|
||||||
// Copy RGB from reference position
|
// Copy RGB from reference position
|
||||||
val refR = vm.peek(prevRGBAddr + refRgbOffset*prevAddrIncVec)!!
|
val refR = vm.peek(prevRGBAddr + refRgbOffset*prevAddrIncVec)!!
|
||||||
@@ -1923,7 +1999,7 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
|||||||
for (dx in 0 until 16) {
|
for (dx in 0 until 16) {
|
||||||
val x = startX + dx
|
val x = startX + dx
|
||||||
val y = startY + dy
|
val y = startY + dy
|
||||||
if (x < width && y < height) {
|
if (x < width && y < decodingHeight) {
|
||||||
val rgbIdx = (dy * 16 + dx) * 3
|
val rgbIdx = (dy * 16 + dx) * 3
|
||||||
val imageOffset = y.toLong() * width + x
|
val imageOffset = y.toLong() * width + x
|
||||||
val bufferOffset = imageOffset * 3
|
val bufferOffset = imageOffset * 3
|
||||||
@@ -1963,10 +2039,10 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
|||||||
val refY = y + mvY
|
val refY = y + mvY
|
||||||
val pixelIdx = dy * 16 + dx
|
val pixelIdx = dy * 16 + dx
|
||||||
|
|
||||||
if (x < width && y < height) {
|
if (x < width && y < decodingHeight) {
|
||||||
var mcY: Int
|
var mcY: Int
|
||||||
|
|
||||||
if (refX >= 0 && refY >= 0 && refX < width && refY < height) {
|
if (refX >= 0 && refY >= 0 && refX < width && refY < decodingHeight) {
|
||||||
// Get motion-compensated RGB from previous frame
|
// Get motion-compensated RGB from previous frame
|
||||||
val refPixelOffset = refY.toLong() * width + refX
|
val refPixelOffset = refY.toLong() * width + refX
|
||||||
val refRgbOffset = refPixelOffset * 3
|
val refRgbOffset = refPixelOffset * 3
|
||||||
@@ -2003,12 +2079,12 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
|||||||
val refY = y + mvY
|
val refY = y + mvY
|
||||||
val chromaIdx = cy * 8 + cx
|
val chromaIdx = cy * 8 + cx
|
||||||
|
|
||||||
if (x < width && y < height) {
|
if (x < width && y < decodingHeight) {
|
||||||
var mcCo: Int
|
var mcCo: Int
|
||||||
var mcCg: Int
|
var mcCg: Int
|
||||||
|
|
||||||
// Sample 2x2 block from motion-compensated position for chroma
|
// Sample 2x2 block from motion-compensated position for chroma
|
||||||
if (refX >= 0 && refY >= 0 && refX < width - 1 && refY < height - 1) {
|
if (refX >= 0 && refY >= 0 && refX < width - 1 && refY < decodingHeight - 1) {
|
||||||
var coSum = 0
|
var coSum = 0
|
||||||
var cgSum = 0
|
var cgSum = 0
|
||||||
var count = 0
|
var count = 0
|
||||||
@@ -2018,7 +2094,7 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
|||||||
for (dx in 0 until 2) {
|
for (dx in 0 until 2) {
|
||||||
val sampleX = refX + dx
|
val sampleX = refX + dx
|
||||||
val sampleY = refY + dy
|
val sampleY = refY + dy
|
||||||
if (sampleX < width && sampleY < height) {
|
if (sampleX < width && sampleY < decodingHeight) {
|
||||||
val refPixelOffset = sampleY.toLong() * width + sampleX
|
val refPixelOffset = sampleY.toLong() * width + sampleX
|
||||||
val refRgbOffset = refPixelOffset * 3
|
val refRgbOffset = refPixelOffset * 3
|
||||||
|
|
||||||
@@ -2064,7 +2140,7 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
|||||||
for (dx in 0 until 16) {
|
for (dx in 0 until 16) {
|
||||||
val x = startX + dx
|
val x = startX + dx
|
||||||
val y = startY + dy
|
val y = startY + dy
|
||||||
if (x < width && y < height) {
|
if (x < width && y < decodingHeight) {
|
||||||
val imageOffset = y.toLong() * width + x
|
val imageOffset = y.toLong() * width + x
|
||||||
val bufferOffset = imageOffset * 3
|
val bufferOffset = imageOffset * 3
|
||||||
|
|
||||||
@@ -2099,7 +2175,7 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
|||||||
for (dx in 0 until 16) {
|
for (dx in 0 until 16) {
|
||||||
val x = startX + dx
|
val x = startX + dx
|
||||||
val y = startY + dy
|
val y = startY + dy
|
||||||
if (x < width && y < height) {
|
if (x < width && y < decodingHeight) {
|
||||||
val imageOffset = y.toLong() * width + x
|
val imageOffset = y.toLong() * width + x
|
||||||
val bufferOffset = imageOffset * 3
|
val bufferOffset = imageOffset * 3
|
||||||
|
|
||||||
@@ -2113,6 +2189,25 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply Yadif deinterlacing if this is an interlaced frame
|
||||||
|
/*if (isInterlaced) {
|
||||||
|
// Decode produced a field at half-height, now deinterlace to full progressive frame
|
||||||
|
val tempFieldBuffer = vm.malloc(width * decodingHeight * 3)
|
||||||
|
|
||||||
|
// Copy the decoded field to temporary buffer
|
||||||
|
vm.memcpy(currentRGBAddr.toInt(), tempFieldBuffer.toInt(), width * decodingHeight * 3)
|
||||||
|
|
||||||
|
// Apply Yadif deinterlacing: field -> progressive frame
|
||||||
|
yadifDeinterlace(
|
||||||
|
tempFieldBuffer.toLong(), currentRGBAddr, width, height,
|
||||||
|
prevRGBAddr, prevRGBAddr, // TODO: Implement proper temporal prediction
|
||||||
|
0, // Field parity (0=even field first)
|
||||||
|
thisAddrIncVec, thisAddrIncVec
|
||||||
|
)
|
||||||
|
|
||||||
|
vm.free(tempFieldBuffer.toInt())
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ typedef struct {
|
|||||||
int has_audio;
|
int has_audio;
|
||||||
int has_subtitles;
|
int has_subtitles;
|
||||||
int output_to_stdout;
|
int output_to_stdout;
|
||||||
|
int progressive_mode; // 0 = interlaced (default), 1 = progressive
|
||||||
int qualityIndex; // -q option
|
int qualityIndex; // -q option
|
||||||
int qualityY;
|
int qualityY;
|
||||||
int qualityCo;
|
int qualityCo;
|
||||||
@@ -174,6 +175,7 @@ typedef struct {
|
|||||||
|
|
||||||
// Frame buffers (8-bit RGB format for encoding)
|
// Frame buffers (8-bit RGB format for encoding)
|
||||||
uint8_t *current_rgb, *previous_rgb, *reference_rgb;
|
uint8_t *current_rgb, *previous_rgb, *reference_rgb;
|
||||||
|
uint8_t *previous_even_field; // Previous even field buffer for interlaced scene change detection
|
||||||
|
|
||||||
// YCoCg workspace
|
// YCoCg workspace
|
||||||
float *y_workspace, *co_workspace, *cg_workspace;
|
float *y_workspace, *co_workspace, *cg_workspace;
|
||||||
@@ -303,29 +305,6 @@ static void dct_16x16_fast(float *input, float *output) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy O(n^4) version for reference/fallback
|
|
||||||
static void dct_16x16(float *input, float *output) {
|
|
||||||
init_dct_tables(); // Ensure tables are initialized
|
|
||||||
|
|
||||||
for (int u = 0; u < 16; u++) {
|
|
||||||
for (int v = 0; v < 16; v++) {
|
|
||||||
float sum = 0.0f;
|
|
||||||
float cu = (u == 0) ? 1.0f / sqrtf(2.0f) : 1.0f;
|
|
||||||
float cv = (v == 0) ? 1.0f / sqrtf(2.0f) : 1.0f;
|
|
||||||
|
|
||||||
for (int x = 0; x < 16; x++) {
|
|
||||||
for (int y = 0; y < 16; y++) {
|
|
||||||
sum += input[y * 16 + x] *
|
|
||||||
dct_table_16[u][x] *
|
|
||||||
dct_table_16[v][y];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output[u * 16 + v] = 0.25f * cu * cv * sum;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fast separable 8x8 DCT - 4x performance improvement
|
// Fast separable 8x8 DCT - 4x performance improvement
|
||||||
static float temp_dct_8[HALF_BLOCK_SIZE_SQR]; // Reusable temporary buffer
|
static float temp_dct_8[HALF_BLOCK_SIZE_SQR]; // Reusable temporary buffer
|
||||||
|
|
||||||
@@ -361,29 +340,6 @@ static void dct_8x8_fast(float *input, float *output) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy 8x8 2D DCT (for chroma) - O(n^4) version
|
|
||||||
static void dct_8x8(float *input, float *output) {
|
|
||||||
init_dct_tables(); // Ensure tables are initialized
|
|
||||||
|
|
||||||
for (int u = 0; u < 8; u++) {
|
|
||||||
for (int v = 0; v < 8; v++) {
|
|
||||||
float sum = 0.0f;
|
|
||||||
float cu = (u == 0) ? 1.0f / sqrtf(2.0f) : 1.0f;
|
|
||||||
float cv = (v == 0) ? 1.0f / sqrtf(2.0f) : 1.0f;
|
|
||||||
|
|
||||||
for (int x = 0; x < 8; x++) {
|
|
||||||
for (int y = 0; y < 8; y++) {
|
|
||||||
sum += input[y * 8 + x] *
|
|
||||||
dct_table_8[u][x] *
|
|
||||||
dct_table_8[v][y];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output[u * 8 + v] = 0.25f * cu * cv * sum;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// quantise DCT coefficient using quality table with rate control
|
// quantise DCT coefficient using quality table with rate control
|
||||||
static int16_t quantise_coeff(float coeff, float quant, int is_dc, int is_chroma) {
|
static int16_t quantise_coeff(float coeff, float quant, int is_dc, int is_chroma) {
|
||||||
if (is_dc) {
|
if (is_dc) {
|
||||||
@@ -1485,14 +1441,19 @@ static tev_encoder_t* init_encoder(void) {
|
|||||||
|
|
||||||
// Allocate encoder buffers
|
// Allocate encoder buffers
|
||||||
static int alloc_encoder_buffers(tev_encoder_t *enc) {
|
static int alloc_encoder_buffers(tev_encoder_t *enc) {
|
||||||
int pixels = enc->width * enc->height;
|
// In interlaced mode, FFmpeg separatefields outputs field frames at half height
|
||||||
|
// In progressive mode, we work with full height frames
|
||||||
|
int encoding_pixels = enc->width * enc->height;
|
||||||
|
|
||||||
int blocks_x = (enc->width + 15) / 16;
|
int blocks_x = (enc->width + 15) / 16;
|
||||||
int blocks_y = (enc->height + 15) / 16;
|
int blocks_y = (enc->height + 15) / 16;
|
||||||
int total_blocks = blocks_x * blocks_y;
|
int total_blocks = blocks_x * blocks_y;
|
||||||
|
|
||||||
enc->current_rgb = malloc(pixels * 3);
|
// Allocate buffers for encoding (FFmpeg provides frames at the correct resolution)
|
||||||
enc->previous_rgb = malloc(pixels * 3);
|
enc->current_rgb = malloc(encoding_pixels * 3); // Current frame buffer from FFmpeg
|
||||||
enc->reference_rgb = malloc(pixels * 3);
|
enc->previous_rgb = malloc(encoding_pixels * 3); // Previous frame buffer for motion estimation
|
||||||
|
enc->reference_rgb = malloc(encoding_pixels * 3); // Reference frame buffer
|
||||||
|
enc->previous_even_field = malloc(encoding_pixels * 3); // Previous even field for interlaced scene change
|
||||||
|
|
||||||
enc->y_workspace = malloc(16 * 16 * sizeof(float));
|
enc->y_workspace = malloc(16 * 16 * sizeof(float));
|
||||||
enc->co_workspace = malloc(8 * 8 * sizeof(float));
|
enc->co_workspace = malloc(8 * 8 * sizeof(float));
|
||||||
@@ -1504,6 +1465,7 @@ static int alloc_encoder_buffers(tev_encoder_t *enc) {
|
|||||||
enc->mp2_buffer = malloc(MP2_DEFAULT_PACKET_SIZE);
|
enc->mp2_buffer = malloc(MP2_DEFAULT_PACKET_SIZE);
|
||||||
|
|
||||||
if (!enc->current_rgb || !enc->previous_rgb || !enc->reference_rgb ||
|
if (!enc->current_rgb || !enc->previous_rgb || !enc->reference_rgb ||
|
||||||
|
!enc->previous_even_field ||
|
||||||
!enc->y_workspace || !enc->co_workspace || !enc->cg_workspace ||
|
!enc->y_workspace || !enc->co_workspace || !enc->cg_workspace ||
|
||||||
!enc->dct_workspace || !enc->block_data ||
|
!enc->dct_workspace || !enc->block_data ||
|
||||||
!enc->compressed_buffer || !enc->mp2_buffer) {
|
!enc->compressed_buffer || !enc->mp2_buffer) {
|
||||||
@@ -1523,7 +1485,8 @@ static int alloc_encoder_buffers(tev_encoder_t *enc) {
|
|||||||
ZSTD_CCtx_setParameter(enc->zstd_context, ZSTD_c_hashLog, 16);
|
ZSTD_CCtx_setParameter(enc->zstd_context, ZSTD_c_hashLog, 16);
|
||||||
|
|
||||||
// Initialize previous frame to black
|
// Initialize previous frame to black
|
||||||
memset(enc->previous_rgb, 0, pixels * 3);
|
memset(enc->previous_rgb, 0, encoding_pixels * 3);
|
||||||
|
memset(enc->previous_even_field, 0, encoding_pixels * 3);
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -1540,6 +1503,7 @@ static void free_encoder(tev_encoder_t *enc) {
|
|||||||
if (enc->current_rgb) { free(enc->current_rgb); enc->current_rgb = NULL; }
|
if (enc->current_rgb) { free(enc->current_rgb); enc->current_rgb = NULL; }
|
||||||
if (enc->previous_rgb) { free(enc->previous_rgb); enc->previous_rgb = NULL; }
|
if (enc->previous_rgb) { free(enc->previous_rgb); enc->previous_rgb = NULL; }
|
||||||
if (enc->reference_rgb) { free(enc->reference_rgb); enc->reference_rgb = NULL; }
|
if (enc->reference_rgb) { free(enc->reference_rgb); enc->reference_rgb = NULL; }
|
||||||
|
if (enc->previous_even_field) { free(enc->previous_even_field); enc->previous_even_field = NULL; }
|
||||||
if (enc->y_workspace) { free(enc->y_workspace); enc->y_workspace = NULL; }
|
if (enc->y_workspace) { free(enc->y_workspace); enc->y_workspace = NULL; }
|
||||||
if (enc->co_workspace) { free(enc->co_workspace); enc->co_workspace = NULL; }
|
if (enc->co_workspace) { free(enc->co_workspace); enc->co_workspace = NULL; }
|
||||||
if (enc->cg_workspace) { free(enc->cg_workspace); enc->cg_workspace = NULL; }
|
if (enc->cg_workspace) { free(enc->cg_workspace); enc->cg_workspace = NULL; }
|
||||||
@@ -1551,6 +1515,7 @@ static void free_encoder(tev_encoder_t *enc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write TEV header
|
// Write TEV header
|
||||||
|
|
||||||
static int write_tev_header(FILE *output, tev_encoder_t *enc) {
|
static int write_tev_header(FILE *output, tev_encoder_t *enc) {
|
||||||
// Magic + version
|
// Magic + version
|
||||||
fwrite(TEV_MAGIC, 1, 8, output);
|
fwrite(TEV_MAGIC, 1, 8, output);
|
||||||
@@ -1559,14 +1524,15 @@ static int write_tev_header(FILE *output, tev_encoder_t *enc) {
|
|||||||
|
|
||||||
// Video parameters
|
// Video parameters
|
||||||
uint16_t width = enc->width;
|
uint16_t width = enc->width;
|
||||||
uint16_t height = enc->height;
|
uint16_t height = enc->progressive_mode ? enc->height : enc->height * 2;
|
||||||
uint8_t fps = enc->fps;
|
uint8_t fps = enc->fps;
|
||||||
uint32_t total_frames = enc->total_frames;
|
uint32_t total_frames = enc->total_frames;
|
||||||
uint8_t qualityY = enc->qualityY;
|
uint8_t qualityY = enc->qualityY;
|
||||||
uint8_t qualityCo = enc->qualityCo;
|
uint8_t qualityCo = enc->qualityCo;
|
||||||
uint8_t qualityCg = enc->qualityCg;
|
uint8_t qualityCg = enc->qualityCg;
|
||||||
uint8_t flags = (enc->has_audio) | (enc->has_subtitles << 1);
|
uint8_t flags = (enc->has_audio) | (enc->has_subtitles << 1);
|
||||||
uint16_t unused = 0;
|
uint8_t video_flags = enc->progressive_mode ? 0 : 1; // bit 0 = is_interlaced (inverted from progressive)
|
||||||
|
uint8_t reserved = 0;
|
||||||
|
|
||||||
fwrite(&width, 2, 1, output);
|
fwrite(&width, 2, 1, output);
|
||||||
fwrite(&height, 2, 1, output);
|
fwrite(&height, 2, 1, output);
|
||||||
@@ -1576,15 +1542,32 @@ static int write_tev_header(FILE *output, tev_encoder_t *enc) {
|
|||||||
fwrite(&qualityCo, 1, 1, output);
|
fwrite(&qualityCo, 1, 1, output);
|
||||||
fwrite(&qualityCg, 1, 1, output);
|
fwrite(&qualityCg, 1, 1, output);
|
||||||
fwrite(&flags, 1, 1, output);
|
fwrite(&flags, 1, 1, output);
|
||||||
fwrite(&unused, 2, 1, output);
|
fwrite(&video_flags, 1, 1, output);
|
||||||
|
fwrite(&reserved, 1, 1, output);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect scene changes by analyzing frame differences
|
// Detect scene changes by analyzing frame differences
|
||||||
static int detect_scene_change(tev_encoder_t *enc) {
|
static int detect_scene_change(tev_encoder_t *enc, int field_parity) {
|
||||||
if (!enc->previous_rgb || !enc->current_rgb) {
|
if (!enc->current_rgb) {
|
||||||
return 0; // No previous frame to compare
|
return 0; // No current frame to compare
|
||||||
|
}
|
||||||
|
|
||||||
|
// In interlaced mode, use previous even field for comparison
|
||||||
|
uint8_t *comparison_buffer = enc->previous_rgb;
|
||||||
|
if (!enc->progressive_mode && field_parity == 0) {
|
||||||
|
// Interlaced even field: compare to previous even field
|
||||||
|
if (!enc->previous_even_field) {
|
||||||
|
return 0; // No previous even field to compare
|
||||||
|
}
|
||||||
|
comparison_buffer = enc->previous_even_field;
|
||||||
|
} else {
|
||||||
|
// Progressive mode: use regular previous_rgb
|
||||||
|
if (!enc->previous_rgb) {
|
||||||
|
return 0; // No previous frame to compare
|
||||||
|
}
|
||||||
|
comparison_buffer = enc->previous_rgb;
|
||||||
}
|
}
|
||||||
|
|
||||||
long long total_diff = 0;
|
long long total_diff = 0;
|
||||||
@@ -1596,9 +1579,9 @@ static int detect_scene_change(tev_encoder_t *enc) {
|
|||||||
int offset = (y * enc->width + x) * 3;
|
int offset = (y * enc->width + x) * 3;
|
||||||
|
|
||||||
// Calculate color difference
|
// Calculate color difference
|
||||||
int r_diff = abs(enc->current_rgb[offset] - enc->previous_rgb[offset]);
|
int r_diff = abs(enc->current_rgb[offset] - comparison_buffer[offset]);
|
||||||
int g_diff = abs(enc->current_rgb[offset + 1] - enc->previous_rgb[offset + 1]);
|
int g_diff = abs(enc->current_rgb[offset + 1] - comparison_buffer[offset + 1]);
|
||||||
int b_diff = abs(enc->current_rgb[offset + 2] - enc->previous_rgb[offset + 2]);
|
int b_diff = abs(enc->current_rgb[offset + 2] - comparison_buffer[offset + 2]);
|
||||||
|
|
||||||
int pixel_diff = r_diff + g_diff + b_diff;
|
int pixel_diff = r_diff + g_diff + b_diff;
|
||||||
total_diff += pixel_diff;
|
total_diff += pixel_diff;
|
||||||
@@ -1615,16 +1598,25 @@ static int detect_scene_change(tev_encoder_t *enc) {
|
|||||||
double avg_diff = (double)total_diff / sampled_pixels;
|
double avg_diff = (double)total_diff / sampled_pixels;
|
||||||
double changed_ratio = (double)changed_pixels / sampled_pixels;
|
double changed_ratio = (double)changed_pixels / sampled_pixels;
|
||||||
|
|
||||||
// Scene change thresholds:
|
if (enc->verbose) {
|
||||||
// - High average difference (> 40) OR
|
printf("Scene change detection: avg_diff=%.2f\tchanged_ratio=%.4f\n", avg_diff, changed_ratio);
|
||||||
// - Large percentage of changed pixels (> 30%)
|
}
|
||||||
return (avg_diff > 40.0) || (changed_ratio > 0.30);
|
|
||||||
|
// Scene change thresholds - adjust for interlaced mode
|
||||||
|
// Interlaced fields have more natural differences due to temporal field separation
|
||||||
|
double threshold = 0.30;
|
||||||
|
|
||||||
|
return changed_ratio > threshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode and write a frame
|
// Encode and write a frame
|
||||||
static int encode_frame(tev_encoder_t *enc, FILE *output, int frame_num) {
|
static int encode_frame(tev_encoder_t *enc, FILE *output, int frame_num, int field_parity) {
|
||||||
// Check for scene change or time-based keyframe
|
// In interlaced mode, only do scene change detection for even fields (field_parity = 0)
|
||||||
int is_scene_change = detect_scene_change(enc);
|
// to avoid false scene changes between fields of the same frame
|
||||||
|
int is_scene_change = 0;
|
||||||
|
if (enc->progressive_mode || field_parity == 0) {
|
||||||
|
is_scene_change = detect_scene_change(enc, field_parity);
|
||||||
|
}
|
||||||
int is_time_keyframe = (frame_num % KEYFRAME_INTERVAL) == 0;
|
int is_time_keyframe = (frame_num % KEYFRAME_INTERVAL) == 0;
|
||||||
int is_keyframe = is_time_keyframe || is_scene_change;
|
int is_keyframe = is_time_keyframe || is_scene_change;
|
||||||
|
|
||||||
@@ -1692,6 +1684,13 @@ static int encode_frame(tev_encoder_t *enc, FILE *output, int frame_num) {
|
|||||||
// No global rate control needed - per-block complexity-based control only
|
// No global rate control needed - per-block complexity-based control only
|
||||||
|
|
||||||
// Swap frame buffers for next frame
|
// Swap frame buffers for next frame
|
||||||
|
if (!enc->progressive_mode && field_parity == 0) {
|
||||||
|
// Interlaced even field: save to previous_even_field for scene change detection
|
||||||
|
size_t field_size = enc->width * enc->height * 3;
|
||||||
|
memcpy(enc->previous_even_field, enc->current_rgb, field_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal buffer swap for motion estimation
|
||||||
uint8_t *temp_rgb = enc->previous_rgb;
|
uint8_t *temp_rgb = enc->previous_rgb;
|
||||||
enc->previous_rgb = enc->current_rgb;
|
enc->previous_rgb = enc->current_rgb;
|
||||||
enc->current_rgb = temp_rgb;
|
enc->current_rgb = temp_rgb;
|
||||||
@@ -1782,7 +1781,8 @@ static int get_video_metadata(tev_encoder_t *config) {
|
|||||||
fprintf(stderr, " FPS: %d\n", config->fps);
|
fprintf(stderr, " FPS: %d\n", config->fps);
|
||||||
fprintf(stderr, " Duration: %.2fs\n", config->duration);
|
fprintf(stderr, " Duration: %.2fs\n", config->duration);
|
||||||
fprintf(stderr, " Audio: %s\n", config->has_audio ? "Yes" : "No");
|
fprintf(stderr, " Audio: %s\n", config->has_audio ? "Yes" : "No");
|
||||||
fprintf(stderr, " Resolution: %dx%d\n", config->width, config->height);
|
fprintf(stderr, " Resolution: %dx%d (%s)\n", config->width, config->height,
|
||||||
|
config->progressive_mode ? "progressive" : "interlaced");
|
||||||
|
|
||||||
return (config->total_frames > 0 && config->fps > 0);
|
return (config->total_frames > 0 && config->fps > 0);
|
||||||
}
|
}
|
||||||
@@ -1792,20 +1792,39 @@ static int start_video_conversion(tev_encoder_t *enc) {
|
|||||||
char command[2048];
|
char command[2048];
|
||||||
|
|
||||||
// Build FFmpeg command with potential frame rate conversion
|
// Build FFmpeg command with potential frame rate conversion
|
||||||
if (enc->output_fps > 0 && enc->output_fps != enc->fps) {
|
if (enc->progressive_mode) {
|
||||||
// Frame rate conversion requested
|
if (enc->output_fps > 0 && enc->output_fps != enc->fps) {
|
||||||
snprintf(command, sizeof(command),
|
// Frame rate conversion requested
|
||||||
"ffmpeg -v quiet -i \"%s\" -f rawvideo -pix_fmt rgb24 "
|
snprintf(command, sizeof(command),
|
||||||
"-vf \"fps=%d,scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d\" "
|
"ffmpeg -v error -i \"%s\" -f rawvideo -pix_fmt rgb24 "
|
||||||
"-y - 2>&1",
|
"-vf \"fps=%d,scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d\" "
|
||||||
enc->input_file, enc->output_fps, enc->width, enc->height, enc->width, enc->height);
|
"-y - 2>&1",
|
||||||
|
enc->input_file, enc->output_fps, enc->width, enc->height, enc->width, enc->height);
|
||||||
|
} else {
|
||||||
|
// No frame rate conversion
|
||||||
|
snprintf(command, sizeof(command),
|
||||||
|
"ffmpeg -v error -i \"%s\" -f rawvideo -pix_fmt rgb24 "
|
||||||
|
"-vf \"scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d\" "
|
||||||
|
"-y -",
|
||||||
|
enc->input_file, enc->width, enc->height, enc->width, enc->height);
|
||||||
|
}
|
||||||
|
// let FFmpeg handle the interlacing
|
||||||
} else {
|
} else {
|
||||||
// No frame rate conversion
|
if (enc->output_fps > 0 && enc->output_fps != enc->fps) {
|
||||||
snprintf(command, sizeof(command),
|
// Frame rate conversion requested
|
||||||
"ffmpeg -v quiet -i \"%s\" -f rawvideo -pix_fmt rgb24 "
|
snprintf(command, sizeof(command),
|
||||||
"-vf \"scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d\" "
|
"ffmpeg -v error -i \"%s\" -f rawvideo -pix_fmt rgb24 "
|
||||||
"-y -",
|
"-vf \"fps=%d,scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d,tinterlace=interleave_top,separatefields\" "
|
||||||
enc->input_file, enc->width, enc->height, enc->width, enc->height);
|
"-y - 2>&1",
|
||||||
|
enc->input_file, enc->output_fps, enc->width, enc->height * 2, enc->width, enc->height * 2);
|
||||||
|
} else {
|
||||||
|
// No frame rate conversion
|
||||||
|
snprintf(command, sizeof(command),
|
||||||
|
"ffmpeg -v error -i \"%s\" -f rawvideo -pix_fmt rgb24 "
|
||||||
|
"-vf \"scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d,tinterlace=interleave_top,separatefields\" "
|
||||||
|
"-y -",
|
||||||
|
enc->input_file, enc->width, enc->height * 2, enc->width, enc->height * 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enc->verbose) {
|
if (enc->verbose) {
|
||||||
@@ -1981,6 +2000,7 @@ static void show_usage(const char *program_name) {
|
|||||||
printf(" -q, --quality N Quality level 0-4 (default: 2, only decides audio rate in bitrate mode and quantiser mode)\n");
|
printf(" -q, --quality N Quality level 0-4 (default: 2, only decides audio rate in bitrate mode and quantiser mode)\n");
|
||||||
printf(" -Q, --quantiser N Quantiser level 0-100 (100: lossless, 0: potato)\n");
|
printf(" -Q, --quantiser N Quantiser level 0-100 (100: lossless, 0: potato)\n");
|
||||||
// printf(" -b, --bitrate N Target bitrate in kbps (enables bitrate control mode; DON'T USE - NOT WORKING AS INTENDED)\n");
|
// printf(" -b, --bitrate N Target bitrate in kbps (enables bitrate control mode; DON'T USE - NOT WORKING AS INTENDED)\n");
|
||||||
|
printf(" -p, --progressive Use progressive scan (default: interlaced)\n");
|
||||||
printf(" -v, --verbose Verbose output\n");
|
printf(" -v, --verbose Verbose output\n");
|
||||||
printf(" -t, --test Test mode: generate solid colour frames\n");
|
printf(" -t, --test Test mode: generate solid colour frames\n");
|
||||||
printf(" --help Show this help\n\n");
|
printf(" --help Show this help\n\n");
|
||||||
@@ -2062,6 +2082,7 @@ int main(int argc, char *argv[]) {
|
|||||||
{"quantiser", required_argument, 0, 'Q'},
|
{"quantiser", required_argument, 0, 'Q'},
|
||||||
{"quantizer", required_argument, 0, 'Q'},
|
{"quantizer", required_argument, 0, 'Q'},
|
||||||
{"bitrate", required_argument, 0, 'b'},
|
{"bitrate", required_argument, 0, 'b'},
|
||||||
|
{"progressive", no_argument, 0, 'p'},
|
||||||
{"verbose", no_argument, 0, 'v'},
|
{"verbose", no_argument, 0, 'v'},
|
||||||
{"test", no_argument, 0, 't'},
|
{"test", no_argument, 0, 't'},
|
||||||
{"help", no_argument, 0, '?'},
|
{"help", no_argument, 0, '?'},
|
||||||
@@ -2071,7 +2092,7 @@ int main(int argc, char *argv[]) {
|
|||||||
int option_index = 0;
|
int option_index = 0;
|
||||||
int c;
|
int c;
|
||||||
|
|
||||||
while ((c = getopt_long(argc, argv, "i:o:s:w:h:f:q:b:Q:vt", long_options, &option_index)) != -1) {
|
while ((c = getopt_long(argc, argv, "i:o:s:w:h:f:q:b:Q:pvt", long_options, &option_index)) != -1) {
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case 'i':
|
case 'i':
|
||||||
enc->input_file = strdup(optarg);
|
enc->input_file = strdup(optarg);
|
||||||
@@ -2109,6 +2130,9 @@ int main(int argc, char *argv[]) {
|
|||||||
enc->bitrate_mode = 1; // Enable bitrate control
|
enc->bitrate_mode = 1; // Enable bitrate control
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'p':
|
||||||
|
enc->progressive_mode = 1;
|
||||||
|
break;
|
||||||
case 'v':
|
case 'v':
|
||||||
enc->verbose = 1;
|
enc->verbose = 1;
|
||||||
break;
|
break;
|
||||||
@@ -2134,6 +2158,11 @@ int main(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// halve the internal representation of frame height
|
||||||
|
if (!enc->progressive_mode) {
|
||||||
|
enc->height /= 2;
|
||||||
|
}
|
||||||
|
|
||||||
if (!test_mode && (!enc->input_file || !enc->output_file)) {
|
if (!test_mode && (!enc->input_file || !enc->output_file)) {
|
||||||
fprintf(stderr, "Input and output files are required (unless using --test mode)\n");
|
fprintf(stderr, "Input and output files are required (unless using --test mode)\n");
|
||||||
show_usage(argv[0]);
|
show_usage(argv[0]);
|
||||||
@@ -2278,7 +2307,9 @@ int main(int argc, char *argv[]) {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Read RGB data directly from FFmpeg pipe
|
// Read RGB data directly from FFmpeg pipe
|
||||||
size_t rgb_size = enc->width * enc->height * 3;
|
// height-halving is already done on the encoder initialisation
|
||||||
|
int frame_height = enc->height;
|
||||||
|
size_t rgb_size = enc->width * frame_height * 3;
|
||||||
size_t bytes_read = fread(enc->current_rgb, 1, rgb_size, enc->ffmpeg_video_pipe);
|
size_t bytes_read = fread(enc->current_rgb, 1, rgb_size, enc->ffmpeg_video_pipe);
|
||||||
|
|
||||||
if (bytes_read != rgb_size) {
|
if (bytes_read != rgb_size) {
|
||||||
@@ -2293,6 +2324,10 @@ int main(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
break; // End of video or error
|
break; // End of video or error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In interlaced mode, FFmpeg separatefields filter already provides field-separated frames
|
||||||
|
// Each frame from FFmpeg is now a single field at half height
|
||||||
|
// Frame parity: even frames (0,2,4...) = bottom fields, odd frames (1,3,5...) = top fields
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process audio for this frame
|
// Process audio for this frame
|
||||||
@@ -2302,7 +2337,9 @@ int main(int argc, char *argv[]) {
|
|||||||
process_subtitles(enc, frame_count, output);
|
process_subtitles(enc, frame_count, output);
|
||||||
|
|
||||||
// Encode frame
|
// Encode frame
|
||||||
if (!encode_frame(enc, output, frame_count)) {
|
// Pass field parity for interlaced mode, -1 for progressive mode
|
||||||
|
int frame_field_parity = enc->progressive_mode ? -1 : (frame_count % 2);
|
||||||
|
if (!encode_frame(enc, output, frame_count, frame_field_parity)) {
|
||||||
fprintf(stderr, "Failed to encode frame %d\n", frame_count);
|
fprintf(stderr, "Failed to encode frame %d\n", frame_count);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user