This commit is contained in:
minjaesong
2025-09-13 22:02:56 +09:00
parent 722e8e893f
commit 712506c91c
4 changed files with 648 additions and 13 deletions

View File

@@ -0,0 +1,477 @@
// Created by Claude on 2025-09-13.
// TSVM Advanced Video (TAV) Format Decoder - DWT-based compression
// Adapted from the working playtev.js decoder
// Usage: playtav moviefile.tav [options]
// Options: -i (interactive), -debug-mv (show motion vector debug visualization)
// -deinterlace=algorithm (yadif or bwdif, default: yadif)
// -deblock (enable post-processing deblocking filter)
const WIDTH = 560
const HEIGHT = 448
const TILE_SIZE = 64 // 64x64 tiles for DWT (vs 16x16 blocks in TEV)
const TAV_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x54, 0x41, 0x56] // "\x1FTSVM TAV"
const TAV_VERSION = 1 // Initial DWT version
const SND_BASE_ADDR = audio.getBaseAddr()
const pcm = require("pcm")
const MP2_FRAME_SIZE = [144,216,252,288,360,432,504,576,720,864,1008,1152,1440,1728]
// Tile encoding modes (same as TEV block modes)
const TAV_MODE_SKIP = 0x00
const TAV_MODE_INTRA = 0x01
const TAV_MODE_INTER = 0x02
const TAV_MODE_MOTION = 0x03
// Packet types (same as TEV)
const TAV_PACKET_IFRAME = 0x10
const TAV_PACKET_PFRAME = 0x11
const TAV_PACKET_AUDIO_MP2 = 0x20
const TAV_PACKET_SUBTITLE = 0x30
const TAV_PACKET_SYNC = 0xFF
// Wavelet filter types
const WAVELET_5_3_REVERSIBLE = 0
const WAVELET_9_7_IRREVERSIBLE = 1
// Subtitle opcodes (SSF format - same as TEV)
const SSF_OP_NOP = 0x00
const SSF_OP_SHOW = 0x01
const SSF_OP_HIDE = 0x02
const SSF_OP_MOVE = 0x03
const SSF_OP_UPLOAD_LOW_FONT = 0x80
const SSF_OP_UPLOAD_HIGH_FONT = 0x81
// Subtitle state
let subtitleVisible = false
let subtitleText = ""
let subtitlePosition = 0 // 0=bottom center (default)
// Parse command line options
let interactive = false
let debugMotionVectors = false
let deinterlaceAlgorithm = "yadif"
let enableDeblocking = false // Default: disabled (use -deblock to enable)
if (exec_args.length > 2) {
for (let i = 2; i < exec_args.length; i++) {
const arg = exec_args[i].toLowerCase()
if (arg === "-i") {
interactive = true
} else if (arg === "-debug-mv") {
debugMotionVectors = true
} else if (arg === "-deblock") {
enableDeblocking = true
} else if (arg.startsWith("-deinterlace=")) {
deinterlaceAlgorithm = arg.substring(13)
}
}
}
const fullFilePath = _G.shell.resolvePathInput(exec_args[1])
const FILE_LENGTH = files.open(fullFilePath.full).size
let videoRateBin = []
let errorlevel = 0
let notifHideTimer = 0
const NOTIF_SHOWUPTIME = 3000000000
let [cy, cx] = con.getyx()
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.prepare(fullFilePathStr)
seqread.seek(0)
} else {
seqread = seqreadserial
seqread.prepare(fullFilePathStr)
}
con.clear()
con.curs_set(0)
graphics.setGraphicsMode(4) // 4096-color mode
graphics.clearPixels(0)
graphics.clearPixels2(0)
// Initialize audio
audio.resetParams(0)
audio.purgeQueue(0)
// TAV header structure (32 bytes vs TEV's 24 bytes)
let header = {
magic: new Array(8),
version: 0,
width: 0,
height: 0,
fps: 0,
totalFrames: 0,
waveletFilter: 0, // TAV-specific: wavelet filter type
decompLevels: 0, // TAV-specific: decomposition levels
qualityY: 0, // TAV-specific: Y channel quality
qualityCo: 0, // TAV-specific: Co channel quality
qualityCg: 0, // TAV-specific: Cg channel quality
extraFlags: 0,
videoFlags: 0,
reserved: new Array(7)
}
// Read and validate header
for (let i = 0; i < 8; i++) {
header.magic[i] = seqread.readOneByte()
}
// Validate magic number
let magicValid = true
for (let i = 0; i < 8; i++) {
if (header.magic[i] !== TAV_MAGIC[i]) {
magicValid = false
break
}
}
if (!magicValid) {
con.puts("Error: Invalid TAV file format")
errorlevel = 1
return
}
header.version = seqread.readOneByte()
header.width = seqread.readShort()
header.height = seqread.readShort()
header.fps = seqread.readOneByte()
header.totalFrames = seqread.readInt()
header.waveletFilter = seqread.readOneByte()
header.decompLevels = seqread.readOneByte()
header.qualityY = seqread.readOneByte()
header.qualityCo = seqread.readOneByte()
header.qualityCg = seqread.readOneByte()
header.extraFlags = seqread.readOneByte()
header.videoFlags = seqread.readOneByte()
// Skip reserved bytes
for (let i = 0; i < 7; i++) {
seqread.readOneByte()
}
if (header.version !== TAV_VERSION) {
con.puts(`Error: Unsupported TAV version ${header.version}`)
errorlevel = 1
return
}
const hasAudio = (header.extraFlags & 0x01) !== 0
const hasSubtitles = (header.extraFlags & 0x02) !== 0
const progressiveTransmission = (header.extraFlags & 0x04) !== 0
const roiCoding = (header.extraFlags & 0x08) !== 0
const isInterlaced = (header.videoFlags & 0x01) !== 0
const isNTSC = (header.videoFlags & 0x02) !== 0
const isLossless = (header.videoFlags & 0x04) !== 0
const multiResolution = (header.videoFlags & 0x08) !== 0
// Calculate tile dimensions (64x64 vs TEV's 16x16 blocks)
const tilesX = Math.ceil(header.width / TILE_SIZE)
const tilesY = Math.ceil(header.height / TILE_SIZE)
const numTiles = tilesX * tilesY
console.log(`TAV Decoder`)
console.log(`Resolution: ${header.width}x${header.height}`)
console.log(`FPS: ${header.fps}`)
console.log(`Total frames: ${header.totalFrames}`)
console.log(`Wavelet filter: ${header.waveletFilter === WAVELET_5_3_REVERSIBLE ? "5/3 reversible" : "9/7 irreversible"}`)
console.log(`Decomposition levels: ${header.decompLevels}`)
console.log(`Quality: Y=${header.qualityY}, Co=${header.qualityCo}, Cg=${header.qualityCg}`)
console.log(`Tiles: ${tilesX}x${tilesY} (${numTiles} total)`)
console.log(`Features: ${hasAudio ? "Audio " : ""}${hasSubtitles ? "Subtitles " : ""}${progressiveTransmission ? "Progressive " : ""}${roiCoding ? "ROI " : ""}`)
// Frame buffer addresses - same as TEV
const FRAME_PIXELS = header.width * header.height
const FRAME_SIZE = FRAME_PIXELS * 3 // RGB buffer size
const RGB_BUFFER_A = sys.malloc(FRAME_SIZE)
const RGB_BUFFER_B = sys.malloc(FRAME_SIZE)
// Ping-pong buffer pointers (swap instead of copy)
let CURRENT_RGB_ADDR = RGB_BUFFER_A
let PREV_RGB_ADDR = RGB_BUFFER_B
// Motion vector storage
let motionVectors = new Array(numTiles)
for (let i = 0; i < numTiles; i++) {
motionVectors[i] = { mvX: 0, mvY: 0, rcf: 1.0 }
}
// Audio state
let audioBufferBytesLastFrame = 0
let frame_cnt = 0
let frametime = 1000000000.0 / header.fps
let nextFrameTime = 0
// Performance tracking variables (from TEV)
let decompressTime = 0
let decodeTime = 0
let uploadTime = 0
let biasTime = 0
const BIAS_LIGHTING_MIN = 1.0 / 16.0
let oldBgcol = [BIAS_LIGHTING_MIN, BIAS_LIGHTING_MIN, BIAS_LIGHTING_MIN]
let notifHidden = false
function getRGBfromScr(x, y) {
let offset = y * WIDTH + x
let rg = sys.peek(-1048577 - offset)
let ba = sys.peek(-1310721 - offset)
return [(rg >>> 4) / 15.0, (rg & 15) / 15.0, (ba >>> 4) / 15.0]
}
function setBiasLighting() {
let samples = []
let nativeWidth = graphics.getPixelDimension()[0]
let nativeHeight = graphics.getPixelDimension()[1]
let width = header.width; let height = header.height
let offsetX = Math.floor((nativeWidth - width) / 2)
let offsetY = Math.floor((nativeHeight - height) / 2)
let sampleStepX = Math.max(8, Math.floor(width / 18))
let sampleStepY = Math.max(8, Math.floor(height / 17))
let borderMargin = Math.min(8, Math.floor(width / 70))
for (let x = borderMargin; x < width - borderMargin; x += sampleStepX) {
samples.push(getRGBfromScr(x + offsetX, borderMargin + offsetY))
samples.push(getRGBfromScr(x + offsetX, height - borderMargin - 1 + offsetY))
}
for (let y = borderMargin; y < height - borderMargin; y += sampleStepY) {
samples.push(getRGBfromScr(borderMargin + offsetX, y + offsetY))
samples.push(getRGBfromScr(width - borderMargin - 1 + offsetX, y + offsetY))
}
let out = [0.0, 0.0, 0.0]
samples.forEach(rgb=>{
out[0] += rgb[0]
out[1] += rgb[1]
out[2] += rgb[2]
})
out[0] = BIAS_LIGHTING_MIN + (out[0] / samples.length / 2.0)
out[1] = BIAS_LIGHTING_MIN + (out[1] / samples.length / 2.0)
out[2] = BIAS_LIGHTING_MIN + (out[2] / samples.length / 2.0)
let bgr = (oldBgcol[0]*5 + out[0]) / 6.0
let bgg = (oldBgcol[1]*5 + out[1]) / 6.0
let bgb = (oldBgcol[2]*5 + out[2]) / 6.0
oldBgcol = [bgr, bgg, bgb]
graphics.setBackground(Math.round(bgr * 255), Math.round(bgg * 255), Math.round(bgb * 255))
}
function updateDataRateBin(rate) {
videoRateBin.push(rate)
if (videoRateBin.length > header.fps) {
videoRateBin.shift()
}
}
let FRAME_TIME = 1.0 / header.fps
let frameCount = 0
let trueFrameCount = 0
let frameDuped = false
let stopPlay = false
let akku = FRAME_TIME
let akku2 = 0.0
let blockDataPtr = sys.malloc(560*448*3)
// Playback loop - properly adapted from TEV
try {
let t1 = sys.nanoTime()
while (!stopPlay && seqread.getReadCount() < FILE_LENGTH && frameCount < header.totalFrames) {
// Handle interactive controls
if (interactive) {
sys.poke(-40, 1)
if (sys.peek(-41) == 67) { // Backspace
stopPlay = true
break
}
}
if (akku >= FRAME_TIME) {
// Read packet header
const packetType = seqread.readOneByte()
if (packetType === TAV_PACKET_SYNC) {
// Sync packet - no additional data
akku -= FRAME_TIME
frameCount++
trueFrameCount++
// Swap ping-pong buffers instead of expensive memcpy (752KB copy eliminated!)
let temp = CURRENT_RGB_ADDR
CURRENT_RGB_ADDR = PREV_RGB_ADDR
PREV_RGB_ADDR = temp
} else if (packetType === TAV_PACKET_IFRAME || packetType === TAV_PACKET_PFRAME) {
// Video packet
const compressedSize = seqread.readInt()
const isKeyframe = (packetType === TAV_PACKET_IFRAME)
// Read compressed tile data
let compressedPtr = seqread.readBytes(compressedSize)
updateDataRateBin(compressedSize)
let actualSize
let decompressStart = sys.nanoTime()
try {
// Use gzip decompression (only compression format supported in TSVM JS)
actualSize = gzip.decompFromTo(compressedPtr, compressedSize, blockDataPtr)
decompressTime = (sys.nanoTime() - decompressStart) / 1000000.0
} catch (e) {
decompressTime = (sys.nanoTime() - decompressStart) / 1000000.0
console.log(`Frame ${frameCount}: Gzip decompression failed, skipping (compressed size: ${compressedSize}, error: ${e})`)
sys.free(compressedPtr)
continue
}
try {
// Duplicate every 1000th frame if NTSC (same as TEV)
if (!isNTSC || frameCount % 1000 != 501 || frameDuped) {
frameDuped = false
let decodeStart = sys.nanoTime()
// Call TAV hardware decoder (like TEV's tevDecode but with RGB buffer outputs)
graphics.tavDecode(
blockDataPtr,
CURRENT_RGB_ADDR, PREV_RGB_ADDR, // RGB buffer pointers (not float arrays!)
header.width, header.height,
header.qualityY, header.qualityCo, header.qualityCg,
frameCount,
debugMotionVectors,
header.waveletFilter, // TAV-specific parameter
header.decompLevels, // TAV-specific parameter
enableDeblocking,
isLossless
)
decodeTime = (sys.nanoTime() - decodeStart) / 1000000.0
// Upload RGB buffer to display framebuffer (like TEV)
let uploadStart = sys.nanoTime()
graphics.uploadRGBToFramebuffer(CURRENT_RGB_ADDR, header.width, header.height, frameCount, true)
uploadTime = (sys.nanoTime() - uploadStart) / 1000000.0
} else {
frameCount -= 1
frameDuped = true
console.log(`Frame ${frameCount}: Duplicating previous frame`)
}
} catch (e) {
console.log(`Frame ${frameCount}: decode failed: ${e}`)
}
sys.free(compressedPtr)
let biasStart = sys.nanoTime()
setBiasLighting()
biasTime = (sys.nanoTime() - biasStart) / 1000000.0
// Log performance data every 60 frames
if (frameCount % 60 == 0 || frameCount == 0) {
let totalTime = decompressTime + decodeTime + uploadTime + biasTime
console.log(`Frame ${frameCount}: Decompress=${decompressTime.toFixed(1)}ms, Decode=${decodeTime.toFixed(1)}ms, Upload=${uploadTime.toFixed(1)}ms, Bias=${biasTime.toFixed(1)}ms, Total=${totalTime.toFixed(1)}ms`)
}
} else if (packetType === TAV_PACKET_AUDIO_MP2 && hasAudio) {
// Audio packet - same as TEV
let audioPtr = seqread.readBytes(compressedSize)
// Send to audio hardware
for (let i = 0; i < compressedSize; i++) {
vm.poke(SND_BASE_ADDR + audioBufferBytesLastFrame + i, sys.peek(audioPtr + i))
}
audioBufferBytesLastFrame += compressedSize
sys.free(audioPtr)
} else if (packetType === TAV_PACKET_SUBTITLE && hasSubtitles) {
// Subtitle packet - same format as TEV
let subtitlePtr = seqread.readBytes(compressedSize)
// Process subtitle (simplified)
if (compressedSize >= 4) {
const index = (sys.peek(subtitlePtr) << 16) | (sys.peek(subtitlePtr + 1) << 8) | sys.peek(subtitlePtr + 2)
const opcode = sys.peek(subtitlePtr + 3)
if (opcode === SSF_OP_SHOW && compressedSize > 4) {
let text = ""
for (let i = 4; i < compressedSize && sys.peek(subtitlePtr + i) !== 0; i++) {
text += String.fromCharCode(sys.peek(subtitlePtr + i))
}
subtitleText = text
subtitleVisible = true
} else if (opcode === SSF_OP_HIDE) {
subtitleVisible = false
}
}
sys.free(subtitlePtr)
} else if (packetType == 0x00) {
// Silently discard, faulty subtitle creation can cause this as 0x00 is used as an argument terminator
} else {
println(`Unknown packet type: 0x${packetType.toString(16)}`)
break
}
}
let t2 = sys.nanoTime()
akku += (t2 - t1) / 1000000000.0
akku2 += (t2 - t1) / 1000000000.0
// Simple progress display
if (interactive) {
notifHideTimer += (t2 - t1)
if (!notifHidden && notifHideTimer > (NOTIF_SHOWUPTIME + FRAME_TIME)) {
con.move(1, 1)
print(' '.repeat(79))
notifHidden = true
}
if (notifHidden) {
con.move(31, 1)
con.color_pair(253, 0)
print(`Frame: ${frameCount}/${header.totalFrames} (${((frameCount / akku2 * 100)|0) / 100}f) `)
}
}
t1 = t2
}
}
catch (e) {
printerrln(`TAV decode error: ${e}`)
errorlevel = 1
}
finally {
// Cleanup
sys.free(blockDataPtr)
sys.free(RGB_BUFFER_A)
sys.free(RGB_BUFFER_B)
graphics.setGraphicsMode(0) // Return to text mode
con.curs_set(1)
con.clear()
if (errorlevel === 0) {
console.log(`Playback completed: ${frameCount} frames`)
} else {
console.log(`Playbook failed with error ${errorlevel}`)
}
}
graphics.setPalette(0, 0, 0, 0, 0)
con.move(cy, cx) // restore cursor
return errorlevel

View File

@@ -438,13 +438,89 @@ class VM(
(memspace as PeriBase).poke(offset, value)
}
fun peek(addr:Long): Byte? {
fun pokeShort(addr: Long, value: Short) {
val value0 = value.toByte()
val value1 = value.toInt().shr(8).toByte()
val (memspace, offset) = translateAddr(addr)
if (memspace == null)
throw ErrorIllegalAccess(this, addr)
else if (memspace is UnsafePtr) {
if (addr >= memspace.size)
throw ErrorIllegalAccess(this, addr)
else {
memspace.set(offset+0, value0)
memspace.set(offset+1, value1)
}
}
else {
(memspace as PeriBase).poke(offset+0, value0)
(memspace as PeriBase).poke(offset+1, value1)
}
}
fun pokeFloat(addr: Long, value: Float) {
val vi = value.toRawBits()
val value0 = vi.toByte()
val value1 = vi.shr(8).toByte()
val value2 = vi.shr(16).toByte()
val value3 = vi.shr(24).toByte()
val (memspace, offset) = translateAddr(addr)
if (memspace == null)
throw ErrorIllegalAccess(this, addr)
else if (memspace is UnsafePtr) {
if (addr >= memspace.size)
throw ErrorIllegalAccess(this, addr)
else {
memspace.set(offset+0, value0)
memspace.set(offset+1, value1)
memspace.set(offset+2, value2)
memspace.set(offset+3, value3)
}
}
else {
(memspace as PeriBase).poke(offset+0, value0)
(memspace as PeriBase).poke(offset+1, value1)
(memspace as PeriBase).poke(offset+2, value2)
(memspace as PeriBase).poke(offset+3, value3)
}
}
fun pokeInt(addr: Long, value: Int) {
val value0 = value.toByte()
val value1 = value.shr(8).toByte()
val value2 = value.shr(16).toByte()
val value3 = value.shr(24).toByte()
val (memspace, offset) = translateAddr(addr)
if (memspace == null)
throw ErrorIllegalAccess(this, addr)
else if (memspace is UnsafePtr) {
if (addr >= memspace.size)
throw ErrorIllegalAccess(this, addr)
else {
memspace.set(offset+0, value0)
memspace.set(offset+1, value1)
memspace.set(offset+2, value2)
memspace.set(offset+3, value3)
}
}
else {
(memspace as PeriBase).poke(offset+0, value0)
(memspace as PeriBase).poke(offset+1, value1)
(memspace as PeriBase).poke(offset+2, value2)
(memspace as PeriBase).poke(offset+3, value3)
}
}
fun peek(addr:Long): Byte {
val (memspace, offset) = translateAddr(addr)
// println("peek $addr -> ${offset}@${memspace?.javaClass?.canonicalName}")
return if (memspace == null)
null
throw NullPointerException()//null
else if (memspace is UnsafePtr) {
if (addr >= memspace.size)
throw ErrorIllegalAccess(this, addr)
@@ -452,7 +528,76 @@ class VM(
memspace.get(offset)
}
else
(memspace as PeriBase).peek(offset)
(memspace as PeriBase).peek(offset)!!
}
fun peekShort(addr: Long): Short {
val (memspace, offset) = translateAddr(addr)
return if (memspace == null)
throw NullPointerException()//null
else if (memspace is UnsafePtr) {
if (addr >= memspace.size)
throw ErrorIllegalAccess(this, addr)
else {
(memspace.get(offset+0).toUint() or
memspace.get(offset+1).toUint().shl(8)).toShort()
}
}
else {
((memspace as PeriBase).peek(offset+0)!!.toUint() or
(memspace as PeriBase).peek(offset+1)!!.toUint().shl(8)).toShort()
}
}
fun peekFloat(addr: Long): Float {
val (memspace, offset) = translateAddr(addr)
return if (memspace == null)
throw NullPointerException()//null
else if (memspace is UnsafePtr) {
if (addr >= memspace.size)
throw ErrorIllegalAccess(this, addr)
else {
Float.fromBits(memspace.get(offset+0).toUint() or
memspace.get(offset+1).toUint().shl(8) or
memspace.get(offset+2).toUint().shl(16) or
memspace.get(offset+3).toUint().shl(24)
)
}
}
else {
Float.fromBits((memspace as PeriBase).peek(offset+0)!!.toUint() or
(memspace as PeriBase).peek(offset+1)!!.toUint().shl(8) or
(memspace as PeriBase).peek(offset+2)!!.toUint().shl(16) or
(memspace as PeriBase).peek(offset+3)!!.toUint().shl(24)
)
}
}
fun peekInt(addr: Long): Int? {
val (memspace, offset) = translateAddr(addr)
return if (memspace == null)
throw NullPointerException()//null
else if (memspace is UnsafePtr) {
if (addr >= memspace.size)
throw ErrorIllegalAccess(this, addr)
else {
(memspace.get(offset+0).toUint() or
memspace.get(offset+1).toUint().shl(8) or
memspace.get(offset+2).toUint().shl(16) or
memspace.get(offset+3).toUint().shl(24)
)
}
}
else {
((memspace as PeriBase).peek(offset+0)!!.toUint() or
(memspace as PeriBase).peek(offset+1)!!.toUint().shl(8) or
(memspace as PeriBase).peek(offset+2)!!.toUint().shl(16) or
(memspace as PeriBase).peek(offset+3)!!.toUint().shl(24)
)
}
}
private fun findEmptySpace(blockSize: Int): Int? {

View File

@@ -6,16 +6,19 @@ CFLAGS = -std=c99 -Wall -Wextra -O2 -D_GNU_SOURCE
LIBS = -lm -lzstd
# Source files and targets
SOURCES = encoder_tev.c
TARGETS = encoder_tev
TARGETS = encoder_tev encoder_tav
# Build all encoders
all: $(TARGETS)
# Build main encoder
encoder_tev: encoder_tev.c
tev: encoder_tev.c
rm -f encoder_tev
$(CC) $(CFLAGS) -o $@ $< $(LIBS)
$(CC) $(CFLAGS) -o encoder_tev $< $(LIBS)
tav: encoder_tav.c
rm -f encoder_tav
$(CC) $(CFLAGS) -o encoder_tav $< $(LIBS)
# Default target
$(TARGETS): all
@@ -45,8 +48,8 @@ help:
@echo ""
@echo "Targets:"
@echo " all - Build both encoders (default)"
@echo " encoder_tev - Build the main TEV encoder"
@echo " encoder_tev_xyb - Build the XYB color space encoder"
@echo " tev - Build the main TEV encoder"
@echo " tav - Build the advanced TAV encoder"
@echo " debug - Build with debug symbols"
@echo " clean - Remove build artifacts"
@echo " install - Install to /usr/local/bin"
@@ -54,8 +57,9 @@ help:
@echo " help - Show this help"
@echo ""
@echo "Usage:"
@echo " make # Build both encoders"
@echo " ./encoder_tev input.mp4 -o output.tev"
@echo " ./encoder_tev_xyb input.mp4 -o output.tev"
@echo " make # Build both encoders"
@echo " make tev # Build TEV encoder"
@echo " make tav # Build TAV encoder"
@echo " sudo make install # Install both encoders"
.PHONY: all clean install check-deps help debug

View File

@@ -1193,6 +1193,11 @@ int main(int argc, char *argv[]) {
fprintf(stderr, "Error: Failed to compress frame %d\n", frame_count);
break;
}
else {
// Write a sync packet only after a video is been coded
uint8_t sync_packet = TAV_PACKET_SYNC;
fwrite(&sync_packet, 1, 1, enc->output_fp);
}
// Copy current frame to previous frame buffer
size_t float_frame_size = enc->width * enc->height * sizeof(float);
@@ -1213,7 +1218,11 @@ int main(int argc, char *argv[]) {
// Update actual frame count in encoder struct
enc->total_frames = frame_count;
// Write final sync packet
uint8_t sync_packet = TAV_PACKET_SYNC;
fwrite(&sync_packet, 1, 1, enc->output_fp);
// Update header with actual frame count (seek back to header position)
if (enc->output_fp != stdout) {
long current_pos = ftell(enc->output_fp);