mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
taud: cue and pattern compression
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
27
it2taud.py
27
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('<IBHBBHfB',
|
||||
song_offset, C, num_taud_pats,
|
||||
bpm_stored, speed,
|
||||
0xA000, # C9
|
||||
8363.0,
|
||||
flags_byte,
|
||||
song_table = encode_song_entry(
|
||||
song_offset=song_offset,
|
||||
num_voices=C,
|
||||
num_patterns=num_taud_pats,
|
||||
bpm_stored=bpm_stored,
|
||||
tick_rate=speed,
|
||||
base_note=0xA000, # C9
|
||||
base_freq=8363.0,
|
||||
flags_byte=flags_byte,
|
||||
pat_bin_comp_size=len(pat_comp),
|
||||
cue_sheet_comp_size=len(cue_comp),
|
||||
)
|
||||
assert len(song_table) == TAUD_SONG_ENTRY
|
||||
|
||||
return header + compressed + song_table + bytes(pat_bin) + bytes(sheet)
|
||||
return header + compressed + song_table + pat_comp + cue_comp
|
||||
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
42
mod2taud.py
42
mod2taud.py
@@ -40,6 +40,7 @@ from taud_common import (
|
||||
SEL_SET, SEL_UP, SEL_DOWN, SEL_FINE,
|
||||
J_SEMI_TABLE,
|
||||
d_arg_to_col, resample_linear, encode_cue, deduplicate_patterns,
|
||||
encode_song_entry,
|
||||
)
|
||||
|
||||
|
||||
@@ -745,27 +746,34 @@ def assemble_taud(mod: dict) -> 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('<IBHBBHfB',
|
||||
song_offset,
|
||||
n_channels,
|
||||
num_taud_pats,
|
||||
bpm_stored,
|
||||
speed,
|
||||
0xA000,
|
||||
8363.0,
|
||||
flags_byte,
|
||||
)
|
||||
assert len(song_table) == TAUD_SONG_ENTRY
|
||||
|
||||
vprint(" building cue sheet…")
|
||||
cue_sheet = build_cue_sheet(order_list, n_patterns, n_channels, pat_remap)
|
||||
assert len(cue_sheet) == NUM_CUES * CUE_SIZE
|
||||
|
||||
return header + compressed + song_table + bytes(pat_bin) + cue_sheet
|
||||
pat_comp = gzip.compress(bytes(pat_bin), compresslevel=9, mtime=0)
|
||||
cue_comp = gzip.compress(bytes(cue_sheet), compresslevel=9, mtime=0)
|
||||
vprint(f" pattern bin: {len(pat_bin)} → {len(pat_comp)} bytes (gzip)")
|
||||
vprint(f" cue sheet: {len(cue_sheet)} → {len(cue_comp)} bytes (gzip)")
|
||||
|
||||
# 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 = encode_song_entry(
|
||||
song_offset=song_offset,
|
||||
num_voices=n_channels,
|
||||
num_patterns=num_taud_pats,
|
||||
bpm_stored=bpm_stored,
|
||||
tick_rate=speed,
|
||||
base_note=0xA000,
|
||||
base_freq=8363.0,
|
||||
flags_byte=flags_byte,
|
||||
pat_bin_comp_size=len(pat_comp),
|
||||
cue_sheet_comp_size=len(cue_comp),
|
||||
)
|
||||
assert len(song_table) == TAUD_SONG_ENTRY
|
||||
|
||||
return header + compressed + song_table + pat_comp + cue_comp
|
||||
|
||||
|
||||
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
43
s3m2taud.py
43
s3m2taud.py
@@ -44,7 +44,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,
|
||||
)
|
||||
|
||||
|
||||
@@ -818,28 +818,35 @@ def assemble_taud(h: S3MHeader, instruments: list, patterns: list) -> 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('<IBHBBHfB',
|
||||
song_offset,
|
||||
C,
|
||||
num_taud_pats,
|
||||
bpm_stored,
|
||||
speed,
|
||||
0xA000, # C9
|
||||
8363.0, # Hz
|
||||
flags_byte,
|
||||
)
|
||||
assert len(song_table) == TAUD_SONG_ENTRY
|
||||
|
||||
# Cue sheet (using remapped pattern indices)
|
||||
vprint(" building cue sheet…")
|
||||
cue_sheet = build_cue_sheet(h.order_list, P, C, pat_remap)
|
||||
assert len(cue_sheet) == NUM_CUES * CUE_SIZE
|
||||
|
||||
return header + compressed + song_table + bytes(pat_bin) + cue_sheet
|
||||
# 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(cue_sheet), compresslevel=9, mtime=0)
|
||||
vprint(f" pattern bin: {len(pat_bin)} → {len(pat_comp)} bytes (gzip)")
|
||||
vprint(f" cue sheet: {len(cue_sheet)} → {len(cue_comp)} bytes (gzip)")
|
||||
|
||||
# Song table row (32 bytes; see encode_song_entry).
|
||||
# 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 = encode_song_entry(
|
||||
song_offset=song_offset,
|
||||
num_voices=C,
|
||||
num_patterns=num_taud_pats,
|
||||
bpm_stored=bpm_stored,
|
||||
tick_rate=speed,
|
||||
base_note=0xA000, # C9
|
||||
base_freq=8363.0,
|
||||
flags_byte=flags_byte,
|
||||
pat_bin_comp_size=len(pat_comp),
|
||||
cue_sheet_comp_size=len(cue_comp),
|
||||
)
|
||||
assert len(song_table) == TAUD_SONG_ENTRY
|
||||
|
||||
return header + compressed + song_table + pat_comp + cue_comp
|
||||
|
||||
|
||||
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -28,8 +28,8 @@ def vprint(*a, **kw) -> 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('<IBHBBHfBBBII',
|
||||
song_offset,
|
||||
num_voices & 0xFF,
|
||||
num_patterns & 0xFFFF,
|
||||
bpm_stored & 0xFF,
|
||||
tick_rate & 0xFF,
|
||||
base_note & 0xFFFF,
|
||||
float(base_freq),
|
||||
flags_byte & 0xFF,
|
||||
global_vol & 0xFF,
|
||||
mixing_vol & 0xFF,
|
||||
pat_bin_comp_size & 0xFFFFFFFF,
|
||||
cue_sheet_comp_size & 0xFFFFFFFF,
|
||||
) + b'\x00' * 6
|
||||
assert len(entry) == TAUD_SONG_ENTRY
|
||||
return entry
|
||||
|
||||
|
||||
def normalise_sample(raw: bytes, signed: bool, is_16bit: bool,
|
||||
is_stereo: bool, name: str) -> bytes:
|
||||
"""Return unsigned 8-bit mono sample bytes, downmixing/depthing as needed."""
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user