From 2cdd731c3b8b8dda0accf9384c064aaa4dc14003 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sun, 10 May 2026 05:56:56 +0900 Subject: [PATCH] video_decoder removed; fix video regression and updated to no-zstd --- .idea/libraries/badlogicgames_gdx.xml | 11 + .../badlogicgames_gdx_backend_lwjgl3.xml | 62 + 2taud.sh | 12 +- .../TerranBASICexecutable.iml | 2 + assets/disk0/tvdos/bin/playtad.js | 6 +- assets/disk0/tvdos/bin/playtav.js | 4 +- .../net/torvald/tsvm/AudioJSR223Delegate.kt | 1 + .../torvald/tsvm/GraphicsJSR223Delegate.kt | 27 +- .../torvald/tsvm/peripheral/AudioAdapter.kt | 24 +- tsvm_core/tsvm_core.iml | 2 + tsvm_executable.iml | 2 + video_encoder/Makefile | 221 - video_encoder/TAD_README.md | 350 -- video_encoder/TAV_README.md | 261 -- video_encoder/create_ucf_payload.c | 424 -- video_encoder/encoder_ipf1d.c | 935 ---- video_encoder/encoder_tav_opencv.cpp | 183 - video_encoder/encoder_tav_text.c | 795 ---- video_encoder/encoder_tev.c | 3052 ------------- video_encoder/estimate_affine_from_blocks.cpp | 169 - video_encoder/exponential_numeric_system.ods | Bin 15985 -> 0 bytes video_encoder/include/coefficient_compress.h | 65 - video_encoder/include/decoder_tad.h | 39 - video_encoder/include/encoder_tad.h | 63 - video_encoder/include/entropy_coder.h | 74 - video_encoder/include/tav_avx512.h | 837 ---- video_encoder/include/tav_encoder_lib.h | 295 -- video_encoder/include/tav_simd_dispatch.h | 275 -- video_encoder/include/tav_video_decoder.h | 78 - video_encoder/lib/libfec/ldpc.c | 397 -- video_encoder/lib/libfec/ldpc.h | 68 - video_encoder/lib/libfec/ldpc_payload.c | 478 --- video_encoder/lib/libfec/ldpc_payload.h | 97 - video_encoder/lib/libfec/reed_solomon.c | 417 -- video_encoder/lib/libfec/reed_solomon.h | 82 - video_encoder/lib/libtaddec/decoder_tad.c | 1192 ------ video_encoder/lib/libtadenc/encoder_tad.c | 1291 ------ .../lib/libtavdec/tav_video_decoder.c | 1913 --------- .../lib/libtavenc/tav_encoder_color.c | 255 -- .../lib/libtavenc/tav_encoder_color.h | 67 - video_encoder/lib/libtavenc/tav_encoder_dwt.c | 619 --- video_encoder/lib/libtavenc/tav_encoder_dwt.h | 88 - .../lib/libtavenc/tav_encoder_ezbc.c | 415 -- .../lib/libtavenc/tav_encoder_ezbc.h | 61 - video_encoder/lib/libtavenc/tav_encoder_lib.c | 1528 ------- .../lib/libtavenc/tav_encoder_quantize.c | 635 --- .../lib/libtavenc/tav_encoder_quantize.h | 138 - .../lib/libtavenc/tav_encoder_tile.c | 159 - .../lib/libtavenc/tav_encoder_tile.h | 103 - .../lib/libtavenc/tav_encoder_utils.c | 441 -- .../lib/libtavenc/tav_encoder_utils.h | 165 - video_encoder/range_coder.c | 152 - video_encoder/range_coder.h | 42 - video_encoder/src/decoder_tav.c | 2330 ---------- video_encoder/src/decoder_tav_dt.c | 2180 ---------- video_encoder/src/encoder_tad_standalone.c | 344 -- video_encoder/src/encoder_tav.c | 3796 ----------------- video_encoder/src/encoder_tav_dt.c | 1502 ------- video_encoder/tav_inspector.c | 1307 ------ video_encoder/tav_visualise_coefficients.c | 294 -- video_encoder/tavdt_noise_injector.c | 402 -- video_encoder/test_mesh_roundtrip.cpp | 328 -- video_encoder/test_mesh_warp.cpp | 422 -- 63 files changed, 127 insertions(+), 31850 deletions(-) create mode 100644 .idea/libraries/badlogicgames_gdx.xml create mode 100644 .idea/libraries/badlogicgames_gdx_backend_lwjgl3.xml delete mode 100644 video_encoder/Makefile delete mode 100644 video_encoder/TAD_README.md delete mode 100644 video_encoder/TAV_README.md delete mode 100644 video_encoder/create_ucf_payload.c delete mode 100644 video_encoder/encoder_ipf1d.c delete mode 100644 video_encoder/encoder_tav_opencv.cpp delete mode 100644 video_encoder/encoder_tav_text.c delete mode 100644 video_encoder/encoder_tev.c delete mode 100644 video_encoder/estimate_affine_from_blocks.cpp delete mode 100644 video_encoder/exponential_numeric_system.ods delete mode 100644 video_encoder/include/coefficient_compress.h delete mode 100644 video_encoder/include/decoder_tad.h delete mode 100644 video_encoder/include/encoder_tad.h delete mode 100644 video_encoder/include/entropy_coder.h delete mode 100644 video_encoder/include/tav_avx512.h delete mode 100644 video_encoder/include/tav_encoder_lib.h delete mode 100644 video_encoder/include/tav_simd_dispatch.h delete mode 100644 video_encoder/include/tav_video_decoder.h delete mode 100644 video_encoder/lib/libfec/ldpc.c delete mode 100644 video_encoder/lib/libfec/ldpc.h delete mode 100644 video_encoder/lib/libfec/ldpc_payload.c delete mode 100644 video_encoder/lib/libfec/ldpc_payload.h delete mode 100644 video_encoder/lib/libfec/reed_solomon.c delete mode 100644 video_encoder/lib/libfec/reed_solomon.h delete mode 100644 video_encoder/lib/libtaddec/decoder_tad.c delete mode 100644 video_encoder/lib/libtadenc/encoder_tad.c delete mode 100644 video_encoder/lib/libtavdec/tav_video_decoder.c delete mode 100644 video_encoder/lib/libtavenc/tav_encoder_color.c delete mode 100644 video_encoder/lib/libtavenc/tav_encoder_color.h delete mode 100644 video_encoder/lib/libtavenc/tav_encoder_dwt.c delete mode 100644 video_encoder/lib/libtavenc/tav_encoder_dwt.h delete mode 100644 video_encoder/lib/libtavenc/tav_encoder_ezbc.c delete mode 100644 video_encoder/lib/libtavenc/tav_encoder_ezbc.h delete mode 100644 video_encoder/lib/libtavenc/tav_encoder_lib.c delete mode 100644 video_encoder/lib/libtavenc/tav_encoder_quantize.c delete mode 100644 video_encoder/lib/libtavenc/tav_encoder_quantize.h delete mode 100644 video_encoder/lib/libtavenc/tav_encoder_tile.c delete mode 100644 video_encoder/lib/libtavenc/tav_encoder_tile.h delete mode 100644 video_encoder/lib/libtavenc/tav_encoder_utils.c delete mode 100644 video_encoder/lib/libtavenc/tav_encoder_utils.h delete mode 100644 video_encoder/range_coder.c delete mode 100644 video_encoder/range_coder.h delete mode 100644 video_encoder/src/decoder_tav.c delete mode 100644 video_encoder/src/decoder_tav_dt.c delete mode 100644 video_encoder/src/encoder_tad_standalone.c delete mode 100644 video_encoder/src/encoder_tav.c delete mode 100644 video_encoder/src/encoder_tav_dt.c delete mode 100644 video_encoder/tav_inspector.c delete mode 100644 video_encoder/tav_visualise_coefficients.c delete mode 100644 video_encoder/tavdt_noise_injector.c delete mode 100644 video_encoder/test_mesh_roundtrip.cpp delete mode 100644 video_encoder/test_mesh_warp.cpp diff --git a/.idea/libraries/badlogicgames_gdx.xml b/.idea/libraries/badlogicgames_gdx.xml new file mode 100644 index 0000000..cd1db0b --- /dev/null +++ b/.idea/libraries/badlogicgames_gdx.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/badlogicgames_gdx_backend_lwjgl3.xml b/.idea/libraries/badlogicgames_gdx_backend_lwjgl3.xml new file mode 100644 index 0000000..5302a7a --- /dev/null +++ b/.idea/libraries/badlogicgames_gdx_backend_lwjgl3.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/2taud.sh b/2taud.sh index 6266dff..e44bfcc 100755 --- a/2taud.sh +++ b/2taud.sh @@ -1,8 +1,8 @@ #!/usr/bin/env fish -for f in *.mod; python3 mod2taud.py $f assets/disk0/(basename $f .mod).taud; end -for f in *.s3m; python3 s3m2taud.py $f assets/disk0/(basename $f .s3m).taud; end -for f in *.it; python3 it2taud.py $f assets/disk0/(basename $f .it).taud; end -for f in *.xm; python3 xm2taud.py $f assets/disk0/(basename $f .xm).taud; end -for f in *.mon; python3 mon2taud.py $f assets/disk0/(basename $f .mon).taud; end -for f in *.MON; python3 mon2taud.py $f assets/disk0/(basename $f .MON).taud; end +for f in *.mod; python3 mod2taud.py $f assets/disk0/home/music/(basename $f .mod).taud; end +for f in *.s3m; python3 s3m2taud.py $f assets/disk0/home/music/(basename $f .s3m).taud; end +for f in *.it; python3 it2taud.py $f assets/disk0/home/music/(basename $f .it).taud; end +for f in *.xm; python3 xm2taud.py $f assets/disk0/home/music/(basename $f .xm).taud; end +for f in *.mon; python3 mon2taud.py $f assets/disk0/home/music/(basename $f .mon).taud; end +for f in *.MON; python3 mon2taud.py $f assets/disk0/home/music/(basename $f .MON).taud; end diff --git a/TerranBASICexecutable/TerranBASICexecutable.iml b/TerranBASICexecutable/TerranBASICexecutable.iml index d98a81a..5829148 100644 --- a/TerranBASICexecutable/TerranBASICexecutable.iml +++ b/TerranBASICexecutable/TerranBASICexecutable.iml @@ -10,5 +10,7 @@ + + \ No newline at end of file diff --git a/assets/disk0/tvdos/bin/playtad.js b/assets/disk0/tvdos/bin/playtad.js index cd1bf3a..d43f076 100644 --- a/assets/disk0/tvdos/bin/playtad.js +++ b/assets/disk0/tvdos/bin/playtad.js @@ -1,7 +1,9 @@ const SND_BASE_ADDR = audio.getBaseAddr() const SND_MEM_ADDR = audio.getMemAddr() -const TAD_INPUT_ADDR = SND_MEM_ADDR - 262144 // TAD input buffer (matches TAV packet 0x24) -const TAD_DECODED_ADDR = SND_MEM_ADDR - 262144 + 65536 // TAD decoded buffer +// tadInputBin lives at audio-local offset 917504 and tadDecodedBin at 983040 +// (post-bef85f6 memory map; the old 262144 offset now hits the enlarged sampleBin). +const TAD_INPUT_ADDR = SND_MEM_ADDR - 917504 // TAD input buffer (matches TAV packet 0x24) +const TAD_DECODED_ADDR = SND_MEM_ADDR - 983040 // TAD decoded buffer if (!SND_BASE_ADDR) return 10 diff --git a/assets/disk0/tvdos/bin/playtav.js b/assets/disk0/tvdos/bin/playtav.js index a689f62..390d672 100644 --- a/assets/disk0/tvdos/bin/playtav.js +++ b/assets/disk0/tvdos/bin/playtav.js @@ -1746,7 +1746,9 @@ try { tadInitialised = true } - seqread.readBytes(payloadLen, SND_MEM_ADDR - 262144) + // tadInputBin lives at audio-local offset 917504 (post-bef85f6 memory map); + // the previous 262144 offset now points into the enlarged sampleBin. + seqread.readBytes(payloadLen, SND_MEM_ADDR - 917504) audio.tadDecode() audio.tadUploadDecoded(AUDIO_DEVICE, sampleLen) } diff --git a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt index 1e4d613..fbe39b0 100644 --- a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt @@ -275,6 +275,7 @@ class AudioJSR223Delegate(private val vm: VM) { + // while the following code does work, it was decided that MP3 is "too new" for tsvm and thus removed. /* js-mp3 https://github.com/soundbus-technologies/js-mp3 diff --git a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt index 522cdf0..dac0497 100644 --- a/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/GraphicsJSR223Delegate.kt @@ -5433,6 +5433,18 @@ class GraphicsJSR223Delegate(private val vm: VM) { private val TAV_QLUT = intArrayOf(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,256,264,272,280,288,296,304,312,320,328,336,344,352,360,368,376,384,392,400,408,416,424,432,440,448,456,464,472,480,488,496,504,512,528,544,560,576,592,608,624,640,656,672,688,704,720,736,752,768,784,800,816,832,848,864,880,896,912,928,944,960,976,992,1008,1024,1056,1088,1120,1152,1184,1216,1248,1280,1312,1344,1376,1408,1440,1472,1504,1536,1568,1600,1632,1664,1696,1728,1760,1792,1824,1856,1888,1920,1952,1984,2016,2048,2112,2176,2240,2304,2368,2432,2496,2560,2624,2688,2752,2816,2880,2944,3008,3072,3136,3200,3264,3328,3392,3456,3520,3584,3648,3712,3776,3840,3904,3968,4032,4096) + // Zstd magic = 0x28 0xB5 0x2F 0xFD (little-endian frame magic). + // Newer TAV files default to no Zstd (Video Flags bit 4); detecting the magic + // lets the decoder accept both compressed and raw payloads transparently. + private fun tavDecompressIfZstd(data: ByteArray): ByteArray { + if (data.size >= 4 && + data[0] == 0x28.toByte() && data[1] == 0xB5.toByte() && + data[2] == 0x2F.toByte() && data[3] == 0xFD.toByte()) { + return ZstdInputStream(ByteArrayInputStream(data)).use { it.readBytes() } + } + return data + } + // New tavDecode function that accepts compressed data and decompresses internally fun tavDecodeCompressed(compressedDataPtr: Long, compressedSize: Int, currentRGBAddr: Long, prevRGBAddr: Long, width: Int, height: Int, qIndex: Int, qYGlobal: Int, qCoGlobal: Int, qCgGlobal: Int, channelLayout: Int, @@ -5445,12 +5457,9 @@ class GraphicsJSR223Delegate(private val vm: VM) { } return try { - // Decompress using Zstd - val bais = ByteArrayInputStream(compressedData) - val zis = ZstdInputStream(bais) - val decompressedData = zis.readBytes() - zis.close() - bais.close() + // Decompress with Zstd if the payload starts with the Zstd frame magic; + // otherwise pass through (TAV files written without --zstd-level). + val decompressedData = tavDecompressIfZstd(compressedData) // Allocate buffer for decompressed data val decompressedBuffer = vm.malloc(decompressedData.size) @@ -6725,9 +6734,9 @@ class GraphicsJSR223Delegate(private val vm: VM) { ) val decompressedData = try { - ZstdInputStream(java.io.ByteArrayInputStream(compressedData)).use { zstd -> - zstd.readBytes() - } + // Decompress with Zstd if the payload starts with the Zstd frame magic; + // otherwise pass through (TAV files written without --zstd-level). + tavDecompressIfZstd(compressedData) } catch (e: Exception) { println("ERROR: Zstd decompression failed: ${e.message}") return arrayOf(0, dbgOut) diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index b4a34d2..e80a64f 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -911,24 +911,32 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { ((tadInputBin[offset++].toUint()) shl 8) ) val maxIndex = tadInputBin[offset++].toUint() - val payloadSize = ( + val payloadSizeField = ( (tadInputBin[offset++].toUint()) or ((tadInputBin[offset++].toUint()) shl 8) or ((tadInputBin[offset++].toUint()) shl 16) or ((tadInputBin[offset++].toUint()) shl 24) ) - // Decompress payload + // MSB of payload size = 1 means the payload is stored uncompressed (no Zstd). + val payloadIsRaw = (payloadSizeField and 0x80000000.toInt()) != 0 + val payloadSize = payloadSizeField and 0x7FFFFFFF + + // Read payload bytes val compressed = ByteArray(payloadSize) UnsafeHelper.memcpyRaw(null, tadInputBin.ptr + offset, compressed, UnsafeHelper.getArrayOffset(compressed), payloadSize.toLong()) - val payload: ByteArray = try { - ZstdInputStream(ByteArrayInputStream(compressed)).use { zstd -> - zstd.readBytes() + val payload: ByteArray = if (payloadIsRaw) { + compressed + } else { + try { + ZstdInputStream(ByteArrayInputStream(compressed)).use { zstd -> + zstd.readBytes() + } + } catch (e: Exception) { + println("ERROR: Zstd decompression failed: ${e.message}") + return } - } catch (e: Exception) { - println("ERROR: Zstd decompression failed: ${e.message}") - return } // Decode using binary tree EZBC - FIXED! diff --git a/tsvm_core/tsvm_core.iml b/tsvm_core/tsvm_core.iml index 516f4f1..45571f2 100644 --- a/tsvm_core/tsvm_core.iml +++ b/tsvm_core/tsvm_core.iml @@ -12,5 +12,7 @@ + + \ No newline at end of file diff --git a/tsvm_executable.iml b/tsvm_executable.iml index 942bf2b..b01ef5a 100644 --- a/tsvm_executable.iml +++ b/tsvm_executable.iml @@ -10,5 +10,7 @@ + + \ No newline at end of file diff --git a/video_encoder/Makefile b/video_encoder/Makefile deleted file mode 100644 index 413592e..0000000 --- a/video_encoder/Makefile +++ /dev/null @@ -1,221 +0,0 @@ -# Created by CuriousTorvald and Claude on 2025-08-17. -# Makefile for TSVM Enhanced Video (TEV) encoder and libraries - -CC = gcc -CXX = g++ -CFLAGS = -std=c99 -Wall -Wextra -Ofast -D_GNU_SOURCE -march=native -mavx512f -mavx512dq -mavx512bw -mavx512vl -Iinclude -CXXFLAGS = -std=c++11 -Wall -Wextra -Ofast -D_GNU_SOURCE -march=native -mavx512f -mavx512dq -mavx512bw -mavx512vl -Iinclude -DBGFLAGS = -PREFIX = /usr/local - -# Zstd flags (use pkg-config if available, fallback for cross-platform compatibility) -ZSTD_CFLAGS = $(shell pkg-config --cflags libzstd 2>/dev/null || echo "") -ZSTD_LIBS = $(shell pkg-config --libs libzstd 2>/dev/null || echo "-lzstd") -LIBS = -lm $(ZSTD_LIBS) - -# ============================================================================= -# Library Object Files -# ============================================================================= - -# libtavenc - TAV encoder library -LIBTAVENC_OBJ = lib/libtavenc/tav_encoder_lib.o \ - lib/libtavenc/tav_encoder_color.o \ - lib/libtavenc/tav_encoder_dwt.o \ - lib/libtavenc/tav_encoder_quantize.o \ - lib/libtavenc/tav_encoder_ezbc.o \ - lib/libtavenc/tav_encoder_utils.o \ - lib/libtavenc/tav_encoder_tile.o - -# libtavdec - TAV decoder library -LIBTAVDEC_OBJ = lib/libtavdec/tav_video_decoder.o - -# libtadenc - TAD encoder library -LIBTADENC_OBJ = lib/libtadenc/encoder_tad.o - -# libtaddec - TAD decoder library -LIBTADDEC_OBJ = lib/libtaddec/decoder_tad.o - -# libfec - Forward Error Correction library (LDPC + Reed-Solomon) -LIBFEC_OBJ = lib/libfec/ldpc.o lib/libfec/reed_solomon.o lib/libfec/ldpc_payload.o - -# ============================================================================= -# Targets -# ============================================================================= - -# Source files and targets -TARGETS = libs encoder_tav_ref decoder_tav_ref tav_inspector tad tav_dt -LIBRARIES = lib/libtavenc.a lib/libtavdec.a lib/libtadenc.a lib/libtaddec.a lib/libfec.a -TAV_TARGETS = encoder_tav_ref decoder_tav_ref tav_inspector -TAD_TARGETS = encoder_tad decoder_tad -DT_TARGETS = encoder_tav_dt decoder_tav_dt tavdt_noise_injector - -# Build all encoders (default) -all: clean $(TARGETS) - -# Build all libraries -libs: $(LIBRARIES) - -# Reference encoder using libtavenc (replaces old monolithic encoder) -encoder_tav_ref: src/encoder_tav.c lib/libtavenc.a lib/libtadenc.a - rm -f encoder_tav_ref - $(CC) $(CFLAGS) $(ZSTD_CFLAGS) -Iinclude -o encoder_tav_ref src/encoder_tav.c lib/libtavenc.a lib/libtadenc.a $(LIBS) - @echo "" - @echo "Reference encoder built: encoder_tav_ref" - @echo "This is the official reference implementation with all features" - -# Reference decoder using libtavdec (replaces old monolithic decoder) -decoder_tav_ref: src/decoder_tav.c lib/libtavdec.a lib/libtaddec.a - rm -f decoder_tav_ref - $(CC) $(CFLAGS) $(ZSTD_CFLAGS) -Iinclude -o decoder_tav_ref src/decoder_tav.c lib/libtavdec.a lib/libtaddec.a $(LIBS) - @echo "" - @echo "Reference decoder built: decoder_tav_ref" - @echo "This is the official reference implementation with all features" - -tav_inspector: tav_inspector.c lib/libfec.a - rm -f tav_inspector - $(CC) $(CFLAGS) $(ZSTD_CFLAGS) -Ilib/libfec -o tav_inspector $< lib/libfec.a $(LIBS) - -tav: $(TAV_TARGETS) - -# Build TAD (Terrarum Advanced Audio) tools -encoder_tad: src/encoder_tad_standalone.c lib/libtadenc/encoder_tad.c include/encoder_tad.h - rm -f encoder_tad encoder_tad_standalone.o encoder_tad.o - $(CC) $(CFLAGS) $(ZSTD_CFLAGS) -c lib/libtadenc/encoder_tad.c -o encoder_tad.o - $(CC) $(CFLAGS) $(ZSTD_CFLAGS) -c src/encoder_tad_standalone.c -o encoder_tad_standalone.o - $(CC) $(DBGFLAGS) -o encoder_tad encoder_tad_standalone.o encoder_tad.o $(LIBS) - -decoder_tad: lib/libtaddec/decoder_tad.c - rm -f decoder_tad - $(CC) $(CFLAGS) $(ZSTD_CFLAGS) -o decoder_tad $< $(LIBS) - -# Build all TAD tools -tad: $(TAD_TARGETS) - -# ============================================================================= -# Library Build Rules -# ============================================================================= - -# Compile library object files -lib/libtavenc/%.o: lib/libtavenc/%.c - $(CC) $(CFLAGS) $(ZSTD_CFLAGS) -c $< -o $@ - -lib/libtavdec/%.o: lib/libtavdec/%.c - $(CC) $(CFLAGS) $(ZSTD_CFLAGS) -c $< -o $@ - -lib/libtadenc/%.o: lib/libtadenc/%.c - $(CC) $(CFLAGS) $(ZSTD_CFLAGS) -c $< -o $@ - -lib/libtaddec/%.o: lib/libtaddec/%.c - $(CC) $(CFLAGS) $(ZSTD_CFLAGS) -DTAD_DECODER_LIB -c $< -o $@ - -lib/libfec/%.o: lib/libfec/%.c - $(CC) $(CFLAGS) -Ilib/libfec -c $< -o $@ - -# Build static libraries -lib/libtavenc.a: $(LIBTAVENC_OBJ) - ar rcs $@ $^ - -lib/libtavdec.a: $(LIBTAVDEC_OBJ) - ar rcs $@ $^ - -lib/libtadenc.a: $(LIBTADENC_OBJ) - ar rcs $@ $^ - -lib/libtaddec.a: $(LIBTADDEC_OBJ) - ar rcs $@ $^ - -lib/libfec.a: $(LIBFEC_OBJ) - ar rcs $@ $^ - -# ============================================================================= -# TAV-DT (Digital Tape) Encoder/Decoder -# ============================================================================= - -# TAV-DT encoder with FEC (multithreaded) -encoder_tav_dt: src/encoder_tav_dt.c lib/libtavenc.a lib/libtadenc.a lib/libfec.a - rm -f encoder_tav_dt - $(CC) $(CFLAGS) $(ZSTD_CFLAGS) -Iinclude -Ilib/libfec -o encoder_tav_dt src/encoder_tav_dt.c lib/libtavenc.a lib/libtadenc.a lib/libfec.a $(LIBS) -lpthread - @echo "" - @echo "TAV-DT encoder built: encoder_tav_dt" - @echo "Digital Tape format with LDPC and Reed-Solomon FEC (multithreaded)" - -# TAV-DT decoder with FEC (multithreaded) -decoder_tav_dt: src/decoder_tav_dt.c lib/libtavdec.a lib/libtaddec.a lib/libfec.a - rm -f decoder_tav_dt - $(CC) $(CFLAGS) $(ZSTD_CFLAGS) -Iinclude -Ilib/libfec -o decoder_tav_dt src/decoder_tav_dt.c lib/libtavdec.a lib/libtaddec.a lib/libfec.a $(LIBS) -lpthread - @echo "" - @echo "TAV-DT decoder built: decoder_tav_dt" - @echo "Digital Tape format with LDPC and Reed-Solomon FEC (multithreaded)" - -# TAV-DT noise injector (channel simulator) -tavdt_noise_injector: tavdt_noise_injector.c - rm -f tavdt_noise_injector - $(CC) -std=c99 -Wall -Ofast -D_GNU_SOURCE -o tavdt_noise_injector tavdt_noise_injector.c -lm - @echo "" - @echo "TAV-DT noise injector built: tavdt_noise_injector" - @echo "Simulates QPSK satellite channel noise (AWGN + burst)" - -# Build all TAV-DT tools -tav_dt: $(DT_TARGETS) - -# Build with debug symbols -debug: CFLAGS += -g -DDEBUG -fsanitize=address -fno-omit-frame-pointer -debug: DBGFLAGS += -fsanitize=address -fno-omit-frame-pointer -debug: clean $(TARGETS) - -# Clean build artifacts -clean: - rm -f $(TARGETS) $(TAD_TARGETS) $(DT_TARGETS) $(LIBRARIES) *.o lib/*/*.o - -# Install (copy to PATH) -install: $(TARGETS) - cp encoder_tav_ref $(PREFIX)/bin/ - cp decoder_tav_ref $(PREFIX)/bin/ - cp encoder_tad $(PREFIX)/bin/ - cp decoder_tad $(PREFIX)/bin/ - cp encoder_tav_dt $(PREFIX)/bin/ - cp decoder_tav_dt $(PREFIX)/bin/ - cp tav_inspector $(PREFIX)/bin/ - -# Check for required dependencies -check-deps: - @echo "Checking dependencies..." - @pkg-config --exists libzstd || (echo "Error: libzstd-dev not found. Install libzstd-dev or equivalent" && exit 1) - @echo "All dependencies found." - -# Help -help: - @echo "TSVM Advanced Video (TAV) and Audio (TAD) Encoders" - @echo "" - @echo "Targets:" - @echo " all - Build video encoders (default)" - @echo " libs - Build all codec libraries (.a files)" - @echo " tav - Build the TAV advanced video encoder" - @echo " tav_dt - Build all TAV-DT (Digital Tape) tools with FEC" - @echo " tavdt_noise_injector - Build TAV-DT channel noise simulator" - @echo " tad - Build all TAD audio tools (encoder, decoder)" - @echo " encoder_tad - Build TAD audio encoder" - @echo " decoder_tad - Build TAD audio decoder" - @echo " tests - Build test programs" - @echo " debug - Build with debug symbols" - @echo " clean - Remove build artifacts" - @echo " install - Install to /usr/local/bin" - @echo " check-deps - Check for required dependencies" - @echo " help - Show this help" - @echo "" - @echo "Libraries:" - @echo " lib/libtavenc.a - TAV encoder library" - @echo " lib/libtavdec.a - TAV decoder library" - @echo " lib/libtadenc.a - TAD encoder library" - @echo " lib/libtaddec.a - TAD decoder library" - @echo " lib/libfec.a - Forward Error Correction library (LDPC + RS)" - @echo "" - @echo "Usage:" - @echo " make # Build video encoders" - @echo " make libs # Build all libraries" - @echo " make tav # Build TAV encoder" - @echo " make tav_dt # Build TAV-DT encoder/decoder with FEC" - @echo " make tad # Build all TAD audio tools" - @echo " sudo make install # Install all encoders" - -.PHONY: all libs clean install check-deps help debug tad tav_dt tests diff --git a/video_encoder/TAD_README.md b/video_encoder/TAD_README.md deleted file mode 100644 index 81be478..0000000 --- a/video_encoder/TAD_README.md +++ /dev/null @@ -1,350 +0,0 @@ -# TAD - TSVM Advanced Audio Codec - -A perceptually-optimised wavelet-based audio codec designed for resource-constrained systems, featuring CDF 9/7 wavelets, EZBC sparse coding, and sophisticated perceptual quantisation. - -## Overview - -TAD (TSVM Advanced Audio) is a modern audio codec built on discrete wavelet transform (DWT) using Cohen-Daubechies-Feauveau (CDF) 9/7 biorthogonal wavelets. It combines perceptual quantisation, advanced entropy coding, and careful optimisation for resource-constrained systems. - -### Key Advantages - -- **Perceptual optimisation**: HVS-aware quantisation preserves audio quality where it matters -- **Efficient sparse coding**: EZBC encoding exploits coefficient sparsity (86.9% zeros in typical content) -- **Variable chunk sizes**: Supports any chunk size ≥1024 samples, including non-power-of-2 -- **Stereo decorrelation**: Mid/Side encoding exploits stereo correlation for better compression -- **Hardware-friendly**: Designed for efficient decoding on resource-constrained platforms - -## Features - -### Compression Technology - -- **CDF 9/7 Biorthogonal Wavelets** - - 9-level fixed decomposition for all chunk sizes - - Lifting scheme implementation for efficient computation - - Optimal frequency discrimination for audio signals - -- **Pre-processing** - - First-order IIR pre-emphasis filter (α=0.5) shifts quantisation noise to lower frequencies, where they are less objectionable to listeners - - Gamma companding (γ=0.5) for dynamic range compression before quantisation - - Mid/Side stereo transformation exploits stereo correlation - - Lambda companding (λ=6.0) with Laplacian CDF mapping for full bit utilisation - -- **Perceptual Quantisation** - - Channel-specific (Mid/Side) frequency-dependent weights - - Subband-aware quantisation preserves perceptually important frequencies - -- **EZBC Encoding** - - Binary tree embedded zero block coding - - Exploits coefficient sparsity (86.9% Mid, 97.8% Side typical) - - Progressive refinement structure - - Spatial clustering of non-zero coefficients - -- **Entropy Coding** - - Zstandard compression (level 7) on concatenated EZBC bitstreams - - Cross-channel compression optimisation - - Optional Zstd bypass for debugging - -### Audio Format - -- **Sample Rate**: 32 KHz (TSVM audio hardware native format) -- **Channels**: Stereo (L/R input, Mid/Side internal representation) -- **Chunk Sizes**: Variable, any size ≥1024 samples (including non-power-of-2) -- **Bit Depth**: 32-bit float internal, 8-bit unsigned PCM output with noise-shaped dithering -- **Bandwidth**: Full 0-16 KHz frequency range preserved - -### Quality Levels - -Six quality levels (0-5) provide a wide range of compression/quality trade-offs: -- **Level 0**: Lowest quality, smallest file size -- **Level 3**: Default, balanced quality/compression (2.51:1 vs PCMu8) -- **Level 5**: Highest quality, largest file size - -Quality levels are designed to be synchronised with TAV video codec for unified encoding. - -## Building - -### Prerequisites - -- C compiler (GCC/Clang) -- Zstandard library (libzstd) -- Math library (libm) - -### Compilation - -```bash -# Build TAD encoder/decoder -make tad - -# Build all tools -make all - -# Clean build artifacts -make clean -``` - -### Build Targets - -- `encoder_tad` - Standalone audio encoder with FFmpeg calls -- `decoder_tad` - Standalone audio decoder - -## Usage - -### Basic Encoding - -Encoding requires FFmpeg executable installed in your system. - -```bash -# Default encoding (quality level 3) -./encoder_tad -i input.mp3 -o output.tad - -# Specify quality level (0-5) -./encoder_tad -i input.m4a -o output.tad -q 0 # Lowest quality -./encoder_tad -i input.ogg -o output.tad -q 5 # Highest quality - -# Disable Zstd compression (for debugging) -./encoder_tad -i input.opus -o output.tad --no-zstd - -# Verbose output with statistics -./encoder_tad -i input.flac -o output.tad -v -``` - -### Decoding - -```bash -# Decode to PCMu8 -./decoder_tad -i input.tad -o output.pcm --raw-pcm - -# Decode to WAV -./decoder_tad -i input.tad -o output.wav -``` - -### Input Formats - -TAD encoder accepts any audio format supported by FFmpeg: -- Audio files: WAV, MP3, FLAC, OGG, AAC, etc. -- Video files with audio streams: MP4, MKV, AVI, etc. -- Raw PCM formats - -Audio is automatically resampled to 32 KHz stereo if necessary. - -## Technical Architecture - -### Encoder Pipeline - -1. **Input Processing** - - FFmpeg demuxing and audio stream extraction - - Resampling to 32 KHz stereo - - Conversion to PCM32f - -2. **Pre-emphasis Filter** - - First-order IIR filter with α=0.5 - - Shifts quantisation noise toward lower frequencies - - Improves perceptual quality - -3. **Gamma Companding** - - Dynamic range compression with γ=0.5 - - Applied independently to each sample - - Reduces quantisation error for low-amplitude signals - -4. **Stereo Decorrelation** - - Left/Right to Mid/Side transformation - - Mid = (L + R) / 2 - - Side = (L - R) / 2 - - Exploits stereo correlation for better compression - -5. **9-Level CDF 9/7 DWT** - - Fixed 9 decomposition levels for all chunk sizes - - Forward lifting scheme implementation - - Correct length tracking for non-power-of-2 sizes - -6. **Perceptual Quantisation** - - Channel-specific (Mid/Side) subband weights - - Lambda companding with λ=6.0 - - Laplacian CDF mapping: `sign(x) * floor(λ * log(1 + |x|/λ))` - - Quantised to int8 coefficients - -7. **EZBC Encoding** - - Binary tree structure per channel - - Progressive refinement by bitplanes - - Zero block coding exploits sparsity - - Independent bitstreams for Mid and Side - -8. **Zstd Compression** - - Level 7 compression on concatenated `[Mid_bitstream][Side_bitstream]` - - Cross-channel optimisation opportunities - - Adaptive compression based on content - -### Decoder Pipeline - -1. **Container Parsing** - - TAD packet identification (type 0x24) - - Chunk size extraction - - Compressed data boundaries - -2. **Zstd Decompression** - - Decompress concatenated bitstreams - - Split into Mid and Side EZBC streams - -3. **EZBC Decoding** - - Binary tree decoder per channel - - Reconstruct quantised int8 coefficients - - Progressive refinement reconstruction - -4. **Lambda Decompanding** - - Inverse Laplacian CDF with channel-specific weights - - Reconstruct float32 DWT coefficients - - Apply subband-specific perceptual weights - -5. **9-Level Inverse CDF 9/7 DWT** - - Inverse lifting scheme implementation - - Correct length tracking for non-power-of-2 chunk sizes - - Pre-calculated length sequence from forward transform - -6. **Mid/Side to Left/Right** - - L = Mid + Side - - R = Mid - Side - - Reconstruct stereo channels - -7. **Gamma Decompanding** - - Inverse gamma with γ⁻¹=2.0 - - Restore original dynamic range - -8. **De-emphasis Filter** - - Reverse pre-emphasis with α=0.5 - - Remove frequency shaping - - Restore flat frequency response - -9. **PCM32f to PCM8u Conversion** - - Noise-shaped dithering for 8-bit output - - Clamping to valid range - - Final output format - -### Wavelet Implementation - -CDF 9/7 wavelet follows a **two-stage lifting scheme**: - -```c -// Forward Transform: Predict → Update -// Predict step (generate high-pass) -temp[half + i] = data[odd] - α * (data[even_left] + data[even_right]); - -// Update step (generate low-pass) -temp[i] = data[even] + β * (temp[half + i - 1] + temp[half + i]); - -// Normalization (K factor) -temp[i] *= K; -temp[half + i] /= K; - -// Inverse Transform: Denormalize → Undo Update → Undo Predict (reversed order) -temp[i] /= K; -temp[half + i] *= K; - -temp[i] -= β * (temp[half + i - 1] + temp[half + i]); -data[odd] = temp[half + i] + α * (temp[i] + temp[i + 1]); -data[even] = temp[i]; -``` - -**CDF 9/7 Coefficients**: -- α = -1.586134342 -- β = -0.052980118 -- γ = +0.882911075 -- δ = +0.443506852 -- K = 1.230174105 - -### Non-Power-of-2 Chunk Size Handling - -Critical implementation detail for variable chunk sizes: - -```c -// Pre-calculate exact length sequence from forward transform -int lengths[MAX_LEVELS + 1]; -lengths[0] = chunk_size; -for (int i = 1; i <= levels; i++) { - lengths[i] = (lengths[i - 1] + 1) / 2; -} - -// Apply inverse DWT using lengths[level] for each level -// NEVER use simple doubling (length *= 2) - incorrect for non-power-of-2! -``` - -Incorrect length tracking causes mirrored subband artefacts in decoded audio. - -### Perceptual Quantisation Weights - -Channel-specific weights for Mid (channel 0) and Side (channel 1): - -```c -// Base quantiser weights per subband (9 levels + approximation) -float BASE_QUANTISER_WEIGHTS[2][10] = { - // Mid channel (0) - {4.0f, 2.0f, 1.8f, 1.6f, 1.4f, 1.2f, 1.0f, 1.0f, 1.3f, 2.0f}, - - // Side channel (1) - {6.0f, 5.0f, 2.6f, 2.4f, 1.8f, 1.3f, 1.0f, 1.0f, 1.6f, 3.2f} -}; - -// During dequantisation: -float weight = BASE_QUANTISER_WEIGHTS[channel][subband] * quantiser_scale; -coeffs[i] = normalised_val * TAD32_COEFF_SCALARS[subband] * weight; -``` - -Different weights for Mid and Side channels reflect perceptual importance of frequency bands in each channel. DC frequency has highest weight (4.0 Mid, 6.0 Side) due to energy concentration. - -## Performance Characteristics - -### Compression Efficiency - -- **Target Compression**: 2:1 against PCMu8 baseline (4:1 against PCM16LE input) -- **Achieved Compression**: 2.51:1 against PCMu8 at quality level 3 -- **Audio Quality**: Preserves full 0-16 KHz bandwidth -- **Coefficient Sparsity**: 86.9% zeros in Mid channel, 97.8% in Side channel (typical) -- **EZBC Benefits**: Exploits sparsity, progressive refinement, spatial clustering - -### Computational Complexity - -- **Encoding**: O(n log n) per chunk for DWT, O(n) for EZBC encoding -- **Decoding**: O(n log n) per chunk for inverse DWT, O(n) for EZBC decoding -- **Memory**: O(n) working memory for chunk processing - -### Quality Characteristics - -- **Frequency Response**: Flat 0-16 KHz within perceptual limits -- **Dynamic Range**: Preserved through gamma companding -- **Stereo Imaging**: Maintained through Mid/Side decorrelation -- **Perceptual Quality**: Optimised for human auditory system characteristics - -## Integration with TAV - -TAD is designed as an includable API for TAV video encoder integration: - -- **Variable Chunk Sizes**: Audio chunks can match video GOP boundaries (e.g., 32016 samples for 1-second TAV GOP) -- **Unified Quality Levels**: TAD quality 0-5 synchronised with TAV quality 0-5 -- **Embedded Packets**: TAV embeds TAD-compressed audio using packet type 0x24 -- **Shared Container**: Single .tav file contains both video and audio streams - -### TAV Integration Example - -```c -// TAD handles non-power-of-2 chunk size correctly -tad_encode_chunk(audio_buffer, audio_samples_per_gop, output_buffer, &output_size); - -// TAV embeds TAD packet -tav_write_packet(TAV_PACKET_AUDIO, output_buffer, output_size); -``` - -## Format Specification - -For complete packet structure and bitstream format details, refer to `format documentation.txt`. - -### Key Packet Types - -- `0x24`: TAD audio packet (used in standalone .tad files and embedded in .tav files) - -## Related Projects - -- **TAV** (TSVM Advanced Video): Wavelet-based video codec with integrated TAD audio -- **TSVM**: Target virtual machine platform for TAD playback - -## Licence - -MIT. diff --git a/video_encoder/TAV_README.md b/video_encoder/TAV_README.md deleted file mode 100644 index 5d003cc..0000000 --- a/video_encoder/TAV_README.md +++ /dev/null @@ -1,261 +0,0 @@ -# TAV - TSVM Advanced Video Codec - -A perceptually-optimised wavelet-based video codec designed for resource-constrained systems, featuring multiple wavelet types, temporal 3D DWT, and sophisticated compression techniques. - -## Overview - -TAV (TSVM Advanced Video) is a modern video codec built on discrete wavelet transformation (DWT). It combines cutting-edge compression techniques with careful optimisation for resource-constrained systems. - -### Key Advantages - -- **No blocking artefacts**: Large-tile DWT encoding with padding eliminates DCT block boundaries -- **No colour banding**: Wavelets spreads gradients across scales, preventing banding in the first place -- **Perceptual optimisation**: HVS-aware quantisation preserves visual quality where it matters -- **Temporal coherence**: 3D DWT with GOP encoding exploits inter-frame similarity -- **Efficient sparse coding**: EZBC encoding exploits coefficient sparsity for 16-18% additional compression -- **Hardware-friendly**: Designed for efficient decoding on resource-constrained platforms - -## Features - -### Compression Technology - -- **Wavelet Types** - - **5/3 Reversible** (JPEG 2000 standard): Lossless-capable, good for archival - - **9/7 Irreversible** (default): Best overall compression, CDF 9/7 variant - -- **Spatial Encoding** - - Large-tile encoding with padding, with optional single-tile mode (no blocking artefacts) - - 6-level DWT decomposition for deep frequency analysis - - Perceptual quantisation with HVS-optimised coefficient scaling - - YCoCg-R colour space with anisotropic chroma quantisation - -- **Temporal Encoding** (3D DWT Mode) - - Group-of-pictures (GOP) encoding with adaptive size (typically 20 frames) - - Unified EZBC encoding across temporal dimension - - Adaptive GOP boundaries with scene change detection - -- **EZBC Encoding** - - Binary tree embedded zero block coding exploits coefficient sparsity - - Progressive refinement structure with bitplane encoding - - Concatenated channel layout for cross-channel compression optimisation - - Typical sparsity: 86.9% (Y), 97.8% (Co), 99.5% (Cg) - - 16-18% compression improvement over naive coefficient encoding - -### Audio Integration - -TAV seamlessly integrates with the TAD (TSVM Advanced Audio) codec for synchronised audio/video encoding: -- Variable chunk sizes match video GOP boundaries -- Embedded TAD packets (type 0x24) with Zstd compression -- Unified container format - -## Building - -### Prerequisites - -- C compiler (GCC/Clang) -- Zstandard library -- OpenCV 4 library (only used by experimental motion estimation feature) - -### Compilation - -```bash -# Build TAV encoder/decoder -make tav - -# Build all tools including TAD audio codec -make all - -# Clean build artefacts -make clean -``` - -### Build Targets - -- `encoder_tav` - Main video encoder -- `decoder_tav` - Standalone video decoder -- `tav_inspector` - Packet analysis and debugging tool - -## Usage - -### Basic Encoding - -Encoding requires FFmpeg executable installed in your system. - -```bash -# Default encoding (CDF 9/7 wavelet, quality level 3) -./encoder_tav -i input.mp4 -o output.tav - -# Quality levels (0-5) -./encoder_tav -i input.avi -q 0 -o output.tav # Lowest quality, smallest file -./encoder_tav -i input.mkv -q 5 -o output.tav # Highest quality, largest file -``` - -### Intra-only Encoding - -```bash -# Enable Intra-only encoding -./encoder_tav -i input.mp4 --intra-only -o output.tav -``` - -### Decoding and Inspection - -```bash -# Decode TAV to raw video -./decoder_tav -i input.tav -o output.mkv - -# Inspect packet structure (debugging) -./tav_inspector input.tav -v -``` - -### Frame Limiting - -```bash -# Encode only first N frames (useful for testing) -./encoder_tav -i input.mp4 -o output.tav --encode-limit 100 -``` - -## Technical Architecture - -### Encoder Pipeline - -1. **Input Processing** - - FFmpeg demuxing and frame extraction - - RGB to YCoCg-R colour space conversion - - Resolution validation and padding - -2. **DWT Transform** - - Spatial: 6-level decomposition per frame - - Temporal: 1D DWT across GOP frames (3D DWT mode) - - Lifting scheme implementation for all wavelets - -3. **Perceptual Quantisation** - - HVS-based subband weights - - Anisotropic chroma quantisation (YCoCg-R specific) - - Quality-dependent quantisation matrices - -4. **EZBC Encoding** - - Binary tree embedded zero block coding per channel - - Progressive refinement by bitplanes - - Concatenated bitstream layout: `[Y_bitstream][Co_bitstream][Cg_bitstream]` - - Cross-channel compression optimisation - -5. **Entropy Coding** - - Zstandard compression (level 7) on concatenated EZBC bitstreams - - Cross-channel compression opportunities - - Adaptive compression based on GOP structure - -### Decoder Pipeline - -1. **Container Parsing** - - Packet type identification (0x00-0xFF) - - Timecode synchronisation - - GOP boundary detection - -2. **Entropy Decoding** - - Zstd decompression of concatenated bitstreams - - EZBC binary tree decoding per channel - - Progressive coefficient reconstruction - -3. **Inverse Quantisation** - - Perceptual weight application - - Subband-specific scaling - - Coefficient reconstruction from sparse representation - -4. **Inverse DWT** - - Temporal: 1D inverse DWT across frames (3D DWT mode) - - Spatial: 6-level inverse wavelet reconstruction - -5. **Output Conversion** - - YCoCg-R to RGB colour space - - Clamping and dithering - - Frame buffering for display - -### Wavelet Implementation - -All wavelets follow a **lifting scheme** pattern with symmetric boundary extension: - -```c -// Forward Transform: Predict → Update -temp[half + i] = data[odd] - predict(data[even]); // High-pass -temp[i] = data[even] + update(temp[half]); // Low-pass - -// Inverse Transform: Undo Update → Undo Predict (reversed order) -data[even] = temp[i] - update(temp[half]); // Undo low-pass -data[odd] = temp[half + i] + predict(data[even]); // Undo high-pass -``` - -**Critical**: Forward and inverse transforms must use identical coefficient indexing and exactly reverse operations to avoid grid artefacts. - -### Coefficient Layout - -TAV uses **2D Spatial Layout** in memory for each decomposition level: - -``` -[LL] [LH] [HL] [HH] [LH] [HL] [HH] ... - └── Level 0 ──┘ └─── Level 1 ───┘ -``` - -- `LL`: Low-pass (approximation) - progressively smaller with each level -- `LH`, `HL`, `HH`: High-pass subbands (horizontal, vertical, diagonal detail) - -## Performance Characteristics - -### Compression Efficiency - -- **Sparsity Exploitation**: Typical quantised coefficient sparsity - - Y channel: 86.9% zeros - - Co channel: 97.8% zeros - - Cg channel: 99.5% zeros - -- **EZBC Benefits**: 16-18% compression improvement over naive coefficient encoding through sparsity exploitation - -- **Temporal Coherence**: Additional 15-25% improvement with 3D DWT (content-dependent) - -### Computational Complexity - -- **Encoding**: O(n log n) per frame for spatial DWT -- **Decoding**: O(n log n) per frame, optimised lifting scheme implementation -- **Memory**: Single-tile encoding requires O(w × h) working memory - -### Quality Characteristics - -- **No blocking artefacts**: Wavelet-based encoding is inherently smooth -- **Perceptual optimisation**: Better subjective quality than bitrate-equivalent DCT codecs -- **Scalability**: 6 quality levels (0-5) provide wide range of bitrate/quality trade-offs -- **Temporal stability**: 3D DWT mode reduces flickering and temporal artefacts - -## Format Specification - -For complete packet structure and bitstream format details, refer to `format documentation.txt`. - -### Key Packet Types - -- `0x00`: Metadata and initialisation -- `0x01`: I-frame (intra-coded frame) -- `0x12`: GOP unified packet (3D DWT mode) -- `0x24`: Embedded TAD audio -- `0xFC`: GOP synchronisation -- `0xFD`: Timecode - -## Debugging Tools - -### TAV Inspector - -Analyse TAV packet structure and decode individual frames: - -```bash -# Verbose packet analysis -./tav_inspector input.tav -v - -# Extract specific frame ranges -./tav_inspector input.tav --frame-range 100-200 -``` - -## Related Projects - -- **TAD** (TSVM Advanced Audio): Perceptual audio codec using CDF 9/7 wavelets -- **TSVM**: Target virtual machine platform for TAV playback - -## Licence - -MIT. diff --git a/video_encoder/create_ucf_payload.c b/video_encoder/create_ucf_payload.c deleted file mode 100644 index c0a2e34..0000000 --- a/video_encoder/create_ucf_payload.c +++ /dev/null @@ -1,424 +0,0 @@ -/** - * TAV+UCF Payload Writer for TAV Files - * Creates a TAV header-only (32 bytes) + UCF cue file (4KB) for concatenated TAV files - * Total output size: 4096 bytes (32 + 4064) - * Usage: ./create_ucf_payload input.tav output.ucf [track_names.txt] - */ - -#include -#include -#include -#include - -#define TAV_HEADER_SIZE 32 -#define UCF_SIZE 4064 -#define TAV_OFFSET_BIAS (TAV_HEADER_SIZE + UCF_SIZE) -#define TAV_MAGIC "\x1FTSVMTA" // Matches both TAV and TAP - -typedef struct { - uint8_t magic[8]; - uint8_t version; - uint16_t width; - uint16_t height; - uint8_t fps; - uint32_t total_frames; - // ... rest of header fields -} __attribute__((packed)) TAVHeader; - -// Write TAV header-only payload (File Role = 1) -static void write_tav_header_only(FILE *out) { - uint8_t header[TAV_HEADER_SIZE] = {0}; - - // Magic: "\x1FTSVMTAV" - header[0] = 0x1F; - header[1] = 'T'; - header[2] = 'S'; - header[3] = 'V'; - header[4] = 'M'; - header[5] = 'T'; - header[6] = 'A'; - header[7] = 'V'; - - // Version: 5 (YCoCg-R perceptual) - header[8] = 5; - - // Width: 560 (little-endian) - header[9] = 0x30; - header[10] = 0x02; - - // Height: 448 (little-endian) - header[11] = 0xC0; - header[12] = 0x01; - - // FPS: 30 - header[13] = 30; - - // Total Frames: 0xFFFFFFFF (still image marker / not applicable) - header[14] = 0xFF; - header[15] = 0xFF; - header[16] = 0xFF; - header[17] = 0xFF; - - // Wavelet Filter Type: 1 (9/7 irreversible, default) - header[18] = 1; - - // Decomposition Levels: 6 - header[19] = 6; - - // Quantiser Indices (Y, Co, Cg): 255 (not applicable for header-only) - header[20] = 0xFF; - header[21] = 0xFF; - header[22] = 0xFF; - - // Extra Feature Flags: 0x80 (bit 7 = has no actual packets) - header[23] = 0x80; - - // Video Flags: 0 - header[24] = 0; - - // Encoder quality level: 0 - header[25] = 0; - - // Channel layout: 0 (Y-Co-Cg) - header[26] = 0; - - // Reserved[4]: zeros (27-30 already initialised to 0) - - // File Role: 1 (header-only, UCF payload follows) - header[31] = 1; - - fwrite(header, 1, TAV_HEADER_SIZE, out); -} - -// Write UCF header -static void write_ucf_header(FILE *out, uint16_t num_cues) { - uint8_t magic[8] = {0x1F, 'T', 'S', 'V', 'M', 'U', 'C', 'F'}; - uint8_t version = 1; - uint32_t cue_file_size = TAV_OFFSET_BIAS; - uint8_t reserved = 0; - - fwrite(magic, 1, 8, out); - fwrite(&version, 1, 1, out); - fwrite(&num_cues, 2, 1, out); - fwrite(&cue_file_size, 4, 1, out); - fwrite(&reserved, 1, 1, out); -} - -// Write UCF cue element (internal addressing, human+machine interactable) -static void write_cue_element(FILE *out, uint64_t offset, const char *name) { - uint8_t addressing_mode = 0x22; // 0x20 (human) | 0x01 (machine) | 0x02 (internal) - uint16_t name_len = strlen(name); - - // Offset with 4KB bias - uint64_t biased_offset = offset + TAV_OFFSET_BIAS; - - fwrite(&addressing_mode, 1, 1, out); - fwrite(&name_len, 2, 1, out); - fwrite(name, 1, name_len, out); - - // Write 48-bit (6-byte) offset - fwrite(&biased_offset, 6, 1, out); -} - -// Read track names from file (newline-delimited) -static char **read_track_names(const char *filename, int *count_out) { - FILE *f = fopen(filename, "r"); - if (!f) { - return NULL; - } - - char **names = NULL; - int count = 0; - int capacity = 16; - char line[256]; - - names = malloc(capacity * sizeof(char *)); - if (!names) { - fclose(f); - return NULL; - } - - while (fgets(line, sizeof(line), f)) { - // 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--; - } - - // Skip empty lines - if (len == 0) { - continue; - } - - // Expand capacity if needed - if (count >= capacity) { - capacity *= 2; - char **new_names = realloc(names, capacity * sizeof(char *)); - if (!new_names) { - // Cleanup on failure - for (int i = 0; i < count; i++) { - free(names[i]); - } - free(names); - fclose(f); - return NULL; - } - names = new_names; - } - - // Allocate and copy name - names[count] = strdup(line); - if (!names[count]) { - // Cleanup on failure - for (int i = 0; i < count; i++) { - free(names[i]); - } - free(names); - fclose(f); - return NULL; - } - count++; - } - - fclose(f); - *count_out = count; - return names; -} - -// Find all TAV headers in the file (with smart packet-wise skipping) -static int find_tav_headers(FILE *in, uint64_t **offsets_out) { - uint64_t *offsets = NULL; - int count = 0; - int capacity = 16; - - offsets = malloc(capacity * sizeof(uint64_t)); - if (!offsets) { - fprintf(stderr, "Error: Memory allocation failed\n"); - return -1; - } - - // Seek to beginning - fseek(in, 0, SEEK_SET); - - uint8_t magic[8]; - - while (1) { - // Remember current position before reading - uint64_t pos = ftell(in); - - // Try to read magic - if (fread(magic, 1, 8, in) != 8) { - // End of file - break; - } - - // Check for TAV magic signature - if (memcmp(magic, TAV_MAGIC, 7) == 0 && (magic[7] == 'V' || magic[7] == 'P')) { - // Found TAV header - if (count >= capacity) { - capacity *= 2; - uint64_t *new_offsets = realloc(offsets, capacity * sizeof(uint64_t)); - if (!new_offsets) { - fprintf(stderr, "Error: Memory reallocation failed\n"); - free(offsets); - return -1; - } - offsets = new_offsets; - } - - offsets[count++] = pos; - printf("Found TAV header at offset: 0x%lX (%lu)\n", pos, pos); - - // Skip past this header (32 bytes total) - uint64_t packet_pos = pos + 32; - fseek(in, packet_pos, SEEK_SET); - - // Smart packet-wise skipping - while (1) { - uint8_t packet_type; - if (fread(&packet_type, 1, 1, in) != 1) { - // End of file - break; - } - - // Check if this is the start of next TAV file (0x1F is prohibited as packet type) - if (packet_type == 0x1F) { - // Rewind 1 byte to re-read as magic at the top of outer loop - fseek(in, packet_pos, SEEK_SET); - break; - } - - // printf("TAV Packet 0x%02X at 0x%lX\n", packet_type, packet_pos); - - // Sync packets (0xFE, 0xFF) have no payload size - they're single-byte packets - if (packet_type == 0xFE || packet_type == 0xFF) { - packet_pos += 1; - fseek(in, packet_pos, SEEK_SET); - continue; - } - - // Read payload size (uint32, little-endian) - uint32_t payload_size = 0; - if (fread(&payload_size, 4, 1, in) != 1) { - // End of file - break; - } - - // Skip packet: 1 byte (type) + 4 bytes (size) + payload_size - packet_pos += 1 + 4 + payload_size; - fseek(in, packet_pos, SEEK_SET); - } - } else { - // Move forward by 1 byte for next search - fseek(in, pos + 1, SEEK_SET); - } - } - - *offsets_out = offsets; - return count; -} - -int main(int argc, char *argv[]) { - if (argc < 3 || argc > 4) { - fprintf(stderr, "Usage: %s [track_names.txt]\n", argv[0]); - fprintf(stderr, "Creates a 4KB UCF payload for concatenated TAV file\n"); - fprintf(stderr, " track_names.txt: Optional file with track names (one per line)\n"); - return 1; - } - - const char *input_path = argv[1]; - const char *output_path = argv[2]; - const char *names_path = (argc == 4) ? argv[3] : NULL; - - // Read track names if provided - char **track_names = NULL; - int num_names = 0; - if (names_path) { - track_names = read_track_names(names_path, &num_names); - if (track_names) { - printf("Loaded %d track name(s) from '%s'\n", num_names, names_path); - } else { - fprintf(stderr, "Warning: Could not read track names from '%s', using defaults\n", names_path); - } - } - - // Open input file - FILE *in = fopen(input_path, "rb"); - if (!in) { - fprintf(stderr, "Error: Cannot open input file '%s'\n", input_path); - if (track_names) { - for (int i = 0; i < num_names; i++) { - free(track_names[i]); - } - free(track_names); - } - return 1; - } - - // Find all TAV headers - uint64_t *offsets = NULL; - int num_tracks = find_tav_headers(in, &offsets); - fclose(in); - - if (num_tracks < 0) { - fprintf(stderr, "Error: Failed to scan input file\n"); - if (track_names) { - for (int i = 0; i < num_names; i++) { - free(track_names[i]); - } - free(track_names); - } - return 1; - } - - if (num_tracks == 0) { - fprintf(stderr, "Error: No TAV headers found in input file\n"); - free(offsets); - if (track_names) { - for (int i = 0; i < num_names; i++) { - free(track_names[i]); - } - free(track_names); - } - return 1; - } - - printf("\nFound %d TAV header(s)\n", num_tracks); - - // Create output UCF file - FILE *out = fopen(output_path, "wb"); - if (!out) { - fprintf(stderr, "Error: Cannot create output file '%s'\n", output_path); - free(offsets); - if (track_names) { - for (int i = 0; i < num_names; i++) { - free(track_names[i]); - } - free(track_names); - } - return 1; - } - - // Write TAV header-only payload (File Role = 1) - write_tav_header_only(out); - printf("Written TAV header-only payload (%d bytes)\n", TAV_HEADER_SIZE); - - // Write UCF header - write_ucf_header(out, num_tracks); - - // Write cue elements - for (int i = 0; i < num_tracks; i++) { - char default_name[32]; - const char *name; - - // Use custom name if available, otherwise generate default - if (track_names && i < num_names) { - name = track_names[i]; - } else { - snprintf(default_name, sizeof(default_name), "Track %d", i + 1); - name = default_name; - } - - write_cue_element(out, offsets[i], name); - printf("Written cue element: '%s' at offset 0x%lX (biased: 0x%lX)\n", - name, offsets[i], offsets[i] + TAV_OFFSET_BIAS); - } - - // Get current file position - long current_pos = ftell(out); - - // Fill remaining space with zeros to reach TAV header + 4KB UCF - size_t target_size = TAV_HEADER_SIZE + UCF_SIZE; - if (current_pos < target_size) { - size_t remaining = target_size - current_pos; - uint8_t *zeros = calloc(remaining, 1); - if (zeros) { - fwrite(zeros, 1, remaining, out); - free(zeros); - } - } - - fclose(out); - free(offsets); - - // Clean up track names - if (track_names) { - for (int i = 0; i < num_names; i++) { - free(track_names[i]); - } - free(track_names); - } - - printf("\nTAV+UCF payload created successfully: %s\n", output_path); - printf("File size: %zu bytes (TAV header: %d + UCF: %d)\n", - (size_t)(TAV_HEADER_SIZE + UCF_SIZE), TAV_HEADER_SIZE, UCF_SIZE); - printf("\nTo create seekable TAV file, prepend this payload to your concatenated TAV file:\n"); - printf(" cat %s input.tav > output_seekable.tav\n", output_path); - - return 0; -} diff --git a/video_encoder/encoder_ipf1d.c b/video_encoder/encoder_ipf1d.c deleted file mode 100644 index d546e2a..0000000 --- a/video_encoder/encoder_ipf1d.c +++ /dev/null @@ -1,935 +0,0 @@ -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// TVDOS Movie format constants -#define TVDOS_MAGIC "\x1F\x54\x53\x56\x4D\x4D\x4F\x56" // "\x1FTSVM MOV" -#define IPF_BLOCK_SIZE 12 - -// iPF1-delta opcodes -#define SKIP_OP 0x00 -#define PATCH_OP 0x01 -#define REPEAT_OP 0x02 -#define END_OP 0xFF - -// Video packet types -#define IPF1_PACKET_TYPE 0x04, 0x00 // iPF Type 1 (4 + 0) -#define IPF1_DELTA_PACKET_TYPE 0x04, 0x02 // iPF Type 1 delta -#define SYNC_PACKET_TYPE 0xFF, 0xFF // Sync packet - -// Audio constants -#define MP2_SAMPLE_RATE 32000 -#define MP2_DEFAULT_PACKET_SIZE 0x240 -#define MP2_PACKET_TYPE_BASE 0x11 - -// Default values -#define DEFAULT_WIDTH 560 -#define DEFAULT_HEIGHT 448 -#define TEMP_AUDIO_FILE "/tmp/tvdos_temp_audio.mp2" - -typedef struct { - char *input_file; - char *output_file; - int width; - int height; - int fps; - int total_frames; - double duration; - int has_audio; - int output_to_stdout; - - // Internal buffers - uint8_t *previous_ipf_frame; - uint8_t *current_ipf_frame; - uint8_t *delta_buffer; - uint8_t *rgb_buffer; - uint8_t *compressed_buffer; - uint8_t *mp2_buffer; - size_t frame_buffer_size; - - // Audio handling - FILE *mp2_file; - int mp2_packet_size; - int mp2_rate_index; - size_t audio_remaining; - int audio_frames_in_buffer; - int target_audio_buffer_size; - - // FFmpeg processes - FILE *ffmpeg_video_pipe; - FILE *ffmpeg_audio_pipe; - - // Progress tracking - struct timeval start_time; - struct timeval last_progress_time; - size_t total_output_bytes; - - // Dithering mode - int dither_mode; -} encoder_config_t; - -// CORRECTED YCoCg conversion matching Kotlin implementation -typedef struct { - float y, co, cg; -} ycocg_t; - -static ycocg_t rgb_to_ycocg_correct(uint8_t r, uint8_t g, uint8_t b, float ditherThreshold) { - ycocg_t result; - float rf = floor((ditherThreshold / 15.0 + r / 255.0) * 15.0) / 15.0; - float gf = floor((ditherThreshold / 15.0 + g / 255.0) * 15.0) / 15.0; - float bf = floor((ditherThreshold / 15.0 + b / 255.0) * 15.0) / 15.0; - - // CORRECTED: Match Kotlin implementation exactly - float co = rf - bf; // co = r - b [-1..1] - float tmp = bf + co / 2.0f; // tmp = b + co/2 - float cg = gf - tmp; // cg = g - tmp [-1..1] - float y = tmp + cg / 2.0f; // y = tmp + cg/2 [0..1] - - result.y = y; - result.co = co; - result.cg = cg; - - return result; -} - -static int quantise_4bit_y(float value) { - // Y quantisation: round(y * 15) - return (int)round(fmaxf(0.0f, fminf(15.0f, value * 15.0f))); -} - -static int chroma_to_four_bits(float f) { - // CORRECTED: Match Kotlin chromaToFourBits function exactly - // return (round(f * 8) + 7).coerceIn(0..15) - int result = (int)round(f * 8.0f) + 7; - return fmaxf(0, fminf(15, result)); -} - -// Parse resolution string like "1024x768" -static int parse_resolution(const char *res_str, int *width, int *height) { - if (!res_str) return 0; - return sscanf(res_str, "%dx%d", width, height) == 2; -} - -// Execute command and capture output -static char *execute_command(const char *command) { - FILE *pipe = popen(command, "r"); - if (!pipe) return NULL; - - char *result = malloc(4096); - size_t len = fread(result, 1, 4095, pipe); - result[len] = '\0'; - - pclose(pipe); - return result; -} - -// Get video metadata using ffprobe -static int get_video_metadata(encoder_config_t *config) { - char command[1024]; - char *output; - - // Get frame count - snprintf(command, sizeof(command), - "ffprobe -v quiet -select_streams v:0 -count_frames -show_entries stream=nb_read_frames -of csv=p=0 \"%s\"", - config->input_file); - output = execute_command(command); - if (!output) { - fprintf(stderr, "Failed to get frame count\n"); - return 0; - } - config->total_frames = atoi(output); - free(output); - - // Get frame rate - snprintf(command, sizeof(command), - "ffprobe -v quiet -select_streams v:0 -show_entries stream=r_frame_rate -of csv=p=0 \"%s\"", - config->input_file); - output = execute_command(command); - if (!output) { - fprintf(stderr, "Failed to get frame rate\n"); - return 0; - } - - // Parse framerate (could be "30/1" or "29.97") - int num, den; - if (sscanf(output, "%d/%d", &num, &den) == 2) { - config->fps = (den > 0) ? (num / den) : 30; - } else { - config->fps = (int)round(atof(output)); - } - free(output); - - // Get duration - snprintf(command, sizeof(command), - "ffprobe -v quiet -show_entries format=duration -of csv=p=0 \"%s\"", - config->input_file); - output = execute_command(command); - if (output) { - config->duration = atof(output); - free(output); - } - - // Check if has audio - snprintf(command, sizeof(command), - "ffprobe -v quiet -select_streams a:0 -show_entries stream=index -of csv=p=0 \"%s\"", - config->input_file); - output = execute_command(command); - config->has_audio = (output && strlen(output) > 0 && atoi(output) >= 0); - if (output) free(output); - - // Validate frame count using duration if needed - if (config->total_frames <= 0 && config->duration > 0) { - config->total_frames = (int)(config->duration * config->fps); - } - - fprintf(stderr, "Video metadata:\n"); - fprintf(stderr, " Frames: %d\n", config->total_frames); - fprintf(stderr, " FPS: %d\n", config->fps); - fprintf(stderr, " Duration: %.2fs\n", config->duration); - fprintf(stderr, " Audio: %s\n", config->has_audio ? "Yes" : "No"); - fprintf(stderr, " Resolution: %dx%d\n", config->width, config->height); - - return (config->total_frames > 0 && config->fps > 0); -} - -// Start FFmpeg process for video conversion -static int start_video_conversion(encoder_config_t *config) { - char command[2048]; - snprintf(command, sizeof(command), - "ffmpeg -i \"%s\" -f rawvideo -pix_fmt rgb24 -vf scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d -y - 2>/dev/null", - config->input_file, config->width, config->height, config->width, config->height); - - config->ffmpeg_video_pipe = popen(command, "r"); - return (config->ffmpeg_video_pipe != NULL); -} - -// Start FFmpeg process for audio conversion -static int start_audio_conversion(encoder_config_t *config) { - if (!config->has_audio) return 1; - - char command[2048]; - snprintf(command, sizeof(command), - "ffmpeg -i \"%s\" -acodec libtwolame -psymodel 4 -b:a 192k -ar %d -ac 2 -y \"%s\" 2>/dev/null", - config->input_file, MP2_SAMPLE_RATE, TEMP_AUDIO_FILE); - - int result = system(command); - if (result == 0) { - config->mp2_file = fopen(TEMP_AUDIO_FILE, "rb"); - if (config->mp2_file) { - fseek(config->mp2_file, 0, SEEK_END); - config->audio_remaining = ftell(config->mp2_file); - fseek(config->mp2_file, 0, SEEK_SET); - return 1; - } - } - - fprintf(stderr, "Warning: Failed to convert audio, proceeding without audio\n"); - config->has_audio = 0; - return 1; -} - -// Write variable-length integer -static void write_varint(uint8_t **ptr, uint32_t value) { - while (value >= 0x80) { - **ptr = (uint8_t)((value & 0x7F) | 0x80); - (*ptr)++; - value >>= 7; - } - **ptr = (uint8_t)(value & 0x7F); - (*ptr)++; -} - -// Get MP2 packet size and rate index -static int get_mp2_packet_size(uint8_t *header) { - int bitrate_index = (header[2] >> 4) & 0xF; - int padding_bit = (header[2] >> 1) & 0x1; - - int bitrates[] = {0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, -1}; - int bitrate = bitrates[bitrate_index]; - - if (bitrate <= 0) return MP2_DEFAULT_PACKET_SIZE; - - int frame_size = (144 * bitrate * 1000) / MP2_SAMPLE_RATE + padding_bit; - return frame_size; -} - -static int mp2_packet_size_to_rate_index(int packet_size, int is_mono) { - int rate_index; - switch (packet_size) { - case 144: rate_index = 0; break; - case 216: rate_index = 2; break; - case 252: rate_index = 4; break; - case 288: rate_index = 6; break; - case 360: rate_index = 8; break; - case 432: rate_index = 10; break; - case 504: rate_index = 12; break; - case 576: rate_index = 14; break; - case 720: rate_index = 16; break; - case 864: rate_index = 18; break; - case 1008: rate_index = 20; break; - case 1152: rate_index = 22; break; - case 1440: rate_index = 24; break; - case 1728: rate_index = 26; break; - default: rate_index = 14; break; - } - return rate_index + (is_mono ? 1 : 0); -} - -// Gzip compress function (instead of zlib) -static size_t gzip_compress(uint8_t *src, size_t src_len, uint8_t *dst, size_t dst_max) { - z_stream stream = {0}; - stream.next_in = src; - stream.avail_in = src_len; - stream.next_out = dst; - stream.avail_out = dst_max; - - // Use deflateInit2 with gzip format - if (deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) { - return 0; - } - - if (deflate(&stream, Z_FINISH) != Z_STREAM_END) { - deflateEnd(&stream); - return 0; - } - - size_t compressed_size = stream.total_out; - deflateEnd(&stream); - return compressed_size; -} - -// Bayer dithering kernels (4 patterns, each 4x4) -static const float bayerKernels[4][16] = { - { // Pattern 0 - (0.0f + 0.5f) / 16.0f, (8.0f + 0.5f) / 16.0f, (2.0f + 0.5f) / 16.0f, (10.0f + 0.5f) / 16.0f, - (12.0f + 0.5f) / 16.0f, (4.0f + 0.5f) / 16.0f, (14.0f + 0.5f) / 16.0f, (6.0f + 0.5f) / 16.0f, - (3.0f + 0.5f) / 16.0f, (11.0f + 0.5f) / 16.0f, (1.0f + 0.5f) / 16.0f, (9.0f + 0.5f) / 16.0f, - (15.0f + 0.5f) / 16.0f, (7.0f + 0.5f) / 16.0f, (13.0f + 0.5f) / 16.0f, (5.0f + 0.5f) / 16.0f - }, - { // Pattern 1 - (8.0f + 0.5f) / 16.0f, (2.0f + 0.5f) / 16.0f, (10.0f + 0.5f) / 16.0f, (0.0f + 0.5f) / 16.0f, - (4.0f + 0.5f) / 16.0f, (14.0f + 0.5f) / 16.0f, (6.0f + 0.5f) / 16.0f, (12.0f + 0.5f) / 16.0f, - (11.0f + 0.5f) / 16.0f, (1.0f + 0.5f) / 16.0f, (9.0f + 0.5f) / 16.0f, (3.0f + 0.5f) / 16.0f, - (7.0f + 0.5f) / 16.0f, (13.0f + 0.5f) / 16.0f, (5.0f + 0.5f) / 16.0f, (15.0f + 0.5f) / 16.0f - }, - { // Pattern 2 - (7.0f + 0.5f) / 16.0f, (13.0f + 0.5f) / 16.0f, (5.0f + 0.5f) / 16.0f, (15.0f + 0.5f) / 16.0f, - (8.0f + 0.5f) / 16.0f, (2.0f + 0.5f) / 16.0f, (10.0f + 0.5f) / 16.0f, (0.0f + 0.5f) / 16.0f, - (4.0f + 0.5f) / 16.0f, (14.0f + 0.5f) / 16.0f, (6.0f + 0.5f) / 16.0f, (12.0f + 0.5f) / 16.0f, - (11.0f + 0.5f) / 16.0f, (1.0f + 0.5f) / 16.0f, (9.0f + 0.5f) / 16.0f, (3.0f + 0.5f) / 16.0f - }, - { // Pattern 3 - (15.0f + 0.5f) / 16.0f, (7.0f + 0.5f) / 16.0f, (13.0f + 0.5f) / 16.0f, (5.0f + 0.5f) / 16.0f, - (0.0f + 0.5f) / 16.0f, (8.0f + 0.5f) / 16.0f, (2.0f + 0.5f) / 16.0f, (10.0f + 0.5f) / 16.0f, - (12.0f + 0.5f) / 16.0f, (4.0f + 0.5f) / 16.0f, (14.0f + 0.5f) / 16.0f, (6.0f + 0.5f) / 16.0f, - (3.0f + 0.5f) / 16.0f, (11.0f + 0.5f) / 16.0f, (1.0f + 0.5f) / 16.0f, (9.0f + 0.5f) / 16.0f - } -}; - -// CORRECTED: Encode a 4x4 block to iPF1 format matching Kotlin implementation -static void encode_ipf1_block_correct(uint8_t *rgb_data, int width, int height, int block_x, int block_y, - int channels, int pattern, uint8_t *output) { - ycocg_t pixels[16]; - int y_values[16]; - float co_values[16]; // Keep full precision for subsampling - float cg_values[16]; // Keep full precision for subsampling - - // Convert 4x4 block to YCoCg using corrected transform - for (int py = 0; py < 4; py++) { - for (int px = 0; px < 4; px++) { - int src_x = block_x * 4 + px; - int src_y = block_y * 4 + py; - float t = (pattern < 0) ? 0.0f : bayerKernels[pattern % 4][4 * (py % 4) + (px % 4)]; - int idx = py * 4 + px; - - if (src_x < width && src_y < height) { - int pixel_offset = (src_y * width + src_x) * channels; - uint8_t r = rgb_data[pixel_offset]; - uint8_t g = rgb_data[pixel_offset + 1]; - uint8_t b = rgb_data[pixel_offset + 2]; - pixels[idx] = rgb_to_ycocg_correct(r, g, b, t); - } else { - pixels[idx] = (ycocg_t){0.0f, 0.0f, 0.0f}; - } - - y_values[idx] = quantise_4bit_y(pixels[idx].y); - co_values[idx] = pixels[idx].co; - cg_values[idx] = pixels[idx].cg; - } - } - - // CORRECTED: Chroma subsampling (4:2:0 for iPF1) with correct averaging - int cos1 = chroma_to_four_bits((co_values[0] + co_values[1] + co_values[4] + co_values[5]) / 4.0f); - int cos2 = chroma_to_four_bits((co_values[2] + co_values[3] + co_values[6] + co_values[7]) / 4.0f); - int cos3 = chroma_to_four_bits((co_values[8] + co_values[9] + co_values[12] + co_values[13]) / 4.0f); - int cos4 = chroma_to_four_bits((co_values[10] + co_values[11] + co_values[14] + co_values[15]) / 4.0f); - - int cgs1 = chroma_to_four_bits((cg_values[0] + cg_values[1] + cg_values[4] + cg_values[5]) / 4.0f); - int cgs2 = chroma_to_four_bits((cg_values[2] + cg_values[3] + cg_values[6] + cg_values[7]) / 4.0f); - int cgs3 = chroma_to_four_bits((cg_values[8] + cg_values[9] + cg_values[12] + cg_values[13]) / 4.0f); - int cgs4 = chroma_to_four_bits((cg_values[10] + cg_values[11] + cg_values[14] + cg_values[15]) / 4.0f); - - // CORRECTED: Pack into iPF1 format matching Kotlin exactly - // Co values (2 bytes): cos2|cos1, cos4|cos3 - output[0] = ((cos2 << 4) | cos1); - output[1] = ((cos4 << 4) | cos3); - - // Cg values (2 bytes): cgs2|cgs1, cgs4|cgs3 - output[2] = ((cgs2 << 4) | cgs1); - output[3] = ((cgs4 << 4) | cgs3); - - // CORRECTED: Y values (8 bytes) with correct ordering from Kotlin - output[4] = ((y_values[1] << 4) | y_values[0]); // Y1|Y0 - output[5] = ((y_values[5] << 4) | y_values[4]); // Y5|Y4 - output[6] = ((y_values[3] << 4) | y_values[2]); // Y3|Y2 - output[7] = ((y_values[7] << 4) | y_values[6]); // Y7|Y6 - output[8] = ((y_values[9] << 4) | y_values[8]); // Y9|Y8 - output[9] = ((y_values[13] << 4) | y_values[12]); // Y13|Y12 - output[10] = ((y_values[11] << 4) | y_values[10]); // Y11|Y10 - output[11] = ((y_values[15] << 4) | y_values[14]); // Y15|Y14 -} - -// Helper function for contrast weighting -static double contrast_weight(int v1, int v2, int delta, int weight) { - double avg = (v1 + v2) / 2.0; - double contrast = (avg < 4 || avg > 11) ? 1.5 : 1.0; - return delta * weight * contrast; -} - -// Check if two iPF1 blocks are significantly different -static int is_significantly_different(uint8_t *block_a, uint8_t *block_b) { - double score = 0.0; - - // Co values (bytes 0-1) - uint16_t co_a = block_a[0] | (block_a[1] << 8); - uint16_t co_b = block_b[0] | (block_b[1] << 8); - for (int i = 0; i < 4; i++) { - int va = (co_a >> (i * 4)) & 0xF; - int vb = (co_b >> (i * 4)) & 0xF; - int delta = abs(va - vb); - score += contrast_weight(va, vb, delta, 3); - } - - // Cg values (bytes 2-3) - uint16_t cg_a = block_a[2] | (block_a[3] << 8); - uint16_t cg_b = block_b[2] | (block_b[3] << 8); - for (int i = 0; i < 4; i++) { - int va = (cg_a >> (i * 4)) & 0xF; - int vb = (cg_b >> (i * 4)) & 0xF; - int delta = abs(va - vb); - score += contrast_weight(va, vb, delta, 3); - } - - // Y values (bytes 4-11) - for (int i = 4; i < 12; i++) { - int byte_a = block_a[i] & 0xFF; - int byte_b = block_b[i] & 0xFF; - - int y_a_high = (byte_a >> 4) & 0xF; - int y_a_low = byte_a & 0xF; - int y_b_high = (byte_b >> 4) & 0xF; - int y_b_low = byte_b & 0xF; - - int delta_high = abs(y_a_high - y_b_high); - int delta_low = abs(y_a_low - y_b_low); - - score += contrast_weight(y_a_high, y_b_high, delta_high, 2); - score += contrast_weight(y_a_low, y_b_low, delta_low, 2); - } - - return score > 4.0; -} - -// Encode iPF1 frame to buffer -static void encode_ipf1_frame(uint8_t *rgb_data, int width, int height, int channels, int pattern, - uint8_t *ipf_buffer) { - int blocks_per_row = (width + 3) / 4; - int blocks_per_col = (height + 3) / 4; - - for (int block_y = 0; block_y < blocks_per_col; block_y++) { - for (int block_x = 0; block_x < blocks_per_row; block_x++) { - int block_index = block_y * blocks_per_row + block_x; - uint8_t *output_block = ipf_buffer + block_index * IPF_BLOCK_SIZE; - encode_ipf1_block_correct(rgb_data, width, height, block_x, block_y, channels, pattern, output_block); - } - } -} - -// Create iPF1-delta encoded frame -static size_t encode_ipf1_delta(uint8_t *previous_frame, uint8_t *current_frame, - int width, int height, uint8_t *delta_buffer) { - int blocks_per_row = (width + 3) / 4; - int blocks_per_col = (height + 3) / 4; - int total_blocks = blocks_per_row * blocks_per_col; - - uint8_t *output_ptr = delta_buffer; - int skip_count = 0; - uint8_t *patch_blocks = malloc(total_blocks * IPF_BLOCK_SIZE); - int patch_count = 0; - - for (int block_index = 0; block_index < total_blocks; block_index++) { - uint8_t *prev_block = previous_frame + block_index * IPF_BLOCK_SIZE; - uint8_t *curr_block = current_frame + block_index * IPF_BLOCK_SIZE; - - if (is_significantly_different(prev_block, curr_block)) { - if (skip_count > 0) { - *output_ptr++ = SKIP_OP; - write_varint(&output_ptr, skip_count); - skip_count = 0; - } - - memcpy(patch_blocks + patch_count * IPF_BLOCK_SIZE, curr_block, IPF_BLOCK_SIZE); - patch_count++; - } else { - if (patch_count > 0) { - *output_ptr++ = PATCH_OP; - write_varint(&output_ptr, patch_count); - memcpy(output_ptr, patch_blocks, patch_count * IPF_BLOCK_SIZE); - output_ptr += patch_count * IPF_BLOCK_SIZE; - patch_count = 0; - } - skip_count++; - } - } - - if (patch_count > 0) { - *output_ptr++ = PATCH_OP; - write_varint(&output_ptr, patch_count); - memcpy(output_ptr, patch_blocks, patch_count * IPF_BLOCK_SIZE); - output_ptr += patch_count * IPF_BLOCK_SIZE; - } - - *output_ptr++ = END_OP; - - free(patch_blocks); - return output_ptr - delta_buffer; -} - -// Get current time in seconds -static double get_current_time_sec(struct timeval *tv) { - gettimeofday(tv, NULL); - return tv->tv_sec + tv->tv_usec / 1000000.0; -} - -// Display progress information similar to FFmpeg -static void display_progress(encoder_config_t *config, int frame_num) { - struct timeval current_time; - double current_sec = get_current_time_sec(¤t_time); - - // Only update progress once per second - double last_progress_sec = config->last_progress_time.tv_sec + config->last_progress_time.tv_usec / 1000000.0; - if (current_sec - last_progress_sec < 1.0) { - return; - } - - config->last_progress_time = current_time; - - // Calculate timing - double start_sec = config->start_time.tv_sec + config->start_time.tv_usec / 1000000.0; - double elapsed_sec = current_sec - start_sec; - double current_video_time = (double)frame_num / config->fps; - double fps = frame_num / elapsed_sec; - double speed = (elapsed_sec > 0) ? current_video_time / elapsed_sec : 0.0; - double bitrate = (elapsed_sec > 0) ? (config->total_output_bytes * 8.0 / 1024.0) / elapsed_sec : 0.0; - - // Format output size in human readable format - char size_str[32]; - if (config->total_output_bytes >= 1024 * 1024) { - snprintf(size_str, sizeof(size_str), "%.1fMB", config->total_output_bytes / (1024.0 * 1024.0)); - } else if (config->total_output_bytes >= 1024) { - snprintf(size_str, sizeof(size_str), "%.1fkB", config->total_output_bytes / 1024.0); - } else { - snprintf(size_str, sizeof(size_str), "%zuB", config->total_output_bytes); - } - - // Format current time as HH:MM:SS.xx - int hours = (int)(current_video_time / 3600); - int minutes = (int)((current_video_time - hours * 3600) / 60); - double seconds = current_video_time - hours * 3600 - minutes * 60; - - // Print progress line (overwrite previous line) - fprintf(stderr, "\rframe=%d fps=%.1f size=%s time=%02d:%02d:%05.2f bitrate=%.1fkbits/s speed=%4.2fx", - frame_num, fps, size_str, hours, minutes, seconds, bitrate, speed); - fflush(stderr); -} - -// Process audio for current frame -static int process_audio(encoder_config_t *config, int frame_num, FILE *output) { - if (!config->has_audio || !config->mp2_file || config->audio_remaining <= 0) { - return 1; - } - - // Initialise packet size on first frame - if (config->mp2_packet_size == 0) { - uint8_t header[4]; - if (fread(header, 1, 4, config->mp2_file) != 4) return 1; - fseek(config->mp2_file, 0, SEEK_SET); - - config->mp2_packet_size = get_mp2_packet_size(header); - int is_mono = (header[3] >> 6) == 3; - config->mp2_rate_index = mp2_packet_size_to_rate_index(config->mp2_packet_size, is_mono); - } - - // Calculate how much audio time each frame represents (in seconds) - double frame_audio_time = 1.0 / config->fps; - - // Calculate how much audio time each MP2 packet represents - // MP2 frame contains 1152 samples at 32kHz = 0.036 seconds - double packet_audio_time = 1152.0 / MP2_SAMPLE_RATE; - - // Estimate how many packets we consume per video frame - double packets_per_frame = frame_audio_time / packet_audio_time; - - // Only insert audio when buffer would go below 2 frames - // Initialise with 2 packets on first frame to prime the buffer - int packets_to_insert = 0; - if (frame_num == 1) { - packets_to_insert = 2; - config->audio_frames_in_buffer = 2; - } else { - // Simulate buffer consumption (packets consumed per frame) - config->audio_frames_in_buffer -= (int)ceil(packets_per_frame); - - // Only insert packets when buffer gets low (≤ 2 frames) - if (config->audio_frames_in_buffer <= 2) { - packets_to_insert = config->target_audio_buffer_size - config->audio_frames_in_buffer; - packets_to_insert = (packets_to_insert > 0) ? packets_to_insert : 1; - } - } - - // Insert the calculated number of audio packets - for (int q = 0; q < packets_to_insert; q++) { - size_t bytes_to_read = config->mp2_packet_size; - if (bytes_to_read > config->audio_remaining) { - bytes_to_read = config->audio_remaining; - } - - size_t bytes_read = fread(config->mp2_buffer, 1, bytes_to_read, config->mp2_file); - if (bytes_read == 0) break; - - uint8_t audio_packet_type[2] = {config->mp2_rate_index, MP2_PACKET_TYPE_BASE}; - fwrite(audio_packet_type, 1, 2, output); - fwrite(config->mp2_buffer, 1, bytes_read, output); - - // Track audio bytes written - config->total_output_bytes += 2 + bytes_read; - config->audio_remaining -= bytes_read; - config->audio_frames_in_buffer++; - } - - return 1; -} - -// Write TVDOS header -static void write_tvdos_header(encoder_config_t *config, FILE *output) { - fwrite(TVDOS_MAGIC, 1, 8, output); - fwrite(&config->width, 2, 1, output); - fwrite(&config->height, 2, 1, output); - fwrite(&config->fps, 2, 1, output); - fwrite(&config->total_frames, 4, 1, output); - - uint16_t unused = 0x00FF; - fwrite(&unused, 2, 1, output); - - int audio_sample_size = 2 * (((MP2_SAMPLE_RATE / config->fps) + 1)); - int audio_queue_size = config->has_audio ? - (int)ceil(audio_sample_size / 2304.0) + 1 : 0; - - uint16_t audio_queue_info = config->has_audio ? - (MP2_DEFAULT_PACKET_SIZE >> 2) | (audio_queue_size << 12) : 0x0000; - fwrite(&audio_queue_info, 2, 1, output); - - // Store target buffer size for audio timing - config->target_audio_buffer_size = audio_queue_size; - - uint8_t reserved[10] = {0}; - fwrite(reserved, 1, 10, output); -} - -// Initialise encoder configuration -static encoder_config_t *init_encoder_config() { - encoder_config_t *config = calloc(1, sizeof(encoder_config_t)); - if (!config) return NULL; - - config->width = DEFAULT_WIDTH; - config->height = DEFAULT_HEIGHT; - - return config; -} - -// Allocate encoder buffers -static int allocate_buffers(encoder_config_t *config) { - config->frame_buffer_size = ((config->width + 3) / 4) * ((config->height + 3) / 4) * IPF_BLOCK_SIZE; - - config->rgb_buffer = malloc(config->width * config->height * 3); - config->previous_ipf_frame = malloc(config->frame_buffer_size); - config->current_ipf_frame = malloc(config->frame_buffer_size); - config->delta_buffer = malloc(config->frame_buffer_size * 2); - config->compressed_buffer = malloc(config->frame_buffer_size * 2); - config->mp2_buffer = malloc(2048); - - return (config->rgb_buffer && config->previous_ipf_frame && - config->current_ipf_frame && config->delta_buffer && - config->compressed_buffer && config->mp2_buffer); -} - -// Process one frame - CORRECTED ORDER: Audio -> Video -> Sync -static int process_frame(encoder_config_t *config, int frame_num, int is_keyframe, FILE *output) { - // Read RGB data from FFmpeg pipe first - size_t rgb_size = config->width * config->height * 3; - if (fread(config->rgb_buffer, 1, rgb_size, config->ffmpeg_video_pipe) != rgb_size) { - if (feof(config->ffmpeg_video_pipe)) return 0; - return -1; - } - - // Step 1: Process audio FIRST (matches working file pattern) - if (!process_audio(config, frame_num, output)) { - return -1; - } - - // Step 2: Encode and write video - int pattern; - switch (config->dither_mode) { - case 0: pattern = -1; break; // No dithering - case 1: pattern = 0; break; // Static pattern - case 2: pattern = frame_num % 4; break; // Dynamic pattern - default: pattern = 0; break; // Fallback to static - } - encode_ipf1_frame(config->rgb_buffer, config->width, config->height, 3, pattern, - config->current_ipf_frame); - - // Determine if we should use delta encoding - int use_delta = 0; - size_t data_size = config->frame_buffer_size; - uint8_t *frame_data = config->current_ipf_frame; - - if (frame_num > 1 && !is_keyframe) { - size_t delta_size = encode_ipf1_delta(config->previous_ipf_frame, - config->current_ipf_frame, - config->width, config->height, - config->delta_buffer); - - if (delta_size < config->frame_buffer_size * 0.576) { - use_delta = 1; - data_size = delta_size; - frame_data = config->delta_buffer; - } - } - - // Compress the frame data using gzip - size_t compressed_size = gzip_compress(frame_data, data_size, - config->compressed_buffer, - config->frame_buffer_size * 2); - if (compressed_size == 0) { - fprintf(stderr, "Gzip compression failed\n"); - return -1; - } - - // Write video packet - if (use_delta) { - uint8_t packet_type[2] = {IPF1_DELTA_PACKET_TYPE}; - fwrite(packet_type, 1, 2, output); - } else { - uint8_t packet_type[2] = {IPF1_PACKET_TYPE}; - fwrite(packet_type, 1, 2, output); - } - - uint32_t size_le = compressed_size; - fwrite(&size_le, 4, 1, output); - fwrite(config->compressed_buffer, 1, compressed_size, output); - - // Step 3: Write sync packet AFTER video (matches working file pattern) - uint8_t sync[2] = {SYNC_PACKET_TYPE}; - fwrite(sync, 1, 2, output); - - // Track video bytes written (packet type + size + compressed data + sync) - config->total_output_bytes += 2 + 4 + compressed_size + 2; - - // Swap frame buffers - uint8_t *temp = config->previous_ipf_frame; - config->previous_ipf_frame = config->current_ipf_frame; - config->current_ipf_frame = temp; - - // Display progress - display_progress(config, frame_num); - - return 1; -} - -// Cleanup function -static void cleanup_config(encoder_config_t *config) { - if (!config) return; - - if (config->ffmpeg_video_pipe) pclose(config->ffmpeg_video_pipe); - if (config->mp2_file) fclose(config->mp2_file); - - free(config->input_file); - free(config->output_file); - free(config->rgb_buffer); - free(config->previous_ipf_frame); - free(config->current_ipf_frame); - free(config->delta_buffer); - free(config->compressed_buffer); - free(config->mp2_buffer); - - // Remove temporary audio file - unlink(TEMP_AUDIO_FILE); - - free(config); -} - -// Print usage information -static void print_usage(const char *program_name) { - printf("TVDOS Movie Encoder\n\n"); - printf("Usage: %s [options] input_video\n\n", program_name); - printf("Options:\n"); - printf(" -o, --output FILE Output TVDOS movie file (default: stdout)\n"); - printf(" -s, --size WxH Video resolution (default: 560x448)\n"); - printf(" -d, --dither MODE Dithering mode (default: 1)\n"); - printf(" 0: No dithering\n"); - printf(" 1: Static pattern\n"); - printf(" 2: Dynamic pattern (better quality, larger files)\n"); - printf(" -h, --help Show this help message\n\n"); - printf("Examples:\n"); - printf(" %s input.mp4 -o output.mov\n", program_name); - printf(" %s input.avi -s 1024x768 -o output.mov\n", program_name); - printf(" yt-dlp -o - \"https://youtube.com/watch?v=VIDEO_ID\" | ffmpeg -i pipe:0 -c copy temp.mp4 && %s temp.mp4 -o youtube_video.mov && rm temp.mp4\n", program_name); -} - -int main(int argc, char *argv[]) { - encoder_config_t *config = init_encoder_config(); - if (!config) { - fprintf(stderr, "Failed to initialise encoder\n"); - return 1; - } - - config->output_to_stdout = 1; // Default to stdout - config->dither_mode = 1; // Default to static dithering - - // Parse command line arguments - static struct option long_options[] = { - {"output", required_argument, 0, 'o'}, - {"size", required_argument, 0, 's'}, - {"dither", required_argument, 0, 'd'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - int c; - while ((c = getopt_long(argc, argv, "o:s:d:h", long_options, NULL)) != -1) { - switch (c) { - case 'o': - config->output_file = strdup(optarg); - config->output_to_stdout = 0; - break; - case 's': - if (!parse_resolution(optarg, &config->width, &config->height)) { - fprintf(stderr, "Invalid resolution format: %s\n", optarg); - cleanup_config(config); - return 1; - } - break; - case 'd': - config->dither_mode = atoi(optarg); - if (config->dither_mode < 0 || config->dither_mode > 2) { - fprintf(stderr, "Invalid dither mode: %s (must be 0, 1, or 2)\n", optarg); - cleanup_config(config); - return 1; - } - break; - case 'h': - print_usage(argv[0]); - cleanup_config(config); - return 0; - default: - print_usage(argv[0]); - cleanup_config(config); - return 1; - } - } - - if (optind >= argc) { - fprintf(stderr, "Error: Input video file required\n\n"); - print_usage(argv[0]); - cleanup_config(config); - return 1; - } - - config->input_file = strdup(argv[optind]); - - // Get video metadata - if (!get_video_metadata(config)) { - fprintf(stderr, "Failed to analyze video metadata\n"); - cleanup_config(config); - return 1; - } - - // Allocate buffers - if (!allocate_buffers(config)) { - fprintf(stderr, "Failed to allocate memory buffers\n"); - cleanup_config(config); - return 1; - } - - // Start video conversion - if (!start_video_conversion(config)) { - fprintf(stderr, "Failed to start video conversion\n"); - cleanup_config(config); - return 1; - } - - // Start audio conversion - if (!start_audio_conversion(config)) { - fprintf(stderr, "Failed to start audio conversion\n"); - cleanup_config(config); - return 1; - } - - // Open output - FILE *output = config->output_to_stdout ? stdout : fopen(config->output_file, "wb"); - if (!output) { - fprintf(stderr, "Failed to open output file\n"); - cleanup_config(config); - return 1; - } - - // Write TVDOS header - write_tvdos_header(config, output); - - // Initialise progress tracking - gettimeofday(&config->start_time, NULL); - config->last_progress_time = config->start_time; - config->total_output_bytes = 8 + 2 + 2 + 2 + 4 + 2 + 2 + 10; // TVDOS header size - - // Process frames with correct order: Audio -> Video -> Sync - for (int frame = 1; frame <= config->total_frames; frame++) { - int is_keyframe = (frame == 1) || (frame % 30 == 0); - - int result = process_frame(config, frame, is_keyframe, output); - if (result <= 0) { - if (result == 0) { - fprintf(stderr, "End of video at frame %d\n", frame); - } - break; - } - } - - // Final progress update and newline - fprintf(stderr, "\n"); - - if (!config->output_to_stdout) { - fclose(output); - fprintf(stderr, "Encoding complete: %s\n", config->output_file); - } - - cleanup_config(config); - return 0; -} diff --git a/video_encoder/encoder_tav_opencv.cpp b/video_encoder/encoder_tav_opencv.cpp deleted file mode 100644 index f74d2d1..0000000 --- a/video_encoder/encoder_tav_opencv.cpp +++ /dev/null @@ -1,183 +0,0 @@ -// Created by CuriousTorvald and Claude on 2025-10-17 -// MPEG-style bidirectional block motion compensation for TAV encoder -// Simplified: Single-level diamond search, variable blocks, overlaps, sub-pixel refinement - -#include -#include -#include -#include - -extern "C" { - -// Dense optical flow estimation using Farneback algorithm -// Computes flow at every pixel, then samples at block centers for motion vectors -// Much more spatially coherent than independent block matching -void estimate_optical_flow_motion( - const float *current_y, // Current frame Y channel (width×height) - const float *reference_y, // Reference frame Y channel - int width, int height, - int block_size, // Block size (e.g., 16) - int16_t *mvs_x, // Output: motion vectors X (in 1/4-pixel units) - int16_t *mvs_y // Output: motion vectors Y (in 1/4-pixel units) -) { - // Convert float Y channels to 8-bit grayscale for OpenCV - cv::Mat cur_gray(height, width, CV_8UC1); - cv::Mat ref_gray(height, width, CV_8UC1); - - // Detect if Y is in [0,1] range and scale to [0,255] if needed - float y_min = current_y[0], y_max = current_y[0]; - for (int i = 1; i < width * height; i++) { - if (current_y[i] < y_min) y_min = current_y[i]; - if (current_y[i] > y_max) y_max = current_y[i]; - } - float scale = (y_max <= 1.1f) ? 255.0f : 1.0f; - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int idx = y * width + x; - cur_gray.at(y, x) = (uint8_t)std::round(std::max(0.0f, std::min(255.0f, current_y[idx] * scale))); - ref_gray.at(y, x) = (uint8_t)std::round(std::max(0.0f, std::min(255.0f, reference_y[idx] * scale))); - } - } - - // Compute dense optical flow using Farneback algorithm - // IMPORTANT: We need BACKWARD flow (current → reference) for motion compensation - // This tells us where to PULL pixels FROM in the reference frame - cv::Mat flow; - cv::calcOpticalFlowFarneback( - cur_gray, // Current frame (source) - ref_gray, // Reference frame (destination) - flow, // Output flow (2-channel float: dx, dy per pixel) - 0.5, // pyr_scale: pyramid scale (0.5 = each layer is half size) - 3, // levels: number of pyramid levels - 20, // winsize: averaging window size - 3, // iterations: number of iterations at each pyramid level - 5, // poly_n: size of pixel neighborhood (5 or 7) - 1.2, // poly_sigma: standard deviation of Gaussian for polynomial expansion - 0 // flags: 0 = normal, OPTFLOW_USE_INITIAL_FLOW = use input flow as initial estimate - ); - - // Sample flow at block centers to get motion vectors - int num_blocks_x = (width + block_size - 1) / block_size; - int num_blocks_y = (height + block_size - 1) / block_size; - - for (int by = 0; by < num_blocks_y; by++) { - for (int bx = 0; bx < num_blocks_x; bx++) { - int block_idx = by * num_blocks_x + bx; - - // Block center position - int center_x = bx * block_size + block_size / 2; - int center_y = by * block_size + block_size / 2; - - // Clamp to frame boundaries - if (center_x >= width) center_x = width - 1; - if (center_y >= height) center_y = height - 1; - - // Get flow at block center - cv::Point2f flow_vec = flow.at(center_y, center_x); - - // Convert to 1/4-pixel units and store - // Flow is in pixels, positive = motion to the right/down - mvs_x[block_idx] = (int16_t)std::round(flow_vec.x * 4.0f); - mvs_y[block_idx] = (int16_t)std::round(flow_vec.y * 4.0f); - } - } -} - -// Block-based motion compensation with bilinear interpolation (sub-pixel precision) -// MVs are in 1/4-pixel units -// This implements the warp() function from MC-EZBC pseudocode -void warp_block_motion( - const float *src, // Source frame - int width, int height, - const int16_t *mvs_x, // Motion vectors X (1/4-pixel units) - const int16_t *mvs_y, // Motion vectors Y (1/4-pixel units) - int block_size, // Block size (e.g., 16) - float *dst // Output warped frame -) { - int num_blocks_x = (width + block_size - 1) / block_size; - int num_blocks_y = (height + block_size - 1) / block_size; - - // Process each block - for (int by = 0; by < num_blocks_y; by++) { - for (int bx = 0; bx < num_blocks_x; bx++) { - int block_idx = by * num_blocks_x + bx; - - // Get motion vector for this block (in 1/4-pixel units) - float mv_x = mvs_x[block_idx] / 4.0f; // Convert to pixels - float mv_y = mvs_y[block_idx] / 4.0f; - - // Block boundaries in destination frame - int block_x_start = bx * block_size; - int block_y_start = by * block_size; - int block_x_end = std::min(block_x_start + block_size, width); - int block_y_end = std::min(block_y_start + block_size, height); - - // Warp each pixel in the block - for (int y = block_y_start; y < block_y_end; y++) { - for (int x = block_x_start; x < block_x_end; x++) { - // Source position (backward warping) - float src_x = x - mv_x; - float src_y = y - mv_y; - - // Clamp to valid range - src_x = std::max(0.0f, std::min((float)(width - 1), src_x)); - src_y = std::max(0.0f, std::min((float)(height - 1), src_y)); - - // Bilinear interpolation - int x0 = (int)src_x; - int y0 = (int)src_y; - int x1 = std::min(x0 + 1, width - 1); - int y1 = std::min(y0 + 1, height - 1); - - float fx = src_x - x0; - float fy = src_y - y0; - - float val00 = src[y0 * width + x0]; - float val10 = src[y0 * width + x1]; - float val01 = src[y1 * width + x0]; - float val11 = src[y1 * width + x1]; - - float val_top = (1.0f - fx) * val00 + fx * val10; - float val_bot = (1.0f - fx) * val01 + fx * val11; - float val = (1.0f - fy) * val_top + fy * val_bot; - - dst[y * width + x] = val; - } - } - } - } -} - -// Bidirectional motion compensation for MC-EZBC predict step -// Implements: prediction = 0.5 * (warp(f0, MV_fwd) + warp(f1, MV_bwd)) -void warp_bidirectional( - const float *f0, const float *f1, - int width, int height, - const int16_t *mvs_fwd_x, const int16_t *mvs_fwd_y, // F0 → F1 - const int16_t *mvs_bwd_x, const int16_t *mvs_bwd_y, // F1 → F0 - int block_size, - float *prediction // Output: 0.5 * (warped_f0 + warped_f1) -) { - int num_pixels = width * height; - - // Allocate temporary buffers - float *warped_f0 = new float[num_pixels]; - float *warped_f1 = new float[num_pixels]; - - // Warp f0 forward using forward MVs - warp_block_motion(f0, width, height, mvs_fwd_x, mvs_fwd_y, block_size, warped_f0); - - // Warp f1 backward using backward MVs - warp_block_motion(f1, width, height, mvs_bwd_x, mvs_bwd_y, block_size, warped_f1); - - // Average the two warped frames - for (int i = 0; i < num_pixels; i++) { - prediction[i] = 0.5f * (warped_f0[i] + warped_f1[i]); - } - - delete[] warped_f0; - delete[] warped_f1; -} - -} // extern "C" diff --git a/video_encoder/encoder_tav_text.c b/video_encoder/encoder_tav_text.c deleted file mode 100644 index fbddb1f..0000000 --- a/video_encoder/encoder_tav_text.c +++ /dev/null @@ -1,795 +0,0 @@ -/* -encoder_tav_text.c -Text-based video encoder for TSVM using custom font ROMs - -Outputs Videotex files with custom header and packet type 0x3F (text mode) - -File structure: - - Videotex header (32 bytes): magic "\x1FTSVM-VT", version, grid dims, fps, total_frames - - Extended header packet (0xEF): BGNT, ENDT, CDAT, VNDR, FMPG - - Font ROM packets (0x30): lowrom and highrom (1920 bytes each) - - Per-frame sequence: [audio 0x20], [timecode 0xFD], [videotex 0x3F], [sync 0xFF] - -Videotex packet structure (0x3F): Zstd([rows][cols][fg-array][bg-array][char-array]) - - rows: uint8 (32) - - cols: uint8 (80) - - fg-array: rows*cols bytes (foreground colors, 0xF0=black, 0xFE=white) - - bg-array: rows*cols bytes (background colors, 0xF0=black, 0xFE=white) - - char-array: rows*cols bytes (glyph indices 0-255) - -Total uncompressed size: 2 + (80*32*3) = 7682 bytes -Separated arrays compress much better (fg/bg are just 0xF0/0xFE runs) -Video size: 80×32 characters (560×448 pixels with 7×14 font) -Audio: MP2 encoding at 96 kbps, 32 KHz stereo (packet 0x20) -Each text frame is treated as an I-frame with sync packet - -Usage: - gcc -Ofast -std=c11 -Wall encoder_tav_text.c -o encoder_tav_text -lm -lzstd - ./encoder_tav_text -i video.mp4 -f font.chr -o output.mv3 -*/ - -#define _POSIX_C_SOURCE 200809L -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define ENCODER_VENDOR_STRING "Encoder-TAV-Text 20251121 (videotex)" - -#define CHAR_W 7 -#define CHAR_H 14 -#define GRID_W 80 -#define GRID_H 32 -#define PIXEL_W (GRID_W * CHAR_W) // 560 -#define PIXEL_H (GRID_H * CHAR_H) // 448 -#define PATCH_SZ (CHAR_W * CHAR_H) -#define SAMPLE_RATE 32000 -#define MP2_DEFAULT_PACKET_SIZE 1152 - -// TAV packet types -#define PACKET_TIMECODE 0xFD -#define PACKET_SYNC 0xFF -#define PACKET_AUDIO_MP2 0x20 -#define PACKET_SSF 0x30 -#define PACKET_TEXT 0x3F -#define PACKET_EXTENDED_HDR 0xEF - -// SSF opcodes for font ROM -#define SSF_OPCODE_LOWROM 0x80 -#define SSF_OPCODE_HIGHROM 0x81 - -// Font ROM size constants -#define FONTROM_PADDED_SIZE 1920 -#define GLYPHS_PER_ROM 128 - -// Color mapping (4-bit RGB to TSVM palette) -#define COLOR_BLACK 0xF0 -#define COLOR_WHITE 0xFE - -// Generate random filename for temporary audio file -static void generate_random_filename(char *filename) { - srand(time(NULL)); - - const char charset[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - const int charset_size = sizeof(charset) - 1; - - // Start with the prefix - strcpy(filename, "/tmp/"); - - // Generate 32 random characters - for (int i = 0; i < 32; i++) { - filename[5 + i] = charset[rand() % charset_size]; - } - - // Add the .mp2 extension - strcpy(filename + 37, ".mp2"); - filename[41] = '\0'; // Null terminate -} - -char TEMP_AUDIO_FILE[42]; - -// Global flag to disable inverted character matching -int g_no_invert_char = 0; - -typedef struct { - uint8_t *data; // Binary glyph data (PATCH_SZ bytes per glyph) - int count; // Number of glyphs -} FontROM; - -// Get FFmpeg version string -char *get_ffmpeg_version(void) { - FILE *pipe = popen("ffmpeg -version 2>&1 | head -1", "r"); - if (!pipe) return NULL; - - char *version = malloc(256); - if (!version) { - pclose(pipe); - return NULL; - } - - if (fgets(version, 256, pipe)) { - // Remove trailing newline - size_t len = strlen(version); - if (len > 0 && version[len - 1] == '\n') { - version[len - 1] = '\0'; - } - pclose(pipe); - return version; - } - - free(version); - pclose(pipe); - return NULL; -} - -// Detect video FPS using ffprobe -float detect_fps(const char *video_path) { - char cmd[1024]; - snprintf(cmd, sizeof(cmd), - "ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate " - "-of default=noprint_wrappers=1:nokey=1 \"%s\" 2>/dev/null", - video_path); - - FILE *pipe = popen(cmd, "r"); - if (!pipe) return 30.0f; // fallback - - char fps_str[64] = {0}; - if (fgets(fps_str, sizeof(fps_str), pipe)) { - // Parse fraction like "30/1" or "24000/1001" - int num = 0, den = 1; - if (sscanf(fps_str, "%d/%d", &num, &den) == 2 && den > 0) { - pclose(pipe); - return (float)num / (float)den; - } - } - pclose(pipe); - return 30.0f; // fallback -} - -// Load font ROM (14 bytes per glyph, no header) -FontROM *load_font_rom(const char *path) { - FILE *f = fopen(path, "rb"); - if (!f) return NULL; - - fseek(f, 0, SEEK_END); - long size = ftell(f); - fseek(f, 0, SEEK_SET); - - if (size % 14 != 0) { - fprintf(stderr, "Warning: ROM size not divisible by 14 (got %ld bytes)\n", size); - } - - int glyph_count = size / 14; - FontROM *rom = malloc(sizeof(FontROM)); - rom->count = glyph_count; - rom->data = malloc(glyph_count * PATCH_SZ); - - // Read and unpack glyphs - for (int g = 0; g < glyph_count; g++) { - uint8_t row_bytes[14]; - if (fread(row_bytes, 14, 1, f) != 1) { - free(rom->data); - free(rom); - fclose(f); - return NULL; - } - - // Unpack bits to binary pixels - for (int row = 0; row < CHAR_H; row++) { - for (int col = 0; col < CHAR_W; col++) { - // Bit 6 = leftmost, bit 0 = rightmost - int bit = (row_bytes[row] >> (6 - col)) & 1; - rom->data[g * PATCH_SZ + row * CHAR_W + col] = bit; - } - } - } - - fclose(f); - fprintf(stderr, "Loaded font ROM: %d glyphs\n", glyph_count); - return rom; -} - -// Find best matching glyph for a grayscale patch -int find_best_glyph(const uint8_t *patch, const FontROM *rom, uint8_t *out_bg, uint8_t *out_fg) { - // Try both normal and inverted matching (unless --no-invert-char is set) - int best_glyph = 0; - float best_error = INFINITY; - uint8_t best_bg = COLOR_BLACK, best_fg = COLOR_WHITE; - - for (int g = 0; g < rom->count; g++) { - const uint8_t *glyph = &rom->data[g * PATCH_SZ]; - - // Try normal: glyph 1 = fg, glyph 0 = bg - float err_normal = 0; - for (int i = 0; i < PATCH_SZ; i++) { - int expected = glyph[i] ? 255 : 0; - int diff = patch[i] - expected; - err_normal += diff * diff; - } - - if (err_normal < best_error) { - best_error = err_normal; - best_glyph = g; - best_bg = COLOR_BLACK; - best_fg = COLOR_WHITE; - } - - // Try inverted: glyph 0 = fg, glyph 1 = bg (skip if --no-invert-char) - if (!g_no_invert_char) { - float err_inverted = 0; - for (int i = 0; i < PATCH_SZ; i++) { - int expected = glyph[i] ? 0 : 255; - int diff = patch[i] - expected; - err_inverted += diff * diff; - } - - if (err_inverted < best_error) { - best_error = err_inverted; - best_glyph = g; - best_bg = COLOR_WHITE; - best_fg = COLOR_BLACK; - } - } - } - - *out_bg = best_bg; - *out_fg = best_fg; - return best_glyph; -} - -// Convert frame to text mode -void frame_to_text(const uint8_t *pixels, const FontROM *rom, - uint8_t *bg_col, uint8_t *fg_col, uint8_t *chars) { - uint8_t patch[PATCH_SZ]; - - for (int gr = 0; gr < GRID_H; gr++) { - for (int gc = 0; gc < GRID_W; gc++) { - int idx = gr * GRID_W + gc; - - // Extract patch - for (int y = 0; y < CHAR_H; y++) { - for (int x = 0; x < CHAR_W; x++) { - int px = gc * CHAR_W + x; - int py = gr * CHAR_H + y; - patch[y * CHAR_W + x] = pixels[py * PIXEL_W + px]; - } - } - - // Find best match - chars[idx] = find_best_glyph(patch, rom, &bg_col[idx], &fg_col[idx]); - } - } -} - -// Get current time in nanoseconds since UNIX epoch -uint64_t get_current_time_ns(void) { - struct timeval tv; - gettimeofday(&tv, NULL); - return (uint64_t)tv.tv_sec * 1000000000ULL + (uint64_t)tv.tv_usec * 1000ULL; -} - -// Parse MP2 packet header to get accurate packet size -int get_mp2_packet_size(uint8_t *header) { - int bitrate_index = (header[2] >> 4) & 0x0F; - int bitrates[] = {0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384}; - if (bitrate_index >= 15) return MP2_DEFAULT_PACKET_SIZE; - - int bitrate = bitrates[bitrate_index]; - if (bitrate == 0) return MP2_DEFAULT_PACKET_SIZE; - - int sampling_freq_index = (header[2] >> 2) & 0x03; - int sampling_freqs[] = {44100, 48000, 32000, 0}; - int sampling_freq = sampling_freqs[sampling_freq_index]; - if (sampling_freq == 0) return MP2_DEFAULT_PACKET_SIZE; - - int padding = (header[2] >> 1) & 0x01; - return (144 * bitrate * 1000) / sampling_freq + padding; -} - -// Write Videotex header (32 bytes, similar to TAV but simpler) -void write_videotex_header(FILE *f, uint8_t fps, uint32_t total_frames) { - fwrite("\x1FTSVMTAV", 8, 1, f); - - // Version: 1 (uint8) - fputc(1, f); - - // Grid dimensions (uint8 each) - uint16_t width = GRID_W; - uint16_t height = GRID_H; - fwrite(&width, sizeof(uint16_t), 1, f); // cols = 80 - fwrite(&height, sizeof(uint16_t), 1, f); // rows = 32 - - // FPS (uint8) - fputc(fps, f); - - // Total frames (uint32, little-endian) - fwrite(&total_frames, sizeof(uint32_t), 1, f); - - fputc(0, f); // wavelet filter type - fputc(0, f); // decomposition levels - fputc(0, f); // quantiser Y - fputc(0, f); // quantiser Co - fputc(0, f); // quantiser Cg - - // Feature Flags - fputc(0x03, f); // bit 0 = has audio; bit 1 = has subtitle (Videotex is classified as subtitles) - - // Video Flags - fputc(0x80, f); // bit 7 = has no video (Videotex is classified as subtitles) - - - fputc(0, f); // encoder quality level - fputc(0x02, f); // channel layout: Y only - fputc(0, f); // entropy coder - - fputc(0, f); // reserved - fputc(0, f); // reserved - - fputc(0, f); // device orientation: no rotation - fputc(0, f); // file role: generic -} - -// Write extended header packet with metadata -// Returns the file offset where ENDT value is written (for later update) -long write_extended_header(FILE *f, uint64_t creation_time_ns, const char *ffmpeg_version) { - fputc(PACKET_EXTENDED_HDR, f); - - // Helper macros for key-value pairs - #define WRITE_KV_UINT64(key_str, value) do { \ - fwrite(key_str, 1, 4, f); \ - uint8_t value_type = 0x04; /* Uint64 */ \ - fwrite(&value_type, 1, 1, f); \ - uint64_t val = (value); \ - fwrite(&val, sizeof(uint64_t), 1, f); \ - } while(0) - - #define WRITE_KV_BYTES(key_str, data, len) do { \ - fwrite(key_str, 1, 4, f); \ - uint8_t value_type = 0x10; /* Bytes */ \ - fwrite(&value_type, 1, 1, f); \ - uint16_t length = (len); \ - fwrite(&length, sizeof(uint16_t), 1, f); \ - fwrite((data), 1, (len), f); \ - } while(0) - - // Count key-value pairs (BGNT, ENDT, CDAT, VNDR, FMPG) - uint16_t num_pairs = ffmpeg_version ? 5 : 4; // FMPG is optional - fwrite(&num_pairs, sizeof(uint16_t), 1, f); - - // BGNT: Video begin time (0 for frame 0) - WRITE_KV_UINT64("BGNT", 0ULL); - - // ENDT: Video end time (placeholder, will be updated at end) - long endt_offset = ftell(f); - WRITE_KV_UINT64("ENDT", 0ULL); - - // CDAT: Creation time in nanoseconds since UNIX epoch - WRITE_KV_UINT64("CDAT", creation_time_ns); - - // VNDR: Encoder name and version - const char *vendor_str = ENCODER_VENDOR_STRING; - WRITE_KV_BYTES("VNDR", vendor_str, strlen(vendor_str)); - - // FMPG: FFmpeg version (if available) - if (ffmpeg_version) { - WRITE_KV_BYTES("FMPG", ffmpeg_version, strlen(ffmpeg_version)); - } - - #undef WRITE_KV_UINT64 - #undef WRITE_KV_BYTES - - // Return offset of ENDT value (skip key, type byte) - return endt_offset + 4 + 1; // 4 bytes for "ENDT", 1 byte for type -} - -// Write font ROM packet (SSF packet type 0x30) -void write_fontrom_packet(FILE *f, const uint8_t *rom_data, size_t data_size, uint8_t opcode) { - // Prepare padded ROM data (pad to FONTROM_PADDED_SIZE with zeros) - uint8_t *padded_data = calloc(1, FONTROM_PADDED_SIZE); - memcpy(padded_data, rom_data, data_size); - - // Packet structure: - // [type:0x30][size:uint32][index:uint24][opcode:uint8][length:uint16][data][terminator:0x00] - uint32_t packet_size = 3 + 1 + 2 + FONTROM_PADDED_SIZE + 1; - - // Write packet type and size - fputc(PACKET_SSF, f); - fwrite(&packet_size, sizeof(uint32_t), 1, f); - - // Write SSF payload - // Index (3 bytes, always 0 for font ROM) - fputc(0, f); - fputc(0, f); - fputc(0, f); - - // Opcode (0x80=lowrom, 0x81=highrom) - fputc(opcode, f); - - // Payload length (uint16, little-endian) - uint16_t payload_len = FONTROM_PADDED_SIZE; - fwrite(&payload_len, sizeof(uint16_t), 1, f); - - // Font data (padded to 1920 bytes) - fwrite(padded_data, 1, FONTROM_PADDED_SIZE, f); - - // Terminator - fputc(0x00, f); - - free(padded_data); - - fprintf(stderr, "Font ROM uploaded: %zu bytes (padded to %d), opcode 0x%02X\n", - data_size, FONTROM_PADDED_SIZE, opcode); -} - -// Write timecode packet (nanoseconds) -void write_timecode(FILE *f, uint64_t timecode_ns) { - fputc(PACKET_TIMECODE, f); - fwrite(&timecode_ns, sizeof(uint64_t), 1, f); -} - -// Write sync packet -void write_sync(FILE *f) { - fputc(PACKET_SYNC, f); -} - -// Write MP2 audio packet -void write_audio_mp2(FILE *f, const uint8_t *data, uint32_t size) { - fputc(PACKET_AUDIO_MP2, f); - fwrite(&size, sizeof(uint32_t), 1, f); - fwrite(data, 1, size, f); -} - -// Write text packet with separated arrays (better compression) -void write_text_packet(FILE *f, const uint8_t *bg_col, const uint8_t *fg_col, - const uint8_t *chars, int rows, int cols) { - int grid_size = rows * cols; - - // Prepare uncompressed data: [rows][cols][fg-array][bg-array][char-array] - // Separated arrays compress much better (fg/bg are just 0xF0/0xFE runs) - size_t uncompressed_size = 2 + grid_size * 3; - uint8_t *uncompressed = malloc(uncompressed_size); - - uncompressed[0] = rows; - uncompressed[1] = cols; - - // Copy arrays in order: foreground, background, characters - memcpy(&uncompressed[2], fg_col, grid_size); // Foreground first - memcpy(&uncompressed[2 + grid_size], bg_col, grid_size); // Background second - memcpy(&uncompressed[2 + grid_size * 2], chars, grid_size); // Characters third - - // Compress with Zstd - size_t max_compressed = ZSTD_compressBound(uncompressed_size); - uint8_t *compressed = malloc(max_compressed); - size_t compressed_size = ZSTD_compress(compressed, max_compressed, - uncompressed, uncompressed_size, 3); - - if (ZSTD_isError(compressed_size)) { - fprintf(stderr, "Zstd compression error\n"); - exit(1); - } - - // Write packet: [type][size][data] - fputc(PACKET_TEXT, f); - uint32_t size32 = compressed_size; - fwrite(&size32, 4, 1, f); - fwrite(compressed, compressed_size, 1, f); - - free(compressed); - free(uncompressed); -} - -int main(int argc, char **argv) { - if (argc < 7) { - fprintf(stderr, "Usage: %s -i