Taud: 8 MB sample rom/it and xm resampling too-long samples

This commit is contained in:
minjaesong
2026-05-08 18:10:25 +09:00
parent dcd191b734
commit 27b0f2e63f
11 changed files with 301 additions and 77 deletions

View File

@@ -41,7 +41,7 @@ import sys
from taud_common import (
set_verbose, vprint,
TAUD_MAGIC, TAUD_VERSION, TAUD_HEADER_SIZE, TAUD_SONG_ENTRY,
SAMPLEBIN_SIZE, INSTBIN_SIZE, SAMPLEINST_SIZE,
SAMPLEBIN_SIZE, INSTBIN_SIZE, SAMPLEINST_SIZE, SAMPLE_LEN_LIMIT,
PATTERN_ROWS, PATTERN_BYTES, NUM_PATTERNS_MAX, NUM_CUES, CUE_SIZE, NUM_VOICES,
NOTE_NOP, NOTE_KEYOFF, NOTE_CUT, TAUD_C4,
TOP_NONE, TOP_A, TOP_B, TOP_C, TOP_D, TOP_E, TOP_F, TOP_G, TOP_H, TOP_I,
@@ -51,7 +51,8 @@ from taud_common import (
EFF_K, EFF_L, EFF_M, EFF_N, EFF_O, EFF_P, EFF_Q, EFF_R, EFF_S, EFF_T,
EFF_U, EFF_V, EFF_W, EFF_X, EFF_Y, EFF_Z,
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_per_slot,
encode_cue, deduplicate_patterns,
normalise_sample, encode_song_entry, nearest_minifloat, compress_blob,
CUE_INST_NOP, CUE_INST_HALT, CUE_INST_LEN, cue_instruction_len,
)
@@ -1090,25 +1091,60 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
sample_detune, nna, dct, dca.
All optional; missing keys default to neutral values.
Returns (bin_bytes[SAMPLEINST_SIZE], offsets_dict).
Returns (bin_bytes[SAMPLEINST_SIZE], offsets_dict, slot_ratios) where
slot_ratios maps Taud slot index → effective TOP_O scale (combined
global × per-sample resample ratio).
"""
pcm_list = [(i, s) for i, s in enumerate(samples_or_proxy)
if s is not None and s.sample_data]
def _scale_sample(s, r):
s.sample_data = resample_linear(s.sample_data, r)
s.length = len(s.sample_data)
s.loop_beg = max(0, int(s.loop_beg * r))
s.loop_end = max(0, min(int(s.loop_end * r), s.length))
s.sus_beg = max(0, int(s.sus_beg * r))
s.sus_end = max(0, min(int(s.sus_end * r), s.length))
s.c5_speed = max(1, int(s.c5_speed * r))
# ── Pass 1: global pool-overflow resample (8 MB cap) ────────────────────
total = sum(len(s.sample_data) for _, s in pcm_list)
ratio = 1.0
global_ratio = 1.0
if total > SAMPLEBIN_SIZE:
ratio = SAMPLEBIN_SIZE / total
vprint(f" info: sample bin overflow ({total} bytes); resampling all by {ratio:.4f}")
global_ratio = SAMPLEBIN_SIZE / total
vprint(f" info: sample bin overflow ({total} bytes); resampling all by {global_ratio:.4f}")
seen_g = set()
for _, s in pcm_list:
new_data = resample_linear(s.sample_data, ratio)
s.sample_data = new_data
s.length = len(new_data)
s.loop_beg = max(0, int(s.loop_beg * ratio))
s.loop_end = max(0, min(int(s.loop_end * ratio), s.length))
s.sus_beg = max(0, int(s.sus_beg * ratio))
s.sus_end = max(0, min(int(s.sus_end * ratio), s.length))
s.c5_speed = max(1, int(s.c5_speed * ratio))
if id(s) in seen_g:
continue
seen_g.add(id(s))
_scale_sample(s, global_ratio)
# ── Pass 2: per-sample u16 cap (each sample must fit in 65535 bytes) ────
# The Taud instrument record stores the sample length as u16, and TOP_O
# offsets address up to 0xFF00 bytes — anything longer would silently
# truncate at load time and over-shoot O-jumps. Resample only the
# over-long samples and remember each one's individual ratio so the
# caller can rescale TOP_O args per channel rather than globally.
per_sample_ratio = {} # id(s) → per-sample ratio (after global)
seen_p = set()
for _, s in pcm_list:
if id(s) in seen_p:
continue
seen_p.add(id(s))
if len(s.sample_data) > SAMPLE_LEN_LIMIT:
r = SAMPLE_LEN_LIMIT / len(s.sample_data)
vprint(f" info: '{s.name}' exceeds {SAMPLE_LEN_LIMIT}-byte cap "
f"({len(s.sample_data)}); resampling by {r:.4f}")
_scale_sample(s, r)
per_sample_ratio[id(s)] = r
# Effective slot → ratio for TOP_O rescaling. Slots sharing a sample
# object (IT use_instruments mode) get the same ratio.
slot_ratios = {}
for slot_idx, s in pcm_list:
slot_ratios[slot_idx] = global_ratio * per_sample_ratio.get(id(s), 1.0)
ratio = slot_ratios
sample_bin = bytearray(SAMPLEBIN_SIZE)
offsets = {}
@@ -1719,8 +1755,13 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
pat_bin += build_pattern_it(cg, ch, default_pans[vi], inst_vols,
amiga_mode=not h.linear_slides)
# Rescale TOP_O sample-offset args if samples were globally downsampled.
pat_bin = rescale_offset_effects(bytes(pat_bin), sample_ratio)
# Rescale TOP_O sample-offset args per channel using the active slot's
# ratio (combined global + per-sample). Walks pat_bin in cue-major /
# channel-minor order, tracking the most recent inst byte seen on each
# channel — must run before deduplication so the channel state stays
# linear.
pat_bin = rescale_offset_effects_per_slot(
bytes(pat_bin), len(taud_cue_list), C, sample_ratio)
orig_count = len(taud_cue_list) * C
pat_bin, pat_remap, num_taud_pats = deduplicate_patterns(pat_bin, orig_count)