mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-03-07 19:51:51 +09:00
subtitle wip
This commit is contained in:
@@ -67,6 +67,203 @@ audio.purgeQueue(0)
|
||||
audio.setPcmMode(0)
|
||||
audio.setMasterVolume(0, 255)
|
||||
|
||||
// Subtitle display functions
|
||||
function clearSubtitleArea() {
|
||||
// Clear the subtitle area at the bottom of the screen
|
||||
// Text mode is 80x32, so clear the bottom few lines
|
||||
let oldFgColor = con.get_color_fore()
|
||||
let oldBgColor = con.get_color_back()
|
||||
|
||||
con.color_pair(255, 255) // transparent to clear
|
||||
|
||||
// Clear bottom 4 lines for subtitles
|
||||
for (let row = 29; row <= 32; row++) {
|
||||
con.move(row, 1)
|
||||
for (let col = 1; col <= 80; col++) {
|
||||
print(" ")
|
||||
}
|
||||
}
|
||||
|
||||
con.color_pair(oldFgColor, oldBgColor)
|
||||
}
|
||||
|
||||
function displaySubtitle(text, position = 0) {
|
||||
if (!text || text.length === 0) {
|
||||
clearSubtitleArea()
|
||||
return
|
||||
}
|
||||
|
||||
// Set subtitle colors: yellow (230) on black (0)
|
||||
let oldFgColor = con.get_color_fore()
|
||||
let oldBgColor = con.get_color_back()
|
||||
con.color_pair_pair(230, 0)
|
||||
|
||||
// Split text into lines
|
||||
let lines = text.split('\n')
|
||||
|
||||
// Calculate position based on subtitle position setting
|
||||
let startRow, startCol
|
||||
|
||||
switch (position) {
|
||||
case 0: // bottom center
|
||||
startRow = 32 - lines.length + 1
|
||||
break
|
||||
case 1: // bottom left
|
||||
startRow = 32 - lines.length + 1
|
||||
break
|
||||
case 2: // center left
|
||||
startRow = 16 - Math.floor(lines.length / 2)
|
||||
break
|
||||
case 3: // top left
|
||||
startRow = 2
|
||||
break
|
||||
case 4: // top center
|
||||
startRow = 2
|
||||
break
|
||||
case 5: // top right
|
||||
startRow = 2
|
||||
break
|
||||
case 6: // center right
|
||||
startRow = 16 - Math.floor(lines.length / 2)
|
||||
break
|
||||
case 7: // bottom right
|
||||
startRow = 32 - lines.length + 1
|
||||
break
|
||||
default:
|
||||
startRow = 32 - lines.length + 1 // Default to bottom center
|
||||
}
|
||||
|
||||
// Display each line
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i].trim()
|
||||
if (line.length === 0) continue
|
||||
|
||||
let row = startRow + i
|
||||
if (row < 1) row = 1
|
||||
if (row > 32) row = 32
|
||||
|
||||
// Calculate column based on alignment
|
||||
switch (position) {
|
||||
case 0: // bottom center
|
||||
case 4: // top center
|
||||
startCol = Math.max(1, Math.floor((80 - line.length) / 2) + 1)
|
||||
break
|
||||
case 1: // bottom left
|
||||
case 2: // center left
|
||||
case 3: // top left
|
||||
startCol = 2
|
||||
break
|
||||
case 5: // top right
|
||||
case 6: // center right
|
||||
case 7: // bottom right
|
||||
startCol = Math.max(1, 80 - line.length)
|
||||
break
|
||||
default:
|
||||
startCol = Math.max(1, Math.floor((80 - line.length) / 2) + 1)
|
||||
}
|
||||
|
||||
con.move(row, startCol)
|
||||
print(line) // Unicode-capable print function
|
||||
}
|
||||
|
||||
con.color_pair(oldFgColor, oldBgColor)
|
||||
}
|
||||
|
||||
function processSubtitlePacket(packetSize) {
|
||||
// Read subtitle packet data according to SSF format
|
||||
// uint24 index + uint8 opcode + variable arguments
|
||||
|
||||
let index = 0
|
||||
// Read 24-bit index (little-endian)
|
||||
let indexByte0 = seqread.readOneByte()
|
||||
let indexByte1 = seqread.readOneByte()
|
||||
let indexByte2 = seqread.readOneByte()
|
||||
index = indexByte0 | (indexByte1 << 8) | (indexByte2 << 16)
|
||||
|
||||
let opcode = seqread.readOneByte()
|
||||
let remainingBytes = packetSize - 4 // Subtract 3 bytes for index + 1 byte for opcode
|
||||
|
||||
switch (opcode) {
|
||||
case SSF_OP_SHOW: {
|
||||
// Read UTF-8 text until null terminator
|
||||
if (remainingBytes > 1) {
|
||||
let textBytes = seqread.readBytes(remainingBytes)
|
||||
let textStr = ""
|
||||
|
||||
// Convert bytes to string, stopping at null terminator
|
||||
for (let i = 0; i < remainingBytes - 1; i++) { // -1 for null terminator
|
||||
let byte = sys.peek(textBytes + i)
|
||||
if (byte === 0) break
|
||||
textStr += String.fromCharCode(byte)
|
||||
}
|
||||
|
||||
sys.free(textBytes)
|
||||
subtitleText = textStr
|
||||
subtitleVisible = true
|
||||
displaySubtitle(subtitleText, subtitlePosition)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case SSF_OP_HIDE: {
|
||||
subtitleVisible = false
|
||||
subtitleText = ""
|
||||
clearSubtitleArea()
|
||||
break
|
||||
}
|
||||
|
||||
case SSF_OP_MOVE: {
|
||||
if (remainingBytes >= 2) { // Need at least 1 byte for position + 1 null terminator
|
||||
let newPosition = seqread.readOneByte()
|
||||
seqread.readOneByte() // Read null terminator
|
||||
|
||||
if (newPosition >= 0 && newPosition <= 7) {
|
||||
subtitlePosition = newPosition
|
||||
|
||||
// Re-display current subtitle at new position if visible
|
||||
if (subtitleVisible && subtitleText.length > 0) {
|
||||
clearSubtitleArea()
|
||||
displaySubtitle(subtitleText, subtitlePosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case SSF_OP_UPLOAD_LOW_FONT:
|
||||
case SSF_OP_UPLOAD_HIGH_FONT: {
|
||||
// Font upload - read payload length and font data
|
||||
if (remainingBytes >= 3) { // uint16 length + at least 1 byte data
|
||||
let payloadLen = seqread.readShort()
|
||||
if (remainingBytes >= payloadLen + 2) {
|
||||
let fontData = seqread.readBytes(payloadLen)
|
||||
|
||||
// upload font data
|
||||
for (let i = 0; i < Math.min(payloadLen, 1920); i++) sys.poke(-1300607 - i, sys.peek(fontData + i))
|
||||
sys.poke(-1299460, (opcode == SSF_OP_UPLOAD_LOW_FONT) ? 18 : 19)
|
||||
|
||||
sys.free(fontData)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case SSF_OP_NOP:
|
||||
default: {
|
||||
// Skip remaining bytes
|
||||
if (remainingBytes > 0) {
|
||||
let skipBytes = seqread.readBytes(remainingBytes)
|
||||
sys.free(skipBytes)
|
||||
}
|
||||
|
||||
if (interactive && opcode !== SSF_OP_NOP) {
|
||||
serial.println(`[SUBTITLE UNKNOWN] Index: ${index}, Opcode: 0x${opcode.toString(16).padStart(2, '0')}`)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check magic number
|
||||
let magic = seqread.readBytes(8)
|
||||
let magicMatching = true
|
||||
@@ -124,18 +321,17 @@ const DISPLAY_BA_ADDR = -1310721 // Main graphics BA plane (displayed)
|
||||
// RGB frame buffers (24-bit: R,G,B per pixel)
|
||||
const FRAME_SIZE = 560*448*3 // Total frame size = 752,640 bytes
|
||||
|
||||
// Allocate frame buffers - malloc works correctly, addresses are start addresses
|
||||
const CURRENT_RGB_ADDR = sys.malloc(FRAME_SIZE)
|
||||
const PREV_RGB_ADDR = sys.malloc(FRAME_SIZE)
|
||||
// Ping-pong frame buffers to eliminate memcpy overhead
|
||||
const RGB_BUFFER_A = sys.malloc(FRAME_SIZE)
|
||||
const RGB_BUFFER_B = sys.malloc(FRAME_SIZE)
|
||||
|
||||
|
||||
// Working memory for blocks (minimal allocation)
|
||||
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)
|
||||
// Ping-pong buffer pointers (swap instead of copy)
|
||||
let CURRENT_RGB_ADDR = RGB_BUFFER_A
|
||||
let PREV_RGB_ADDR = RGB_BUFFER_B
|
||||
|
||||
// Initialize RGB frame buffers to black (0,0,0)
|
||||
sys.memset(CURRENT_RGB_ADDR, 0, FRAME_PIXELS * 3)
|
||||
sys.memset(PREV_RGB_ADDR, 0, FRAME_PIXELS * 3)
|
||||
sys.memset(RGB_BUFFER_A, 0, FRAME_PIXELS * 3)
|
||||
sys.memset(RGB_BUFFER_B, 0, FRAME_PIXELS * 3)
|
||||
|
||||
// Initialize display framebuffer to black
|
||||
sys.memset(DISPLAY_RG_ADDR, 0, FRAME_PIXELS) // Black in RG plane
|
||||
@@ -148,31 +344,15 @@ let akku2 = 0.0
|
||||
let mp2Initialised = false
|
||||
let audioFired = false
|
||||
|
||||
// Performance tracking variables
|
||||
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]
|
||||
|
||||
// 4x4 Bayer dithering matrix
|
||||
const BAYER_MATRIX = [
|
||||
[ 0, 8, 2,10],
|
||||
[12, 4,14, 6],
|
||||
[ 3,11, 1, 9],
|
||||
[15, 7,13, 5]
|
||||
]
|
||||
|
||||
|
||||
// Apply Bayer dithering to reduce banding when quantizing to 4-bit
|
||||
function ditherValue(value, x, y) {
|
||||
// Get the dither threshold for this pixel position
|
||||
const threshold = BAYER_MATRIX[y & 3][x & 3]
|
||||
|
||||
// Scale threshold from 0-15 to 0-15.9375 (16 steps over 16 values)
|
||||
const scaledThreshold = threshold / 16.0
|
||||
|
||||
// Add dither and quantize to 4-bit (0-15)
|
||||
const dithered = value + scaledThreshold
|
||||
return Math.max(0, Math.min(15, Math.floor(dithered * 15 / 255)))
|
||||
}
|
||||
|
||||
function getRGBfromScr(x, y) {
|
||||
let offset = y * WIDTH + x
|
||||
let rg = sys.peek(-1048577 - offset)
|
||||
@@ -237,9 +417,10 @@ try {
|
||||
// Sync packet - frame complete
|
||||
frameCount++
|
||||
|
||||
// Copy current RGB frame to previous frame buffer for next frame reference
|
||||
// memcpy(source, destination, length) - so CURRENT (source) -> PREV (destination)
|
||||
sys.memcpy(CURRENT_RGB_ADDR, PREV_RGB_ADDR, FRAME_PIXELS * 3)
|
||||
// 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 == TEV_PACKET_IFRAME || packetType == TEV_PACKET_PFRAME) {
|
||||
// Video frame packet (always includes rate control factor)
|
||||
@@ -275,11 +456,14 @@ try {
|
||||
let decompressedSize = Math.max(payloadLen * 4, blocksX * blocksY * tevBlockSize) // More efficient sizing
|
||||
|
||||
let actualSize
|
||||
let decompressStart = sys.nanoTime()
|
||||
try {
|
||||
// Use gzip decompression (only compression format supported in TSVM JS)
|
||||
actualSize = gzip.decompFromTo(compressedPtr, payloadLen, blockDataPtr)
|
||||
decompressTime = (sys.nanoTime() - decompressStart) / 1000000.0 // Convert to milliseconds
|
||||
} catch (e) {
|
||||
// Decompression failed - skip this frame
|
||||
decompressTime = (sys.nanoTime() - decompressStart) / 1000000.0 // Still measure time
|
||||
serial.println(`Frame ${frameCount}: Gzip decompression failed, skipping (compressed size: ${payloadLen}, error: ${e})`)
|
||||
sys.free(compressedPtr)
|
||||
continue
|
||||
@@ -287,11 +471,15 @@ try {
|
||||
|
||||
// Hardware-accelerated TEV YCoCg-R decoding to RGB buffers (with rate control factor)
|
||||
try {
|
||||
let decodeStart = sys.nanoTime()
|
||||
graphics.tevDecode(blockDataPtr, CURRENT_RGB_ADDR, PREV_RGB_ADDR, width, height, quality, debugMotionVectors, rateControlFactor)
|
||||
decodeTime = (sys.nanoTime() - decodeStart) / 1000000.0 // Convert to milliseconds
|
||||
|
||||
// Upload RGB buffer to display framebuffer with dithering
|
||||
graphics.uploadRGBToFramebuffer(CURRENT_RGB_ADDR, DISPLAY_RG_ADDR, DISPLAY_BA_ADDR,
|
||||
width, height, frameCount)
|
||||
let uploadStart = sys.nanoTime()
|
||||
graphics.uploadRGBToFramebuffer(CURRENT_RGB_ADDR, width, height, frameCount)
|
||||
uploadTime = (sys.nanoTime() - uploadStart) / 1000000.0 // Convert to milliseconds
|
||||
|
||||
|
||||
// Defer audio playback until a first frame is sent
|
||||
if (!audioFired) {
|
||||
@@ -304,7 +492,15 @@ try {
|
||||
|
||||
sys.free(compressedPtr)
|
||||
|
||||
let biasStart = sys.nanoTime()
|
||||
setBiasLighting()
|
||||
biasTime = (sys.nanoTime() - biasStart) / 1000000.0 // Convert to milliseconds
|
||||
|
||||
// Log performance data every 60 frames (and also frame 0 for debugging)
|
||||
if (frameCount % 60 == 0 || frameCount == 0) {
|
||||
let totalTime = decompressTime + decodeTime + uploadTime + biasTime
|
||||
serial.println(`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 == TEV_PACKET_AUDIO_MP2) {
|
||||
// MP2 Audio packet
|
||||
@@ -319,6 +515,10 @@ try {
|
||||
audio.mp2Decode()
|
||||
audio.mp2UploadDecoded(0)
|
||||
|
||||
} else if (packetType == TEV_PACKET_SUBTITLE) {
|
||||
// Subtitle packet - NEW!
|
||||
let packetSize = seqread.readInt()
|
||||
processSubtitlePacket(packetSize)
|
||||
} else {
|
||||
println(`Unknown packet type: 0x${packetType.toString(16)}`)
|
||||
break
|
||||
@@ -349,11 +549,9 @@ catch (e) {
|
||||
}
|
||||
finally {
|
||||
// Cleanup working memory (graphics memory is automatically managed)
|
||||
sys.free(ycocgWorkspace)
|
||||
sys.free(dctWorkspace)
|
||||
sys.free(blockDataPtr)
|
||||
if (CURRENT_RGB_ADDR > 0) sys.free(CURRENT_RGB_ADDR)
|
||||
if (PREV_RGB_ADDR > 0) sys.free(PREV_RGB_ADDR)
|
||||
if (RGB_BUFFER_A > 0) sys.free(RGB_BUFFER_A)
|
||||
if (RGB_BUFFER_B > 0) sys.free(RGB_BUFFER_B)
|
||||
|
||||
audio.stop(0)
|
||||
audio.purgeQueue(0)
|
||||
|
||||
@@ -707,6 +707,7 @@ DCT-based compression, motion compensation, and efficient temporal coding.
|
||||
0x10: I-frame (intra-coded frame)
|
||||
0x11: P-frame (predicted frame)
|
||||
0x20: MP2 audio packet
|
||||
0x30: Subtitle in "Simple" format
|
||||
0xFF: sync packet
|
||||
|
||||
## Video Packet Structure
|
||||
@@ -767,6 +768,22 @@ to larger block sizes and hardware acceleration.
|
||||
Reuses existing MP2 audio infrastructure from TSVM MOV format for seamless
|
||||
compatibility with existing audio processing pipeline.
|
||||
|
||||
## Simple Subtitle Format
|
||||
SSF is a simple subtitle that is intended to use text buffer to display texts.
|
||||
The format is designed to be compatible with SubRip and SAMI (without markups).
|
||||
|
||||
### SSF Packet Structure
|
||||
uint24 index (used to specify target subtitle object)
|
||||
uint8 opcode
|
||||
0x00 = <argument terminator>, is NOP when used here
|
||||
0x01 = show (arguments: UTF-8 text)
|
||||
0x02 = hide (arguments: none)
|
||||
0x10 = move to different nonant (arguments: 0x00-bottom centre; 0x01-bottom left; 0x02-centre left; 0x03-top left; 0x04-top centre; 0x05-top right; 0x06-centre right; 0x07-bottom right; 0x08-centre
|
||||
0x30 = upload to low font rom (arguments: uint16 payload length, var bytes)
|
||||
0x31 = upload to high font rom (arguments: uint16 payload length, var bytes)
|
||||
note: changing the font rom will change the appearance of the every subtitle currently being displayed
|
||||
* arguments separated AND terminated by 0x00
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Sound Adapter
|
||||
|
||||
@@ -17,6 +17,7 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
||||
private val idctTempBuffer = FloatArray(64)
|
||||
private val idct16TempBuffer = FloatArray(256) // For 16x16 IDCT
|
||||
private val idct16SeparableBuffer = FloatArray(256) // For separable 16x16 IDCT
|
||||
|
||||
|
||||
private fun getFirstGPU(): GraphicsAdapter? {
|
||||
return vm.findPeribyType(VM.PERITYPE_GPU_AND_TERM)?.peripheral as? GraphicsAdapter
|
||||
@@ -483,6 +484,33 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
||||
)
|
||||
).map{ it.map { (it.toFloat() + 0.5f) / 16f }.toFloatArray() }
|
||||
|
||||
private val bayerKernels2 = arrayOf(
|
||||
intArrayOf(
|
||||
0,8,2,10,
|
||||
12,4,14,6,
|
||||
3,11,1,9,
|
||||
15,7,13,5,
|
||||
),
|
||||
intArrayOf(
|
||||
8,2,10,0,
|
||||
4,14,6,12,
|
||||
11,1,9,3,
|
||||
7,13,5,15,
|
||||
),
|
||||
intArrayOf(
|
||||
7,13,5,15,
|
||||
8,2,10,0,
|
||||
4,14,6,12,
|
||||
11,1,9,3,
|
||||
),
|
||||
intArrayOf(
|
||||
15,7,13,5,
|
||||
0,8,2,10,
|
||||
12,4,14,6,
|
||||
3,11,1,9,
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* This method always assume that you're using the default palette
|
||||
*
|
||||
@@ -1307,22 +1335,35 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
||||
/**
|
||||
* Upload RGB frame buffer to graphics framebuffer with dithering
|
||||
* @param rgbAddr Source RGB buffer (24-bit: R,G,B bytes)
|
||||
* @param rgPlaneAddr Destination RG framebuffer
|
||||
* @param baPlaneAddr Destination BA framebuffer
|
||||
* @param width Frame width
|
||||
* @param height Frame height
|
||||
*/
|
||||
fun uploadRGBToFramebuffer(rgbAddr: Long, rgPlaneAddr: Long, baPlaneAddr: Long, width: Int, height: Int, frameCounter: Int) {
|
||||
val rgAddrIncVec = if (rgPlaneAddr >= 0) 1 else -1
|
||||
val baAddrIncVec = if (baPlaneAddr >= 0) 1 else -1
|
||||
fun uploadRGBToFramebuffer(rgbAddr: Long, width: Int, height: Int, frameCounter: Int) {
|
||||
val gpu = (vm.peripheralTable[1].peripheral as GraphicsAdapter)
|
||||
|
||||
val rgbAddrIncVec = if (rgbAddr >= 0) 1 else -1
|
||||
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
val pixelOffset = y.toLong() * width + x
|
||||
val rgbOffset = pixelOffset * 3 * rgbAddrIncVec
|
||||
val totalPixels = width * height
|
||||
|
||||
// Process in 8KB chunks to balance memory usage and performance
|
||||
val chunkSize = 8192
|
||||
val rgChunk = ByteArray(chunkSize)
|
||||
val baChunk = ByteArray(chunkSize)
|
||||
|
||||
var pixelsProcessed = 0
|
||||
|
||||
while (pixelsProcessed < totalPixels) {
|
||||
val pixelsInChunk = kotlin.math.min(chunkSize, totalPixels - pixelsProcessed)
|
||||
|
||||
// Batch process chunk of pixels
|
||||
for (i in 0 until pixelsInChunk) {
|
||||
val pixelIndex = pixelsProcessed + i
|
||||
val y = pixelIndex / width
|
||||
val x = pixelIndex % width
|
||||
|
||||
val rgbOffset = (pixelIndex.toLong() * 3) * rgbAddrIncVec
|
||||
|
||||
// Read RGB values
|
||||
// Read RGB values (3 peek operations per pixel - still the bottleneck)
|
||||
val r = vm.peek(rgbAddr + rgbOffset)!!.toUint()
|
||||
val g = vm.peek(rgbAddr + rgbOffset + rgbAddrIncVec)!!.toUint()
|
||||
val b = vm.peek(rgbAddr + rgbOffset + rgbAddrIncVec * 2)!!.toUint()
|
||||
@@ -1332,14 +1373,24 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
||||
val g4 = ditherValue(g, x, y, frameCounter)
|
||||
val b4 = ditherValue(b, x, y, frameCounter)
|
||||
|
||||
// Pack into 4096-color format
|
||||
val rgValue = (r4 shl 4) or g4 // R in MSB, G in LSB
|
||||
val baValue = (b4 shl 4) or 15 // B in MSB, A=15 (opaque) in LSB
|
||||
|
||||
// Write to framebuffer
|
||||
vm.poke(rgPlaneAddr + pixelOffset * rgAddrIncVec, rgValue.toByte())
|
||||
vm.poke(baPlaneAddr + pixelOffset * baAddrIncVec, baValue.toByte())
|
||||
// Pack and store in chunk buffers
|
||||
rgChunk[i] = ((r4 shl 4) or g4).toByte()
|
||||
baChunk[i] = ((b4 shl 4) or 15).toByte()
|
||||
}
|
||||
|
||||
// Batch write entire chunk to framebuffer
|
||||
val pixelOffset = (pixelsProcessed).toLong()
|
||||
|
||||
gpu.let {
|
||||
UnsafeHelper.memcpyRaw(
|
||||
rgChunk, UnsafeHelper.getArrayOffset(rgChunk),
|
||||
null, it.framebuffer.ptr + pixelOffset, pixelsInChunk.toLong())
|
||||
UnsafeHelper.memcpyRaw(
|
||||
baChunk, UnsafeHelper.getArrayOffset(baChunk),
|
||||
null, it.framebuffer2!!.ptr + pixelOffset, pixelsInChunk.toLong())
|
||||
}
|
||||
|
||||
pixelsProcessed += pixelsInChunk
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1356,6 +1407,7 @@ class GraphicsJSR223Delegate(private val vm: VM) {
|
||||
return round(15f * q)
|
||||
}
|
||||
|
||||
|
||||
val dctBasis8 = Array(8) { u ->
|
||||
FloatArray(8) { x ->
|
||||
val cu = if (u == 0) 1.0 / sqrt(2.0) else 1.0
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#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_SUBTITLE 0x30 // Subtitle packet
|
||||
#define TEV_PACKET_SYNC 0xFF // Sync packet
|
||||
|
||||
// Utility macros
|
||||
@@ -100,9 +101,18 @@ typedef struct __attribute__((packed)) {
|
||||
int16_t cg_coeffs[64]; // quantised Cg DCT coefficients (8x8)
|
||||
} tev_block_t;
|
||||
|
||||
// Subtitle entry structure
|
||||
typedef struct subtitle_entry {
|
||||
int start_frame;
|
||||
int end_frame;
|
||||
char *text;
|
||||
struct subtitle_entry *next;
|
||||
} subtitle_entry_t;
|
||||
|
||||
typedef struct {
|
||||
char *input_file;
|
||||
char *output_file;
|
||||
char *subtitle_file; // SubRip (.srt) file path
|
||||
int width;
|
||||
int height;
|
||||
int fps;
|
||||
@@ -110,6 +120,7 @@ typedef struct {
|
||||
int total_frames;
|
||||
double duration;
|
||||
int has_audio;
|
||||
int has_subtitles;
|
||||
int output_to_stdout;
|
||||
int quality; // 0-4, higher = better quality
|
||||
int verbose;
|
||||
@@ -156,6 +167,10 @@ typedef struct {
|
||||
float complexity_history[60]; // Rolling window for complexity
|
||||
int complexity_history_index;
|
||||
float average_complexity;
|
||||
|
||||
// Subtitle handling
|
||||
subtitle_entry_t *subtitle_list;
|
||||
subtitle_entry_t *current_subtitle;
|
||||
} tev_encoder_t;
|
||||
|
||||
// RGB to YCoCg-R transform (per YCoCg-R specification with truncated division)
|
||||
@@ -820,6 +835,223 @@ static void encode_block(tev_encoder_t *enc, int block_x, int block_y, int is_ke
|
||||
block->cbp = 0x07; // Y, Co, Cg all present
|
||||
}
|
||||
|
||||
// Convert SubRip time format (HH:MM:SS,mmm) to frame number
|
||||
static int srt_time_to_frame(const char *time_str, int fps) {
|
||||
int hours, minutes, seconds, milliseconds;
|
||||
if (sscanf(time_str, "%d:%d:%d,%d", &hours, &minutes, &seconds, &milliseconds) != 4) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
double total_seconds = hours * 3600.0 + minutes * 60.0 + seconds + milliseconds / 1000.0;
|
||||
return (int)(total_seconds * fps + 0.5); // Round to nearest frame
|
||||
}
|
||||
|
||||
// Parse SubRip subtitle file
|
||||
static subtitle_entry_t* parse_srt_file(const char *filename, int fps) {
|
||||
FILE *file = fopen(filename, "r");
|
||||
if (!file) {
|
||||
fprintf(stderr, "Failed to open subtitle file: %s\n", filename);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
subtitle_entry_t *head = NULL;
|
||||
subtitle_entry_t *tail = NULL;
|
||||
char line[1024];
|
||||
int state = 0; // 0=index, 1=time, 2=text, 3=blank
|
||||
|
||||
subtitle_entry_t *current_entry = NULL;
|
||||
char *text_buffer = NULL;
|
||||
size_t text_buffer_size = 0;
|
||||
|
||||
while (fgets(line, sizeof(line), file)) {
|
||||
// Remove trailing newline
|
||||
size_t len = strlen(line);
|
||||
if (len > 0 && line[len-1] == '\n') {
|
||||
line[len-1] = '\0';
|
||||
len--;
|
||||
}
|
||||
if (len > 0 && line[len-1] == '\r') {
|
||||
line[len-1] = '\0';
|
||||
len--;
|
||||
}
|
||||
|
||||
if (state == 0) { // Expecting subtitle index
|
||||
if (strlen(line) == 0) continue; // Skip empty lines
|
||||
// Create new subtitle entry
|
||||
current_entry = calloc(1, sizeof(subtitle_entry_t));
|
||||
if (!current_entry) break;
|
||||
state = 1;
|
||||
} else if (state == 1) { // Expecting time range
|
||||
char start_time[32], end_time[32];
|
||||
if (sscanf(line, "%31s --> %31s", start_time, end_time) == 2) {
|
||||
current_entry->start_frame = srt_time_to_frame(start_time, fps);
|
||||
current_entry->end_frame = srt_time_to_frame(end_time, fps);
|
||||
|
||||
if (current_entry->start_frame < 0 || current_entry->end_frame < 0) {
|
||||
free(current_entry);
|
||||
current_entry = NULL;
|
||||
state = 3; // Skip to next blank line
|
||||
continue;
|
||||
}
|
||||
|
||||
// Initialize text buffer
|
||||
text_buffer_size = 256;
|
||||
text_buffer = malloc(text_buffer_size);
|
||||
if (!text_buffer) {
|
||||
free(current_entry);
|
||||
current_entry = NULL;
|
||||
fprintf(stderr, "Memory allocation failed while parsing subtitles\n");
|
||||
break;
|
||||
}
|
||||
text_buffer[0] = '\0';
|
||||
state = 2;
|
||||
} else {
|
||||
free(current_entry);
|
||||
current_entry = NULL;
|
||||
state = 3; // Skip malformed entry
|
||||
}
|
||||
} else if (state == 2) { // Collecting subtitle text
|
||||
if (strlen(line) == 0) {
|
||||
// End of subtitle text
|
||||
current_entry->text = strdup(text_buffer);
|
||||
free(text_buffer);
|
||||
text_buffer = NULL;
|
||||
|
||||
// Add to list
|
||||
if (!head) {
|
||||
head = current_entry;
|
||||
tail = current_entry;
|
||||
} else {
|
||||
tail->next = current_entry;
|
||||
tail = current_entry;
|
||||
}
|
||||
current_entry = NULL;
|
||||
state = 0;
|
||||
} else {
|
||||
// Append text line
|
||||
size_t current_len = strlen(text_buffer);
|
||||
size_t line_len = strlen(line);
|
||||
size_t needed = current_len + line_len + 2; // +2 for newline and null
|
||||
|
||||
if (needed > text_buffer_size) {
|
||||
text_buffer_size = needed + 256;
|
||||
char *new_buffer = realloc(text_buffer, text_buffer_size);
|
||||
if (!new_buffer) {
|
||||
free(text_buffer);
|
||||
free(current_entry);
|
||||
current_entry = NULL;
|
||||
fprintf(stderr, "Memory allocation failed while parsing subtitles\n");
|
||||
break;
|
||||
}
|
||||
text_buffer = new_buffer;
|
||||
}
|
||||
|
||||
if (current_len > 0) {
|
||||
strcat(text_buffer, "\n");
|
||||
}
|
||||
strcat(text_buffer, line);
|
||||
}
|
||||
} else if (state == 3) { // Skip to next blank line
|
||||
if (strlen(line) == 0) {
|
||||
state = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle final subtitle if file doesn't end with blank line
|
||||
if (current_entry && text_buffer) {
|
||||
current_entry->text = strdup(text_buffer);
|
||||
free(text_buffer);
|
||||
|
||||
if (!head) {
|
||||
head = current_entry;
|
||||
} else {
|
||||
tail->next = current_entry;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
return head;
|
||||
}
|
||||
|
||||
// Free subtitle list
|
||||
static void free_subtitle_list(subtitle_entry_t *list) {
|
||||
while (list) {
|
||||
subtitle_entry_t *next = list->next;
|
||||
free(list->text);
|
||||
free(list);
|
||||
list = next;
|
||||
}
|
||||
}
|
||||
|
||||
// Write subtitle packet to output
|
||||
static int write_subtitle_packet(FILE *output, uint32_t index, uint8_t opcode, const char *text) {
|
||||
// Calculate packet size
|
||||
size_t text_len = text ? strlen(text) : 0;
|
||||
size_t packet_size = 3 + 1 + text_len + 1; // index (3 bytes) + opcode + text + null terminator
|
||||
|
||||
// Write packet type and size
|
||||
uint8_t packet_type = TEV_PACKET_SUBTITLE;
|
||||
fwrite(&packet_type, 1, 1, output);
|
||||
fwrite(&packet_size, 4, 1, output);
|
||||
|
||||
// Write subtitle packet data
|
||||
uint8_t index_bytes[3];
|
||||
index_bytes[0] = index & 0xFF;
|
||||
index_bytes[1] = (index >> 8) & 0xFF;
|
||||
index_bytes[2] = (index >> 16) & 0xFF;
|
||||
fwrite(index_bytes, 1, 3, output);
|
||||
|
||||
fwrite(&opcode, 1, 1, output);
|
||||
|
||||
if (text && text_len > 0) {
|
||||
fwrite(text, 1, text_len, output);
|
||||
}
|
||||
|
||||
// Write null terminator
|
||||
uint8_t null_term = 0x00;
|
||||
fwrite(&null_term, 1, 1, output);
|
||||
|
||||
return packet_size + 5; // packet_size + packet_type + size field
|
||||
}
|
||||
|
||||
// Process subtitles for the current frame
|
||||
static int process_subtitles(tev_encoder_t *enc, int frame_num, FILE *output) {
|
||||
if (!enc->has_subtitles) return 0;
|
||||
|
||||
int bytes_written = 0;
|
||||
|
||||
// Check if any subtitles need to be shown at this frame
|
||||
subtitle_entry_t *sub = enc->current_subtitle;
|
||||
while (sub && sub->start_frame <= frame_num) {
|
||||
if (sub->start_frame == frame_num) {
|
||||
// Show subtitle
|
||||
bytes_written += write_subtitle_packet(output, 0, 0x01, sub->text);
|
||||
if (enc->verbose) {
|
||||
printf("Frame %d: Showing subtitle: %.50s%s\n",
|
||||
frame_num, sub->text, strlen(sub->text) > 50 ? "..." : "");
|
||||
}
|
||||
}
|
||||
|
||||
if (sub->end_frame == frame_num) {
|
||||
// Hide subtitle
|
||||
bytes_written += write_subtitle_packet(output, 0, 0x02, NULL);
|
||||
if (enc->verbose) {
|
||||
printf("Frame %d: Hiding subtitle\n", frame_num);
|
||||
}
|
||||
}
|
||||
|
||||
// Move to next subtitle if we're past the end of current one
|
||||
if (sub->end_frame <= frame_num) {
|
||||
enc->current_subtitle = sub->next;
|
||||
}
|
||||
|
||||
sub = sub->next;
|
||||
}
|
||||
|
||||
return bytes_written;
|
||||
}
|
||||
|
||||
// Initialize encoder
|
||||
static tev_encoder_t* init_encoder(void) {
|
||||
tev_encoder_t *enc = calloc(1, sizeof(tev_encoder_t));
|
||||
@@ -836,6 +1068,10 @@ static tev_encoder_t* init_encoder(void) {
|
||||
enc->fps = 0; // Will be detected from input
|
||||
enc->output_fps = 0; // No frame rate conversion by default
|
||||
enc->verbose = 0;
|
||||
enc->subtitle_file = NULL;
|
||||
enc->has_subtitles = 0;
|
||||
enc->subtitle_list = NULL;
|
||||
enc->current_subtitle = NULL;
|
||||
|
||||
// Rate control defaults
|
||||
enc->target_bitrate_kbps = 0; // 0 = quality mode
|
||||
@@ -1092,6 +1328,11 @@ static char *execute_command(const char *command) {
|
||||
if (!pipe) return NULL;
|
||||
|
||||
char *result = malloc(4096);
|
||||
if (!result) {
|
||||
pclose(pipe);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t len = fread(result, 1, 4095, pipe);
|
||||
result[len] = '\0';
|
||||
|
||||
@@ -1197,7 +1438,7 @@ static int start_video_conversion(tev_encoder_t *enc) {
|
||||
"ffmpeg -v quiet -i \"%s\" -f rawvideo -pix_fmt rgb24 "
|
||||
"-vf \"fps=%d,scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d\" "
|
||||
"-y - 2>&1",
|
||||
enc->input_file, enc->width, enc->height, enc->width, enc->height, enc->output_fps);
|
||||
enc->input_file, enc->output_fps, enc->width, enc->height, enc->width, enc->height);
|
||||
} else {
|
||||
// No frame rate conversion
|
||||
snprintf(command, sizeof(command),
|
||||
@@ -1373,6 +1614,7 @@ static void show_usage(const char *program_name) {
|
||||
printf("Options:\n");
|
||||
printf(" -i, --input FILE Input video file\n");
|
||||
printf(" -o, --output FILE Output video file (use '-' for stdout)\n");
|
||||
printf(" -s, --subtitles FILE SubRip (.srt) subtitle file\n");
|
||||
printf(" -w, --width N Video width (default: %d)\n", DEFAULT_WIDTH);
|
||||
printf(" -h, --height N Video height (default: %d)\n", DEFAULT_HEIGHT);
|
||||
printf(" -f, --fps N Output frames per second (enables frame rate conversion)\n");
|
||||
@@ -1398,6 +1640,7 @@ static void show_usage(const char *program_name) {
|
||||
printf("Examples:\n");
|
||||
printf(" %s -i input.mp4 -o output.mv2 # Use default setting (q=2)\n", program_name);
|
||||
printf(" %s -i input.avi -f 15 -q 3 -o output.mv2 # 15fps @ q=3\n", program_name);
|
||||
printf(" %s -i input.mp4 -s input.srt -o output.mv2 # With SubRip subtitles\n", program_name);
|
||||
// printf(" %s -i input.mp4 -b 800 -o output.mv2 # 800 kbps bitrate target\n", program_name);
|
||||
// printf(" %s -i input.avi -f 15 -b 500 -o output.mv2 # 15fps @ 500 kbps\n", program_name);
|
||||
// printf(" %s --test -b 1000 -o test.mv2 # Test with 1000 kbps target\n", program_name);
|
||||
@@ -1414,6 +1657,11 @@ static void cleanup_encoder(tev_encoder_t *enc) {
|
||||
unlink(TEMP_AUDIO_FILE); // Remove temporary audio file
|
||||
}
|
||||
|
||||
free(enc->input_file);
|
||||
free(enc->output_file);
|
||||
free(enc->subtitle_file);
|
||||
free_subtitle_list(enc->subtitle_list);
|
||||
|
||||
free_encoder(enc);
|
||||
}
|
||||
|
||||
@@ -1432,6 +1680,7 @@ int main(int argc, char *argv[]) {
|
||||
static struct option long_options[] = {
|
||||
{"input", required_argument, 0, 'i'},
|
||||
{"output", required_argument, 0, 'o'},
|
||||
{"subtitles", required_argument, 0, 's'},
|
||||
{"width", required_argument, 0, 'w'},
|
||||
{"height", required_argument, 0, 'h'},
|
||||
{"fps", required_argument, 0, 'f'},
|
||||
@@ -1446,7 +1695,7 @@ int main(int argc, char *argv[]) {
|
||||
int option_index = 0;
|
||||
int c;
|
||||
|
||||
while ((c = getopt_long(argc, argv, "i:o:w:h:f:q:b:vt", long_options, &option_index)) != -1) {
|
||||
while ((c = getopt_long(argc, argv, "i:o:s:w:h:f:q:b:vt", long_options, &option_index)) != -1) {
|
||||
switch (c) {
|
||||
case 'i':
|
||||
enc->input_file = strdup(optarg);
|
||||
@@ -1455,6 +1704,9 @@ int main(int argc, char *argv[]) {
|
||||
enc->output_file = strdup(optarg);
|
||||
enc->output_to_stdout = (strcmp(optarg, "-") == 0);
|
||||
break;
|
||||
case 's':
|
||||
enc->subtitle_file = strdup(optarg);
|
||||
break;
|
||||
case 'w':
|
||||
enc->width = atoi(optarg);
|
||||
break;
|
||||
@@ -1528,6 +1780,21 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
// Load subtitle file if specified
|
||||
if (enc->subtitle_file) {
|
||||
enc->subtitle_list = parse_srt_file(enc->subtitle_file, enc->fps);
|
||||
if (enc->subtitle_list) {
|
||||
enc->has_subtitles = 1;
|
||||
enc->current_subtitle = enc->subtitle_list;
|
||||
if (enc->verbose) {
|
||||
printf("Loaded subtitles from: %s\n", enc->subtitle_file);
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "Failed to parse subtitle file: %s\n", enc->subtitle_file);
|
||||
// Continue without subtitles
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate buffers
|
||||
if (!alloc_encoder_buffers(enc)) {
|
||||
fprintf(stderr, "Failed to allocate encoder buffers\n");
|
||||
@@ -1641,6 +1908,9 @@ int main(int argc, char *argv[]) {
|
||||
// Process audio for this frame
|
||||
process_audio(enc, frame_count, output);
|
||||
|
||||
// Process subtitles for this frame
|
||||
process_subtitles(enc, frame_count, output);
|
||||
|
||||
// Encode frame
|
||||
if (!encode_frame(enc, output, frame_count)) {
|
||||
fprintf(stderr, "Failed to encode frame %d\n", frame_count);
|
||||
|
||||
Reference in New Issue
Block a user