mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-08 06:14:04 +09:00
reflecting spec changes
This commit is contained in:
107
s3m2taud.py
107
s3m2taud.py
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user