diff --git a/assets/disk0/tvdos/bin/playtad.js b/assets/disk0/tvdos/bin/playtad.js new file mode 100644 index 0000000..cd1bf3a --- /dev/null +++ b/assets/disk0/tvdos/bin/playtad.js @@ -0,0 +1,361 @@ +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 + +if (!SND_BASE_ADDR) return 10 + +// Check for help flag or missing arguments +if (!exec_args[1] || exec_args[1] == "-h" || exec_args[1] == "--help") { + serial.println("Usage: playtad [-i | -d] [quality]") + serial.println(" -i Interactive mode (progress bar, press Backspace to exit)") + serial.println(" -d Dump mode (show first 3 chunks with payload hex and decoded samples)") + serial.println("") + serial.println("Examples:") + serial.println(" playtad audio.tad -i # Play with progress bar") + serial.println(" playtad audio.tad -d # Dump first 3 chunks for debugging") + return 0 +} + +const pcm = require("pcm") +const interactive = exec_args[2] && exec_args[2].toLowerCase() == "-i" +const dumpCoeffs = exec_args[2] && exec_args[2].toLowerCase() == "-d" + +function printdbg(s) { if (0) serial.println(s) } + + +class SequentialFileBuffer { + + constructor(path, offset, length) { + if (Array.isArray(path)) throw Error("arg #1 is path(string), not array") + + this.path = path + this.file = files.open(path) + + this.offset = offset || 0 + this.originalOffset = offset + this.length = length || this.file.size + + this.seq = require("seqread") + this.seq.prepare(path) + } + + readBytes(size, ptr) { + return this.seq.readBytes(size, ptr) + } + + readByte() { + let ptr = this.seq.readBytes(1) + let val = sys.peek(ptr) + sys.free(ptr) + return val + } + + readShort() { + let ptr = this.seq.readBytes(2) + let val = sys.peek(ptr) | (sys.peek(ptr + 1) << 8) + sys.free(ptr) + return val + } + + readInt() { + let ptr = this.seq.readBytes(4) + let val = sys.peek(ptr) | (sys.peek(ptr + 1) << 8) | (sys.peek(ptr + 2) << 16) | (sys.peek(ptr + 3) << 24) + sys.free(ptr) + return val + } + + readStr(n) { + let ptr = this.seq.readBytes(n) + let s = '' + for (let i = 0; i < n; i++) { + if (i >= this.length) break + s += String.fromCharCode(sys.peek(ptr + i)) + } + sys.free(ptr) + return s + } + + unread(diff) { + let newSkipLen = this.seq.getReadCount() - diff + this.seq.prepare(this.path) + this.seq.skip(newSkipLen) + } + + rewind() { + this.seq.prepare(this.path) + } + + seek(p) { + this.seq.prepare(this.path) + this.seq.skip(p) + } + + get byteLength() { + return this.length + } + + get fileHeader() { + return this.seq.fileHeader + } + + getReadCount() { + return this.seq.getReadCount() + } +} + + +// Read TAD chunk header to determine format +let filebuf = new SequentialFileBuffer(_G.shell.resolvePathInput(exec_args[1]).full) +const FILE_SIZE = filebuf.length + +if (FILE_SIZE < 7) { + serial.println(`ERROR: File too small (${FILE_SIZE} bytes). Expected TAD format.`) + return 1 +} + +// Read first chunk header (standalone TAD format: no TAV wrapper) +let firstSampleCount = filebuf.readShort() +let firstMaxIndex = filebuf.readByte() +let firstPayloadSize = filebuf.readInt() + +// Validate first chunk +if (firstSampleCount < 0 || firstSampleCount > 65536) { + serial.println(`ERROR: Invalid sample count ${firstSampleCount}. File may be corrupted.`) + return 1 +} +if (firstMaxIndex < 0 || firstMaxIndex > 255) { + serial.println(`ERROR: Invalid max index ${firstMaxIndex}. File may be corrupted.`) + return 1 +} +if (firstPayloadSize < 1 || firstPayloadSize > 65536) { + serial.println(`ERROR: Invalid payload size ${firstPayloadSize}. File may be corrupted.`) + return 1 +} + +// Rewind to start +filebuf.rewind() + +// Calculate approximate frame info +const AVG_CHUNK_SIZE = 7 + firstPayloadSize // TAD header (2+1+4) + payload +const SAMPLE_RATE = 32000 +const bufRealTimeLen = Math.floor((firstSampleCount / SAMPLE_RATE) * 1000) // milliseconds per chunk + +if (dumpCoeffs) { + serial.println(`TAD Coefficient Dump Mode`) + serial.println(`File: ${filebuf.file.name}`) + serial.println(`First chunk header:`) + serial.println(` Sample Count: ${firstSampleCount}`) + serial.println(` Max Index: ${firstMaxIndex}`) + serial.println(` Payload Size: ${firstPayloadSize} bytes`) + serial.println(`Chunk Duration: ${bufRealTimeLen} ms`) + serial.println(``) +} + + +let bytes_left = FILE_SIZE +let decodedLength = 0 +let chunkNumber = 0 + + +con.curs_set(0) +let [__, CONSOLE_WIDTH] = con.getmaxyx() +if (interactive) { + let [cy, cx] = con.getyx() + // file name + con.mvaddch(cy, 1) + con.prnch(0xC9);con.prnch(0xCD);con.prnch(0xB5) + print(filebuf.file.name) + con.prnch(0xC6);con.prnch(0xCD) + print("\x84205u".repeat(CONSOLE_WIDTH - 26 - filebuf.file.name.length)) + con.prnch(0xB5) + print("Hold Bksp to Exit") + con.prnch(0xC6);con.prnch(0xCD);con.prnch(0xBB) + + // L R pillar + con.prnch(0xBA) + con.mvaddch(cy+1, CONSOLE_WIDTH, 0xBA) + + // media info + let mediaInfoStr = `TAD Q${firstMaxIndex} ${SAMPLE_RATE/1000}kHz` + con.move(cy+2,1) + con.prnch(0xC8) + print("\x84205u".repeat(CONSOLE_WIDTH - 5 - mediaInfoStr.length)) + con.prnch(0xB5) + print(mediaInfoStr) + con.prnch(0xC6);con.prnch(0xCD);con.prnch(0xBC) + + con.move(cy+1, 2) +} +let [cy, cx] = con.getyx() +let paintWidth = CONSOLE_WIDTH - 20 + +function bytesToSec(i) { + // Approximate: use first chunk's ratio + return Math.round((i / FILE_SIZE) * (FILE_SIZE / AVG_CHUNK_SIZE) * (bufRealTimeLen / 1000)) +} + +function secToReadable(n) { + let mins = ''+((n/60)|0) + let secs = ''+(n % 60) + return `${mins.padStart(2,'0')}:${secs.padStart(2,'0')}` +} + +function printPlayBar() { + if (interactive) { + let currently = decodedLength + let total = FILE_SIZE + + let currentlySec = bytesToSec(currently) + let totalSec = bytesToSec(total) + + con.move(cy, 3) + print(' '.repeat(15)) + con.move(cy, 3) + + print(`${secToReadable(currentlySec)} / ${secToReadable(totalSec)}`) + + con.move(cy, 17) + print(' ') + let progressbar = '\x84196u'.repeat(paintWidth + 1) + print(progressbar) + + con.mvaddch(cy, 18 + Math.round(paintWidth * (currently / total)), 0xDB) + } +} + + +audio.resetParams(0) +audio.purgeQueue(0) +audio.setPcmMode(0) +audio.setPcmQueueCapacityIndex(0, 2) // queue size is now 8 +const QUEUE_MAX = audio.getPcmQueueCapacity(0) +audio.setMasterVolume(0, 255) +audio.play(0) + + +let stopPlay = false +let errorlevel = 0 + +try { + while (bytes_left > 0 && !stopPlay) { + + if (interactive) { + sys.poke(-40, 1) + if (sys.peek(-41) == 67) { // Backspace key + stopPlay = true + } + } + + printPlayBar() + + // Read TAD chunk header (standalone TAD format) + // Format: [sample_count][max_index][payload_size][payload] + let sampleCount = filebuf.readShort() + let maxIndex = filebuf.readByte() + let payloadSize = filebuf.readInt() + + // Validate every chunk (not just first one) + if (sampleCount < 0 || sampleCount > 65536) { + serial.println(`ERROR: Chunk ${chunkNumber}: Invalid sample count ${sampleCount}. File may be corrupted.`) + errorlevel = 1 + break + } + if (maxIndex < 0 || maxIndex > 255) { + serial.println(`ERROR: Chunk ${chunkNumber}: Invalid max index ${maxIndex}. File may be corrupted.`) + errorlevel = 1 + break + } + if (payloadSize < 1 || payloadSize > 65536) { + serial.println(`ERROR: Chunk ${chunkNumber}: Invalid payload size ${payloadSize}. File may be corrupted.`) + errorlevel = 1 + break + } + if (payloadSize + 7 > bytes_left) { + serial.println(`ERROR: Chunk ${chunkNumber}: Chunk size ${payloadSize + 7} exceeds remaining file size ${bytes_left}`) + errorlevel = 1 + break + } + + if (dumpCoeffs && chunkNumber < 3) { + serial.println(`=== Chunk ${chunkNumber} ===`) + serial.println(` Sample Count: ${sampleCount}`) + serial.println(` Max Index: ${maxIndex}`) + serial.println(` Payload Size: ${payloadSize} bytes`) + serial.println(` Bytes remaining in file: ${bytes_left}`) + } + + // Rewind 7 bytes to re-read the header along with payload + // This allows reading the complete chunk (header + payload) in one call + filebuf.unread(7) + + // Read entire chunk (header + payload) to TAD input buffer + // This matches TAV's approach for packet 0x24 + let totalChunkSize = 7 + payloadSize + filebuf.readBytes(totalChunkSize, TAD_INPUT_ADDR) + + if (dumpCoeffs && chunkNumber < 3) { + // Dump first 32 bytes of compressed payload (skip 7-byte header) + serial.print(` Compressed data (first 32 bytes): `) + for (let i = 0; i < Math.min(32, payloadSize); i++) { + let b = sys.peek(TAD_INPUT_ADDR + 7 + i) + serial.print(`${(b & 0xFF).toString(16).padStart(2, '0')} `) + } + serial.println('') + } + + // Decode TAD chunk + audio.tadDecode() + + if (dumpCoeffs && chunkNumber < 3) { + // After decoding, the decoded PCMu8 samples are in tadDecodedBin + serial.println(` Decoded ${sampleCount} samples`) + + // Dump first 16 decoded samples (PCMu8 stereo interleaved) + serial.print(` Decoded (first 16 L samples): `) + for (let i = 0; i < 16; i++) { + serial.print(`${sys.peek(TAD_DECODED_ADDR + i * 2) & 0xFF} `) + } + serial.println('') + serial.print(` Decoded (first 16 R samples): `) + for (let i = 0; i < 16; i++) { + serial.print(`${sys.peek(TAD_DECODED_ADDR + i * 2 + 1) & 0xFF} `) + } + serial.println('') + serial.println('') + } + + // Upload decoded audio to queue + audio.tadUploadDecoded(0, sampleCount) + + if (!dumpCoeffs) { + // Sleep for the duration of the audio chunk to pace playback + // This prevents uploading everything at once + sys.sleep(bufRealTimeLen) + } + + // Chunk size = header (7 bytes) + payload + let chunkSize = 7 + payloadSize + bytes_left -= chunkSize + decodedLength += chunkSize + chunkNumber++ + + // Limit coefficient dump to first 3 chunks + if (dumpCoeffs && chunkNumber >= 3) { + serial.println(`... (remaining chunks omitted)`) + // Keep playing but don't dump more + } + } +} +catch (e) { + printerrln(e) + errorlevel = 1 +} +finally { + if (interactive) { + con.move(cy + 3, 1) + con.curs_set(1) + } +} + +return errorlevel diff --git a/assets/disk0/tvdos/bin/playtav.js b/assets/disk0/tvdos/bin/playtav.js index 775a44a..76705a1 100644 --- a/assets/disk0/tvdos/bin/playtav.js +++ b/assets/disk0/tvdos/bin/playtav.js @@ -1352,7 +1352,6 @@ try { if (!tadInitialised) { tadInitialised = true - audio.tadSetQuality(header.qualityLevel) } seqread.readBytes(payloadLen, SND_MEM_ADDR - 262144) diff --git a/assets/disk0/tvdos/bin/zfm.js b/assets/disk0/tvdos/bin/zfm.js index 0a886a8..6a166f3 100644 --- a/assets/disk0/tvdos/bin/zfm.js +++ b/assets/disk0/tvdos/bin/zfm.js @@ -27,6 +27,7 @@ const COL_HL_EXT = { "adpcm": 31, "pcm": 32, "mp3": 33, + "tad": 33, "mp2": 34, "mv1": 213, "mv2": 213, @@ -48,6 +49,7 @@ const EXEC_FUNS = { "mv2": (f) => _G.shell.execute(`playtev "${f}" -i`), "mv3": (f) => _G.shell.execute(`playtav "${f}" -i`), "tav": (f) => _G.shell.execute(`playtav "${f}" -i`), + "tad": (f) => _G.shell.execute(`playtad "${f}" -i`), "pcm": (f) => _G.shell.execute(`playpcm "${f}" -i`), "ipf1": (f) => _G.shell.execute(`decodeipf "${f}" -i`), "ipf2": (f) => _G.shell.execute(`decodeipf "${f}" -i`), diff --git a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt index b90cafc..64d05ac 100644 --- a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt @@ -94,13 +94,6 @@ class AudioJSR223Delegate(private val vm: VM) { } } - // TAD (Terrarum Advanced Audio) decoder functions - fun tadSetQuality(quality: Int) { - getFirstSnd()?.mmio_write(43L, quality.toByte()) - } - - fun tadGetQuality() = getFirstSnd()?.mmio_read(43L)?.toInt() - fun tadDecode() { getFirstSnd()?.mmio_write(42L, 1) } diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index 9b051cb..401c014 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -372,9 +372,9 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { // Lambda-based decompanding decoder (inverse of Laplacian CDF-based encoder) // Converts quantized index back to normalized float in [-1, 1] - private fun lambdaDecompanding(quantVal: Short, maxIndex: Int): Float { + private fun lambdaDecompanding(quantVal: Byte, maxIndex: Int): Float { // Handle zero - if (quantVal == 0.toShort()) { + if (quantVal == 0.toByte()) { return 0.0f } @@ -477,17 +477,19 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { var offset = 0L val sampleCount = ( - (tadInputBin[offset++].toInt() and 0xFF) or - ((tadInputBin[offset++].toInt() and 0xFF) shl 8) + (tadInputBin[offset++].toUint()) or + ((tadInputBin[offset++].toUint()) shl 8) ) - val maxIndex = tadInputBin[offset++].toInt() and 0xFF + val maxIndex = tadInputBin[offset++].toUint() val payloadSize = ( - (tadInputBin[offset++].toInt() and 0xFF) or - ((tadInputBin[offset++].toInt() and 0xFF) shl 8) or - ((tadInputBin[offset++].toInt() and 0xFF) shl 16) or - ((tadInputBin[offset++].toInt() and 0xFF) shl 24) + (tadInputBin[offset++].toUint()) or + ((tadInputBin[offset++].toUint()) shl 8) or + ((tadInputBin[offset++].toUint()) shl 16) or + ((tadInputBin[offset++].toUint()) shl 24) ) +// println("Q$maxIndex, SampleCount: $sampleCount, payloadSize: $payloadSize") + // Decompress payload val compressed = ByteArray(payloadSize) UnsafeHelper.memcpyRaw(null, tadInputBin.ptr + offset, compressed, UnsafeHelper.getArrayOffset(compressed), payloadSize.toLong()) @@ -501,15 +503,9 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { return } - // Decode significance maps - val quantMid = ShortArray(sampleCount) - val quantSide = ShortArray(sampleCount) - - var payloadOffset = 0 - val midBytes = decodeSigmap2bit(payload, payloadOffset, quantMid, sampleCount) - payloadOffset += midBytes - - val sideBytes = decodeSigmap2bit(payload, payloadOffset, quantSide, sampleCount) + // Decode raw int8_t storage (no significance map - encoder uses raw format) + val quantMid = payload.sliceArray(0 until sampleCount) + val quantSide = payload.sliceArray(sampleCount until sampleCount*2) // Calculate DWT levels from sample count val dwtLevels = calculateDwtLevels(sampleCount) @@ -542,42 +538,6 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { } } - private fun decodeSigmap2bit(input: ByteArray, offset: Int, values: ShortArray, count: Int): Int { - val mapBytes = (count * 2 + 7) / 8 - var readPtr = offset + mapBytes - var otherIdx = 0 - - for (i in 0 until count) { - val bitPos = i * 2 - val byteIdx = offset + bitPos / 8 - val bitOffset = bitPos % 8 - - var code = ((input[byteIdx].toInt() and 0xFF) shr bitOffset) and 0x03 - - // Handle bit spillover - if (bitOffset == 7) { - code = ((input[byteIdx].toInt() and 0xFF) shr 7) or - (((input[byteIdx + 1].toInt() and 0xFF) and 0x01) shl 1) - } - - values[i] = when (code) { - 0 -> 0 - 1 -> 1 - 2 -> (-1).toShort() - 3 -> { - val v = ((input[readPtr].toInt() and 0xFF) or - ((input[readPtr + 1].toInt() and 0xFF) shl 8)).toShort() - readPtr += 2 - otherIdx++ - v - } - else -> 0 - } - } - - return mapBytes + otherIdx * 2 - } - private fun calculateDwtLevels(chunkSize: Int): Int { // Hard-coded to 9 levels to match C decoder return 9 @@ -628,7 +588,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { } } - private fun dequantizeDwtCoefficients(quantized: ShortArray, coeffs: FloatArray, count: Int, + private fun dequantizeDwtCoefficients(quantized: ByteArray, coeffs: FloatArray, count: Int, maxIndex: Int, dwtLevels: Int) { // Calculate sideband boundaries dynamically val firstBandSize = count shr dwtLevels diff --git a/video_encoder/decoder_tad.c b/video_encoder/decoder_tad.c index a37e29d..b66b0da 100644 --- a/video_encoder/decoder_tad.c +++ b/video_encoder/decoder_tad.c @@ -312,7 +312,7 @@ static void dwt_dd4_inverse_1d(float *data, int length) { free(temp); } -static void dwt_haar_inverse_multilevel(float *data, int length, int levels) { +static void dwt_inverse_multilevel(float *data, int length, int levels) { // Calculate the length at the deepest level (size of low-pass after all forward DWTs) int current_length = length; for (int level = 0; level < levels; level++) { @@ -588,8 +588,8 @@ static int decode_chunk(const uint8_t *input, size_t input_size, uint8_t *pcmu8_ dequantize_dwt_coefficients(quant_side, dwt_side, sample_count, sample_count, dwt_levels, max_index, quantiser_scale); // Inverse DWT - dwt_haar_inverse_multilevel(dwt_mid, sample_count, dwt_levels); - dwt_haar_inverse_multilevel(dwt_side, sample_count, dwt_levels); + dwt_inverse_multilevel(dwt_mid, sample_count, dwt_levels); + dwt_inverse_multilevel(dwt_side, sample_count, dwt_levels); float err[2][2] = {{0,0},{0,0}}; diff --git a/video_encoder/encoder_tad.c b/video_encoder/encoder_tad.c index 929d0bd..0a99884 100644 --- a/video_encoder/encoder_tad.c +++ b/video_encoder/encoder_tad.c @@ -36,9 +36,8 @@ static const float BASE_QUANTISER_WEIGHTS[] = { // Forward declarations for internal functions static void dwt_dd4_forward_1d(float *data, int length); -static void dwt_dd4_forward_multilevel(float *data, int length, int levels); +static void dwt_forward_multilevel(float *data, int length, int levels); static void quantize_dwt_coefficients(const float *coeffs, int8_t *quantized, size_t count, int apply_deadzone, int chunk_size, int dwt_levels, int quant_bits, int *current_subband_index, float quantiser_scale); -static size_t encode_twobitmap(const int8_t *values, size_t count, uint8_t *output); static inline float FCLAMP(float x, float min, float max) { return x < min ? min : (x > max ? max : x); @@ -190,7 +189,7 @@ static void dwt_97_forward_1d(float *data, int length) { } // Apply multi-level DWT (using DD-4 wavelet) -static void dwt_dd4_forward_multilevel(float *data, int length, int levels) { +static void dwt_forward_multilevel(float *data, int length, int levels) { int current_length = length; for (int level = 0; level < levels; level++) { // dwt_dd4_forward_1d(data, current_length); @@ -315,61 +314,6 @@ static void quantize_dwt_coefficients(const float *coeffs, int8_t *quantized, si free(sideband_starts); } -//============================================================================= -// Twobit-map Significance Map Encoding -//============================================================================= - -// Twobit-map encoding: 2 bits per coefficient for common values -// 00 = 0 -// 01 = +1 -// 10 = -1 -// 11 = other value (followed by int8_t in separate array) -static size_t encode_twobitmap(const int8_t *values, size_t count, uint8_t *output) { - // Calculate size needed for twobit map - size_t map_bytes = (count * 2 + 7) / 8; // 2 bits per coefficient - - // First pass: create significance map and count "other" values - uint8_t *map = output; - memset(map, 0, map_bytes); - - size_t other_count = 0; - for (size_t i = 0; i < count; i++) { - int8_t val = values[i]; - uint8_t code; - - if (val == 0) { - code = 0; // 00 - } else if (val == 1) { - code = 1; // 01 - } else if (val == -1) { - code = 2; // 10 - } else { - code = 3; // 11 - other_count++; - } - - // Write 2-bit code into map - size_t bit_offset = i * 2; - size_t byte_idx = bit_offset / 8; - size_t bit_in_byte = bit_offset % 8; - - map[byte_idx] |= (code << bit_in_byte); - } - - // Second pass: write "other" values - int8_t *other_values = (int8_t*)(output + map_bytes); - size_t other_idx = 0; - - for (size_t i = 0; i < count; i++) { - int8_t val = values[i]; - if (val != 0 && val != 1 && val != -1) { - other_values[other_idx++] = val; - } - } - - return map_bytes + other_count; -} - //============================================================================= // Coefficient Statistics //============================================================================= @@ -818,8 +762,8 @@ size_t tad32_encode_chunk(const float *pcm32_stereo, size_t num_samples, dwt_side[i] = pcm32_side[i]; } - dwt_dd4_forward_multilevel(dwt_mid, num_samples, dwt_levels); - dwt_dd4_forward_multilevel(dwt_side, num_samples, dwt_levels); + dwt_forward_multilevel(dwt_mid, num_samples, dwt_levels); + dwt_forward_multilevel(dwt_side, num_samples, dwt_levels); // Step 3.5: Accumulate coefficient statistics if enabled static int stats_enabled = -1; diff --git a/video_encoder/encoder_tad_standalone.c b/video_encoder/encoder_tad_standalone.c index 1d1b4eb..5cd9ccb 100644 --- a/video_encoder/encoder_tad_standalone.c +++ b/video_encoder/encoder_tad_standalone.c @@ -48,8 +48,13 @@ static void print_usage(const char *prog_name) { printf("Options:\n"); printf(" -i Input audio file (any format supported by FFmpeg)\n"); printf(" -o Output TAD32 file (optional, auto-generated as input.qN.tad)\n"); - printf(" -q Positive side quantization steps (default: 47, range: up to 127)\n"); - printf(" Higher = more precision, larger files\n"); + printf(" -q Quality level (0-5, default: %d)\n", TAD32_QUALITY_DEFAULT); + printf(" 0 = lowest quality/smallest (max_index=31)\n"); + printf(" 1 = low quality (max_index=35)\n"); + printf(" 2 = medium quality (max_index=39)\n"); + printf(" 3 = good quality (max_index=47) [DEFAULT]\n"); + printf(" 4 = high quality (max_index=56)\n"); + printf(" 5 = very high quality/largest (max_index=89)\n"); printf(" -s Quantiser scaling factor (default: 1.0, range: 0.5-4.0)\n"); printf(" Higher = more aggressive quantization, smaller files\n"); printf(" 2.0 = quantize 2x coarser than baseline\n"); @@ -65,7 +70,7 @@ int main(int argc, char *argv[]) { char *input_file = NULL; char *output_file = NULL; - int max_index = 47; // Default QUANT_BITS + int quality = TAD32_QUALITY_DEFAULT; // Default quality level (0-5) float quantiser_scale = 1.0f; // Default quantiser scaling int verbose = 0; @@ -86,7 +91,11 @@ int main(int argc, char *argv[]) { output_file = optarg; break; case 'q': - max_index = atoi(optarg); + quality = atoi(optarg); + if (quality < TAD32_QUALITY_MIN || quality > TAD32_QUALITY_MAX) { + fprintf(stderr, "Error: Quality must be in range %d-%d\n", TAD32_QUALITY_MIN, TAD32_QUALITY_MAX); + return 1; + } break; case 's': quantiser_scale = atof(optarg); @@ -113,6 +122,9 @@ int main(int argc, char *argv[]) { return 1; } + // Convert quality (0-5) to max_index for quantization + int max_index = tad32_quality_to_max_index(quality); + // Generate output filename if not provided if (!output_file) { // Allocate space for output filename @@ -140,8 +152,8 @@ int main(int argc, char *argv[]) { strcpy(output_file + dir_len, basename_start); } - // Append .qNN.tad - sprintf(output_file + strlen(output_file), ".q%d.tad", max_index); + // Append .qNN.tad (use quality level for filename) + sprintf(output_file + strlen(output_file), ".q%d.tad", quality); if (verbose) { printf("Auto-generated output path: %s\n", output_file); @@ -152,7 +164,7 @@ int main(int argc, char *argv[]) { printf("%s\n", ENCODER_VENDOR_STRING); printf("Input: %s\n", input_file); printf("Output: %s\n", output_file); - printf("Quant bits: %d\n", max_index); + printf("Quality level: %d (max_index=%d)\n", quality, max_index); printf("Quantiser scale: %.2f\n", quantiser_scale); } diff --git a/video_encoder/encoder_tav.c b/video_encoder/encoder_tav.c index f5a7a23..99f34ea 100644 --- a/video_encoder/encoder_tav.c +++ b/video_encoder/encoder_tav.c @@ -9041,7 +9041,7 @@ static int write_tad_packet_samples(tav_encoder_t *enc, FILE *output, int sample if (enc->verbose) { printf("TAD32 packet: %d samples, %u bytes compressed (Q%d)\n", - sample_count, tad_payload_size, tad_quality); + sample_count, tad_payload_size, quant_size); } // Cleanup