From d2b1e792b9740716a615e807e6f713eb13bc0f49 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sat, 2 May 2026 19:00:07 +0900 Subject: [PATCH] taud: cue and pattern compression --- assets/disk0/tvdos/bin/taut.js | 41 ++++----- assets/disk0/tvdos/include/taud.mjs | 124 +++++++++++++++++++--------- it2taud.py | 27 ++++-- mod2taud.py | 42 ++++++---- s3m2taud.py | 43 ++++++---- taud_common.py | 37 ++++++++- terranmon.txt | 7 +- 7 files changed, 212 insertions(+), 109 deletions(-) diff --git a/assets/disk0/tvdos/bin/taut.js b/assets/disk0/tvdos/bin/taut.js index 4577a63..a706917 100644 --- a/assets/disk0/tvdos/bin/taut.js +++ b/assets/disk0/tvdos/bin/taut.js @@ -435,14 +435,12 @@ function drawCellAtStyled(y, x, cell, back, style) { const TAUD_MAGIC = [0x1F,0x54,0x53,0x56,0x4D,0x61,0x75,0x64] const TAUD_HEADER_SIZE = 32 -const TAUD_SONG_ENTRY = 16 +const TAUD_SONG_ENTRY = 32 const PATTERN_SIZE = 512 const ROWS_PER_PAT = 64 const NUM_CUES = 1024 const CUE_SIZE = 32 const NUM_VOICES = 20 -const NUM_INSTRUMENTS = 256 -const INSTRUMENT_SIZE = 64 const CUE_EMPTY = 0xFFF function _peekU32LE(ptr, off) { @@ -485,52 +483,55 @@ function loadTaud(filePath, songIndex) { ((sys.peek(ptr + entryOff + 6) & 0xFF) << 8) const bpmStored = sys.peek(ptr + entryOff + 7) & 0xFF const tickRate = sys.peek(ptr + entryOff + 8) & 0xFF + const patBinCompSize = _peekU32LE(ptr, entryOff + 18) + const cueSheetCompSize = _peekU32LE(ptr, entryOff + 22) + + // Decompress pattern bin + const patBinSize = numPats * PATTERN_SIZE + const patBinPtr = sys.malloc(patBinSize) + gzip.decompFromTo(ptr + songOff, patBinCompSize, patBinPtr) const patterns = new Array(numPats) for (let p = 0; p < numPats; p++) { const ptn = new Uint8Array(PATTERN_SIZE) for (let k = 0; k < PATTERN_SIZE; k++) { - ptn[k] = sys.peek(ptr + songOff + p * PATTERN_SIZE + k) & 0xFF + ptn[k] = sys.peek(patBinPtr + p * PATTERN_SIZE + k) & 0xFF } patterns[p] = ptn } + sys.free(patBinPtr) + + // Decompress cue sheet + const cueSheetSize = NUM_CUES * CUE_SIZE + const cueSheetPtr = sys.malloc(cueSheetSize) + gzip.decompFromTo(ptr + songOff + patBinCompSize, cueSheetCompSize, cueSheetPtr) - const cueBase = songOff + numPats * PATTERN_SIZE const cues = new Array(NUM_CUES) let lastActiveCue = -1 for (let c = 0; c < NUM_CUES; c++) { const ptns = new Array(NUM_VOICES) for (let i = 0; i < 10; i++) { - const lo = sys.peek(ptr + cueBase + c * CUE_SIZE + i) & 0xFF - const mi = sys.peek(ptr + cueBase + c * CUE_SIZE + 10 + i) & 0xFF - const hi = sys.peek(ptr + cueBase + c * CUE_SIZE + 20 + i) & 0xFF + const lo = sys.peek(cueSheetPtr + c * CUE_SIZE + i) & 0xFF + const mi = sys.peek(cueSheetPtr + c * CUE_SIZE + 10 + i) & 0xFF + const hi = sys.peek(cueSheetPtr + c * CUE_SIZE + 20 + i) & 0xFF ptns[i*2] = ((hi >> 4) << 8) | ((mi >> 4) << 4) | (lo >> 4) ptns[i*2+1] = ((hi & 0xF) << 8) | ((mi & 0xF) << 4) | (lo & 0xF) } - const instr = sys.peek(ptr + cueBase + c * CUE_SIZE + 30) & 0xFF + const instr = sys.peek(cueSheetPtr + c * CUE_SIZE + 30) & 0xFF cues[c] = { ptns, instr } for (let v = 0; v < NUM_VOICES; v++) { if (ptns[v] !== CUE_EMPTY) { lastActiveCue = c; break } } } - - const instrBase = cueBase + NUM_CUES * CUE_SIZE - const instruments = new Array(NUM_INSTRUMENTS) - for (let n = 0; n < NUM_INSTRUMENTS; n++) { - const instr = new Uint8Array(INSTRUMENT_SIZE) - for (let k = 0; k < INSTRUMENT_SIZE; k++) { - instr[k] = sys.peek(ptr + instrBase + n * INSTRUMENT_SIZE + k) & 0xFF - } - instruments[n] = instr - } + sys.free(cueSheetPtr) sys.free(ptr) return { filePath, version, numSongs, numVoices, numPats, bpm: (bpmStored + 24) & 0xFF, tickRate, - patterns, cues, lastActiveCue, instruments + patterns, cues, lastActiveCue } } diff --git a/assets/disk0/tvdos/include/taud.mjs b/assets/disk0/tvdos/include/taud.mjs index ef4ad19..5481a12 100644 --- a/assets/disk0/tvdos/include/taud.mjs +++ b/assets/disk0/tvdos/include/taud.mjs @@ -8,8 +8,8 @@ const TAUD_MAGIC = [0x1F,0x54,0x53,0x56,0x4D,0x61,0x75,0x64] // \x1F TSVMaud const TAUD_VERSION = 1 -const TAUD_HEADER_SIZE = 32 // magic(8) + version(1) + numSongs(1) + compSize(4) + rsvd(2) + sig(16) -const TAUD_SONG_ENTRY = 16 // bytes per song-table row (offset(4)+voices(1)+pats(2)+bpm(1)+tick(1)+pad(7)) +const TAUD_HEADER_SIZE = 32 // magic(8) + version(1) + numSongs(1) + compSize(4) + projOff(4) + sig(14) +const TAUD_SONG_ENTRY = 32 // see encodeSongEntry / decodeSongEntry below const SAMPLEINST_SIZE = 786432 // 737280 sample + 49152 instrument (256 × 192) const PATTERN_SIZE = 512 // bytes per pattern (64 rows × 8 bytes) const NUM_PATTERNS_MAX = 256 @@ -109,27 +109,39 @@ function uploadTaudFile(inFile, songIndex, playhead) { let bpmStored = sys.peek(filePtr + entryOff + 7) & 0xFF let tickRate = sys.peek(filePtr + entryOff + 8) & 0xFF let mixerflags = sys.peek(filePtr + entryOff + 15) & 0xFF + let songGlobalVolume = sys.peek(filePtr + entryOff + 16) & 0xFF // TODO use it + let songMixingVolume = sys.peek(filePtr + entryOff + 17) & 0xFF // TODO use it + let patBinCompSize = _peekU32LE(filePtr, entryOff + 18) + let cueSheetCompSize = _peekU32LE(filePtr, entryOff + 22) let bpm = bpmStored + 24 let patsToLoad = numPatsLo | (numPatsHi << 8) - // -- 6. Upload patterns --------------------------------------------------- - let songBase = filePtr + songOffset - let patBytes = new Array(PATTERN_SIZE) + // -- 6. Decompress + upload patterns -------------------------------------- + let patBinSize = patsToLoad * PATTERN_SIZE + let patBinPtr = sys.malloc(patBinSize) + gzip.decompFromTo(filePtr + songOffset, patBinCompSize, patBinPtr) + + let patBytes = new Array(PATTERN_SIZE) for (let p = 0; p < patsToLoad; p++) { for (let k = 0; k < PATTERN_SIZE; k++) - patBytes[k] = sys.peek(songBase + p * PATTERN_SIZE + k) & 0xFF + patBytes[k] = sys.peek(patBinPtr + p * PATTERN_SIZE + k) & 0xFF audio.uploadPattern(p, patBytes) } + sys.free(patBinPtr) + + // -- 7. Decompress + upload cue sheet ------------------------------------- + let cueSheetSize = NUM_CUES * CUE_SIZE + let cueSheetPtr = sys.malloc(cueSheetSize) + gzip.decompFromTo(filePtr + songOffset + patBinCompSize, cueSheetCompSize, cueSheetPtr) - // -- 7. Upload cue sheet -------------------------------------------------- - let cueBase = songBase + patsToLoad * PATTERN_SIZE let cueBytes = new Array(CUE_SIZE) for (let c = 0; c < NUM_CUES; c++) { for (let k = 0; k < CUE_SIZE; k++) - cueBytes[k] = sys.peek(cueBase + c * CUE_SIZE + k) & 0xFF + cueBytes[k] = sys.peek(cueSheetPtr + c * CUE_SIZE + k) & 0xFF audio.uploadCue(c, cueBytes) } + sys.free(cueSheetPtr) // -- 8. Configure playhead ------------------------------------------------ audio.setTrackerMode(playhead) @@ -189,13 +201,38 @@ function captureTrackerDataToFile(outFile) { let tickRate = audio.getTickRate(0) || 6 let bpmStored = (bpm - 24) & 0xFF - // -- 4. Compute song offset (absolute from file start) -------------------- + // -- 4. Compress pattern bin ---------------------------------------------- + let patBinSize = patsToSave * PATTERN_SIZE + let patBuf = sys.malloc(patBinSize) + sys.memcpy(memBase - 131072, patBuf, patBinSize) + + let patCompBuf = sys.malloc(patBinSize + 4096) + let patCompSize = gzip.compFromTo(patBuf, patBinSize, patCompBuf) + sys.free(patBuf) + + // -- 5. Compress cue sheet ------------------------------------------------ + // Cue entry c, byte k is at MMIO address 32768 + c*32 + k, + // accessed as sys.peek(baseAddr − (32768 + c*32 + k)). + let cueSheetSize = NUM_CUES * CUE_SIZE + let cueBuf = sys.malloc(cueSheetSize) + for (let c = 0; c < NUM_CUES; c++) { + let cueOff = 32768 + c * CUE_SIZE + for (let k = 0; k < CUE_SIZE; k++) + sys.poke(cueBuf + c * CUE_SIZE + k, + sys.peek(baseAddr - (cueOff + k)) & 0xFF) + } + + let cueCompBuf = sys.malloc(cueSheetSize + 4096) + let cueCompSize = gzip.compFromTo(cueBuf, cueSheetSize, cueCompBuf) + sys.free(cueBuf) + + // -- 6. Compute song offset (absolute from file start) -------------------- // Layout: header(32) + compressed(compressedSize) + songTable(1 × TAUD_SONG_ENTRY) let songOffset = TAUD_HEADER_SIZE + compressedSize + 1 * TAUD_SONG_ENTRY - // -- 5. Build header byte array (32 bytes) -------------------------------- - let sigBytes = new Array(16) - for (let i = 0; i < 16; i++) + // -- 7. Build header byte array (32 bytes) -------------------------------- + let sigBytes = new Array(14) + for (let i = 0; i < 14; i++) sigBytes[i] = i < CAPTURE_SIGNATURE.length ? CAPTURE_SIGNATURE.charCodeAt(i) : 0 let header = [ @@ -203,16 +240,16 @@ function captureTrackerDataToFile(outFile) { 0x1F, 0x54, 0x53, 0x56, 0x4D, 0x61, 0x75, 0x64, // version, numSongs TAUD_VERSION, 1, - // compressedSize uint32 LE (4) + // compressedSize uint32 LE (4) -- sample+inst bin (compressedSize ) & 0xFF, (compressedSize >>> 8) & 0xFF, (compressedSize >>> 16) & 0xFF, (compressedSize >>> 24) & 0xFF, - // reserved (4) + // project data offset (4) -- not emitted 0x00, 0x00, 0x00, 0x00, - ].concat(sigBytes) // 8 + 2 + 4 + 2 + 16 = 32 bytes + ].concat(sigBytes) // 8 + 2 + 4 + 4 + 14 = 32 bytes - // -- 6. Build song-table row (16 bytes) ----------------------------------- + // -- 8. Build song-table row (32 bytes) ----------------------------------- let songTable = [ (songOffset ) & 0xFF, (songOffset >>> 8) & 0xFF, @@ -222,40 +259,45 @@ function captureTrackerDataToFile(outFile) { numPats & 0xFF, (numPats >>> 8) & 0xFF, // numPatterns Uint16 LE bpmStored, // BPM with −24 bias tickRate, // initial tick-rate - 0x00,0xA0, // basenote (0xA000 -- C9) - 0x00,0xAC,0x02,0x46, // basefreq (8363 Hz) - sys.peek(baseAddr - 7), // mixer flags + 0x00,0xA0, // basenote (0xA000 -- C9) + 0x00,0xAC,0x02,0x46, // basefreq (8363 Hz) + sys.peek(baseAddr - 7), // mixer flags + 0x80, // global volume (default) + 0x80, // mixing volume (default) + // pattern bin compressed size (4) + (patCompSize ) & 0xFF, + (patCompSize >>> 8) & 0xFF, + (patCompSize >>> 16) & 0xFF, + (patCompSize >>> 24) & 0xFF, + // cue sheet compressed size (4) + (cueCompSize ) & 0xFF, + (cueCompSize >>> 8) & 0xFF, + (cueCompSize >>> 16) & 0xFF, + (cueCompSize >>> 24) & 0xFF, + // reserved (6) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ] - // -- 7. Write header (creates / truncates file) --------------------------- + // -- 9. Write header (creates / truncates file) --------------------------- const fileHandle = files.open(outFile) fileHandle.bwrite(header) - // -- 8. Append compressed sample+inst bin --------------------------------- - fileHandle.pwrite(compBuf, compressedSize, 32) + // -- 10. Append compressed sample+inst bin -------------------------------- + fileHandle.pwrite(compBuf, compressedSize, TAUD_HEADER_SIZE) sys.free(compBuf) - // -- 9. Write song table -------------------------------------------------- + // -- 11. Write song table ------------------------------------------------- fileHandle.bwrite(songTable) - // -- 10. Append pattern bin ----------------------------------------------- - let patBuf = sys.malloc(patsToSave * PATTERN_SIZE) - sys.memcpy(memBase - 131072, patBuf, patsToSave * PATTERN_SIZE) - fileHandle.pwrite(patBuf, patsToSave * PATTERN_SIZE, 32 + compressedSize + songTable.length) - sys.free(patBuf) + // -- 12. Append compressed pattern bin ------------------------------------ + fileHandle.pwrite(patCompBuf, patCompSize, + TAUD_HEADER_SIZE + compressedSize + songTable.length) + sys.free(patCompBuf) - // -- 11. Append cue sheet (all 1024 entries from MMIO space) -------------- - // Cue entry c, byte k is at MMIO address 32768 + c*32 + k, - // accessed as sys.peek(baseAddr − (32768 + c*32 + k)). - let cueBuf = sys.malloc(NUM_CUES * CUE_SIZE) - for (let c = 0; c < NUM_CUES; c++) { - let cueOff = 32768 + c * CUE_SIZE - for (let k = 0; k < CUE_SIZE; k++) - sys.poke(cueBuf + c * CUE_SIZE + k, - sys.peek(baseAddr - (cueOff + k)) & 0xFF) - } - fileHandle.pwrite(cueBuf, NUM_CUES * CUE_SIZE, 32 + compressedSize + songTable.length + patsToSave * PATTERN_SIZE) - sys.free(cueBuf) + // -- 13. Append compressed cue sheet -------------------------------------- + fileHandle.pwrite(cueCompBuf, cueCompSize, + TAUD_HEADER_SIZE + compressedSize + songTable.length + patCompSize) + sys.free(cueCompBuf) fileHandle.flush(); fileHandle.close() diff --git a/it2taud.py b/it2taud.py index 35f2b9e..2f75a12 100644 --- a/it2taud.py +++ b/it2taud.py @@ -53,7 +53,7 @@ from taud_common import ( EFF_U, EFF_V, EFF_W, EFF_X, EFF_Y, EFF_Z, J_SEMI_TABLE, d_arg_to_col, resample_linear, encode_cue, deduplicate_patterns, - normalise_sample, + normalise_sample, encode_song_entry, ) @@ -1734,18 +1734,29 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list, ) assert len(header) == TAUD_HEADER_SIZE + # Compress pattern bin and cue sheet (per Taud spec) + pat_comp = gzip.compress(bytes(pat_bin), compresslevel=9, mtime=0) + cue_comp = gzip.compress(bytes(sheet), compresslevel=9, mtime=0) + vprint(f" pattern bin: {len(pat_bin)} → {len(pat_comp)} bytes (gzip)") + vprint(f" cue sheet: {len(sheet)} → {len(cue_comp)} bytes (gzip)") + # flags byte: bit 1 (f) = Amiga pitch-slide mode (IT linear_slides flag inverted) flags_byte = 0x00 if h.linear_slides else 0x02 - song_table = struct.pack(' bytes: vprint(f" patterns: {orig_count} → {num_taud_pats} unique " f"({orig_count - num_taud_pats} deduplicated)") - # ProTracker is Amiga-period-based by definition, so we set the f bit so - # the engine applies coarse pitch slides in period space (recovers PT's - # characteristic non-linear pitch character). - flags_byte = 0x02 - song_table = struct.pack(' bytes: pat_bin, pat_remap, num_taud_pats = deduplicate_patterns(bytes(pat_bin), orig_count) vprint(f" patterns: {orig_count} → {num_taud_pats} unique ({orig_count - num_taud_pats} deduplicated)") - # Song table row (16 bytes): offset(4)+voices(1)+pats(2)+bpm(1)+tick(1)+basenote(2)+basefreq(4)+flags(1) - # Built after dedup so num_taud_pats reflects the unique count. - # flags byte: bit 1 (f) = Amiga pitch-slide mode (mirrors the S3M linear_slides flag inverted) - flags_byte = 0x00 if h.linear_slides else 0x02 - song_table = struct.pack(' None: TAUD_MAGIC = bytes([0x1F,0x54,0x53,0x56,0x4D,0x61,0x75,0x64]) TAUD_VERSION = 1 -TAUD_HEADER_SIZE = 32 # magic(8)+ver(1)+numSongs(1)+compSize(4)+rsvd(4)+sig(14) -TAUD_SONG_ENTRY = 16 # offset(4)+voices(1)+pats(2)+bpm(1)+tick(1)+basenote(2)+basefreq(4)+flags(1) +TAUD_HEADER_SIZE = 32 # magic(8)+ver(1)+numSongs(1)+compSize(4)+projOff(4)+sig(14) +TAUD_SONG_ENTRY = 32 # full spec entry (see encode_song_entry) SAMPLEBIN_SIZE = 737280 INSTBIN_SIZE = 49152 # 256 instruments × 192 bytes SAMPLEINST_SIZE = SAMPLEBIN_SIZE + INSTBIN_SIZE @@ -166,6 +166,39 @@ def deduplicate_patterns(pat_bin: bytes, num_pats: int) -> tuple: return b''.join(canonical), remap, len(canonical) +def encode_song_entry(song_offset: int, num_voices: int, num_patterns: int, + bpm_stored: int, tick_rate: int, + base_note: int, base_freq: float, flags_byte: int, + pat_bin_comp_size: int, cue_sheet_comp_size: int, + global_vol: int = 0x80, mixing_vol: int = 0x80) -> bytes: + """Pack a 32-byte Taud song table entry. + + Layout: + u32 song_offset, u8 num_voices, u16 num_patterns, + u8 bpm_stored, u8 tick_rate, + u16 base_note, f32 base_freq, + u8 flags, u8 global_vol, u8 mixing_vol, + u32 pat_bin_comp_size, u32 cue_sheet_comp_size, + byte[6] reserved. + """ + entry = struct.pack(' bytes: """Return unsigned 8-bit mono sample bytes, downmixing/depthing as needed.""" diff --git a/terranmon.txt b/terranmon.txt index 2fe62a6..d169f2e 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -2097,10 +2097,9 @@ TODO: [x] implement Wxx command (global volume slide) [x] implement sample loop sustain "Caveat: on a foreground voice, key-off (row.note == 0x0000) currently sets voice.active = false at AudioAdapter.kt:1713, which silences the channel immediately. Sustain-loop escape therefore only takes effect on background voices spawned by NNA "Note Off" — which matches the IT idiom of layering a new note over a sustained one. Let me know if you also want the foreground key-off to keep the voice playing through fadeout." - [ ] cue and pattern compression of the Taud format (taud_common.py, taud.mjs) + [x] cue and pattern compression of the Taud format (taud_common.py, taud.mjs) [ ] figure out how IT (8 bits) and FT2 (12 bits) handles volume fadeout numbers, and come up with a compatible Taud spec, then implement [ ] implement bitcrusher (eff sym '8') - [ ] Figure out why pitch slides are not working right on 2nd_pm.s3m only Play Data: play data are series of tracker-like instructions, visualised as: @@ -2319,7 +2318,9 @@ Endianness: Little * ImpulseTracker has range of 0..128; multiply by (255/128) then round to int Uint8 Song mixing volume * ImpulseTracker has range of 0..128; multiply by (255/128) then round to int - Byte[14] Reserved + Uint32 Compressed size of PATTERN BIN for this song + Uint32 Compressed size of CUE SHEET for this song + Byte[6] Reserved Taud device can queue up to 2 "playdata" in its buffer, which can be interpreted as a song.