reflecting spec changes

This commit is contained in:
minjaesong
2026-05-01 12:25:47 +09:00
parent 50802186ce
commit 01ff4b1d47
7 changed files with 521 additions and 194 deletions

View File

@@ -7,7 +7,7 @@ Usage:
Limits:
- Up to 20 S3M channels (excess disabled; hard error if pattern count
× channel count > 4095).
- Sample bin is 770048 bytes; if all samples together exceed this, every
- Sample bin is 737280 bytes; if all samples together exceed this, every
sample is globally resampled down (with c2spd adjusted) so pitch is
preserved.
- AdLib instruments are skipped.
@@ -34,7 +34,7 @@ from taud_common import (
TAUD_MAGIC, TAUD_VERSION, TAUD_HEADER_SIZE, TAUD_SONG_ENTRY,
SAMPLEBIN_SIZE, INSTBIN_SIZE, SAMPLEINST_SIZE,
PATTERN_ROWS, PATTERN_BYTES, NUM_PATTERNS_MAX, NUM_CUES, CUE_SIZE, NUM_VOICES,
NOTE_NOP, NOTE_KEYOFF, NOTE_CUT, TAUD_C3,
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,
TOP_J, TOP_K, TOP_L, TOP_O, TOP_Q, TOP_R, TOP_S, TOP_T, TOP_U, TOP_V, TOP_Y,
SEL_SET, SEL_UP, SEL_DOWN, SEL_FINE,
@@ -231,7 +231,7 @@ def encode_note(s3m_note: int) -> int:
if pitch > 11:
return NOTE_NOP
semitones = (octave - 4) * 12 + pitch
val = round(TAUD_C3 + semitones * 4096 / 12)
val = round(TAUD_C4 + semitones * 4096 / 12)
return max(1, min(0xFFFD, val))
@@ -455,7 +455,9 @@ def build_sample_inst_bin(instruments: list) -> tuple:
inst.loop_end = min(inst.loop_end, n)
pos += n
# Build instrument bin (256 × 64 bytes)
# Build instrument bin (256 × 192 bytes)
# New layout (terranmon.txt:1997-2070): u32 sample ptr, ..., 25-point envelopes,
# plus a host of optional fields. S3M doesn't supply most of those — they default to 0.
inst_bin = bytearray(INSTBIN_SIZE)
for i, inst in enumerate(instruments):
taud_idx = i + 1
@@ -463,33 +465,37 @@ def build_sample_inst_bin(instruments: list) -> tuple:
break
if inst is None or inst.itype != S3M_TYPE_PCM:
continue
ptr = offsets.get(i, 0)
ptr_lo = ptr & 0xFFFF
ptr_hi = (ptr >> 16)
ptr = offsets.get(i, 0) & 0xFFFFFFFF
s_len = min(inst.length, 65535)
c2spd = min(inst.c2spd, 65535)
ps = 0
ls = min(inst.loop_begin, 65535)
le = min(inst.loop_end, 65535)
loop_mode = 1 if (inst.flags & 1) else 0
flags_byte = (ptr_hi << 4) | (loop_mode & 0x3) # hhhh 00pp
flags_byte = loop_mode & 0x3 # 0b 0000 00pp
base = taud_idx * 64
struct.pack_into('<H', inst_bin, base + 0, ptr_lo)
struct.pack_into('<H', inst_bin, base + 2, s_len)
struct.pack_into('<H', inst_bin, base + 4, c2spd)
struct.pack_into('<H', inst_bin, base + 6, ps)
struct.pack_into('<H', inst_bin, base + 8, ls)
struct.pack_into('<H', inst_bin, base + 10, le)
inst_bin[base + 12] = flags_byte
inst_bin[base + 15] = 0xFF # instrument global volume (S3M has none → full)
# Volume envelope: hold at instrument volume (clamped to 0x3F)
# Volume envelope: hold at instrument volume (clamped to 0x3F).
env_vol = min(inst.volume, 63)
inst_bin[base + 16] = env_vol # volume
inst_bin[base + 17] = 0 # offset minifloat = 0 → hold
# Vol env-flags: enable use-envelope bit (b=1) so engine reads the single point.
vol_env_flags = 0x0020 # b=bit 5
base = taud_idx * 192
struct.pack_into('<I', inst_bin, base + 0, ptr) # u32 sample pointer
struct.pack_into('<H', inst_bin, base + 4, s_len)
struct.pack_into('<H', inst_bin, base + 6, c2spd) # rate at TAUD_C4
struct.pack_into('<H', inst_bin, base + 8, ps)
struct.pack_into('<H', inst_bin, base + 10, ls)
struct.pack_into('<H', inst_bin, base + 12, le)
inst_bin[base + 14] = flags_byte
struct.pack_into('<H', inst_bin, base + 15, vol_env_flags)
struct.pack_into('<H', inst_bin, base + 17, 0) # pan env-flags
struct.pack_into('<H', inst_bin, base + 19, 0) # pitch/filter env-flags
# Volume env point 0: hold at env_vol indefinitely (offset minifloat = 0 → hold).
inst_bin[base + 21] = env_vol
inst_bin[base + 22] = 0
inst_bin[base + 171] = 0xFF # instrument global volume
vprint(f" instrument[{base // 64}] '{inst.name}' ptr: '{ptr}', sampling rate: '{inst.c2spd}'")
vprint(f" instrument[{base // 192}] '{inst.name}' ptr: '{ptr}', sampling rate: '{inst.c2spd}'")
if inst.c2spd > 65535:
vprint(f" warning: sampling rate of '{inst.name}' exceeds 65535 (got '{inst.c2spd}')")
@@ -624,6 +630,61 @@ def build_cue_sheet(order_list: list, num_pats_s3m: int, num_channels: int,
return bytes(sheet)
def relocate_late_note_delays(patterns: list, order_list: list,
num_channels: int, initial_speed: int) -> None:
"""Move SDx-delayed notes to the next row when x ≥ tick speed.
ST3 triggers a Note Delay during the current row; if x reaches the tick
speed, the trigger never lands. When the next row in the same channel is
empty, relocate the note (with delay = x speed) so it actually plays.
"""
visited = set()
for order in order_list:
if order >= S3M_ORDER_END:
break
if order >= len(patterns) or order in visited:
continue
visited.add(order)
grid = patterns[order]
speed = initial_speed
for r in range(PATTERN_ROWS):
for ch in range(min(num_channels, len(grid))):
row = grid[ch][r]
if row.effect == EFF_A and row.effect_arg > 0:
speed = row.effect_arg
break
if r + 1 >= PATTERN_ROWS or speed <= 0:
continue
for ch in range(min(num_channels, len(grid))):
row = grid[ch][r]
if row.effect != EFF_S or row.note == S3M_NOTE_EMPTY:
continue
if ((row.effect_arg >> 4) & 0xF) != 0xD:
continue
x = row.effect_arg & 0xF
if x < speed:
continue
nxt = grid[ch][r + 1]
if (nxt.note != S3M_NOTE_EMPTY or nxt.inst or nxt.effect
or nxt.effect_arg or nxt.vol != -1):
continue
new_delay = x - speed
nxt.note = row.note
nxt.inst = row.inst
nxt.vol = row.vol
if new_delay > 0:
nxt.effect = EFF_S
nxt.effect_arg = 0xD0 | (new_delay & 0xF)
row.note = S3M_NOTE_EMPTY
row.inst = 0
row.vol = -1
row.effect = 0
row.effect_arg = 0
vprint(f" fix: pat{order} ch{ch} row{r}: SD{x:X} ≥ speed{speed}, "
f"moved note to row{r+1}"
+ (f" with SD{new_delay:X}" if new_delay > 0 else ""))
def find_initial_bpm_speed(patterns: list, order_list: list,
default_speed: int, default_tempo: int) -> tuple:
"""Scan first pattern in order for Axx/Txx in row 0 of any channel."""
@@ -668,6 +729,10 @@ def assemble_taud(h: S3MHeader, instruments: list, patterns: list) -> bytes:
resolve_st3_recalls(patterns, h.order_list, 32)
warn_st3_quirks(patterns, h.order_list, 32)
init_speed, _ = find_initial_bpm_speed(patterns, h.order_list,
h.initial_speed, h.initial_tempo)
relocate_late_note_delays(patterns, h.order_list, 32, init_speed)
# Build sample+instrument bin
vprint(" building sample/instrument bin…")
sampleinst_raw, _offsets = build_sample_inst_bin(instruments)