Taud: Zstd compression

This commit is contained in:
minjaesong
2026-05-08 17:27:27 +09:00
parent d706f27e18
commit dcd191b734
7 changed files with 172 additions and 55 deletions

View File

@@ -35,7 +35,6 @@ Effect support:
""" """
import argparse import argparse
import gzip
import struct import struct
import sys import sys
@@ -53,7 +52,7 @@ from taud_common import (
EFF_U, EFF_V, EFF_W, EFF_X, EFF_Y, EFF_Z, EFF_U, EFF_V, EFF_W, EFF_X, EFF_Y, EFF_Z,
J_SEMI_TABLE, J_SEMI_TABLE,
d_arg_to_col, resample_linear, rescale_offset_effects, encode_cue, deduplicate_patterns, d_arg_to_col, resample_linear, rescale_offset_effects, encode_cue, deduplicate_patterns,
normalise_sample, encode_song_entry, nearest_minifloat, normalise_sample, encode_song_entry, nearest_minifloat, compress_blob,
CUE_INST_NOP, CUE_INST_HALT, CUE_INST_LEN, cue_instruction_len, CUE_INST_NOP, CUE_INST_HALT, CUE_INST_LEN, cue_instruction_len,
) )
@@ -702,39 +701,80 @@ def encode_note_it(it_note: int) -> int:
# ── Vol-column decoder ──────────────────────────────────────────────────────── # ── Vol-column decoder ────────────────────────────────────────────────────────
def decode_volcol(vc: int): def decode_volcol(vc: int, recall_volslide: int = 0):
"""Return (vol_sel, vol_value, pan_set, aux_effect) or None for each field.""" """Return (vol_sel, vol_value, pan_set, aux_effect) or None for each field.
IT vol-col x=0 means "recall last value" for the relevant memory cohort
(Schism player/effects.c:2097-2137 — Ax/Bx/Cx/Dx share `mem_vc_volslide`,
a per-channel slot separate from the main column's D memory; Ex/Fx share
`mem_pitchslide` with the main effect column; Gx shares `mem_portanote`
with main G; Hx uses the channel's vibrato state). For pitch/porta/
vibrato we emit Taud E/F/G/H with arg=0 so Taud's own private (E/F-cohort,
G, H/U-cohort) memory recalls naturally. For volume slides Taud has no
recall in the volume column, so the converter passes `recall_volslide`
(the per-channel A/B/C/D shared memory tracked by build_pattern_it) and
substitutes it when x=0.
"""
if vc < 0: # not set if vc < 0: # not set
return SEL_FINE, 0, None, None return SEL_FINE, 0, None, None
if vc <= VC_VOL_HI: if vc <= VC_VOL_HI:
return SEL_SET, min(vc, 0x3F), None, None return SEL_SET, min(vc, 0x3F), None, None
if VC_FVUP_LO <= vc <= VC_FVUP_HI: if VC_FVUP_LO <= vc <= VC_FVUP_HI:
mag = vc - VC_FVUP_LO + 1 # 1..10 mag = vc - VC_FVUP_LO # 0..9 — Schism fmt/it.c:234
if mag == 0:
mag = recall_volslide
if mag == 0:
return SEL_FINE, 0, None, None
return SEL_FINE, (mag & 0x1F) | 0x20, None, None # fine up return SEL_FINE, (mag & 0x1F) | 0x20, None, None # fine up
if VC_FVDN_LO <= vc <= VC_FVDN_HI: if VC_FVDN_LO <= vc <= VC_FVDN_HI:
mag = vc - VC_FVDN_LO + 1 mag = vc - VC_FVDN_LO
if mag == 0:
mag = recall_volslide
if mag == 0:
return SEL_FINE, 0, None, None
return SEL_FINE, mag & 0x1F, None, None # fine down return SEL_FINE, mag & 0x1F, None, None # fine down
if VC_VUP_LO <= vc <= VC_VUP_HI: if VC_VUP_LO <= vc <= VC_VUP_HI:
return SEL_UP, vc - VC_VUP_LO + 1, None, None mag = vc - VC_VUP_LO
if mag == 0:
mag = recall_volslide
if mag == 0:
return SEL_FINE, 0, None, None
return SEL_UP, mag, None, None
if VC_VDN_LO <= vc <= VC_VDN_HI: if VC_VDN_LO <= vc <= VC_VDN_HI:
return SEL_DOWN, vc - VC_VDN_LO + 1, None, None mag = vc - VC_VDN_LO
if mag == 0:
mag = recall_volslide
if mag == 0:
return SEL_FINE, 0, None, None
return SEL_DOWN, mag, None, None
if VC_PDN_LO <= vc <= VC_PDN_HI: if VC_PDN_LO <= vc <= VC_PDN_HI:
# Pitch slide down: each unit = 4 ST3 coarse units (1/16 semitone each) # IT vol-col Ex slides pitch down by 4×e raw IT period units (Schism
units = (vc - VC_PDN_LO + 1) * 4 # player/effects.c:294-298). e=0 recalls mem_pitchslide; emit
# E $0000 so Taud's E/F-cohort memory supplies the value.
e = vc - VC_PDN_LO
units = e * 4
return SEL_FINE, 0, None, (EFF_E, units & 0xFF) return SEL_FINE, 0, None, (EFF_E, units & 0xFF)
if VC_PUP_LO <= vc <= VC_PUP_HI: if VC_PUP_LO <= vc <= VC_PUP_HI:
units = (vc - VC_PUP_LO + 1) * 4 f = vc - VC_PUP_LO
units = f * 4
return SEL_FINE, 0, None, (EFF_F, units & 0xFF) return SEL_FINE, 0, None, (EFF_F, units & 0xFF)
if VC_PAN_LO <= vc <= VC_PAN_HI: if VC_PAN_LO <= vc <= VC_PAN_HI:
pan64 = vc - VC_PAN_LO # 0..64 pan64 = vc - VC_PAN_LO # 0..64
pan6 = min(0x3F, round(pan64 * 63 / 64)) pan6 = min(0x3F, round(pan64 * 63 / 64))
return SEL_FINE, 0, pan6, None return SEL_FINE, 0, pan6, None
if VC_TPORTA_LO <= vc <= VC_TPORTA_HI: if VC_TPORTA_LO <= vc <= VC_TPORTA_HI:
spd = VC_TPORTA_TABLE[vc - VC_TPORTA_LO] # IT Gg tone-porta speed: VC_TPORTA_TABLE[0]=0 → g=0 recalls
# mem_portanote. Emit G $0000; Taud's private G memory recalls.
g = vc - VC_TPORTA_LO
spd = VC_TPORTA_TABLE[g]
return SEL_FINE, 0, None, (EFF_G, spd & 0xFF) return SEL_FINE, 0, None, (EFF_G, spd & 0xFF)
if VC_VIB_LO <= vc <= VC_VIB_HI: if VC_VIB_LO <= vc <= VC_VIB_HI:
depth = vc - VC_VIB_LO + 1 # 1..10 # IT Hh sets vibrato depth (low nybble only) and runs vibrato with
return SEL_FINE, 0, None, (EFF_H, depth & 0x0F) # the channel's current vibrato_speed (Schism player/effects.c:391-398
# via fx_vibrato). h=0 keeps the existing depth; emit H $0000 so
# Taud's H/U cohort memory supplies both speed and depth.
h = vc - VC_VIB_LO
return SEL_FINE, 0, None, (EFF_H, h & 0x0F)
return SEL_FINE, 0, None, None return SEL_FINE, 0, None, None
@@ -1273,10 +1313,41 @@ def build_pattern_it(chunk_grid: list, ch_idx: int, default_pan: int,
out = bytearray(PATTERN_BYTES) out = bytearray(PATTERN_BYTES)
rows = chunk_grid[ch_idx] if ch_idx < len(chunk_grid) else [ITRow()] * PATTERN_ROWS rows = chunk_grid[ch_idx] if ch_idx < len(chunk_grid) else [ITRow()] * PATTERN_ROWS
last_note_it = -1 last_note_it = -1
# IT shares one mem_vc_volslide across A/B/C/D vol-col commands (Schism
# player/effects.c:2099-2131). Track it locally so x=0 resolves to the
# last explicit value within the chunk.
mem_vc_volslide = 0
for r, cell in enumerate(rows[:PATTERN_ROWS]): for r, cell in enumerate(rows[:PATTERN_ROWS]):
# ── Resolve vol-col into overrides ────────────────────────────────── # ── Resolve vol-col into overrides ──────────────────────────────────
vs, vv, pan_from_vc, aux_eff = decode_volcol(cell.volcol) # Update mem_vc_volslide before decode so a fresh non-zero on this
# row stays visible for any later x=0 in the same channel.
if (VC_FVUP_LO <= cell.volcol <= VC_VDN_HI):
raw_mag = (cell.volcol - VC_FVUP_LO) % 10
if raw_mag != 0:
mem_vc_volslide = raw_mag
vs, vv, pan_from_vc, aux_eff = decode_volcol(cell.volcol, mem_vc_volslide)
# ── Slot juggling: combine D + G/H into L/K when both are present ──
# When the main effect is a pure vol-slide (D) and the vol-col aux is
# tone-porta (G) or vibrato depth (H), Taud has dedicated combined
# opcodes that capture both: L $xy00 (porta + vol slide) and K $xy00
# (vibrato + vol slide). Without this swap the vol-col aux would be
# dropped because the main slot is occupied.
if aux_eff is not None and cell.effect == EFF_D and cell.effect_arg != 0:
aux_op, aux_arg = aux_eff
d_arg = cell.effect_arg & 0xFF
if aux_op == EFF_G:
cell.effect, cell.effect_arg = EFF_L, d_arg
aux_eff = None
elif aux_op == EFF_H:
# K runs vibrato with current memory_HU; vol-col Hh's depth
# update is lost (warn so the trade-off is visible).
cell.effect, cell.effect_arg = EFF_K, d_arg
aux_eff = None
if (aux_arg & 0xF) != 0:
vprint(f" ch{ch_idx} row{r}: D+Hh→K, depth update "
f"{aux_arg & 0xF} folded into K vibrato recall")
# If vol-col provides an aux effect and cell has no main effect, use it # If vol-col provides an aux effect and cell has no main effect, use it
if aux_eff is not None and cell.effect == 0: if aux_eff is not None and cell.effect == 0:
@@ -1284,7 +1355,7 @@ def build_pattern_it(chunk_grid: list, ch_idx: int, default_pan: int,
aux_eff = None aux_eff = None
elif aux_eff is not None: elif aux_eff is not None:
vprint(f" ch{ch_idx} row{r}: dropped vol-col aux effect " vprint(f" ch{ch_idx} row{r}: dropped vol-col aux effect "
f"(main effect slot occupied)") f"(main effect slot occupied: cmd={cell.effect:02X} arg={cell.effect_arg:02X})")
aux_eff = None aux_eff = None
# If vol-col has a pan override # If vol-col has a pan override
@@ -1621,9 +1692,8 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
assert len(sampleinst_raw) == SAMPLEINST_SIZE assert len(sampleinst_raw) == SAMPLEINST_SIZE
compressed = gzip.compress(sampleinst_raw, compresslevel=9, mtime=0) compressed = compress_blob(sampleinst_raw, "sample+inst bin")
comp_size = len(compressed) comp_size = len(compressed)
vprint(f" sample+inst bin: {SAMPLEINST_SIZE}{comp_size} bytes (gzip)")
# ── BPM / speed ────────────────────────────────────────────────────────── # ── BPM / speed ──────────────────────────────────────────────────────────
speed, tempo = find_initial_bpm_speed(patterns_rows, h.order_list, speed, tempo = find_initial_bpm_speed(patterns_rows, h.order_list,
@@ -1707,10 +1777,8 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
assert len(header) == TAUD_HEADER_SIZE assert len(header) == TAUD_HEADER_SIZE
# Compress pattern bin and cue sheet (per Taud spec) # Compress pattern bin and cue sheet (per Taud spec)
pat_comp = gzip.compress(bytes(pat_bin), compresslevel=9, mtime=0) pat_comp = compress_blob(bytes(pat_bin), "pattern bin")
cue_comp = gzip.compress(bytes(sheet), compresslevel=9, mtime=0) cue_comp = compress_blob(bytes(sheet), "cue sheet")
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: bit 1 (f) = Amiga pitch-slide mode (IT linear_slides flag inverted).
# bit 2 was the old 'm' fadeout-zero policy flag and is now reserved (always 0); fadeout # bit 2 was the old 'm' fadeout-zero policy flag and is now reserved (always 0); fadeout

View File

@@ -24,7 +24,6 @@ Effect support:
""" """
import argparse import argparse
import gzip
import math import math
import struct import struct
import sys import sys
@@ -40,7 +39,7 @@ from taud_common import (
SEL_SET, SEL_UP, SEL_DOWN, SEL_FINE, SEL_SET, SEL_UP, SEL_DOWN, SEL_FINE,
J_SEMI_TABLE, J_SEMI_TABLE,
d_arg_to_col, resample_linear, rescale_offset_effects, encode_cue, deduplicate_patterns, d_arg_to_col, resample_linear, rescale_offset_effects, encode_cue, deduplicate_patterns,
encode_song_entry, encode_song_entry, compress_blob,
) )
@@ -718,9 +717,8 @@ def assemble_taud(mod: dict) -> bytes:
sampleinst_raw, _offsets, sample_ratio = build_sample_inst_bin(samples) sampleinst_raw, _offsets, sample_ratio = build_sample_inst_bin(samples)
assert len(sampleinst_raw) == SAMPLEINST_SIZE assert len(sampleinst_raw) == SAMPLEINST_SIZE
compressed = gzip.compress(sampleinst_raw, compresslevel=9, mtime=0) compressed = compress_blob(sampleinst_raw, "sample+inst bin")
comp_size = len(compressed) comp_size = len(compressed)
vprint(f" sample+inst bin: {SAMPLEINST_SIZE}{comp_size} bytes (gzip)")
speed, tempo = find_initial_bpm_speed(patterns, order_list) speed, tempo = find_initial_bpm_speed(patterns, order_list)
tempo = max(24, min(280, tempo)) tempo = max(24, min(280, tempo))
@@ -766,10 +764,8 @@ def assemble_taud(mod: dict) -> bytes:
cue_sheet = build_cue_sheet(order_list, n_patterns, n_channels, pat_remap) cue_sheet = build_cue_sheet(order_list, n_patterns, n_channels, pat_remap)
assert len(cue_sheet) == NUM_CUES * CUE_SIZE assert len(cue_sheet) == NUM_CUES * CUE_SIZE
pat_comp = gzip.compress(bytes(pat_bin), compresslevel=9, mtime=0) pat_comp = compress_blob(bytes(pat_bin), "pattern bin")
cue_comp = gzip.compress(bytes(cue_sheet), compresslevel=9, mtime=0) cue_comp = compress_blob(bytes(cue_sheet), "cue sheet")
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 # 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 # the engine applies coarse pitch slides in period space (recovers PT's

View File

@@ -22,7 +22,6 @@ Limits: numVoices ≤ 20, numPatterns × numVoices ≤ 4095.
""" """
import argparse import argparse
import gzip
import struct import struct
import sys import sys
@@ -35,7 +34,7 @@ from taud_common import (
TOP_NONE, TOP_A, TOP_B, TOP_C, TOP_E, TOP_F, TOP_G, TOP_H, TOP_J, TOP_NONE, TOP_A, TOP_B, TOP_C, TOP_E, TOP_F, TOP_G, TOP_H, TOP_J,
SEL_SET, SEL_FINE, SEL_SET, SEL_FINE,
J_SEMI_TABLE, J_SEMI_TABLE,
encode_cue, deduplicate_patterns, encode_song_entry, encode_cue, deduplicate_patterns, encode_song_entry, compress_blob,
) )
@@ -324,9 +323,8 @@ def assemble_taud(mon: dict) -> bytes:
vprint(" building sample/instrument bin…") vprint(" building sample/instrument bin…")
sampleinst_raw = build_sample_inst_bin() sampleinst_raw = build_sample_inst_bin()
assert len(sampleinst_raw) == SAMPLEINST_SIZE assert len(sampleinst_raw) == SAMPLEINST_SIZE
compressed = gzip.compress(sampleinst_raw, compresslevel=9, mtime=0) compressed = compress_blob(sampleinst_raw, "sample+inst bin")
comp_size = len(compressed) comp_size = len(compressed)
vprint(f" sample+inst bin: {SAMPLEINST_SIZE}{comp_size} bytes (gzip)")
vprint(" building pattern bin…") vprint(" building pattern bin…")
pat_bin = bytearray() pat_bin = bytearray()
@@ -346,10 +344,8 @@ def assemble_taud(mon: dict) -> bytes:
cue_sheet = build_cue_sheet(order_list, num_voices, pat_remap) cue_sheet = build_cue_sheet(order_list, num_voices, pat_remap)
assert len(cue_sheet) == NUM_CUES * CUE_SIZE assert len(cue_sheet) == NUM_CUES * CUE_SIZE
pat_comp = gzip.compress(bytes(pat_bin), compresslevel=9, mtime=0) pat_comp = compress_blob(bytes(pat_bin), "pattern bin")
cue_comp = gzip.compress(bytes(cue_sheet), compresslevel=9, mtime=0) cue_comp = compress_blob(bytes(cue_sheet), "cue sheet")
vprint(f" pattern bin: {len(pat_bin)}{len(pat_comp)} bytes (gzip)")
vprint(f" cue sheet: {len(cue_sheet)}{len(cue_comp)} bytes (gzip)")
# Header: magic, version, num_songs=1, comp_size of sample+inst, projOff=0, sig. # Header: magic, version, num_songs=1, comp_size of sample+inst, projOff=0, sig.
sig = (SIGNATURE + b' ' * 14)[:14] sig = (SIGNATURE + b' ' * 14)[:14]

View File

@@ -25,7 +25,6 @@ Effect support:
""" """
import argparse import argparse
import gzip
import math import math
import struct import struct
import sys import sys
@@ -44,7 +43,7 @@ from taud_common import (
EFF_U, EFF_V, EFF_W, EFF_X, EFF_Y, EFF_Z, EFF_U, EFF_V, EFF_W, EFF_X, EFF_Y, EFF_Z,
J_SEMI_TABLE, J_SEMI_TABLE,
d_arg_to_col, resample_linear, rescale_offset_effects, encode_cue, deduplicate_patterns, d_arg_to_col, resample_linear, rescale_offset_effects, encode_cue, deduplicate_patterns,
normalise_sample, encode_song_entry, normalise_sample, encode_song_entry, compress_blob,
) )
@@ -752,9 +751,8 @@ def assemble_taud(h: S3MHeader, instruments: list, patterns: list) -> bytes:
assert len(sampleinst_raw) == SAMPLEINST_SIZE assert len(sampleinst_raw) == SAMPLEINST_SIZE
# Compress # Compress
compressed = gzip.compress(sampleinst_raw, compresslevel=9, mtime=0) compressed = compress_blob(sampleinst_raw, "sample+inst bin")
comp_size = len(compressed) comp_size = len(compressed)
vprint(f" sample+inst bin: {SAMPLEINST_SIZE}{comp_size} bytes (gzip)")
# Initial BPM / speed # Initial BPM / speed
speed, tempo = find_initial_bpm_speed(patterns, h.order_list, speed, tempo = find_initial_bpm_speed(patterns, h.order_list,
@@ -810,10 +808,8 @@ def assemble_taud(h: S3MHeader, instruments: list, patterns: list) -> bytes:
assert len(cue_sheet) == NUM_CUES * CUE_SIZE assert len(cue_sheet) == NUM_CUES * CUE_SIZE
# Compress pattern bin and cue sheet (per Taud spec) # Compress pattern bin and cue sheet (per Taud spec)
pat_comp = gzip.compress(bytes(pat_bin), compresslevel=9, mtime=0) pat_comp = compress_blob(bytes(pat_bin), "pattern bin")
cue_comp = gzip.compress(bytes(cue_sheet), compresslevel=9, mtime=0) cue_comp = compress_blob(bytes(cue_sheet), "cue sheet")
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). # 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: bit 1 (f) = Amiga pitch-slide mode (mirrors the S3M linear_slides flag inverted).

View File

@@ -7,9 +7,16 @@ pattern deduper, sample normaliser) that all three converters used to
duplicate verbatim. duplicate verbatim.
""" """
import gzip as _gzip
import struct import struct
import sys import sys
try:
import zstandard as _zstd
_ZSTD_CCTX = _zstd.ZstdCompressor(level=22)
except ImportError:
_ZSTD_CCTX = None
# ── Verbose logging (shared across converters via set_verbose) ─────────────── # ── Verbose logging (shared across converters via set_verbose) ───────────────
@@ -24,6 +31,37 @@ def vprint(*a, **kw) -> None:
print(*a, **kw, file=sys.stderr) print(*a, **kw, file=sys.stderr)
# ── Compression (gzip vs zstd; whichever is smaller) ─────────────────────────
#
# The Taud loader sniffs the 4-byte magic of every compressed slot and routes
# to GZIPInputStream or ZstdInputStream accordingly (CompressorDelegate.kt:148-149),
# so each blob can independently pick whichever codec compresses it smaller.
def best_compress(payload: bytes) -> tuple:
"""Return (compressed_bytes, method) for the smaller of gzip/zstd output.
Method is "gzip" or "zstd". Falls back to gzip when the `zstandard`
package is not installed.
"""
gz = _gzip.compress(payload, compresslevel=9, mtime=0)
if _ZSTD_CCTX is None:
return gz, "gzip"
zs = _ZSTD_CCTX.compress(payload)
if len(zs) < len(gz):
return zs, "zstd"
return gz, "gzip"
def compress_blob(payload: bytes, label: str) -> bytes:
"""Compress `payload` with whichever of gzip/zstd is smaller; vprint stats; return bytes.
`label` is the human-readable name in the verbose log line, e.g. "sample+inst bin".
"""
out, method = best_compress(payload)
vprint(f" {label}: {len(payload)}{len(out)} bytes ({method})")
return out
# ── Taud container constants ───────────────────────────────────────────────── # ── Taud container constants ─────────────────────────────────────────────────
TAUD_MAGIC = bytes([0x1F,0x54,0x53,0x56,0x4D,0x61,0x75,0x64]) TAUD_MAGIC = bytes([0x1F,0x54,0x53,0x56,0x4D,0x61,0x75,0x64])

View File

@@ -2351,6 +2351,7 @@ TODO:
[ ] remove panning mode selection and replace global panning rule to 3 dB rule (not the equal energy) [ ] remove panning mode selection and replace global panning rule to 3 dB rule (not the equal energy)
[x] FT2/MOD double effects with 00 as arg (500, 600) missing volume column -> easiest solution: fully implement `L xy00` and `K xy00` and map 5xx to L, 6xx to K (xm2taud, mod2taud), Kxy and Lxy verbatim (s3m2taud.py, it2taud.py). This is justified because the volume effects rely on memory when 00 is given, and said memory effect only get recalled when NoteFx is used. TAUD_NOTE_EFFECTS already has detailed implementation notes. Mark those two commands as implemented sorely for tracker compatibility. [x] FT2/MOD double effects with 00 as arg (500, 600) missing volume column -> easiest solution: fully implement `L xy00` and `K xy00` and map 5xx to L, 6xx to K (xm2taud, mod2taud), Kxy and Lxy verbatim (s3m2taud.py, it2taud.py). This is justified because the volume effects rely on memory when 00 is given, and said memory effect only get recalled when NoteFx is used. TAUD_NOTE_EFFECTS already has detailed implementation notes. Mark those two commands as implemented sorely for tracker compatibility.
Also document then implement `Mxx` (set channel volume, not just a note: 0x00 to 0x3F) `Nxy` (channel volume slide: similar to Dxy, but applies to the current channel's volume, not just a note) `Pxy` (channel panning slide. Similar to Dxx: P0y - to the right, Px0 - to the left, PFy - fine pan right, PxF - fine pan left) effects Also document then implement `Mxx` (set channel volume, not just a note: 0x00 to 0x3F) `Nxy` (channel volume slide: similar to Dxy, but applies to the current channel's volume, not just a note) `Pxy` (channel panning slide. Similar to Dxx: P0y - to the right, Px0 - to the left, PFy - fine pan right, PxF - fine pan left) effects
[ ] 8 MB sample RAM via 512k banks
Play Data: play data are series of tracker-like instructions, visualised as: Play Data: play data are series of tracker-like instructions, visualised as:

View File

@@ -37,7 +37,6 @@ Reference:
""" """
import argparse import argparse
import gzip
import math import math
import struct import struct
import sys import sys
@@ -53,7 +52,7 @@ from taud_common import (
SEL_SET, SEL_UP, SEL_DOWN, SEL_FINE, SEL_SET, SEL_UP, SEL_DOWN, SEL_FINE,
J_SEMI_TABLE, J_SEMI_TABLE,
d_arg_to_col, resample_linear, rescale_offset_effects, encode_cue, deduplicate_patterns, d_arg_to_col, resample_linear, rescale_offset_effects, encode_cue, deduplicate_patterns,
normalise_sample, encode_song_entry, nearest_minifloat, normalise_sample, encode_song_entry, nearest_minifloat, compress_blob,
CUE_INST_NOP, CUE_INST_HALT, CUE_INST_LEN, cue_instruction_len, CUE_INST_NOP, CUE_INST_HALT, CUE_INST_LEN, cue_instruction_len,
) )
@@ -1080,6 +1079,32 @@ def build_pattern_xm(chunk_grid: list, ch_idx: int, default_pan: int,
# Pan slide via vol-col D/E (encoded as pan_override below) # Pan slide via vol-col D/E (encoded as pan_override below)
vc_pan_override = _xm_volcol_pan_override(cell.volcol) vc_pan_override = _xm_volcol_pan_override(cell.volcol)
# ── Slot juggling for combined effects ──────────────────────────────
# XM main 0x0A (vol slide → TOP_D) + vol-col Mx (porta → TOP_G aux)
# combine cleanly into Taud L (porta + vol slide). Same for
# vol-col Bx/Ax (vibrato → TOP_H aux) → Taud K (vibrato + vol slide).
# Without this swap the vol-col aux would be dropped because the main
# slot is already occupied by D. The combined K/L take their slide
# nibbles directly from the source D arg (high byte of XM 0x0A),
# matching the encoding used by main XM effects 5 (→ L) and 6 (→ K).
if (aux_eff is not None and cell.effect == 0x0A
and cell.effect_arg != 0):
aux_op, aux_arg = aux_eff
d_arg = cell.effect_arg & 0xFF
if aux_op == TOP_G:
# XM A + vol-col M → Taud L verbatim. Porta speed already
# lives in Taud's private G memory (vol-col aux → G $00xx).
cell.effect, cell.effect_arg = 0x05, d_arg
aux_eff = None
elif aux_op == TOP_H:
# XM A + vol-col B (vibrato depth) → Taud K. K reuses
# memory_HU; the vol-col Bx depth update is lost.
cell.effect, cell.effect_arg = 0x06, d_arg
aux_eff = None
if (aux_arg & 0xFF) != 0:
vprint(f" ch{ch_idx} row{r}: A+Bx→K, depth update "
f"{aux_arg & 0xFF:02X} folded into K vibrato recall")
# ── Main effect translation ───────────────────────────────────────── # ── Main effect translation ─────────────────────────────────────────
op, arg16, vol_override, pan_override = encode_effect_xm( op, arg16, vol_override, pan_override = encode_effect_xm(
cell.effect, cell.effect_arg, ch_idx, r, amiga_mode=amiga_mode) cell.effect, cell.effect_arg, ch_idx, r, amiga_mode=amiga_mode)
@@ -1095,7 +1120,7 @@ def build_pattern_xm(chunk_grid: list, ch_idx: int, default_pan: int,
aux_eff = None aux_eff = None
else: else:
vprint(f" ch{ch_idx} row{r}: dropped vol-col aux effect " vprint(f" ch{ch_idx} row{r}: dropped vol-col aux effect "
f"(main effect slot occupied)") f"(main effect slot occupied: cmd={cell.effect:02X} arg={cell.effect_arg:02X})")
# ── Note ──────────────────────────────────────────────────────────── # ── Note ────────────────────────────────────────────────────────────
note_taud = NOTE_NOP note_taud = NOTE_NOP
@@ -1230,9 +1255,8 @@ def assemble_taud(h: XMHeader, patterns: list, instruments: list) -> bytes:
# ── Sample / instrument bin ───────────────────────────────────────────── # ── Sample / instrument bin ─────────────────────────────────────────────
vprint(f" building sample/inst bin… ({len(proxies) - 1} sample slots used)") vprint(f" building sample/inst bin… ({len(proxies) - 1} sample slots used)")
sampleinst_raw, _, sample_ratio = build_sample_inst_bin_xm(proxies) sampleinst_raw, _, sample_ratio = build_sample_inst_bin_xm(proxies)
compressed = gzip.compress(sampleinst_raw, compresslevel=9, mtime=0) compressed = compress_blob(sampleinst_raw, "sample+inst bin")
comp_size = len(compressed) comp_size = len(compressed)
vprint(f" sample+inst bin: {SAMPLEINST_SIZE}{comp_size} bytes (gzip)")
# ── Tempo / speed ─────────────────────────────────────────────────────── # ── Tempo / speed ───────────────────────────────────────────────────────
speed = h.default_speed if h.default_speed > 0 else 6 speed = h.default_speed if h.default_speed > 0 else 6
@@ -1350,10 +1374,8 @@ def assemble_taud(h: XMHeader, patterns: list, instruments: list) -> bytes:
) )
assert len(header) == TAUD_HEADER_SIZE assert len(header) == TAUD_HEADER_SIZE
pat_comp = gzip.compress(bytes(pat_bin), compresslevel=9, mtime=0) pat_comp = compress_blob(bytes(pat_bin), "pattern bin")
cue_comp = gzip.compress(bytes(sheet), compresslevel=9, mtime=0) cue_comp = compress_blob(bytes(sheet), "cue sheet")
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: # Flags byte:
# bit 1 (f) = Amiga pitch-slide mode (set when XM uses Amiga period table). # bit 1 (f) = Amiga pitch-slide mode (set when XM uses Amiga period table).