mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-03-07 19:51:51 +09:00
NTSC framerate detection
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
// TSVM Enhanced Video (TEV) Format Decoder - YCoCg-R 4:2:0 Version
|
||||
// Usage: playtev moviefile.tev [options]
|
||||
// Options: -i (interactive), -debug-mv (show motion vector debug visualization)
|
||||
// -deinterlace=algorithm (yadif or bwdif, default: yadif)
|
||||
|
||||
const WIDTH = 560
|
||||
const HEIGHT = 448
|
||||
@@ -41,6 +42,7 @@ let subtitlePosition = 0 // 0=bottom center (default)
|
||||
|
||||
const interactive = exec_args[2] && exec_args[2].toLowerCase() == "-i"
|
||||
const debugMotionVectors = exec_args[2] && exec_args[2].toLowerCase() == "-debug-mv"
|
||||
const deinterlaceAlgorithm = "yadif"
|
||||
const fullFilePath = _G.shell.resolvePathInput(exec_args[1])
|
||||
const FILE_LENGTH = files.open(fullFilePath.full).size
|
||||
|
||||
@@ -387,12 +389,13 @@ let hasAudio = !!(flags & 1)
|
||||
let hasSubtitle = !!(flags & 2)
|
||||
let videoFlags = seqread.readOneByte()
|
||||
let isInterlaced = !!(videoFlags & 1)
|
||||
let isNTSC = !!(videoFlags & 2)
|
||||
let unused2 = seqread.readOneByte()
|
||||
|
||||
|
||||
serial.println(`Video metadata:`)
|
||||
serial.println(` Frames: ${totalFrames}`)
|
||||
serial.println(` FPS: ${fps}`)
|
||||
serial.println(` FPS: ${(isNTSC) ? (fps * 1000 / 1001) : fps}`)
|
||||
serial.println(` Duration: ${totalFrames / fps}`)
|
||||
serial.println(` Audio: ${hasAudio ? "Yes" : "No"}`)
|
||||
serial.println(` Resolution: ${width}x${height}, ${isInterlaced ? "interlaced" : "progressive"}`)
|
||||
@@ -460,6 +463,7 @@ sys.memset(DISPLAY_RG_ADDR, 0, FRAME_PIXELS) // Black in RG plane
|
||||
sys.memset(DISPLAY_BA_ADDR, 15, FRAME_PIXELS) // Black with alpha=15 (opaque) in BA plane
|
||||
|
||||
let frameCount = 0
|
||||
let trueFrameCount = 0
|
||||
let stopPlay = false
|
||||
let akku = FRAME_TIME
|
||||
let akku2 = 0.0
|
||||
@@ -537,10 +541,12 @@ function rotateFieldBuffers() {
|
||||
nextFieldAddr = temp
|
||||
}
|
||||
|
||||
let frameDuped = false
|
||||
|
||||
// Main decoding loop - simplified for performance
|
||||
try {
|
||||
let t1 = sys.nanoTime()
|
||||
while (!stopPlay && seqread.getReadCount() < FILE_LENGTH && frameCount < totalFrames) {
|
||||
while (!stopPlay && seqread.getReadCount() < FILE_LENGTH && trueFrameCount < totalFrames) {
|
||||
|
||||
// Handle interactive controls
|
||||
if (interactive) {
|
||||
@@ -560,6 +566,7 @@ try {
|
||||
|
||||
// Sync packet - frame complete
|
||||
frameCount++
|
||||
trueFrameCount++
|
||||
|
||||
// Swap ping-pong buffers instead of expensive memcpy (752KB copy eliminated!)
|
||||
let temp = CURRENT_RGB_ADDR
|
||||
@@ -603,29 +610,39 @@ try {
|
||||
|
||||
// Hardware-accelerated TEV decoding to RGB buffers (YCoCg-R or XYB based on version)
|
||||
try {
|
||||
// duplicate every 1000th frame (pass a turn every 1000n+501st) if NTSC
|
||||
if (!isInterlaced || frameCount % 1000 != 501 || frameDuped) {
|
||||
frameDuped = false
|
||||
|
||||
let decodeStart = sys.nanoTime()
|
||||
let decodingHeight = isInterlaced ? (height / 2)|0 : height
|
||||
|
||||
if (isInterlaced) {
|
||||
// For interlaced: decode current frame into currentFieldAddr
|
||||
// For display: use prevFieldAddr as current, currentFieldAddr as next
|
||||
graphics.tevDecode(blockDataPtr, nextFieldAddr, currentFieldAddr, width, decodingHeight, [qualityY, qualityCo, qualityCg], frameCount, debugMotionVectors, version)
|
||||
graphics.tevDeinterlace(frameCount, width, decodingHeight, prevFieldAddr, currentFieldAddr, nextFieldAddr, CURRENT_RGB_ADDR)
|
||||
graphics.tevDecode(blockDataPtr, nextFieldAddr, currentFieldAddr, width, decodingHeight, [qualityY, qualityCo, qualityCg], trueFrameCount, debugMotionVectors, version)
|
||||
graphics.tevDeinterlace(trueFrameCount, width, decodingHeight, prevFieldAddr, currentFieldAddr, nextFieldAddr, CURRENT_RGB_ADDR, deinterlaceAlgorithm)
|
||||
|
||||
// Rotate field buffers for next frame: NEXT -> CURRENT -> PREV
|
||||
rotateFieldBuffers()
|
||||
} else {
|
||||
// Progressive or first frame: normal decoding without temporal prediction
|
||||
graphics.tevDecode(blockDataPtr, CURRENT_RGB_ADDR, PREV_RGB_ADDR, width, decodingHeight, [qualityY, qualityCo, qualityCg], frameCount, debugMotionVectors, version)
|
||||
graphics.tevDecode(blockDataPtr, CURRENT_RGB_ADDR, PREV_RGB_ADDR, width, decodingHeight, [qualityY, qualityCo, qualityCg], trueFrameCount, debugMotionVectors, version)
|
||||
}
|
||||
|
||||
decodeTime = (sys.nanoTime() - decodeStart) / 1000000.0 // Convert to milliseconds
|
||||
|
||||
|
||||
// Upload RGB buffer to display framebuffer with dithering
|
||||
let uploadStart = sys.nanoTime()
|
||||
graphics.uploadRGBToFramebuffer(CURRENT_RGB_ADDR, width, height, frameCount)
|
||||
uploadTime = (sys.nanoTime() - uploadStart) / 1000000.0 // Convert to milliseconds
|
||||
|
||||
}
|
||||
else {
|
||||
frameCount -= 1
|
||||
frameDuped = true
|
||||
serial.println(`Frame ${frameCount}: Duplicating previous frame`)
|
||||
}
|
||||
|
||||
// Defer audio playback until a first frame is sent
|
||||
if (isInterlaced) {
|
||||
|
||||
@@ -708,6 +708,7 @@ DCT-based compression, motion compensation, and efficient temporal coding.
|
||||
- bit 1 = has subtitle
|
||||
uint8 Video Flags
|
||||
- bit 0 = is interlaced (should be default for most non-archival TEV videos)
|
||||
- bit 1 = is NTSC framerate (repeat every 1000th frame)
|
||||
uint8 Reserved, fill with zero
|
||||
|
||||
## Packet Types
|
||||
|
||||
@@ -1625,6 +1625,123 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
||||
vm.memsetI24(outputRGBAddr.toInt() + destB, col, width * 6)
|
||||
}
|
||||
|
||||
/**
|
||||
* BWDIF (Bob Weaver Deinterlacing with Interpolation and Filtering) implementation
|
||||
* Advanced motion-adaptive deinterlacing with better temporal prediction than YADIF
|
||||
*/
|
||||
fun bwdifDeinterlace(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) with loop unrolling
|
||||
vm.poke(outputRGBAddr + (outputOffset + 0) * outputIncVec, vm.peek(fieldRGBAddr + (fieldOffset + 0) * fieldIncVec)!!)
|
||||
vm.poke(outputRGBAddr + (outputOffset + 1) * outputIncVec, vm.peek(fieldRGBAddr + (fieldOffset + 1) * fieldIncVec)!!)
|
||||
vm.poke(outputRGBAddr + (outputOffset + 2) * outputIncVec, vm.peek(fieldRGBAddr + (fieldOffset + 2) * fieldIncVec)!!)
|
||||
|
||||
// Interpolate missing lines using BWDIF algorithm
|
||||
if (y > 0 && y < fieldHeight - 1) {
|
||||
val interpLine = if (fieldParity == 0) {
|
||||
y * 2 + 1 // Even field: interpolate odd progressive lines (1,3,5...)
|
||||
} else {
|
||||
y * 2 + 2 // Odd field: interpolate even progressive lines (2,4,6...)
|
||||
}
|
||||
|
||||
if (interpLine < height) {
|
||||
val interpOutputOffset = (interpLine * width + x) * 3
|
||||
|
||||
for (c in 0..2) {
|
||||
// Get spatial neighbors from sequential field data
|
||||
val fieldStride = width * 3
|
||||
val aboveOffset = fieldOffset - fieldStride + c
|
||||
val belowOffset = fieldOffset + fieldStride + c
|
||||
val currentOffset = fieldOffset + c
|
||||
|
||||
// Ensure we don't read out of bounds
|
||||
val above = if (y > 0) {
|
||||
vm.peek(fieldRGBAddr + aboveOffset * fieldIncVec)!!.toInt() and 0xFF
|
||||
} else {
|
||||
vm.peek(fieldRGBAddr + currentOffset * fieldIncVec)!!.toInt() and 0xFF
|
||||
}
|
||||
|
||||
val below = if (y < fieldHeight - 1) {
|
||||
vm.peek(fieldRGBAddr + belowOffset * fieldIncVec)!!.toInt() and 0xFF
|
||||
} else {
|
||||
vm.peek(fieldRGBAddr + currentOffset * fieldIncVec)!!.toInt() and 0xFF
|
||||
}
|
||||
|
||||
val current = vm.peek(fieldRGBAddr + currentOffset * fieldIncVec)!!.toInt() and 0xFF
|
||||
|
||||
// BWDIF temporal prediction - more sophisticated than YADIF
|
||||
var interpolatedValue = (above + below) / 2 // Default spatial interpolation
|
||||
|
||||
if (prevFieldAddr != 0L && nextFieldAddr != 0L) {
|
||||
// Get temporal neighbors
|
||||
val tempFieldOffset = (y * width + x) * 3 + c
|
||||
val prevPixel = (vm.peek(prevFieldAddr + tempFieldOffset * fieldIncVec)?.toInt() ?: current) and 0xFF
|
||||
val nextPixel = (vm.peek(nextFieldAddr + tempFieldOffset * fieldIncVec)?.toInt() ?: current) and 0xFF
|
||||
|
||||
// BWDIF-inspired temporal differences (adapted for 3-frame window)
|
||||
// Note: True BWDIF uses 5 frames, we adapt to 3-frame constraint
|
||||
|
||||
// Get spatial neighbors from previous and next fields for temporal comparison
|
||||
// Use same addressing pattern as working YADIF implementation
|
||||
val prevAboveOffset = if (y > 0) ((y-1) * width + x) * 3 + c else tempFieldOffset
|
||||
val prevBelowOffset = if (y < fieldHeight - 1) ((y+1) * width + x) * 3 + c else tempFieldOffset
|
||||
val nextAboveOffset = if (y > 0) ((y-1) * width + x) * 3 + c else tempFieldOffset
|
||||
val nextBelowOffset = if (y < fieldHeight - 1) ((y+1) * width + x) * 3 + c else tempFieldOffset
|
||||
|
||||
val prevAbove = (vm.peek(prevFieldAddr + prevAboveOffset * fieldIncVec)?.toInt() ?: above) and 0xFF
|
||||
val prevBelow = (vm.peek(prevFieldAddr + prevBelowOffset * fieldIncVec)?.toInt() ?: below) and 0xFF
|
||||
val nextAbove = (vm.peek(nextFieldAddr + nextAboveOffset * fieldIncVec)?.toInt() ?: above) and 0xFF
|
||||
val nextBelow = (vm.peek(nextFieldAddr + nextBelowOffset * fieldIncVec)?.toInt() ?: below) and 0xFF
|
||||
|
||||
// BWDIF temporal differences adapted to 3-frame window
|
||||
val temporalDiff0 = kotlin.math.abs(prevPixel - nextPixel) // Main temporal difference
|
||||
val temporalDiff1 = (kotlin.math.abs(prevAbove - above) + kotlin.math.abs(prevBelow - below)) / 2 // Previous frame spatial consistency
|
||||
val temporalDiff2 = (kotlin.math.abs(nextAbove - above) + kotlin.math.abs(nextBelow - below)) / 2 // Next frame spatial consistency
|
||||
val maxTemporalDiff = kotlin.math.max(kotlin.math.max(temporalDiff0 / 2, temporalDiff1), temporalDiff2)
|
||||
|
||||
val spatialDiff = kotlin.math.abs(above - below)
|
||||
|
||||
if (maxTemporalDiff > 16) { // Conservative threshold
|
||||
val temporalInterp = (prevPixel + nextPixel) / 2
|
||||
val spatialInterp = (above + below) / 2
|
||||
|
||||
// BWDIF-style decision making
|
||||
interpolatedValue = if (spatialDiff < maxTemporalDiff) {
|
||||
temporalInterp // Trust temporal when spatial is stable
|
||||
} else {
|
||||
spatialInterp // Trust spatial when temporal is unreliable
|
||||
}
|
||||
} else {
|
||||
// Low temporal variation: use spatial like YADIF
|
||||
interpolatedValue = (above + below) / 2
|
||||
}
|
||||
}
|
||||
|
||||
vm.poke(outputRGBAddr + (interpOutputOffset + c) * outputIncVec,
|
||||
interpolatedValue.coerceIn(0, 255).toByte())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cover up border lines like YADIF
|
||||
val destT = 0
|
||||
val destB = (height - 2) * width * 3
|
||||
val col = (vm.peek(-1299457)!!.toUint() shl 16) or (vm.peek(-1299458)!!.toUint() shl 8) or vm.peek(-1299459)!!.toUint()
|
||||
vm.memsetI24(outputRGBAddr.toInt() + destT, col, width * 6)
|
||||
vm.memsetI24(outputRGBAddr.toInt() + destB, col, width * 6)
|
||||
}
|
||||
|
||||
fun tevYcocgToRGB(yBlock: IntArray, coBlock: IntArray, cgBlock: IntArray): IntArray {
|
||||
val rgbData = IntArray(16 * 16 * 3) // R,G,B for 16x16 pixels
|
||||
|
||||
@@ -2236,17 +2353,37 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
||||
}
|
||||
}
|
||||
|
||||
fun tevDeinterlace(frameCounter: Int, width: Int, height: Int, prevField: Long, currentField: Long, nextField: Long, outputRGB: Long) {
|
||||
// Apply Yadif deinterlacing: field -> progressive frame
|
||||
fun tevDeinterlace(frameCounter: Int, width: Int, height: Int, prevField: Long, currentField: Long, nextField: Long, outputRGB: Long, algorithm: String = "yadif") {
|
||||
// Apply selected deinterlacing algorithm: field -> progressive frame
|
||||
val fieldParity = (frameCounter + 1) % 2
|
||||
|
||||
yadifDeinterlace(
|
||||
when (algorithm.lowercase()) {
|
||||
"bwdif" -> {
|
||||
bwdifDeinterlace(
|
||||
currentField, outputRGB, width, height * 2,
|
||||
prevField, nextField, // Now we have next field for temporal prediction!
|
||||
prevField, nextField,
|
||||
fieldParity,
|
||||
1, 1
|
||||
)
|
||||
|
||||
}
|
||||
"yadif", "" -> {
|
||||
yadifDeinterlace(
|
||||
currentField, outputRGB, width, height * 2,
|
||||
prevField, nextField,
|
||||
fieldParity,
|
||||
1, 1
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
// Default to YADIF for unknown algorithms
|
||||
yadifDeinterlace(
|
||||
currentField, outputRGB, width, height * 2,
|
||||
prevField, nextField,
|
||||
fieldParity,
|
||||
1, 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -162,6 +162,7 @@ typedef struct {
|
||||
int has_subtitles;
|
||||
int output_to_stdout;
|
||||
int progressive_mode; // 0 = interlaced (default), 1 = progressive
|
||||
int is_ntsc_framerate; // 1 if framerate denominator is 1001, 0 otherwise
|
||||
int qualityIndex; // -q option
|
||||
int qualityY;
|
||||
int qualityCo;
|
||||
@@ -1418,6 +1419,7 @@ static tev_encoder_t* init_encoder(void) {
|
||||
enc->height = DEFAULT_HEIGHT;
|
||||
enc->fps = 0; // Will be detected from input
|
||||
enc->output_fps = 0; // No frame rate conversion by default
|
||||
enc->is_ntsc_framerate = 0; // Will be detected from input
|
||||
enc->verbose = 0;
|
||||
enc->subtitle_file = NULL;
|
||||
enc->has_subtitles = 0;
|
||||
@@ -1531,7 +1533,7 @@ static int write_tev_header(FILE *output, tev_encoder_t *enc) {
|
||||
uint8_t qualityCo = enc->qualityCo;
|
||||
uint8_t qualityCg = enc->qualityCg;
|
||||
uint8_t flags = (enc->has_audio) | (enc->has_subtitles << 1);
|
||||
uint8_t video_flags = enc->progressive_mode ? 0 : 1; // bit 0 = is_interlaced (inverted from progressive)
|
||||
uint8_t video_flags = (enc->progressive_mode ? 0 : 1) | (enc->is_ntsc_framerate ? 2 : 0); // bit 0 = is_interlaced, bit 1 = is_ntsc_framerate
|
||||
uint8_t reserved = 0;
|
||||
|
||||
fwrite(&width, 2, 1, output);
|
||||
@@ -1741,7 +1743,7 @@ static int get_video_metadata(tev_encoder_t *config) {
|
||||
|
||||
while (line && line_num < 2) {
|
||||
switch (line_num) {
|
||||
case 0: // Line format: "framerate,framecount" (e.g., "24000/1001,4423")
|
||||
case 0: // Line format: "framerate,framecount" (e.g., "30000/1001,4423")
|
||||
{
|
||||
char *comma = strchr(line, ',');
|
||||
if (comma) {
|
||||
@@ -1750,8 +1752,10 @@ static int get_video_metadata(tev_encoder_t *config) {
|
||||
int num, den;
|
||||
if (sscanf(line, "%d/%d", &num, &den) == 2) {
|
||||
config->fps = (den > 0) ? (int)round((float)num/(float)den) : 30;
|
||||
config->is_ntsc_framerate = (den == 1001) ? 1 : 0;
|
||||
} else {
|
||||
config->fps = (int)round(atof(line));
|
||||
config->is_ntsc_framerate = 0;
|
||||
}
|
||||
// Parse frame count (second part)
|
||||
config->total_frames = atoi(comma + 1);
|
||||
@@ -1778,7 +1782,11 @@ static int get_video_metadata(tev_encoder_t *config) {
|
||||
|
||||
fprintf(stderr, "Video metadata:\n");
|
||||
fprintf(stderr, " Frames: %d\n", config->total_frames);
|
||||
if (config->is_ntsc_framerate) {
|
||||
fprintf(stderr, " FPS: %.2f\n", config->fps * 1000.f / 1001.f);
|
||||
} else {
|
||||
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 (%s)\n", config->width, config->height,
|
||||
|
||||
Reference in New Issue
Block a user