TEV/TAV: SSF-TC impl

This commit is contained in:
minjaesong
2025-11-06 01:18:19 +09:00
parent af3679921d
commit 00c882aa8d
7 changed files with 403 additions and 118 deletions

View File

@@ -35,7 +35,8 @@ const TAV_PACKET_AUDIO_NATIVE = 0x21
const TAV_PACKET_AUDIO_PCM_16LE = 0x22 const TAV_PACKET_AUDIO_PCM_16LE = 0x22
const TAV_PACKET_AUDIO_ADPCM = 0x23 const TAV_PACKET_AUDIO_ADPCM = 0x23
const TAV_PACKET_AUDIO_TAD = 0x24 const TAV_PACKET_AUDIO_TAD = 0x24
const TAV_PACKET_SUBTITLE = 0x30 const TAV_PACKET_SUBTITLE = 0x30 // Legacy SSF (frame-locked)
const TAV_PACKET_SUBTITLE_TC = 0x31 // SSF-TC (timecode-based)
const TAV_PACKET_AUDIO_BUNDLED = 0x40 // Entire MP2 audio file in single packet const TAV_PACKET_AUDIO_BUNDLED = 0x40 // Entire MP2 audio file in single packet
const TAV_PACKET_EXTENDED_HDR = 0xEF const TAV_PACKET_EXTENDED_HDR = 0xEF
const TAV_PACKET_GOP_SYNC = 0xFC // GOP sync (N frames decoded from GOP block) const TAV_PACKET_GOP_SYNC = 0xFC // GOP sync (N frames decoded from GOP block)
@@ -64,6 +65,11 @@ let subtitleVisible = false
let subtitleText = "" let subtitleText = ""
let subtitlePosition = 0 // 0=bottom center (default) let subtitlePosition = 0 // 0=bottom center (default)
// SSF-TC subtitle event buffer
let subtitleEvents = [] // Array of {timecode_ns, index, opcode, text}
let nextSubtitleEventIndex = 0 // Next event to check
let currentTimecodeNs = 0 // Current playback timecode
// Parse command line options // Parse command line options
let interactive = false let interactive = false
let filmGrainLevel = null let filmGrainLevel = null
@@ -123,7 +129,7 @@ if (fullFilePathStr.startsWith('$:/TAPE') || fullFilePathStr.startsWith('$:\\TAP
con.clear() con.clear()
con.curs_set(0) con.curs_set(0)
graphics.setGraphicsMode(4) // initially set to 4bpp mode graphics.setGraphicsMode(4) // initially set to 4bpp mode
graphics.setGraphicsMode(5) // set to 8bpp mode. If GPU don't support it, mode will remain to 4 //graphics.setGraphicsMode(5) // set to 8bpp mode. If GPU don't support it, mode will remain to 4
graphics.clearPixels(0) graphics.clearPixels(0)
graphics.clearPixels2(0) graphics.clearPixels2(0)
graphics.clearPixels3(0) graphics.clearPixels3(0)
@@ -140,6 +146,99 @@ audio.setMasterVolume(0, 255)
// set colour zero as half-opaque black // set colour zero as half-opaque black
graphics.setPalette(0, 0, 0, 0, 7) graphics.setPalette(0, 0, 0, 0, 7)
// Parse SSF-TC subtitle packet and add to event buffer (0x31)
function parseSubtitlePacketTC(packetSize) {
// Read subtitle index (24-bit, little-endian)
let indexByte0 = seqread.readOneByte()
let indexByte1 = seqread.readOneByte()
let indexByte2 = seqread.readOneByte()
let index = indexByte0 | (indexByte1 << 8) | (indexByte2 << 16)
// Read timecode (64-bit, little-endian)
let timecode_ns = 0
for (let i = 0; i < 8; i++) {
let byte = seqread.readOneByte()
timecode_ns += byte * Math.pow(2, i * 8)
}
// Read opcode
let opcode = seqread.readOneByte()
let remainingBytes = packetSize - 12 // Subtract 3 (index) + 8 (timecode) + 1 (opcode)
// Read text if present
let text = null
if (remainingBytes > 1 && (opcode === SSF_OP_SHOW || (opcode >= 0x10 && opcode <= 0x2F))) {
let textBytes = seqread.readBytes(remainingBytes)
text = ""
for (let i = 0; i < remainingBytes - 1; i++) { // -1 for null terminator
let byte = sys.peek(textBytes + i)
if (byte === 0) break
text += String.fromCharCode(byte)
}
sys.free(textBytes)
} else if (remainingBytes > 0) {
// Skip remaining bytes
let skipBytes = seqread.readBytes(remainingBytes)
sys.free(skipBytes)
}
// Add event to buffer
subtitleEvents.push({
timecode_ns: timecode_ns,
index: index,
opcode: opcode,
text: text
})
}
// Process subtitle events based on current playback time
function processSubtitleEvents(currentTimeNs) {
// Process all events whose timecode has been reached
while (nextSubtitleEventIndex < subtitleEvents.length) {
let event = subtitleEvents[nextSubtitleEventIndex]
if (event.timecode_ns > currentTimeNs) {
break // Haven't reached this event yet
}
// Execute the subtitle event
switch (event.opcode) {
case SSF_OP_SHOW:
subtitleText = event.text || ""
subtitleVisible = true
gui.displaySubtitle(subtitleText, fontUploaded, subtitlePosition)
break
case SSF_OP_HIDE:
subtitleVisible = false
subtitleText = ""
gui.clearSubtitleArea()
break
case SSF_OP_MOVE:
if (event.text && event.text.length > 0) {
let newPosition = event.text.charCodeAt(0)
if (newPosition >= 0 && newPosition <= 8) {
subtitlePosition = newPosition
if (subtitleVisible && subtitleText.length > 0) {
gui.clearSubtitleArea()
gui.displaySubtitle(subtitleText, fontUploaded, subtitlePosition)
}
}
}
break
case SSF_OP_UPLOAD_LOW_FONT:
case SSF_OP_UPLOAD_HIGH_FONT:
// Font upload handled during packet parsing
break
}
nextSubtitleEventIndex++
}
}
// Process legacy frame-locked subtitle packet (0x30)
function processSubtitlePacket(packetSize) { function processSubtitlePacket(packetSize) {
// Read subtitle packet data according to SSF format // Read subtitle packet data according to SSF format
@@ -1378,10 +1477,15 @@ try {
sys.free(pcmPtr) sys.free(pcmPtr)
} }
else if (packetType === TAV_PACKET_SUBTITLE) { else if (packetType === TAV_PACKET_SUBTITLE) {
// Subtitle packet - same format as TEV // Legacy frame-locked subtitle packet (0x30)
let packetSize = seqread.readInt() let packetSize = seqread.readInt()
processSubtitlePacket(packetSize) processSubtitlePacket(packetSize)
} }
else if (packetType === TAV_PACKET_SUBTITLE_TC) {
// SSF-TC subtitle packet (0x31) - parse and buffer for later playback
let packetSize = seqread.readInt()
parseSubtitlePacketTC(packetSize)
}
else if (packetType === TAV_PACKET_EXTENDED_HDR) { else if (packetType === TAV_PACKET_EXTENDED_HDR) {
// Extended header packet - metadata key-value pairs // Extended header packet - metadata key-value pairs
let numPairs = seqread.readShort() let numPairs = seqread.readShort()
@@ -1444,6 +1548,14 @@ try {
let timecodeHigh = seqread.readInt() let timecodeHigh = seqread.readInt()
let timecodeNs = timecodeHigh * 0x100000000 + (timecodeLow >>> 0) let timecodeNs = timecodeHigh * 0x100000000 + (timecodeLow >>> 0)
// Update current timecode and process subtitle events
currentTimecodeNs = timecodeNs
// Process SSF-TC subtitle events based on current playback time
if (subtitleEvents.length > 0) {
processSubtitleEvents(currentTimecodeNs)
}
// Optionally display timecode in interactive mode (can be verbose) // Optionally display timecode in interactive mode (can be verbose)
// Uncomment for debugging: // Uncomment for debugging:
// if (interactive && frameCount % 60 === 0) { // if (interactive && frameCount % 60 === 0) {

View File

@@ -26,7 +26,8 @@ const TEV_MODE_MOTION = 0x03
const TEV_PACKET_IFRAME = 0x10 const TEV_PACKET_IFRAME = 0x10
const TEV_PACKET_PFRAME = 0x11 const TEV_PACKET_PFRAME = 0x11
const TEV_PACKET_AUDIO_MP2 = 0x20 const TEV_PACKET_AUDIO_MP2 = 0x20
const TEV_PACKET_SUBTITLE = 0x30 const TEV_PACKET_SUBTITLE = 0x30 // Legacy SSF (frame-locked)
const TEV_PACKET_SUBTITLE_TC = 0x31 // SSF-TC (timecode-based)
const TEV_PACKET_SYNC = 0xFF const TEV_PACKET_SYNC = 0xFF
// Subtitle opcodes (SSF format) // Subtitle opcodes (SSF format)
@@ -42,6 +43,10 @@ let subtitleVisible = false
let subtitleText = "" let subtitleText = ""
let subtitlePosition = 0 // 0=bottom center (default) let subtitlePosition = 0 // 0=bottom center (default)
// SSF-TC subtitle event buffer
let subtitleEvents = [] // Array of {timecode_ns, index, opcode, text}
let nextSubtitleEventIndex = 0 // Next event to check
// Parse command line options // Parse command line options
let interactive = false let interactive = false
let debugMotionVectors = false let debugMotionVectors = false
@@ -274,6 +279,99 @@ function displaySubtitle(text, position = 0) {
con.color_pair(oldFgColor, oldBgColor) con.color_pair(oldFgColor, oldBgColor)
} }
// Parse SSF-TC subtitle packet and add to event buffer (0x31)
function parseSubtitlePacketTC(packetSize) {
// Read subtitle index (24-bit, little-endian)
let indexByte0 = seqread.readOneByte()
let indexByte1 = seqread.readOneByte()
let indexByte2 = seqread.readOneByte()
let index = indexByte0 | (indexByte1 << 8) | (indexByte2 << 16)
// Read timecode (64-bit, little-endian)
let timecode_ns = 0
for (let i = 0; i < 8; i++) {
let byte = seqread.readOneByte()
timecode_ns += byte * Math.pow(2, i * 8)
}
// Read opcode
let opcode = seqread.readOneByte()
let remainingBytes = packetSize - 12 // Subtract 3 (index) + 8 (timecode) + 1 (opcode)
// Read text if present
let text = null
if (remainingBytes > 1 && (opcode === SSF_OP_SHOW || (opcode >= 0x10 && opcode <= 0x2F))) {
let textBytes = seqread.readBytes(remainingBytes)
text = ""
for (let i = 0; i < remainingBytes - 1; i++) { // -1 for null terminator
let byte = sys.peek(textBytes + i)
if (byte === 0) break
text += String.fromCharCode(byte)
}
sys.free(textBytes)
} else if (remainingBytes > 0) {
// Skip remaining bytes
let skipBytes = seqread.readBytes(remainingBytes)
sys.free(skipBytes)
}
// Add event to buffer
subtitleEvents.push({
timecode_ns: timecode_ns,
index: index,
opcode: opcode,
text: text
})
}
// Process subtitle events based on current playback time
function processSubtitleEvents(currentTimeNs) {
// Process all events whose timecode has been reached
while (nextSubtitleEventIndex < subtitleEvents.length) {
let event = subtitleEvents[nextSubtitleEventIndex]
if (event.timecode_ns > currentTimeNs) {
break // Haven't reached this event yet
}
// Execute the subtitle event
switch (event.opcode) {
case SSF_OP_SHOW:
subtitleText = event.text || ""
subtitleVisible = true
displaySubtitle(subtitleText, subtitlePosition)
break
case SSF_OP_HIDE:
subtitleVisible = false
subtitleText = ""
clearSubtitleArea()
break
case SSF_OP_MOVE:
if (event.text && event.text.length > 0) {
let newPosition = event.text.charCodeAt(0)
if (newPosition >= 0 && newPosition <= 8) {
subtitlePosition = newPosition
if (subtitleVisible && subtitleText.length > 0) {
clearSubtitleArea()
displaySubtitle(subtitleText, subtitlePosition)
}
}
}
break
case SSF_OP_UPLOAD_LOW_FONT:
case SSF_OP_UPLOAD_HIGH_FONT:
// Font upload handled during packet parsing
break
}
nextSubtitleEventIndex++
}
}
// Process legacy frame-locked subtitle packet (0x30)
function processSubtitlePacket(packetSize) { function processSubtitlePacket(packetSize) {
// Read subtitle packet data according to SSF format // Read subtitle packet data according to SSF format
// uint24 index + uint8 opcode + variable arguments // uint24 index + uint8 opcode + variable arguments
@@ -450,6 +548,7 @@ function getVideoRate() {
} }
let FRAME_TIME = 1.0 / fps let FRAME_TIME = 1.0 / fps
let FRAME_TIME_NS = (1000000000.0 / fps) // Frame time in nanoseconds for subtitle timing
// Ultra-fast approach: always render to display, use dedicated previous frame buffer // Ultra-fast approach: always render to display, use dedicated previous frame buffer
const FRAME_PIXELS = width * height const FRAME_PIXELS = width * height
@@ -682,6 +781,12 @@ try {
serial.println(`Frame ${frameCount}: Duplicating previous frame`) serial.println(`Frame ${frameCount}: Duplicating previous frame`)
} }
// Process SSF-TC subtitle events based on current playback time
if (subtitleEvents.length > 0) {
let currentTimeNs = frameCount * FRAME_TIME_NS
processSubtitleEvents(currentTimeNs)
}
// Defer audio playback until a first frame is sent // Defer audio playback until a first frame is sent
if (isInterlaced) { if (isInterlaced) {
// fire audio after frame 1 // fire audio after frame 1
@@ -729,9 +834,14 @@ try {
audio.mp2UploadDecoded(0) audio.mp2UploadDecoded(0)
} else if (packetType == TEV_PACKET_SUBTITLE) { } else if (packetType == TEV_PACKET_SUBTITLE) {
// Subtitle packet // Legacy frame-locked subtitle packet (0x30)
let packetSize = seqread.readInt() let packetSize = seqread.readInt()
processSubtitlePacket(packetSize) processSubtitlePacket(packetSize)
} else if (packetType == TEV_PACKET_SUBTITLE_TC) {
// SSF-TC subtitle packet (0x31) - parse and buffer for later playback
let packetSize = seqread.readInt()
parseSubtitlePacketTC(packetSize)
} else if (packetType == 0x00) { } else if (packetType == 0x00) {
// Silently discard, faulty subtitle creation can cause this as 0x00 is used as an argument terminator // Silently discard, faulty subtitle creation can cause this as 0x00 is used as an argument terminator
} else { } else {

View File

@@ -818,7 +818,7 @@ When SSF is interleaved with MP2 audio, the payload must be inserted in-between
## SSF Packet Structure ## SSF Packet Structure
uint24 Subtitle object ID (used to specify target subtitle object) uint24 Subtitle object ID (used to specify target subtitle object)
uint64 Timecode in nanoseconds (only present for SSF-TC format; regular SSF must not write these bytes) uint64 Timecode in nanoseconds (only present on SSF-TC format; regular SSF must not write these bytes)
uint8 opcode uint8 opcode
0x00 = <argument terminator>, is NOP when used here 0x00 = <argument terminator>, is NOP when used here
0x01 = show (arguments: UTF-8 text) 0x01 = show (arguments: UTF-8 text)
@@ -854,7 +854,7 @@ When KSF is interleaved with MP2 audio, the payload must be inserted in-between
on appropriate timings. on appropriate timings.
uint24 Subtitle object ID (used to specify target subtitle object) uint24 Subtitle object ID (used to specify target subtitle object)
uint64 Timecode in nanoseconds (only present for KSF-TC format; regular KSF must not write these bytes) uint64 Timecode in nanoseconds (only present on KSF-TC format; regular KSF must not write these bytes)
uint8 opcode uint8 opcode
<definition opcodes> <definition opcodes>
0x00 = <argument terminator>, is NOP when used here 0x00 = <argument terminator>, is NOP when used here
@@ -1012,12 +1012,14 @@ transmission capability, and region-of-interest coding.
1. TAV Extended header (if any) 1. TAV Extended header (if any)
2. Standard metadata payloads (if any) 2. Standard metadata payloads (if any)
3. SSF-TC/KSF-TC packets (if any) 3. SSF-TC/KSF-TC packets (if any)
When time-coded subtitles are used, the entire subtitle bytes must precede the first video frame.
Think of it as tacking the whole subtitle file before the actual video.
Frame group: Frame group:
1. TC Packet (0xFD) or Next TAV File (0x1F) [mutually exclusive!] 1. TC Packet (0xFD) or Next TAV File (0x1F) [mutually exclusive!]
2. Loop point packet (if any) 2. Loop point packet (if any)
3. Audio packets (if any) 3. Audio packets (if any)
4. Subtitle packets (if any) 4. Subtitle packets (if any) [mutually exclusive with SSF-TC/KSF-TC packets]
5. Main video packets (0x10-0x1E) 5. Main video packets (0x10-0x1E)
6. Multiplexed video packets (0x70-7F; if any) 6. Multiplexed video packets (0x70-7F; if any)

View File

@@ -536,7 +536,7 @@ class GraphicsJSR223Delegate(private val vm: VM) {
3f/16f,5f/16f,1f/16f, 3f/16f,5f/16f,1f/16f,
) )
private val bayerKernels = arrayOf( private val bayerKernelsInt = arrayOf(
intArrayOf( intArrayOf(
0,8,2,10, 0,8,2,10,
12,4,14,6, 12,4,14,6,
@@ -561,7 +561,9 @@ class GraphicsJSR223Delegate(private val vm: VM) {
12,4,14,6, 12,4,14,6,
3,11,1,9, 3,11,1,9,
) )
).map{ it.map { (it.toFloat() + 0.5f) / 16f }.toFloatArray() } )
private val bayerKernels = bayerKernelsInt.map{ it.map { (it.toFloat() + 0.5f) / 16f }.toFloatArray() }
/** /**
* This method always assume that you're using the default palette * This method always assume that you're using the default palette
@@ -6583,13 +6585,6 @@ class GraphicsJSR223Delegate(private val vm: VM) {
val offsetY = (nativeHeight - height) / 2 val offsetY = (nativeHeight - height) / 2
// Dithering pattern for 8bpp → 4bpp conversion // Dithering pattern for 8bpp → 4bpp conversion
val bayerMatrix = arrayOf(
intArrayOf(0, 8, 2, 10),
intArrayOf(12, 4, 14, 6),
intArrayOf(3, 11, 1, 9),
intArrayOf(15, 7, 13, 5)
)
// Process row by row // Process row by row
for (y in 0 until height) { for (y in 0 until height) {
val screenY = y + offsetY val screenY = y + offsetY
@@ -6611,13 +6606,13 @@ class GraphicsJSR223Delegate(private val vm: VM) {
if (graphicsMode == 4) { if (graphicsMode == 4) {
// 4bpp mode: dithered RGB (RG in fb1, B_ in fb2) // 4bpp mode: dithered RGB (RG in fb1, B_ in fb2)
val threshold = bayerMatrix[y % 4][x % 4] val threshold = bayerKernelsInt[frameCount % 4][4 * (y % 4) + (x % 4)]
val rDithered = ((r + (threshold - 8)) shr 4).coerceIn(0, 15) val rDithered = ((r + (threshold - 8)) shr 4).coerceIn(0, 15)
val gDithered = ((g + (threshold - 8)) shr 4).coerceIn(0, 15) val gDithered = ((g + (threshold - 8)) shr 4).coerceIn(0, 15)
val bDithered = ((b + (threshold - 8)) shr 4).coerceIn(0, 15) val bDithered = ((b + (threshold - 8)) shr 4).coerceIn(0, 15)
gpu.framebuffer[screenPixelIdx] = ((rDithered shl 4) or gDithered).toByte() gpu.framebuffer[screenPixelIdx] = ((rDithered shl 4) or gDithered).toByte()
gpu.framebuffer2?.set(screenPixelIdx, (bDithered shl 4).toByte()) gpu.framebuffer2?.set(screenPixelIdx, ((bDithered shl 4) or 15).toByte())
} else if (graphicsMode == 5) { } else if (graphicsMode == 5) {
// 8bpp mode: full RGB (R in fb1, G in fb2, B in fb3) // 8bpp mode: full RGB (R in fb1, G in fb2, B in fb3)
gpu.framebuffer[screenPixelIdx] = r.toByte() gpu.framebuffer[screenPixelIdx] = r.toByte()

View File

@@ -18,7 +18,7 @@
#include <limits.h> #include <limits.h>
#include <float.h> #include <float.h>
#define ENCODER_VENDOR_STRING "Encoder-TAV 20251031 (3d-dwt,tad)" #define ENCODER_VENDOR_STRING "Encoder-TAV 20251106 (3d-dwt,tad,ssf-tc)"
// TSVM Advanced Video (TAV) format constants // TSVM Advanced Video (TAV) format constants
#define TAV_MAGIC "\x1F\x54\x53\x56\x4D\x54\x41\x56" // "\x1FTSVM TAV" #define TAV_MAGIC "\x1F\x54\x53\x56\x4D\x54\x41\x56" // "\x1FTSVM TAV"
@@ -56,7 +56,7 @@
#define TAV_PACKET_AUDIO_MP2 0x20 // MP2 audio #define TAV_PACKET_AUDIO_MP2 0x20 // MP2 audio
#define TAV_PACKET_AUDIO_PCM8 0x21 // 8-bit PCM audio (zstd compressed) #define TAV_PACKET_AUDIO_PCM8 0x21 // 8-bit PCM audio (zstd compressed)
#define TAV_PACKET_AUDIO_TAD 0x24 // TAD audio (DWT-based perceptual codec) #define TAV_PACKET_AUDIO_TAD 0x24 // TAD audio (DWT-based perceptual codec)
#define TAV_PACKET_SUBTITLE 0x30 // Subtitle packet #define TAV_PACKET_SUBTITLE_TC 0x31 // Subtitle packet with timecode (SSF-TC format)
#define TAV_PACKET_AUDIO_TRACK 0x40 // Separate audio track (full MP2 file) #define TAV_PACKET_AUDIO_TRACK 0x40 // Separate audio track (full MP2 file)
#define TAV_PACKET_EXTENDED_HDR 0xEF // Extended header packet #define TAV_PACKET_EXTENDED_HDR 0xEF // Extended header packet
#define TAV_PACKET_GOP_SYNC 0xFC // GOP sync packet (N frames decoded) #define TAV_PACKET_GOP_SYNC 0xFC // GOP sync packet (N frames decoded)
@@ -8349,30 +8349,42 @@ static void free_subtitle_list(subtitle_entry_t *list) {
} }
// Write subtitle packet (copied from TEV) // Write subtitle packet (copied from TEV)
static int write_subtitle_packet(FILE *output, uint32_t index, uint8_t opcode, const char *text) { // Write SSF-TC subtitle packet to output
// Calculate packet size static int write_subtitle_packet_tc(FILE *output, uint32_t index, uint8_t opcode, const char *text, uint64_t timecode_ns) {
// Calculate packet size: index (3 bytes) + timecode (8 bytes) + opcode (1 byte) + text + null terminator
size_t text_len = text ? strlen(text) : 0; size_t text_len = text ? strlen(text) : 0;
size_t packet_size = 3 + 1 + text_len + 1; // index (3 bytes) + opcode + text + null terminator size_t packet_size = 3 + 8 + 1 + text_len + 1;
// Write packet type and size // Write packet type and size
uint8_t packet_type = TAV_PACKET_SUBTITLE; uint8_t packet_type = TAV_PACKET_SUBTITLE_TC;
fwrite(&packet_type, 1, 1, output); fwrite(&packet_type, 1, 1, output);
uint32_t size32 = (uint32_t)packet_size; uint32_t size32 = (uint32_t)packet_size;
fwrite(&size32, 4, 1, output); fwrite(&size32, 4, 1, output);
// Write subtitle data // Write subtitle index (24-bit, little-endian)
uint8_t index_bytes[3] = { uint8_t index_bytes[3] = {
(uint8_t)(index & 0xFF), (uint8_t)(index & 0xFF),
(uint8_t)((index >> 8) & 0xFF), (uint8_t)((index >> 8) & 0xFF),
(uint8_t)((index >> 16) & 0xFF) (uint8_t)((index >> 16) & 0xFF)
}; };
fwrite(index_bytes, 3, 1, output); fwrite(index_bytes, 3, 1, output);
// Write timecode (64-bit, little-endian)
uint8_t timecode_bytes[8];
for (int i = 0; i < 8; i++) {
timecode_bytes[i] = (timecode_ns >> (i * 8)) & 0xFF;
}
fwrite(timecode_bytes, 8, 1, output);
// Write opcode
fwrite(&opcode, 1, 1, output); fwrite(&opcode, 1, 1, output);
// Write text if present
if (text && text_len > 0) { if (text && text_len > 0) {
fwrite(text, 1, text_len, output); fwrite(text, 1, text_len, output);
} }
// Write null terminator
uint8_t null_terminator = 0; uint8_t null_terminator = 0;
fwrite(&null_terminator, 1, 1, output); fwrite(&null_terminator, 1, 1, output);
@@ -9034,47 +9046,59 @@ static int process_audio_for_gop(tav_encoder_t *enc, int *frame_numbers, int num
return 1; return 1;
} }
// Process subtitles for current frame (copied and adapted from TEV) // Write all subtitles upfront in SSF-TC format (called before first frame)
static int process_subtitles(tav_encoder_t *enc, int frame_num, FILE *output) { static int write_all_subtitles_tc(tav_encoder_t *enc, FILE *output) {
if (!enc->subtitles) { if (!enc->subtitles) return 0;
return 1; // No subtitles to process
}
int bytes_written = 0; int bytes_written = 0;
int subtitle_count = 0;
// Check if we need to show a new subtitle // Convert frame timing to nanoseconds
if (!enc->subtitle_visible) { // For NTSC: frame_time = 1001000000 / 30000 nanoseconds
subtitle_entry_t *sub = enc->current_subtitle; // For others: frame_time = 1e9 / fps nanoseconds
if (!sub) sub = enc->subtitles; // Start from beginning if not set uint64_t frame_time_ns;
if (enc->is_ntsc_framerate) {
// Find next subtitle to show frame_time_ns = 1001000000ULL / 30000ULL; // NTSC: 30000/1001 fps
while (sub && sub->start_frame <= frame_num) { } else {
if (sub->end_frame > frame_num) { frame_time_ns = (uint64_t)(1000000000.0 / enc->fps);
// This subtitle should be shown
if (sub != enc->current_subtitle) {
enc->current_subtitle = sub;
enc->subtitle_visible = 1;
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 ? "..." : "");
}
}
break;
}
sub = sub->next;
}
} }
// Check if we need to hide current subtitle // Iterate through all subtitles and write them with timecodes
if (enc->subtitle_visible && enc->current_subtitle) { subtitle_entry_t *sub = enc->subtitles;
if (frame_num >= enc->current_subtitle->end_frame) { while (sub) {
enc->subtitle_visible = 0; // Calculate timecodes for show and hide events
bytes_written += write_subtitle_packet(output, 0, 0x02, NULL); uint64_t show_timecode;
if (enc->verbose) { uint64_t hide_timecode;
printf("Frame %d: Hiding subtitle\n", frame_num);
} if (enc->is_ntsc_framerate) {
// NTSC: time = frame * 1001000000 / 30000
show_timecode = ((uint64_t)sub->start_frame * 1001000000ULL) / 30000ULL;
hide_timecode = ((uint64_t)sub->end_frame * 1001000000ULL) / 30000ULL;
} else {
show_timecode = (uint64_t)sub->start_frame * frame_time_ns;
hide_timecode = (uint64_t)sub->end_frame * frame_time_ns;
} }
// Write show subtitle event
bytes_written += write_subtitle_packet_tc(output, 0, 0x01, sub->text, show_timecode);
// Write hide subtitle event
bytes_written += write_subtitle_packet_tc(output, 0, 0x02, NULL, hide_timecode);
subtitle_count++;
if (enc->verbose) {
printf("SSF-TC: Subtitle %d: show at %.3fs, hide at %.3fs: %.50s%s\n",
subtitle_count,
show_timecode / 1000000000.0,
hide_timecode / 1000000000.0,
sub->text, strlen(sub->text) > 50 ? "..." : "");
}
sub = sub->next;
}
if (enc->verbose && subtitle_count > 0) {
printf("Wrote %d SSF-TC subtitle events (%d bytes)\n", subtitle_count * 2, bytes_written);
} }
return bytes_written; return bytes_written;
@@ -10330,6 +10354,11 @@ int main(int argc, char *argv[]) {
} }
} }
// Write all subtitles upfront in SSF-TC format (before first frame)
if (enc->subtitles) {
write_all_subtitles_tc(enc, enc->output_fp);
}
if (enc->output_fps != enc->fps) { if (enc->output_fps != enc->fps) {
printf("Frame rate conversion enabled: %d fps output\n", enc->output_fps); printf("Frame rate conversion enabled: %d fps output\n", enc->output_fps);
} }
@@ -10544,8 +10573,8 @@ int main(int argc, char *argv[]) {
// Process audio for this frame // Process audio for this frame
process_audio(enc, true_frame_count, enc->output_fp); process_audio(enc, true_frame_count, enc->output_fp);
// Process subtitles for this frame // Note: Subtitles are now written upfront in SSF-TC format (see write_all_subtitles_tc)
process_subtitles(enc, true_frame_count, enc->output_fp); // process_subtitles() is no longer called here
} }
if (enc->enable_temporal_dwt) { if (enc->enable_temporal_dwt) {
@@ -10965,9 +10994,9 @@ int main(int argc, char *argv[]) {
// Skip when temporal DWT is enabled (audio handled in GOP flush) // Skip when temporal DWT is enabled (audio handled in GOP flush)
if (!enc->enable_temporal_dwt && enc->is_ntsc_framerate && (frame_count % 1000 == 500)) { if (!enc->enable_temporal_dwt && enc->is_ntsc_framerate && (frame_count % 1000 == 500)) {
true_frame_count++; true_frame_count++;
// Process audio and subtitles for the duplicated frame to maintain sync // Process audio for the duplicated frame to maintain sync
process_audio(enc, true_frame_count, enc->output_fp); process_audio(enc, true_frame_count, enc->output_fp);
process_subtitles(enc, true_frame_count, enc->output_fp); // Note: Subtitles are now written upfront in SSF-TC format (see write_all_subtitles_tc)
uint8_t sync_packet_ntsc = TAV_PACKET_SYNC_NTSC; uint8_t sync_packet_ntsc = TAV_PACKET_SYNC_NTSC;
fwrite(&sync_packet_ntsc, 1, 1, enc->output_fp); fwrite(&sync_packet_ntsc, 1, 1, enc->output_fp);

View File

@@ -33,7 +33,7 @@
#define TEV_PACKET_IFRAME 0x10 // Intra frame (keyframe) #define TEV_PACKET_IFRAME 0x10 // Intra frame (keyframe)
#define TEV_PACKET_PFRAME 0x11 // Predicted frame #define TEV_PACKET_PFRAME 0x11 // Predicted frame
#define TEV_PACKET_AUDIO_MP2 0x20 // MP2 audio #define TEV_PACKET_AUDIO_MP2 0x20 // MP2 audio
#define TEV_PACKET_SUBTITLE 0x30 // Subtitle packet #define TEV_PACKET_SUBTITLE_TC 0x31 // Subtitle packet with timecode (SSF-TC format)
#define TEV_PACKET_SYNC 0xFF // Sync packet #define TEV_PACKET_SYNC 0xFF // Sync packet
// Utility macros // Utility macros
@@ -1834,26 +1834,35 @@ static void free_subtitle_list(subtitle_entry_t *list) {
} }
} }
// Write subtitle packet to output // Write SSF-TC subtitle packet to output
static int write_subtitle_packet(FILE *output, uint32_t index, uint8_t opcode, const char *text) { static int write_subtitle_packet_tc(FILE *output, uint32_t index, uint8_t opcode, const char *text, uint64_t timecode_ns) {
// Calculate packet size // Calculate packet size: index (3 bytes) + timecode (8 bytes) + opcode (1 byte) + text + null terminator
size_t text_len = text ? strlen(text) : 0; size_t text_len = text ? strlen(text) : 0;
size_t packet_size = 3 + 1 + text_len + 1; // index (3 bytes) + opcode + text + null terminator size_t packet_size = 3 + 8 + 1 + text_len + 1;
// Write packet type and size // Write packet type and size
uint8_t packet_type = TEV_PACKET_SUBTITLE; uint8_t packet_type = TEV_PACKET_SUBTITLE_TC;
fwrite(&packet_type, 1, 1, output); fwrite(&packet_type, 1, 1, output);
fwrite(&packet_size, 4, 1, output); fwrite(&packet_size, 4, 1, output);
// Write subtitle packet data // Write subtitle index (24-bit, little-endian)
uint8_t index_bytes[3]; uint8_t index_bytes[3];
index_bytes[0] = index & 0xFF; index_bytes[0] = index & 0xFF;
index_bytes[1] = (index >> 8) & 0xFF; index_bytes[1] = (index >> 8) & 0xFF;
index_bytes[2] = (index >> 16) & 0xFF; index_bytes[2] = (index >> 16) & 0xFF;
fwrite(index_bytes, 1, 3, output); fwrite(index_bytes, 1, 3, output);
// Write timecode (64-bit, little-endian)
uint8_t timecode_bytes[8];
for (int i = 0; i < 8; i++) {
timecode_bytes[i] = (timecode_ns >> (i * 8)) & 0xFF;
}
fwrite(timecode_bytes, 1, 8, output);
// Write opcode
fwrite(&opcode, 1, 1, output); fwrite(&opcode, 1, 1, output);
// Write text if present
if (text && text_len > 0) { if (text && text_len > 0) {
fwrite(text, 1, text_len, output); fwrite(text, 1, text_len, output);
} }
@@ -1865,40 +1874,46 @@ static int write_subtitle_packet(FILE *output, uint32_t index, uint8_t opcode, c
return packet_size + 5; // packet_size + packet_type + size field return packet_size + 5; // packet_size + packet_type + size field
} }
// Process subtitles for the current frame // Write all subtitles upfront in SSF-TC format (called before first frame)
static int process_subtitles(tev_encoder_t *enc, int frame_num, FILE *output) { static int write_all_subtitles_tc(tev_encoder_t *enc, FILE *output) {
if (!enc->has_subtitles) return 0; if (!enc->has_subtitles) return 0;
int bytes_written = 0; int bytes_written = 0;
int subtitle_count = 0;
// Check if any subtitles need to be shown at this frame // Convert frame timing to nanoseconds
subtitle_entry_t *sub = enc->current_subtitle; // Frame time = 1e9 / fps nanoseconds
while (sub && sub->start_frame <= frame_num) { uint64_t frame_time_ns = (uint64_t)(1000000000.0 / enc->output_fps);
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) { // Iterate through all subtitles and write them with timecodes
// Hide subtitle subtitle_entry_t *sub = enc->subtitle_list;
bytes_written += write_subtitle_packet(output, 0, 0x02, NULL); while (sub) {
if (enc->verbose) { // Calculate timecodes for show and hide events
printf("Frame %d: Hiding subtitle\n", frame_num); uint64_t show_timecode = (uint64_t)sub->start_frame * frame_time_ns;
} uint64_t hide_timecode = (uint64_t)sub->end_frame * frame_time_ns;
}
// Move to next subtitle if we're past the end of current one // Write show subtitle event
if (sub->end_frame <= frame_num) { bytes_written += write_subtitle_packet_tc(output, 0, 0x01, sub->text, show_timecode);
enc->current_subtitle = sub->next;
// Write hide subtitle event
bytes_written += write_subtitle_packet_tc(output, 0, 0x02, NULL, hide_timecode);
subtitle_count++;
if (enc->verbose) {
printf("SSF-TC: Subtitle %d: show at %.3fs, hide at %.3fs: %.50s%s\n",
subtitle_count,
show_timecode / 1000000000.0,
hide_timecode / 1000000000.0,
sub->text, strlen(sub->text) > 50 ? "..." : "");
} }
sub = sub->next; sub = sub->next;
} }
if (enc->verbose && subtitle_count > 0) {
printf("Wrote %d SSF-TC subtitle events (%d bytes)\n", subtitle_count * 2, bytes_written);
}
return bytes_written; return bytes_written;
} }
@@ -2868,6 +2883,12 @@ int main(int argc, char *argv[]) {
// Write TEV header // Write TEV header
write_tev_header(output, enc); write_tev_header(output, enc);
// Write all subtitles upfront in SSF-TC format (before first frame)
if (enc->has_subtitles) {
write_all_subtitles_tc(enc, output);
}
gettimeofday(&enc->start_time, NULL); gettimeofday(&enc->start_time, NULL);
printf("Encoding video with %s 4:2:0 format...\n", enc->ictcp_mode ? "ICtCp" : "YCoCg-R"); printf("Encoding video with %s 4:2:0 format...\n", enc->ictcp_mode ? "ICtCp" : "YCoCg-R");
@@ -2962,8 +2983,8 @@ int main(int argc, char *argv[]) {
// Process audio for this frame // Process audio for this frame
process_audio(enc, frame_count, output); process_audio(enc, frame_count, output);
// Process subtitles for this frame // Note: Subtitles are now written upfront in SSF-TC format (see write_all_subtitles_tc)
process_subtitles(enc, frame_count, output); // process_subtitles() is no longer called here
// Encode frame // Encode frame
// Pass field parity for interlaced mode, -1 for progressive mode // Pass field parity for interlaced mode, -1 for progressive mode

View File

@@ -26,8 +26,8 @@
#define TAV_PACKET_AUDIO_MP2 0x20 #define TAV_PACKET_AUDIO_MP2 0x20
#define TAV_PACKET_AUDIO_PCM8 0x21 #define TAV_PACKET_AUDIO_PCM8 0x21
#define TAV_PACKET_AUDIO_TAD 0x24 #define TAV_PACKET_AUDIO_TAD 0x24
#define TAV_PACKET_SUBTITLE 0x30 #define TAV_PACKET_SUBTITLE 0x30 // Legacy SSF (frame-locked)
#define TAV_PACKET_SUBTITLE_KAR 0x31 #define TAV_PACKET_SUBTITLE_TC 0x31 // SSF-TC (timecode-based)
#define TAV_PACKET_AUDIO_TRACK 0x40 #define TAV_PACKET_AUDIO_TRACK 0x40
#define TAV_PACKET_VIDEO_CH2_I 0x70 #define TAV_PACKET_VIDEO_CH2_I 0x70
#define TAV_PACKET_VIDEO_CH2_P 0x71 #define TAV_PACKET_VIDEO_CH2_P 0x71
@@ -119,8 +119,8 @@ const char* get_packet_type_name(uint8_t type) {
case TAV_PACKET_AUDIO_MP2: return "AUDIO MP2"; case TAV_PACKET_AUDIO_MP2: return "AUDIO MP2";
case TAV_PACKET_AUDIO_PCM8: return "AUDIO PCM8 (zstd)"; case TAV_PACKET_AUDIO_PCM8: return "AUDIO PCM8 (zstd)";
case TAV_PACKET_AUDIO_TAD: return "AUDIO TAD (zstd)"; case TAV_PACKET_AUDIO_TAD: return "AUDIO TAD (zstd)";
case TAV_PACKET_SUBTITLE: return "SUBTITLE (Simple)"; case TAV_PACKET_SUBTITLE: return "SUBTITLE (SSF frame-locked)";
case TAV_PACKET_SUBTITLE_KAR: return "SUBTITLE (Karaoke)"; case TAV_PACKET_SUBTITLE_TC: return "SUBTITLE (SSF-TC timecoded)";
case TAV_PACKET_AUDIO_TRACK: return "AUDIO TRACK (Separate MP2)"; case TAV_PACKET_AUDIO_TRACK: return "AUDIO TRACK (Separate MP2)";
case TAV_PACKET_EXIF: return "METADATA (EXIF)"; case TAV_PACKET_EXIF: return "METADATA (EXIF)";
case TAV_PACKET_ID3V1: return "METADATA (ID3v1)"; case TAV_PACKET_ID3V1: return "METADATA (ID3v1)";
@@ -151,7 +151,7 @@ int should_display_packet(uint8_t type, display_options_t *opts) {
(type >= 0x70 && type <= 0x7F))) return 1; (type >= 0x70 && type <= 0x7F))) return 1;
if (opts->show_audio && (type == TAV_PACKET_AUDIO_MP2 || type == TAV_PACKET_AUDIO_PCM8 || if (opts->show_audio && (type == TAV_PACKET_AUDIO_MP2 || type == TAV_PACKET_AUDIO_PCM8 ||
type == TAV_PACKET_AUDIO_TAD || type == TAV_PACKET_AUDIO_TRACK)) return 1; type == TAV_PACKET_AUDIO_TAD || type == TAV_PACKET_AUDIO_TRACK)) return 1;
if (opts->show_subtitles && (type == TAV_PACKET_SUBTITLE || type == TAV_PACKET_SUBTITLE_KAR)) return 1; if (opts->show_subtitles && (type == TAV_PACKET_SUBTITLE || type == TAV_PACKET_SUBTITLE_TC)) return 1;
if (opts->show_timecode && type == TAV_PACKET_TIMECODE) return 1; if (opts->show_timecode && type == TAV_PACKET_TIMECODE) return 1;
if (opts->show_metadata && (type >= 0xE0 && type <= 0xE4)) return 1; if (opts->show_metadata && (type >= 0xE0 && type <= 0xE4)) return 1;
if (opts->show_sync && (type == TAV_PACKET_SYNC || type == TAV_PACKET_SYNC_NTSC)) return 1; if (opts->show_sync && (type == TAV_PACKET_SYNC || type == TAV_PACKET_SYNC_NTSC)) return 1;
@@ -160,7 +160,7 @@ int should_display_packet(uint8_t type, display_options_t *opts) {
return 0; return 0;
} }
void print_subtitle_packet(FILE *fp, uint32_t size, int is_karaoke, int verbose) { void print_subtitle_packet(FILE *fp, uint32_t size, int is_timecoded, int verbose) {
if (!verbose) { if (!verbose) {
fseek(fp, size, SEEK_CUR); fseek(fp, size, SEEK_CUR);
return; return;
@@ -174,10 +174,26 @@ void print_subtitle_packet(FILE *fp, uint32_t size, int is_karaoke, int verbose)
index |= (byte << (i * 8)); index |= (byte << (i * 8));
} }
// Read timecode if SSF-TC (0x31)
uint64_t timecode_ns = 0;
int header_size = 4; // 3 bytes index + 1 byte opcode
if (is_timecoded) {
uint8_t timecode_bytes[8];
if (fread(timecode_bytes, 1, 8, fp) != 8) return;
for (int i = 0; i < 8; i++) {
timecode_ns |= ((uint64_t)timecode_bytes[i]) << (i * 8);
}
header_size += 8; // Add 8 bytes for timecode
}
uint8_t opcode; uint8_t opcode;
if (fread(&opcode, 1, 1, fp) != 1) return; if (fread(&opcode, 1, 1, fp) != 1) return;
printf(" [Index=%u, Opcode=0x%02X", index, opcode); printf(" [Index=%u", index);
if (is_timecoded) {
printf(", Time=%.3fs", timecode_ns / 1000000000.0);
}
printf(", Opcode=0x%02X", opcode);
switch (opcode) { switch (opcode) {
case 0x01: printf(" (SHOW)"); break; case 0x01: printf(" (SHOW)"); break;
@@ -193,7 +209,7 @@ void print_subtitle_packet(FILE *fp, uint32_t size, int is_karaoke, int verbose)
printf("]"); printf("]");
// Read and display text content for SHOW commands // Read and display text content for SHOW commands
int remaining = size - 4; // Already read 3 (index) + 1 (opcode) int remaining = size - header_size; // Already read index + timecode (if any) + opcode
if ((opcode == 0x01 || (opcode >= 0x10 && opcode <= 0x2F) || (opcode >= 0x30 && opcode <= 0x41)) && remaining > 0) { if ((opcode == 0x01 || (opcode >= 0x10 && opcode <= 0x2F) || (opcode >= 0x30 && opcode <= 0x41)) && remaining > 0) {
char *text = malloc(remaining + 1); char *text = malloc(remaining + 1);
if (text && fread(text, 1, remaining, fp) == remaining) { if (text && fread(text, 1, remaining, fp) == remaining) {
@@ -788,14 +804,14 @@ static const char* VERDESC[] = {"null", "YCoCg tiled, uniform", "ICtCp tiled, un
} }
case TAV_PACKET_SUBTITLE: case TAV_PACKET_SUBTITLE:
case TAV_PACKET_SUBTITLE_KAR: { case TAV_PACKET_SUBTITLE_TC: {
stats.subtitle_count++; stats.subtitle_count++;
uint32_t size; uint32_t size;
if (fread(&size, sizeof(uint32_t), 1, fp) != 1) break; if (fread(&size, sizeof(uint32_t), 1, fp) != 1) break;
if (!opts.summary_only && display) { if (!opts.summary_only && display) {
printf(" - size=%u bytes", size); printf(" - size=%u bytes", size);
print_subtitle_packet(fp, size, packet_type == TAV_PACKET_SUBTITLE_KAR, opts.verbose); print_subtitle_packet(fp, size, packet_type == TAV_PACKET_SUBTITLE_TC, opts.verbose);
} else { } else {
fseek(fp, size, SEEK_CUR); fseek(fp, size, SEEK_CUR);
} }