TAV: experimental separate audio format mode

This commit is contained in:
minjaesong
2025-10-22 09:33:15 +09:00
parent 4eec98cdca
commit 758b134abd
6 changed files with 278 additions and 25 deletions

View File

@@ -29,7 +29,11 @@ const TAV_PACKET_IFRAME = 0x10
const TAV_PACKET_PFRAME = 0x11 const TAV_PACKET_PFRAME = 0x11
const TAV_PACKET_GOP_UNIFIED = 0x12 // Unified 3D DWT GOP (temporal + spatial) const TAV_PACKET_GOP_UNIFIED = 0x12 // Unified 3D DWT GOP (temporal + spatial)
const TAV_PACKET_AUDIO_MP2 = 0x20 const TAV_PACKET_AUDIO_MP2 = 0x20
const TAV_PACKET_AUDIO_NATIVE = 0x21
const TAV_PACKET_AUDIO_PCM_16LE = 0x22
const TAV_PACKET_AUDIO_ADPCM = 0x23
const TAV_PACKET_SUBTITLE = 0x30 const TAV_PACKET_SUBTITLE = 0x30
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)
const TAV_PACKET_TIMECODE = 0xFD const TAV_PACKET_TIMECODE = 0xFD
@@ -498,6 +502,12 @@ let asyncDecodePtr = 0 // Compressed data pointer to free after decode
let asyncDecodeStartTime = 0 // When async decode started (for diagnostics) let asyncDecodeStartTime = 0 // When async decode started (for diagnostics)
let shouldReadPackets = true // Gate packet reading: false when all 3 buffers are full let shouldReadPackets = true // Gate packet reading: false when all 3 buffers are full
// Pre-decoded audio state (for bundled audio packet 0x40)
let predecodedPcmBuffer = null // Buffer holding pre-decoded PCM data
let predecodedPcmSize = 0 // Total size of pre-decoded PCM
let predecodedPcmOffset = 0 // Current position in PCM buffer for streaming
const PCM_UPLOAD_CHUNK = 2304 // Upload 1152 stereo samples per chunk (one MP2 frame worth)
let cueElements = [] let cueElements = []
let currentCueIndex = -1 // Track current cue position let currentCueIndex = -1 // Track current cue position
let iframePositions = [] // Track I-frame positions for seeking: [{offset, frameNum}] let iframePositions = [] // Track I-frame positions for seeking: [{offset, frameNum}]
@@ -526,6 +536,14 @@ function cleanupAsyncDecode() {
} }
decodingGopData = null decodingGopData = null
// Free pre-decoded PCM buffer if present
if (predecodedPcmBuffer !== null) {
sys.free(predecodedPcmBuffer)
predecodedPcmBuffer = null
predecodedPcmSize = 0
predecodedPcmOffset = 0
}
// Reset GOP playback state // Reset GOP playback state
currentGopSize = 0 currentGopSize = 0
currentGopFrameIndex = 0 currentGopFrameIndex = 0
@@ -1186,8 +1204,64 @@ try {
shouldReadPackets = false shouldReadPackets = false
} }
} }
else if (packetType === TAV_PACKET_AUDIO_BUNDLED) {
// Bundled audio packet - entire MP2 file pre-decoded to PCM
// This removes MP2 decoding from the frame timing loop
let totalAudioSize = seqread.readInt()
if (!mp2Initialised) {
mp2Initialised = true
audio.mp2Init()
}
if (interactive) {
serial.println(`Pre-decoding ${(totalAudioSize / 1024).toFixed(1)} KB of MP2 audio...`)
}
// Allocate temporary buffer for MP2 data
let mp2Buffer = sys.malloc(totalAudioSize)
seqread.readBytes(totalAudioSize, mp2Buffer)
// Estimate PCM size: MP2 ~10:1 compression ratio, so PCM ~10x larger
// Each MP2 frame decodes to 2304 bytes PCM (1152 stereo 16-bit samples)
// Allocate generous buffer (12x MP2 size to be safe)
const estimatedPcmSize = totalAudioSize * 12
predecodedPcmBuffer = sys.malloc(estimatedPcmSize)
predecodedPcmSize = 0
predecodedPcmOffset = 0
// Decode entire MP2 file to PCM
const MP2_DECODE_CHUNK = 2304 // ~2 MP2 frames at 192kbps
let srcOffset = 0
while (srcOffset < totalAudioSize) {
let remaining = totalAudioSize - srcOffset
let chunkSize = Math.min(MP2_DECODE_CHUNK, remaining)
// Copy MP2 chunk to audio peripheral decode buffer
sys.memcpy(mp2Buffer + srcOffset, SND_BASE_ADDR - 2368, chunkSize)
// Decode to PCM (goes to SND_BASE_ADDR)
audio.mp2Decode()
// Copy decoded PCM from peripheral to our storage buffer
// Each decode produces 2304 bytes of PCM
sys.memcpy(SND_BASE_ADDR, predecodedPcmBuffer + predecodedPcmSize, 2304)
predecodedPcmSize += 2304
srcOffset += chunkSize
}
// Free MP2 buffer (no longer needed)
sys.free(mp2Buffer)
if (interactive) {
serial.println(`Pre-decoded ${(predecodedPcmSize / 1024).toFixed(1)} KB PCM (from ${(totalAudioSize / 1024).toFixed(1)} KB MP2)`)
}
}
else if (packetType === TAV_PACKET_AUDIO_MP2) { else if (packetType === TAV_PACKET_AUDIO_MP2) {
// MP2 Audio packet // Legacy MP2 Audio packet (for backwards compatibility)
let audioLen = seqread.readInt() let audioLen = seqread.readInt()
if (!mp2Initialised) { if (!mp2Initialised) {
@@ -1200,6 +1274,16 @@ try {
audio.mp2UploadDecoded(0) audio.mp2UploadDecoded(0)
} }
else if (packetType === TAV_PACKET_AUDIO_NATIVE) {
// PCM length must not exceed 65536 bytes!
let zstdLen = seqread.readInt()
let zstdPtr = sys.malloc(zstdLen)
seqread.readBytes(zstdLen, zstdPtr)
let pcmLen = gzip.decompFromTo(zstdPtr, zstdLen, SND_BASE_ADDR - 65536)
if (pcmLen > 65536) throw Error(`PCM data too long -- got ${pcmLen} bytes`)
audio.setSampleUploadLength(0, pcmLen)
audio.startSampleUpload(0)
}
else if (packetType === TAV_PACKET_SUBTITLE) { else if (packetType === TAV_PACKET_SUBTITLE) {
// Subtitle packet - same format as TEV // Subtitle packet - same format as TEV
let packetSize = seqread.readInt() let packetSize = seqread.readInt()
@@ -1359,6 +1443,21 @@ try {
frameCount++ frameCount++
trueFrameCount++ trueFrameCount++
// Upload pre-decoded PCM audio if available (keeps audio queue fed)
if (predecodedPcmBuffer !== null && predecodedPcmOffset < predecodedPcmSize) {
let remaining = predecodedPcmSize - predecodedPcmOffset
let uploadSize = Math.min(PCM_UPLOAD_CHUNK, remaining)
// Copy PCM chunk to audio peripheral memory
sys.memcpy(predecodedPcmBuffer + predecodedPcmOffset, SND_BASE_ADDR, uploadSize)
// Set upload parameters and trigger upload to queue
audio.setSampleUploadLength(0, uploadSize)
audio.startSampleUpload(0)
predecodedPcmOffset += uploadSize
}
// Schedule next frame // Schedule next frame
nextFrameTime += (frametime) // frametime is in nanoseconds from header nextFrameTime += (frametime) // frametime is in nanoseconds from header
} }
@@ -1456,6 +1555,11 @@ finally {
sys.free(NEXT_FIELD_BUFFER) sys.free(NEXT_FIELD_BUFFER)
} }
// Free pre-decoded PCM buffer if present
if (predecodedPcmBuffer !== null) {
sys.free(predecodedPcmBuffer)
}
con.curs_set(1) con.curs_set(1)
con.clear() con.clear()

View File

@@ -960,9 +960,19 @@ transmission capability, and region-of-interest coding.
0x11: P-frame (delta/skip frame) 0x11: P-frame (delta/skip frame)
0x12: GOP Unified (temporal 3D DWT with unified preprocessing) 0x12: GOP Unified (temporal 3D DWT with unified preprocessing)
0x1F: (prohibited) 0x1F: (prohibited)
<audio packets>
0x20: MP2 audio packet 0x20: MP2 audio packet
0x21: Zstd-compressed 8-bit PCM (32 KHz, audio hardware's native format)
0x22: Zstd-compressed 16-bit PCM (32 KHz, little endian)
0x23: Zstd-compressed ADPCM
<subtitles>
0x30: Subtitle in "Simple" format 0x30: Subtitle in "Simple" format
0x31: Subtitle in "Karaoke" format 0x31: Subtitle in "Karaoke" format
<synchronised tracks>
0x40: MP2 audio track
0x41: Zstd-compressed 8-bit PCM (32 KHz, audio hardware's native format)
0x42: Zstd-compressed 16-bit PCM (32 KHz, little endian)
0x43: Zstd-compressed ADPCM
<multiplexed video> <multiplexed video>
0x70/71: Video channel 2 I/P-frame 0x70/71: Video channel 2 I/P-frame
0x72/73: Video channel 3 I/P-frame 0x72/73: Video channel 3 I/P-frame

View File

@@ -3,6 +3,7 @@ package net.torvald.tsvm
import com.badlogic.gdx.utils.compression.Lzma import com.badlogic.gdx.utils.compression.Lzma
import io.airlift.compress.zstd.ZstdInputStream import io.airlift.compress.zstd.ZstdInputStream
import io.airlift.compress.zstd.ZstdOutputStream import io.airlift.compress.zstd.ZstdOutputStream
import net.torvald.UnsafeHelper
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.toUint import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.toUint
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
@@ -19,30 +20,51 @@ class CompressorDelegate(private val vm: VM) {
*/ */
fun compFromTo(input: Int, len: Int, output: Int): Int { fun compFromTo(input: Int, len: Int, output: Int): Int {
val inbytes = ByteArray(len) { vm.peek(input.toLong() + it)!! } val inbytes = ByteArray(len) { vm.peek(input.toLong() + it)!! }
comp(inbytes).let { val bytes = comp(inbytes)
it.forEachIndexed { index, byte -> vm.getDev(output.toLong(), bytes.size.toLong(), true).let {
vm.poke(output.toLong() + index, byte) if (it != null) {
val bytesReversed = bytes.reversedArray() // copy over reversed bytes starting from the end of the destination
UnsafeHelper.memcpyRaw(bytesReversed, UnsafeHelper.getArrayOffset(bytesReversed), null, output.toLong() - bytes.size, bytes.size.toLong())
}
else {
bytes.forEachIndexed { index, byte ->
vm.poke(output.toLong() + index, byte)
}
} }
return it.size
} }
return bytes.size
} }
fun compTo(str: String, output: Int): Int { fun compTo(str: String, output: Int): Int {
comp(str).let { val bytes = comp(str)
it.forEachIndexed { index, byte -> vm.getDev(output.toLong(), bytes.size.toLong(), true).let {
vm.poke(output.toLong() + index, byte) if (it != null) {
val bytesReversed = bytes.reversedArray() // copy over reversed bytes starting from the end of the destination
UnsafeHelper.memcpyRaw(bytesReversed, UnsafeHelper.getArrayOffset(bytesReversed), null, output.toLong() - bytes.size, bytes.size.toLong())
}
else {
bytes.forEachIndexed { index, byte ->
vm.poke(output.toLong() + index, byte)
}
} }
return it.size
} }
return bytes.size
} }
fun compTo(ba: ByteArray, output: Int): Int { fun compTo(ba: ByteArray, output: Int): Int {
comp(ba).let { val bytes = comp(ba)
it.forEachIndexed { index, byte -> vm.getDev(output.toLong(), bytes.size.toLong(), true).let {
vm.poke(output.toLong() + index, byte) if (it != null) {
val bytesReversed = bytes.reversedArray() // copy over reversed bytes starting from the end of the destination
UnsafeHelper.memcpyRaw(bytesReversed, UnsafeHelper.getArrayOffset(bytesReversed), null, output.toLong() - bytes.size, bytes.size.toLong())
}
else {
bytes.forEachIndexed { index, byte ->
vm.poke(output.toLong() + index, byte)
}
} }
return it.size
} }
return bytes.size
} }
@@ -51,16 +73,32 @@ class CompressorDelegate(private val vm: VM) {
fun decompTo(str: String, pointer: Int): Int { fun decompTo(str: String, pointer: Int): Int {
val bytes = decomp(str) val bytes = decomp(str)
bytes.forEachIndexed { index, byte -> vm.getDev(pointer.toLong(), bytes.size.toLong(), true).let {
vm.poke(pointer.toLong() + index, byte) if (it != null) {
val bytesReversed = bytes.reversedArray() // copy over reversed bytes starting from the end of the destination
UnsafeHelper.memcpyRaw(bytesReversed, UnsafeHelper.getArrayOffset(bytesReversed), null, pointer.toLong() - bytes.size, bytes.size.toLong())
}
else {
bytes.forEachIndexed { index, byte ->
vm.poke(pointer.toLong() + index, byte)
}
}
} }
return bytes.size return bytes.size
} }
fun decompTo(ba: ByteArray, pointer: Int): Int { fun decompTo(ba: ByteArray, pointer: Int): Int {
val bytes = decomp(ba) val bytes = decomp(ba)
bytes.forEachIndexed { index, byte -> vm.getDev(pointer.toLong(), bytes.size.toLong(), true).let {
vm.poke(pointer.toLong() + index, byte) if (it != null) {
val bytesReversed = bytes.reversedArray() // copy over reversed bytes starting from the end of the destination
UnsafeHelper.memcpyRaw(bytesReversed, UnsafeHelper.getArrayOffset(bytesReversed), null, pointer.toLong() - bytes.size, bytes.size.toLong())
}
else {
bytes.forEachIndexed { index, byte ->
vm.poke(pointer.toLong() + index, byte)
}
}
} }
return bytes.size return bytes.size
} }
@@ -70,12 +108,19 @@ class CompressorDelegate(private val vm: VM) {
*/ */
fun decompFromTo(input: Int, len: Int, output: Int): Int { fun decompFromTo(input: Int, len: Int, output: Int): Int {
val inbytes = ByteArray(len) { vm.peek(input.toLong() + it)!! } val inbytes = ByteArray(len) { vm.peek(input.toLong() + it)!! }
decomp(inbytes).let { val bytes = decomp(inbytes)
it.forEachIndexed { index, byte -> vm.getDev(output.toLong(), bytes.size.toLong(), true).let {
vm.poke(output.toLong() + index, byte) if (it != null) {
val bytesReversed = bytes.reversedArray() // copy over reversed bytes starting from the end of the destination
UnsafeHelper.memcpyRaw(bytesReversed, UnsafeHelper.getArrayOffset(bytesReversed), null, output.toLong() - bytes.size, bytes.size.toLong())
}
else {
bytes.forEachIndexed { index, byte ->
vm.poke(output.toLong() + index, byte)
}
} }
return it.size
} }
return bytes.size
} }
companion object { companion object {

View File

@@ -725,7 +725,7 @@ class VM(
private fun relPtrInDev(from: Long, len: Long, start: Int, end: Int) = private fun relPtrInDev(from: Long, len: Long, start: Int, end: Int) =
(from in start..end && (from + len) in start..end) (from in start..end && (from + len) in start..end)
private fun getDev(from: Long, len: Long, isDest: Boolean): Long? { internal fun getDev(from: Long, len: Long, isDest: Boolean): Long? {
return if (from >= 0) usermem.ptr + from return if (from >= 0) usermem.ptr + from
// MMIO area // MMIO area
else if (from in -1048576..-1 && (from - len) in -1048577..-1) { else if (from in -1048576..-1 && (from - len) in -1048577..-1) {
@@ -745,6 +745,7 @@ class VM(
else if (dev is AudioAdapter) { else if (dev is AudioAdapter) {
if (relPtrInDev(fromRel, len, 64, 2367)) dev.mediaDecodedBin.ptr + fromRel - 64 if (relPtrInDev(fromRel, len, 64, 2367)) dev.mediaDecodedBin.ptr + fromRel - 64
else if (relPtrInDev(fromRel, len, 2368, 4096)) dev.mediaFrameBin.ptr + fromRel - 2368 else if (relPtrInDev(fromRel, len, 2368, 4096)) dev.mediaFrameBin.ptr + fromRel - 2368
else if (relPtrInDev(fromRel, len, 65536, 131072)) dev.pcmBin.ptr + fromRel - 65536
else null else null
} }
else if (dev is GraphicsAdapter) { else if (dev is GraphicsAdapter) {
@@ -770,7 +771,9 @@ class VM(
if (relPtrInDev(fromRel, len, 0, 250879)) dev.framebuffer.ptr + fromRel - 0 if (relPtrInDev(fromRel, len, 0, 250879)) dev.framebuffer.ptr + fromRel - 0
else if (relPtrInDev(fromRel, len, 250880, 251903)) dev.unusedArea.ptr + fromRel - 250880 else if (relPtrInDev(fromRel, len, 250880, 251903)) dev.unusedArea.ptr + fromRel - 250880
else if (relPtrInDev(fromRel, len, 253950, 261631)) dev.textArea.ptr + fromRel - 253950 else if (relPtrInDev(fromRel, len, 253950, 261631)) dev.textArea.ptr + fromRel - 253950
else if (relPtrInDev(fromRel, len, 262144, 513023)) dev.framebuffer2?.ptr?.plus(fromRel)?.minus(253950) else if (relPtrInDev(fromRel, len, 262144, 513023)) dev.framebuffer2?.ptr?.plus(fromRel)?.minus(262144)
else if (relPtrInDev(fromRel, len, 524288, 775167)) dev.framebuffer3?.ptr?.plus(fromRel)?.minus(524288)
else if (relPtrInDev(fromRel, len, 786432, 1037371)) dev.framebuffer4?.ptr?.plus(fromRel)?.minus(786432)
else null else null
} }
else if (dev is RamBank) { else if (dev is RamBank) {

View File

@@ -55,6 +55,7 @@
#define TAV_PACKET_BFRAME_ADAPTIVE 0x17 // B-frame with adaptive quad-tree block partitioning (bidirectional prediction) #define TAV_PACKET_BFRAME_ADAPTIVE 0x17 // B-frame with adaptive quad-tree block partitioning (bidirectional prediction)
#define TAV_PACKET_AUDIO_MP2 0x20 // MP2 audio #define TAV_PACKET_AUDIO_MP2 0x20 // MP2 audio
#define TAV_PACKET_SUBTITLE 0x30 // Subtitle packet #define TAV_PACKET_SUBTITLE 0x30 // Subtitle packet
#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)
#define TAV_PACKET_TIMECODE 0xFD // Timecode packet #define TAV_PACKET_TIMECODE 0xFD // Timecode packet
@@ -122,9 +123,9 @@ static int needs_alpha_channel(int channel_layout) {
#define MOTION_THRESHOLD 24.0f // Flush if motion exceeds 24 pixels in any direction #define MOTION_THRESHOLD 24.0f // Flush if motion exceeds 24 pixels in any direction
// Audio/subtitle constants (reused from TEV) // Audio/subtitle constants (reused from TEV)
#define MP2_SAMPLE_RATE 32000 #define TSVM_AUDIO_SAMPLE_RATE 32000
#define MP2_DEFAULT_PACKET_SIZE 1152 #define MP2_DEFAULT_PACKET_SIZE 1152
#define PACKET_AUDIO_TIME ((double)MP2_DEFAULT_PACKET_SIZE / MP2_SAMPLE_RATE) #define PACKET_AUDIO_TIME ((double)MP2_DEFAULT_PACKET_SIZE / TSVM_AUDIO_SAMPLE_RATE)
#define MAX_SUBTITLE_LENGTH 2048 #define MAX_SUBTITLE_LENGTH 2048
int debugDumpMade = 0; int debugDumpMade = 0;
@@ -1742,6 +1743,7 @@ typedef struct tav_encoder_s {
int grain_synthesis; // 1 = enable grain synthesis (default), 0 = disable int grain_synthesis; // 1 = enable grain synthesis (default), 0 = disable
int use_delta_encoding; int use_delta_encoding;
int delta_haar_levels; // Number of Haar DWT levels to apply to delta coefficients (0 = disabled) int delta_haar_levels; // Number of Haar DWT levels to apply to delta coefficients (0 = disabled)
int separate_audio_track; // 1 = write entire MP2 file as packet 0x40 after header, 0 = interleave audio (default)
// Frame buffers - ping-pong implementation // Frame buffers - ping-pong implementation
uint8_t *frame_rgb[2]; // [0] and [1] alternate between current and previous uint8_t *frame_rgb[2]; // [0] and [1] alternate between current and previous
@@ -2253,6 +2255,7 @@ static void show_usage(const char *program_name) {
printf(" -c, --channel-layout N Channel layout: 0=Y-Co-Cg, 1=Y-Co-Cg-A, 2=Y-only, 3=Y-A, 4=Co-Cg, 5=Co-Cg-A (default: 0)\n"); printf(" -c, --channel-layout N Channel layout: 0=Y-Co-Cg, 1=Y-Co-Cg-A, 2=Y-only, 3=Y-A, 4=Co-Cg, 5=Co-Cg-A (default: 0)\n");
printf(" -a, --arate N MP2 audio bitrate in kbps (overrides quality-based audio rate)\n"); printf(" -a, --arate N MP2 audio bitrate in kbps (overrides quality-based audio rate)\n");
printf(" Valid values: 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384\n"); printf(" Valid values: 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384\n");
printf(" --separate-audio-track Write entire MP2 file as single packet 0x40 (instead of interleaved)\n");
printf(" -S, --subtitles FILE SubRip (.srt) or SAMI (.smi) subtitle file\n"); printf(" -S, --subtitles FILE SubRip (.srt) or SAMI (.smi) subtitle file\n");
printf(" --fontrom-lo FILE Low font ROM file for internationalised subtitles\n"); printf(" --fontrom-lo FILE Low font ROM file for internationalised subtitles\n");
printf(" --fontrom-hi FILE High font ROM file for internationalised subtitles\n"); printf(" --fontrom-hi FILE High font ROM file for internationalised subtitles\n");
@@ -2340,6 +2343,7 @@ static tav_encoder_t* create_encoder(void) {
enc->grain_synthesis = 0; // Default: disable grain synthesis (only do it on the decoder) enc->grain_synthesis = 0; // Default: disable grain synthesis (only do it on the decoder)
enc->use_delta_encoding = 0; enc->use_delta_encoding = 0;
enc->delta_haar_levels = TEMPORAL_DECOMP_LEVEL; enc->delta_haar_levels = TEMPORAL_DECOMP_LEVEL;
enc->separate_audio_track = 0; // Default: interleave audio packets
// GOP / temporal DWT settings // GOP / temporal DWT settings
enc->enable_temporal_dwt = 1; // Mutually exclusive with use_delta_encoding enc->enable_temporal_dwt = 1; // Mutually exclusive with use_delta_encoding
@@ -8595,8 +8599,65 @@ static long write_extended_header(tav_encoder_t *enc) {
return endt_offset + 4 + 1; // 4 bytes for "ENDT", 1 byte for type return endt_offset + 4 + 1; // 4 bytes for "ENDT", 1 byte for type
} }
// Write separate audio track packet (0x40) - entire MP2 file in one packet
static int write_separate_audio_track(tav_encoder_t *enc, FILE *output) {
if (!enc->has_audio || !enc->mp2_file) {
return 0; // No audio to write
}
// Get file size
fseek(enc->mp2_file, 0, SEEK_END);
size_t mp2_size = ftell(enc->mp2_file);
fseek(enc->mp2_file, 0, SEEK_SET);
if (mp2_size == 0) {
fprintf(stderr, "Warning: MP2 file is empty\n");
return 0;
}
// Allocate buffer for entire MP2 file
uint8_t *mp2_buffer = malloc(mp2_size);
if (!mp2_buffer) {
fprintf(stderr, "Error: Failed to allocate buffer for separate audio track (%zu bytes)\n", mp2_size);
return 0;
}
// Read entire MP2 file
size_t bytes_read = fread(mp2_buffer, 1, mp2_size, enc->mp2_file);
if (bytes_read != mp2_size) {
fprintf(stderr, "Error: Failed to read MP2 file (expected %zu bytes, got %zu)\n", mp2_size, bytes_read);
free(mp2_buffer);
return 0;
}
// Write packet type 0x40
uint8_t packet_type = TAV_PACKET_AUDIO_TRACK;
fwrite(&packet_type, 1, 1, output);
// Write payload size (uint32)
uint32_t payload_size = (uint32_t)mp2_size;
fwrite(&payload_size, sizeof(uint32_t), 1, output);
// Write MP2 data
fwrite(mp2_buffer, 1, mp2_size, output);
// Cleanup
free(mp2_buffer);
if (enc->verbose) {
printf("Separate audio track written: %zu bytes (packet 0x40)\n", mp2_size);
}
return 1;
}
// Process audio for current frame (copied and adapted from TEV) // Process audio for current frame (copied and adapted from TEV)
static int process_audio(tav_encoder_t *enc, int frame_num, FILE *output) { static int process_audio(tav_encoder_t *enc, int frame_num, FILE *output) {
// Skip if separate audio track mode is enabled
if (enc->separate_audio_track) {
return 1;
}
if (!enc->has_audio || !enc->mp2_file || enc->audio_remaining <= 0) { if (!enc->has_audio || !enc->mp2_file || enc->audio_remaining <= 0) {
return 1; return 1;
} }
@@ -8698,6 +8759,11 @@ static int process_audio(tav_encoder_t *enc, int frame_num, FILE *output) {
// Process audio for a GOP (multiple frames at once) // Process audio for a GOP (multiple frames at once)
// Accumulates deficit for N frames and emits all necessary audio packets // Accumulates deficit for N frames and emits all necessary audio packets
static int process_audio_for_gop(tav_encoder_t *enc, int *frame_numbers, int num_frames, FILE *output) { static int process_audio_for_gop(tav_encoder_t *enc, int *frame_numbers, int num_frames, FILE *output) {
// Skip if separate audio track mode is enabled
if (enc->separate_audio_track) {
return 1;
}
if (!enc->has_audio || !enc->mp2_file || enc->audio_remaining <= 0 || num_frames == 0) { if (!enc->has_audio || !enc->mp2_file || enc->audio_remaining <= 0 || num_frames == 0) {
return 1; return 1;
} }
@@ -9081,6 +9147,7 @@ int main(int argc, char *argv[]) {
{"bframes", required_argument, 0, 1023}, {"bframes", required_argument, 0, 1023},
{"gop-size", required_argument, 0, 1024}, {"gop-size", required_argument, 0, 1024},
{"ezbc", no_argument, 0, 1025}, {"ezbc", no_argument, 0, 1025},
{"separate-audio-track", no_argument, 0, 1026},
{"help", no_argument, 0, '?'}, {"help", no_argument, 0, '?'},
{0, 0, 0, 0} {0, 0, 0, 0}
}; };
@@ -9290,6 +9357,10 @@ int main(int argc, char *argv[]) {
enc->enable_ezbc = 1; enc->enable_ezbc = 1;
printf("EZBC (Embedded Zero Block Coding) enabled for significance maps\n"); printf("EZBC (Embedded Zero Block Coding) enabled for significance maps\n");
break; break;
case 1026: // --separate-audio-track
enc->separate_audio_track = 1;
printf("Separate audio track mode enabled (packet 0x40)\n");
break;
case 'a': case 'a':
int bitrate = atoi(optarg); int bitrate = atoi(optarg);
int valid_bitrate = validate_mp2_bitrate(bitrate); int valid_bitrate = validate_mp2_bitrate(bitrate);
@@ -9461,6 +9532,11 @@ int main(int argc, char *argv[]) {
gettimeofday(&enc->start_time, NULL); gettimeofday(&enc->start_time, NULL);
enc->extended_header_offset = write_extended_header(enc); enc->extended_header_offset = write_extended_header(enc);
// Write separate audio track if enabled (packet 0x40)
if (enc->separate_audio_track) {
write_separate_audio_track(enc, enc->output_fp);
}
// Write font ROM packets if provided // Write font ROM packets if provided
if (enc->fontrom_lo_file) { if (enc->fontrom_lo_file) {
if (write_fontrom_packet(enc->output_fp, enc->fontrom_lo_file, 0x80) != 0) { if (write_fontrom_packet(enc->output_fp, enc->fontrom_lo_file, 0x80) != 0) {

View File

@@ -26,6 +26,7 @@
#define TAV_PACKET_AUDIO_MP2 0x20 #define TAV_PACKET_AUDIO_MP2 0x20
#define TAV_PACKET_SUBTITLE 0x30 #define TAV_PACKET_SUBTITLE 0x30
#define TAV_PACKET_SUBTITLE_KAR 0x31 #define TAV_PACKET_SUBTITLE_KAR 0x31
#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
#define TAV_PACKET_VIDEO_CH3_I 0x72 #define TAV_PACKET_VIDEO_CH3_I 0x72
@@ -108,6 +109,7 @@ 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_SUBTITLE: return "SUBTITLE (Simple)"; case TAV_PACKET_SUBTITLE: return "SUBTITLE (Simple)";
case TAV_PACKET_SUBTITLE_KAR: return "SUBTITLE (Karaoke)"; case TAV_PACKET_SUBTITLE_KAR: return "SUBTITLE (Karaoke)";
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)";
case TAV_PACKET_ID3V2: return "METADATA (ID3v2)"; case TAV_PACKET_ID3V2: return "METADATA (ID3v2)";
@@ -629,6 +631,19 @@ int main(int argc, char *argv[]) {
break; break;
} }
case TAV_PACKET_AUDIO_TRACK: {
stats.audio_count++;
uint32_t size;
if (fread(&size, sizeof(uint32_t), 1, fp) != 1) break;
stats.total_audio_bytes += size;
if (!opts.summary_only && display) {
printf(" - size=%u bytes (separate track)", size);
}
fseek(fp, size, SEEK_CUR);
break;
}
case TAV_PACKET_SUBTITLE: case TAV_PACKET_SUBTITLE:
case TAV_PACKET_SUBTITLE_KAR: { case TAV_PACKET_SUBTITLE_KAR: {
stats.subtitle_count++; stats.subtitle_count++;