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

@@ -10,7 +10,7 @@ const TAUD_MAGIC = [0x1F,0x54,0x53,0x56,0x4D,0x61,0x75,0x64] // \x1F TSV
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 SAMPLEINST_SIZE = 786432 // 770047 sample + 16384 instrument
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
const NUM_CUES = 1024
@@ -95,6 +95,7 @@ function uploadTaudFile(inFile, songIndex, playhead) {
// Write decompressed data to peripheral memory (backwards addressing:
// peripheral byte k lives at memBase - k).
for (let i = 0; i < SAMPLEINST_SIZE; i++) {
// TODO use sys.memcpy
sys.poke(memBase - i, sys.peek(decompPtr + i))
}
sys.free(decompPtr)

View File

@@ -47,7 +47,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,
@@ -976,10 +976,10 @@ def encode_note_it(it_note: int) -> int:
if it_note == IT_NOTE_CUT:
return NOTE_CUT
if 0 <= it_note <= 119:
# IT middle C is C-5 (note 60); Taud reference is C-3 (TAUD_C3 = 0x4000).
# IT C-5 anchors to Taud C-3, so offset = it_note - 60.
# IT middle C is C-5 (note 60); Taud reference is C-4 (TAUD_C4 = 0x5000).
# IT C-5 anchors to Taud C-4, so offset = it_note - 60.
semis = it_note - 60
val = round(TAUD_C3 + semis * 4096 / 12)
val = round(TAUD_C4 + semis * 4096 / 12)
return max(1, min(0xFFFD, val))
return NOTE_NOP
@@ -1375,14 +1375,18 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
s.length = n; s.loop_end = min(s.loop_end, n)
pos += n
# New 192-byte instrument layout (terranmon.txt:1997-2070).
# Vol env @ 21..70 (25 pts), Pan env @ 71..120 (25 pts), P/F env @ 121..170 (25 pts).
# Envelope flag bits (16-bit, 0b 0ut sssss pcb eeeee):
# bit 14=u(enable), 13=t(sustain), 12..8=sus_start, 7=p, 6=c(carry),
# 5=b(use envelope), 4..0=sus_end.
USE_ENV_BIT = 0x0020 # b
inst_bin = bytearray(INSTBIN_SIZE)
for i, s in enumerate(samples_or_proxy):
taud_idx = i # samples_or_proxy is 0-based here; slot 0 unused
if i == 0 or i >= 256 or s is None:
continue
ptr = offsets.get(i, 0)
ptr_lo = ptr & 0xFFFF
ptr_hi = ptr >> 16
ptr = offsets.get(i, 0) & 0xFFFFFFFF
s_len = min(s.length, 65535)
c2spd = min(s.c5_speed, 65535)
ls = min(s.loop_beg, 65535)
@@ -1393,44 +1397,67 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
loop_mode = 1 # forward loop
else:
loop_mode = 0 # no loop
flags_byte = (ptr_hi << 4) | (loop_mode & 0x3)
flags_byte = loop_mode & 0x3
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, 0)
struct.pack_into('<H', inst_bin, base + 8, ls)
struct.pack_into('<H', inst_bin, base + 10, le)
inst_bin[base + 12] = flags_byte
base = taud_idx * 192
struct.pack_into('<I', inst_bin, base + 0, ptr)
struct.pack_into('<H', inst_bin, base + 4, s_len)
struct.pack_into('<H', inst_bin, base + 6, c2spd)
struct.pack_into('<H', inst_bin, base + 8, 0) # play start
struct.pack_into('<H', inst_bin, base + 10, ls)
struct.pack_into('<H', inst_bin, base + 12, le)
inst_bin[base + 14] = flags_byte
# Write envelope data (12-point format: vol at +16..+39, pan at +40..+63)
# Write envelope data
env_data = envelopes_by_slot.get(taud_idx) if envelopes_by_slot else None
if env_data and env_data[0]:
vol_env, vol_sus, pan_env, pan_sus, inst_gv = env_data
inst_bin[base + 13] = vol_sus & 0xFF
inst_bin[base + 14] = pan_sus & 0xFF
inst_bin[base + 15] = inst_gv & 0xFF
for k, (val, mf) in enumerate(vol_env[:12]):
inst_bin[base + 16 + k*2] = val & 0xFF
inst_bin[base + 16 + k*2 + 1] = mf & 0xFF
# Old caller passed an 8-bit sustain byte (0b ut eee sss for 12-point indices).
# Convert to new 16-bit layout (5-bit sus indices in bits 12..8 / 4..0).
def _convert_old_sus(b: int, has_env: bool) -> int:
if not has_env:
return 0
sus_start = b & 0x07
sus_end = (b >> 3) & 0x07
t_bit = (b >> 6) & 0x01
u_bit = (b >> 7) & 0x01
out = USE_ENV_BIT
out |= (sus_start & 0x1F) << 8
out |= (sus_end & 0x1F)
out |= (t_bit << 13)
out |= (u_bit << 14)
return out
vol_flags = _convert_old_sus(vol_sus, True)
pan_flags = _convert_old_sus(pan_sus, bool(pan_env))
struct.pack_into('<H', inst_bin, base + 15, vol_flags)
struct.pack_into('<H', inst_bin, base + 17, pan_flags)
struct.pack_into('<H', inst_bin, base + 19, 0) # pitch/filter env unused
inst_bin[base + 171] = inst_gv & 0xFF
for k, (val, mf) in enumerate(vol_env[:25]):
inst_bin[base + 21 + k*2] = val & 0xFF
inst_bin[base + 21 + k*2 + 1] = mf & 0xFF
if pan_env:
for k, (val, mf) in enumerate(pan_env[:12]):
inst_bin[base + 40 + k*2] = val & 0xFF
inst_bin[base + 40 + k*2 + 1] = mf & 0xFF
for k, (val, mf) in enumerate(pan_env[:25]):
inst_bin[base + 71 + k*2] = val & 0xFF
inst_bin[base + 71 + k*2 + 1] = mf & 0xFF
else:
for k in range(12):
inst_bin[base + 40 + k*2] = 0x80 # pan centre
inst_bin[base + 40 + k*2 + 1] = 0x00 # hold
for k in range(25):
inst_bin[base + 71 + k*2] = 0x80 # pan centre
inst_bin[base + 71 + k*2 + 1] = 0x00 # hold
else:
# No instrument envelope: single-point vol, neutral pan, full gv
# No instrument envelope: single-point vol, neutral pan, full gv.
inst_gv = env_data[4] if env_data else 255
inst_bin[base + 15] = inst_gv & 0xFF
inst_bin[base + 16] = min(s.vol, 63) # value 0-63
inst_bin[base + 17] = 0 # offset 0 = hold
for k in range(12):
inst_bin[base + 40 + k*2] = 0x80 # pan centre
inst_bin[base + 40 + k*2 + 1] = 0x00 # hold
struct.pack_into('<H', inst_bin, base + 15, USE_ENV_BIT)
struct.pack_into('<H', inst_bin, base + 17, 0)
struct.pack_into('<H', inst_bin, base + 19, 0)
inst_bin[base + 171] = inst_gv & 0xFF
inst_bin[base + 21] = min(s.vol, 63) # value 0-63
inst_bin[base + 22] = 0 # offset 0 = hold
for k in range(25):
inst_bin[base + 71 + k*2] = 0x80
inst_bin[base + 71 + k*2 + 1] = 0x00
vprint(f" instrument[{taud_idx}] '{s.name}' ptr:{ptr} c5spd:{s.c5_speed}")
return bytes(sample_bin) + bytes(inst_bin), offsets
@@ -1553,6 +1580,68 @@ def build_pattern_it(chunk_grid: list, ch_idx: int, default_pan: int,
# ── Main assembly ─────────────────────────────────────────────────────────────
def relocate_late_note_delays(patterns_rows: list, order_list: list,
num_channels: int, initial_speed: int) -> None:
"""Move SDx-delayed notes to the next row when x ≥ tick speed.
IT 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 >= IT_ORD_END:
break
if order >= len(patterns_rows) or order in visited:
continue
visited.add(order)
grid, rows = patterns_rows[order]
speed = initial_speed
for r in range(rows):
for ch in range(min(num_channels, len(grid))):
cell = grid[ch][r]
if cell.effect == EFF_A and cell.effect_arg > 0:
speed = cell.effect_arg
break
if r + 1 >= rows or speed <= 0:
continue
for ch in range(min(num_channels, len(grid))):
cell = grid[ch][r]
if cell.effect != EFF_S or cell.note < 0:
continue
if ((cell.effect_arg >> 4) & 0xF) != 0xD:
continue
x = cell.effect_arg & 0xF
if x < speed:
continue
nxt = grid[ch][r + 1]
if (nxt.note >= 0 or nxt.inst or nxt.effect or nxt.effect_arg
or nxt.vol != -1 or nxt.volcol != -1
or nxt.pan_set is not None or nxt.aux_effect is not None):
continue
new_delay = x - speed
nxt.note = cell.note
nxt.inst = cell.inst
nxt.vol = cell.vol
nxt.volcol = cell.volcol
nxt.pan_set = cell.pan_set
nxt.aux_effect = cell.aux_effect
if new_delay > 0:
nxt.effect = EFF_S
nxt.effect_arg = 0xD0 | (new_delay & 0xF)
cell.note = -1
cell.inst = 0
cell.vol = -1
cell.volcol = -1
cell.pan_set = None
cell.aux_effect = None
cell.effect = 0
cell.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_rows: list, order_list: list,
default_speed: int, default_tempo: int) -> tuple:
speed = default_speed or 6
@@ -1604,6 +1693,10 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
old_effects=h.old_effects,
initial_global_vol=h.global_vol)
init_speed, _ = find_initial_bpm_speed(patterns_rows, h.order_list,
h.initial_speed, h.initial_tempo)
relocate_late_note_delays(patterns_rows, h.order_list, 64, init_speed)
# ── Check SBx chunk crossing (warn only) ─────────────────────────────────
for pi, (grid, rows) in enumerate(patterns_rows):
if rows <= PATTERN_ROWS: continue

View File

@@ -7,7 +7,7 @@ Usage:
Limits:
- Up to 20 MOD 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.
@@ -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,
@@ -64,7 +64,7 @@ PT_MEM_E_SUB = frozenset({0x1, 0x2, 0xA, 0xB})
SIGNATURE = b"mod2taud/TSVM " # 14 bytes
# PT period 428 (PT "C-2") corresponds to OpenMPT/IT C-4 which s3m2taud
# anchors to Taud C3 (0x4000). We use the same anchor so MOD/S3M imports
# anchors to Taud C4 (0x5000). We use the same anchor so MOD/S3M imports
# share a pitch reference.
PT_REFERENCE_PERIOD = 428.0
@@ -224,7 +224,7 @@ def _signed4(nibble: int) -> int:
def period_to_taud_note(period: int) -> int:
if period <= 0:
return NOTE_NOP
val = round(TAUD_C3 + 4096.0 * math.log2(PT_REFERENCE_PERIOD / period))
val = round(TAUD_C4 + 4096.0 * math.log2(PT_REFERENCE_PERIOD / period))
return max(1, min(0xFFFD, val))
@@ -350,6 +350,61 @@ def encode_effect(cmd: int, arg: int, ch: int = 0, row: int = 0) -> tuple:
return (TOP_NONE, 0, None, None)
def relocate_late_note_delays(patterns: list, order_list: list,
n_channels: int, initial_speed: int) -> None:
"""Move EDx-delayed notes to the next row when x ≥ tick speed.
PT 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 >= 0xFF:
break
if order >= len(patterns) or order in visited:
continue
visited.add(order)
grid = patterns[order]
speed = initial_speed
for r in range(MOD_PATTERN_ROWS):
for ch in range(min(n_channels, len(grid))):
row = grid[ch][r]
if row.effect == 0xF and 0 < row.effect_arg < 0x20:
speed = row.effect_arg
break
if r + 1 >= MOD_PATTERN_ROWS or speed <= 0:
continue
for ch in range(min(n_channels, len(grid))):
row = grid[ch][r]
if row.effect != 0xE or row.period == 0:
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.period or nxt.inst or nxt.effect or nxt.effect_arg
or nxt.vol_set != -1):
continue
new_delay = x - speed
nxt.period = row.period
nxt.inst = row.inst
nxt.vol_set = row.vol_set
if new_delay > 0:
nxt.effect = 0xE
nxt.effect_arg = 0xD0 | (new_delay & 0xF)
row.period = 0
row.inst = 0
row.effect = 0
row.effect_arg = 0
row.vol_set = -1
vprint(f" fix: pat{order} ch{ch} row{r}: ED{x:X} ≥ speed{speed}, "
f"moved note to row{r+1}"
+ (f" with ED{new_delay:X}" if new_delay > 0 else ""))
def resolve_pt_recalls(patterns: list, order_list: list, n_channels: int) -> None:
"""In-place: replace PT zero-arg recalls with each effect's last non-zero arg.
@@ -427,6 +482,7 @@ def build_sample_inst_bin(samples: list) -> tuple:
s.loop_end = min(s.loop_end, n)
pos += n
# New 192-byte instrument layout (terranmon.txt:1997-2070).
inst_bin = bytearray(INSTBIN_SIZE)
for i, s in enumerate(samples):
taud_idx = i + 1 # 1-based instrument number
@@ -434,29 +490,31 @@ def build_sample_inst_bin(samples: list) -> tuple:
break
if not s.sample_data:
continue
ptr = offsets.get(i, 0)
ptr_lo = ptr & 0xFFFF
ptr_hi = (ptr >> 16)
ptr = offsets.get(i, 0) & 0xFFFFFFFF
s_len = min(s.length, 65535)
c2spd = min(s.c2spd, 65535)
ps = 0
ls = min(s.loop_begin, 65535)
le = min(s.loop_end, 65535)
loop_mode = 1 if (s.flags & 1) else 0
flags_byte = (ptr_hi << 4) | (loop_mode & 0x3)
flags_byte = loop_mode & 0x3
env_vol = min(s.volume, 63)
vol_env_flags = 0x0020 # use-envelope bit
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 # global volume — full
env_vol = min(s.volume, 63)
inst_bin[base + 16] = env_vol # envelope hold value
inst_bin[base + 17] = 0 # offset minifloat = 0 → hold
base = taud_idx * 192
struct.pack_into('<I', inst_bin, base + 0, ptr)
struct.pack_into('<H', inst_bin, base + 4, s_len)
struct.pack_into('<H', inst_bin, base + 6, c2spd)
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)
struct.pack_into('<H', inst_bin, base + 19, 0)
inst_bin[base + 21] = env_vol
inst_bin[base + 22] = 0
inst_bin[base + 171] = 0xFF
vprint(f" instrument[{taud_idx}] '{s.name}' ptr={ptr} c2spd={s.c2spd} "
f"vol={s.volume} loop=({ls},{le},{'on' if loop_mode else 'off'})")
@@ -626,6 +684,9 @@ def assemble_taud(mod: dict) -> bytes:
vprint(" resolving PT per-effect recalls…")
resolve_pt_recalls(patterns, order_list, n_channels)
init_speed, _ = find_initial_bpm_speed(patterns, order_list)
relocate_late_note_delays(patterns, order_list, n_channels, init_speed)
vprint(" building sample/instrument bin…")
sampleinst_raw, _offsets = build_sample_inst_bin(samples)
assert len(sampleinst_raw) == SAMPLEINST_SIZE

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)

View File

@@ -30,8 +30,8 @@ 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)
SAMPLEBIN_SIZE = 770048
INSTBIN_SIZE = 16384 # 256 instruments × 64 bytes
SAMPLEBIN_SIZE = 737280
INSTBIN_SIZE = 49152 # 256 instruments × 192 bytes
SAMPLEINST_SIZE = SAMPLEBIN_SIZE + INSTBIN_SIZE
PATTERN_ROWS = 64
PATTERN_BYTES = PATTERN_ROWS * 8 # 512
@@ -44,7 +44,7 @@ NUM_VOICES = 20
NOTE_NOP = 0xFFFF
NOTE_KEYOFF = 0x0000
NOTE_CUT = 0xFFFE
TAUD_C3 = 0x4000
TAUD_C4 = 0x5000 # reference C for instrument sampling rate (was TAUD_C3 = 0x4000)
# Taud effect opcodes (base-36: 0..9 → 0x00..0x09, A..Z → 0x0A..0x23)
TOP_NONE = 0x00

View File

@@ -110,10 +110,10 @@ class AudioJSR223Delegate(private val vm: VM) {
}
}
/** Upload 64 bytes defining instrument `slot` (0-255). */
/** Upload up to 192 bytes defining instrument `slot` (0-255). */
fun uploadInstrument(slot: Int, bytes: IntArray) {
getFirstSnd()?.instruments?.get(slot and 0xFF)?.let { inst ->
for (i in 0 until minOf(64, bytes.size)) inst.setByte(i, bytes[i] and 0xFF)
for (i in 0 until minOf(192, bytes.size)) inst.setByte(i, bytes[i] and 0xFF)
}
}

View File

@@ -124,16 +124,17 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
internal val DBGPRN = false
const val SAMPLING_RATE = 32000
const val TRACKER_CHUNK = 512
const val TRACKER_C3 = 0x4000
// Amiga period at TRACKER_C3 for a standard 8363 Hz instrument (NTSC clock 3579545 Hz).
// Used to implement Amiga-mode pitch slides (effect '1' f-bit or song-table flag).
const val AMIGA_BASE_PERIOD = 214.0
const val TRACKER_C3 = 0x4000 // legacy alias (one octave below the new reference)
const val TRACKER_C4 = 0x5000 // reference C for instrument samplingRate (terranmon.txt:2000)
// Amiga period at TRACKER_C4 for a standard 8363 Hz instrument (NTSC clock 3579545 Hz).
// Reference shifted from C3→C4 (one octave up), so the period halves: 214 → 107.
const val AMIGA_BASE_PERIOD = 107.0
// Scale factor that converts a Taud coarse-slide unit back to one Amiga period unit.
// Taud coarse unit = round(ST3_unit × 64/3), so the inverse is × 3/64.
const val AMIGA_PERIOD_SCALE = 3.0 / 64.0
}
internal val sampleBin = UnsafeHelper.allocate(770048L, this)
internal val sampleBin = UnsafeHelper.allocate(737280L, this)
internal val instruments = Array(256) { TaudInst(it) }
internal val playdata = Array(4096) { Array(64) { TaudPlayData(0xFFFF, 0, 0, 0, 32, 0, 0, 0) } }
internal val playheads: Array<Playhead>
@@ -305,8 +306,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
override fun peek(addr: Long): Byte {
return when (val adi = addr.toInt()) {
in 0..770047 -> sampleBin[addr]
in 770048..786431 -> (adi - 770048).let { instruments[it / 64].getByte(it % 64) }
in 0..737279 -> sampleBin[addr]
in 737280..786431 -> (adi - 737280).let { instruments[it / 192].getByte(it % 192) }
in 786432..851967 -> { val off = adi - 786432; playdata[playheads[0].patBank1 * 128 + off / 512][(off % 512) / 8].getByte(off % 8) }
in 851968..917503 -> { val off = adi - 851968; playdata[playheads[0].patBank2 * 128 + off / 512][(off % 512) / 8].getByte(off % 8) }
in 917504..983039 -> tadInputBin[addr - 917504] // TAD input buffer (65536 bytes)
@@ -319,8 +320,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
val adi = addr.toInt()
val bi = byte.toUint()
when (adi) {
in 0..770047 -> { sampleBin[addr] = byte }
in 770048..786431 -> (adi - 770048).let { instruments[it / 64].setByte(it % 64, bi) }
in 0..737279 -> { sampleBin[addr] = byte }
in 737280..786431 -> (adi - 737280).let { instruments[it / 192].setByte(it % 192, bi) }
in 786432..851967 -> { val off = adi - 786432; playdata[playheads[0].patBank1 * 128 + off / 512][(off % 512) / 8].setByte(off % 8, bi) }
in 851968..917503 -> { val off = adi - 851968; playdata[playheads[0].patBank2 * 128 + off / 512][(off % 512) / 8].setByte(off % 8, bi) }
in 917504..983039 -> tadInputBin[addr - 917504] = byte // TAD input buffer
@@ -1170,56 +1171,65 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
}
private fun computePlaybackRate(inst: TaudInst, noteVal: Int): Double =
inst.samplingRate.toDouble() / SAMPLING_RATE * 2.0.pow((noteVal - TRACKER_C3) / 4096.0)
inst.samplingRate.toDouble() / SAMPLING_RATE * 2.0.pow((noteVal - TRACKER_C4) / 4096.0)
// Applies one tick of Amiga-mode pitch slide. slideArg uses the same sign convention as
// linear mode: negative = pitch down (E effect), positive = pitch up (F effect).
// The Taud coarse-slide value is converted back to Amiga period units via AMIGA_PERIOD_SCALE.
private fun amigaSlide(noteVal: Int, slideArg: Int): Int {
val period = AMIGA_BASE_PERIOD * 2.0.pow(-(noteVal - TRACKER_C3).toDouble() / 4096.0)
val period = AMIGA_BASE_PERIOD * 2.0.pow(-(noteVal - TRACKER_C4).toDouble() / 4096.0)
// Negate slideArg: pitch down (slideArg < 0) → period up, pitch up (slideArg > 0) → period down.
val newPeriod = (period - slideArg * AMIGA_PERIOD_SCALE).coerceAtLeast(1.0)
return (TRACKER_C3 + 4096.0 * log2(AMIGA_BASE_PERIOD / newPeriod)).roundToInt()
return (TRACKER_C4 + 4096.0 * log2(AMIGA_BASE_PERIOD / newPeriod)).roundToInt()
}
private fun advanceEnvelope(voice: Voice, inst: TaudInst, tickSec: Double) {
// Volume envelope
// sustain byte: bit7=enable (u), bit6=sustain (t: 1=breaks on key-off,
// 0=loops forever), bits[5:3]=end_idx, bits[2:0]=start_idx
val vSus = inst.volEnvSustain
val vEnabled = (vSus and 0x80) != 0
val vIsSustain = (vSus and 0x40) != 0
// Loop is "active" when enabled AND (it's a forever-loop OR key not yet released)
val vSusOn = vEnabled && (!vIsSustain || !voice.keyOff)
val vSusStart = vSus and 7
val vSusEnd = (vSus ushr 3) and 7
// 16-bit envelope-flag layout (terranmon.txt:2007-2030):
// 0b 0ut sssss pcb eeeee
// bit 14 = u (enable sustain/loop)
// bit 13 = t (sustain — 1=breaks on key-off, 0=loops forever)
// bits 12..8 = sustain/loop start index (0..24)
// bit 7 = p (channel-specific flag — fadeout zero / use default pan)
// bit 6 = c (envelope carry)
// bit 5 = b (use envelope at all)
// bits 4..0 = sustain/loop end index (0..24)
val maxIdx = 24
if (vSusOn && voice.envIndex == vSusEnd && vSusStart == vSusEnd) {
// slb == sle: hold at this node until key-off (no cycling)
voice.envVolume = (inst.volEnvelopes[voice.envIndex].value / 63.0).coerceIn(0.0, 1.0)
} else if (vSusOn && voice.envIndex == vSusEnd) {
// At sustain-loop end: snap back to start regardless of stored offset.
voice.envTimeSec = 0.0
voice.envIndex = vSusStart
voice.envVolume = (inst.volEnvelopes[voice.envIndex].value / 63.0).coerceIn(0.0, 1.0)
} else if (voice.envIndex >= 11) {
voice.envVolume = (inst.volEnvelopes[11].value / 63.0).coerceIn(0.0, 1.0)
} else {
val vOffset = inst.volEnvelopes[voice.envIndex].offset.toDouble()
if (vOffset == 0.0) {
// Volume envelope
val vSus = inst.volEnvSustain
val vUseEnv = (vSus ushr 5) and 1 != 0
if (vUseEnv) {
val vEnabled = (vSus ushr 14) and 1 != 0
val vIsSustain = (vSus ushr 13) and 1 != 0
val vSusOn = vEnabled && (!vIsSustain || !voice.keyOff)
val vSusStart = (vSus ushr 8) and 0x1F
val vSusEnd = vSus and 0x1F
if (vSusOn && voice.envIndex == vSusEnd && vSusStart == vSusEnd) {
voice.envVolume = (inst.volEnvelopes[voice.envIndex].value / 63.0).coerceIn(0.0, 1.0)
} else if (vSusOn && voice.envIndex == vSusEnd) {
voice.envTimeSec = 0.0
voice.envIndex = vSusStart
voice.envVolume = (inst.volEnvelopes[voice.envIndex].value / 63.0).coerceIn(0.0, 1.0)
} else if (voice.envIndex >= maxIdx) {
voice.envVolume = (inst.volEnvelopes[maxIdx].value / 63.0).coerceIn(0.0, 1.0)
} else {
voice.envTimeSec += tickSec
if (voice.envTimeSec >= vOffset) {
voice.envTimeSec -= vOffset
val nextIdx = if (vSusOn && voice.envIndex == vSusEnd) vSusStart
else (voice.envIndex + 1).coerceAtMost(11)
voice.envIndex = nextIdx
val vOffset = inst.volEnvelopes[voice.envIndex].offset.toDouble()
if (vOffset == 0.0) {
voice.envVolume = (inst.volEnvelopes[voice.envIndex].value / 63.0).coerceIn(0.0, 1.0)
} else {
val cur = (inst.volEnvelopes[voice.envIndex].value / 63.0).coerceIn(0.0, 1.0)
val nxt = (inst.volEnvelopes[(voice.envIndex + 1).coerceAtMost(11)].value / 63.0).coerceIn(0.0, 1.0)
voice.envVolume = cur + (nxt - cur) * (voice.envTimeSec / vOffset)
voice.envTimeSec += tickSec
if (voice.envTimeSec >= vOffset) {
voice.envTimeSec -= vOffset
val nextIdx = if (vSusOn && voice.envIndex == vSusEnd) vSusStart
else (voice.envIndex + 1).coerceAtMost(maxIdx)
voice.envIndex = nextIdx
voice.envVolume = (inst.volEnvelopes[voice.envIndex].value / 63.0).coerceIn(0.0, 1.0)
} else {
val cur = (inst.volEnvelopes[voice.envIndex].value / 63.0).coerceIn(0.0, 1.0)
val nxt = (inst.volEnvelopes[(voice.envIndex + 1).coerceAtMost(maxIdx)].value / 63.0).coerceIn(0.0, 1.0)
voice.envVolume = cur + (nxt - cur) * (voice.envTimeSec / vOffset)
}
}
}
}
@@ -1227,23 +1237,22 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// Pan envelope (only when active for this instrument)
if (!voice.hasPanEnv) return
val pSus = inst.panEnvSustain
val pEnabled = (pSus and 0x80) != 0
val pIsSustain = (pSus and 0x40) != 0
val pUseEnv = (pSus ushr 5) and 1 != 0
if (!pUseEnv) return
val pEnabled = (pSus ushr 14) and 1 != 0
val pIsSustain = (pSus ushr 13) and 1 != 0
val pSusOn = pEnabled && (!pIsSustain || !voice.keyOff)
val pSusStart = pSus and 7
val pSusEnd = (pSus ushr 3) and 7
val pSusStart = (pSus ushr 8) and 0x1F
val pSusEnd = pSus and 0x1F
if (pSusOn && voice.envPanIndex == pSusEnd && pSusStart == pSusEnd) {
// slb == sle: hold at this pan node until key-off
voice.envPan = inst.panEnvelopes[voice.envPanIndex].value / 255.0
} else if (pSusOn && voice.envPanIndex == pSusEnd) {
// At sustain-loop end: snap back to start regardless of stored offset
// (encoder writes mf=0 on the last node by convention).
voice.envPanTimeSec = 0.0
voice.envPanIndex = pSusStart
voice.envPan = inst.panEnvelopes[voice.envPanIndex].value / 255.0
} else if (voice.envPanIndex >= 11) {
voice.envPan = inst.panEnvelopes[11].value / 255.0
} else if (voice.envPanIndex >= maxIdx) {
voice.envPan = inst.panEnvelopes[maxIdx].value / 255.0
} else {
val pOffset = inst.panEnvelopes[voice.envPanIndex].offset.toDouble()
if (pOffset == 0.0) {
@@ -1253,12 +1262,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
if (voice.envPanTimeSec >= pOffset) {
voice.envPanTimeSec -= pOffset
val nextIdx = if (pSusOn && voice.envPanIndex == pSusEnd) pSusStart
else (voice.envPanIndex + 1).coerceAtMost(11)
else (voice.envPanIndex + 1).coerceAtMost(maxIdx)
voice.envPanIndex = nextIdx
voice.envPan = inst.panEnvelopes[voice.envPanIndex].value / 255.0
} else {
val cur = inst.panEnvelopes[voice.envPanIndex].value / 255.0
val nxt = inst.panEnvelopes[(voice.envPanIndex + 1).coerceAtMost(11)].value / 255.0
val nxt = inst.panEnvelopes[(voice.envPanIndex + 1).coerceAtMost(maxIdx)].value / 255.0
voice.envPan = cur + (nxt - cur) * (voice.envPanTimeSec / pOffset)
}
}
@@ -1272,7 +1281,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
val sampleLen = inst.sampleLength.coerceAtLeast(1)
val loopStart = inst.sampleLoopStart.toDouble()
val loopEnd = inst.sampleLoopEnd.toDouble().coerceAtLeast(1.0)
val binMax = 770047 // sampleBin is 770048 bytes (0..770047)
val binMax = 737279 // sampleBin is 737280 bytes (0..737279)
val i0 = voice.samplePos.toInt().coerceIn(0, sampleLen - 1)
val i1 = (i0 + 1).coerceAtMost(sampleLen - 1)
@@ -1323,7 +1332,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
voice.envPanIndex = 0
voice.envPanTimeSec = 0.0
voice.envPan = inst.panEnvelopes[0].value / 255.0
voice.hasPanEnv = inst.panEnvelopes.any { it.offset.toFloat() > 0.0f }
// Pan envelope is active when the `b` (use envelope) flag is set in panEnvSustain.
voice.hasPanEnv = (inst.panEnvSustain ushr 5) and 1 != 0
voice.noteVal = noteVal
voice.basePitch = noteVal
voice.playbackRate = computePlaybackRate(inst, noteVal)
@@ -2322,26 +2332,75 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
}
data class TaudInstEnvPoint(var value: Int, var offset: ThreeFiveMiniUfloat)
/**
* 192-byte instrument record (terranmon.txt:1997-2070).
* Layout:
* 0..3 u32 sample pointer
* 4..5 u16 sample length
* 6..7 u16 sampling rate at TRACKER_C4 (0x5000)
* 8..9 u16 play start
* 10..11 u16 loop start
* 12..13 u16 loop end
* 14 u8 sample flags (low 2 bits = loop mode 0..3)
* 15..16 u16 volume envelope flags (0b 0ut sssss pcb eeeee)
* 17..18 u16 panning envelope flags
* 19..20 u16 pitch/filter envelope flags
* 21..70 Bit16×25 volume envelope points (value 0x00-0x3F + minifloat dt)
* 71..120 Bit16×25 panning envelope points (value 0x00-0xFF, 0x80=centre)
* 121..170 Bit16×25 pitch/filter envelope points
* 171 u8 instrument global volume
* 172 u8 volume fadeout low bits
* 173 u8 fadeout high (low nibble) + vibrato depth (high nibble)
* 174 u8 volume swing
* 175 u8 vibrato speed
* 176 u8 vibrato sweep
* 177 u8 default pan
* 178..179 u16 pitch-pan centre (4096-TET)
* 180 s8 pitch-pan separation
* 181 u8 pan swing
* 182 u8 default cutoff
* 183 u8 default resonance
* 184..191 byte[8] reserved
*/
data class TaudInst(
var index: Int,
var samplePtr: Int, // 20-bit number
var samplePtr: Int, // 32-bit sample bin offset
var sampleLength: Int,
var samplingRate: Int,
var samplingRate: Int, // rate at TRACKER_C4
var samplePlayStart: Int,
var sampleLoopStart: Int,
var sampleLoopEnd: Int,
// flags
var loopMode: Int,
var volEnvSustain: Int, // byte 13: ut eee sss (u=enable, t=sustain (1=breaks on key-off, 0=loops forever))
var panEnvSustain: Int, // byte 14: ut eee sss (u=enable, t=sustain (1=breaks on key-off, 0=loops forever))
var instGlobalVolume: Int, // byte 15: instrument global volume (0..255, 255 = unity)
var volEnvelopes: Array<TaudInstEnvPoint>, // 12 points, value 0x00-0x3F
var panEnvelopes: Array<TaudInstEnvPoint> // 12 points, value 0x00-0xFF (0x80 = centre)
var loopMode: Int, // byte 14, low 2 bits
var volEnvSustain: Int, // bytes 15-16 (16-bit, see flag layout)
var panEnvSustain: Int, // bytes 17-18
var pfEnvSustain: Int, // bytes 19-20 (pitch/filter)
var instGlobalVolume: Int, // byte 171
var volEnvelopes: Array<TaudInstEnvPoint>, // 25 points
var panEnvelopes: Array<TaudInstEnvPoint>, // 25 points
var pfEnvelopes: Array<TaudInstEnvPoint>, // 25 points (pitch/filter)
var volumeFadeoutLow: Int, // byte 172
var fadeoutHighVibDepth: Int, // byte 173
var volumeSwing: Int, // byte 174
var vibratoSpeed: Int, // byte 175
var vibratoSweep: Int, // byte 176
var defaultPan: Int, // byte 177
var pitchPanCentre: Int, // bytes 178-179
var pitchPanSeparation: Int, // byte 180 (signed)
var panSwing: Int, // byte 181
var defaultCutoff: Int, // byte 182
var defaultResonance: Int // byte 183
) {
constructor(index: Int) : this(index, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF,
Array(12) { TaudInstEnvPoint(0x3F, ThreeFiveMiniUfloat(0)) },
Array(12) { TaudInstEnvPoint(0x80, ThreeFiveMiniUfloat(0)) })
constructor(index: Int) : this(
index, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF,
Array(25) { TaudInstEnvPoint(0x3F, ThreeFiveMiniUfloat(0)) },
Array(25) { TaudInstEnvPoint(0x80, ThreeFiveMiniUfloat(0)) },
Array(25) { TaudInstEnvPoint(0x80, ThreeFiveMiniUfloat(0)) },
0, 0, 0, 0, 0, 0x80, 0x5000, 0, 0, 0xFF, 0
)
// Reserved padding at offsets 184..191 (8 bytes per instrument).
private val reserved = ByteArray(8)
// Funk repeat (S$Fx00) bit-mask — non-destructive XOR overlay across the loop region.
// Lazily allocated; a 1-bit flips the byte, a 0-bit leaves it intact.
@@ -2359,68 +2418,116 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
return (mask[idx / 8].toInt() ushr (idx and 7)) and 1 != 0
}
private fun envPointGet(env: Array<TaudInstEnvPoint>, base: Int, offset: Int): Byte {
val rel = offset - base
val pt = env[rel / 2]
return if (rel and 1 == 0) pt.value.toByte() else pt.offset.index.toByte()
}
private fun envPointSet(env: Array<TaudInstEnvPoint>, base: Int, offset: Int, byte: Int) {
val rel = offset - base
val pt = env[rel / 2]
if (rel and 1 == 0) pt.value = byte
else pt.offset = ThreeFiveMiniUfloat(byte)
}
fun getByte(offset: Int): Byte = when (offset) {
0 -> samplePtr.toByte()
1 -> samplePtr.ushr(8).toByte()
2 -> samplePtr.ushr(16).toByte()
3 -> samplePtr.ushr(24).toByte()
2 -> sampleLength.toByte()
3 -> sampleLength.ushr(8).toByte()
4 -> sampleLength.toByte()
5 -> sampleLength.ushr(8).toByte()
4 -> samplingRate.toByte()
5 -> samplingRate.ushr(8).toByte()
6 -> samplingRate.toByte()
7 -> samplingRate.ushr(8).toByte()
6 -> samplePlayStart.toByte()
7 -> samplePlayStart.ushr(8).toByte()
8 -> samplePlayStart.toByte()
9 -> samplePlayStart.ushr(8).toByte()
8 -> sampleLoopStart.toByte()
9 -> sampleLoopStart.ushr(8).toByte()
10 -> sampleLoopStart.toByte()
11 -> sampleLoopStart.ushr(8).toByte()
10 -> sampleLoopEnd.toByte()
11 -> sampleLoopEnd.ushr(8).toByte()
12 -> sampleLoopEnd.toByte()
13 -> sampleLoopEnd.ushr(8).toByte()
12 -> (samplePtr.ushr(16).and(15).shl(4) or loopMode.and(3)).toByte()
13 -> volEnvSustain.toByte()
14 -> panEnvSustain.toByte()
15 -> instGlobalVolume.toByte()
in 16..38 step 2 -> volEnvelopes[(offset - 16) / 2].value.toByte()
in 17..39 step 2 -> volEnvelopes[(offset - 17) / 2].offset.index.toByte()
in 40..62 step 2 -> panEnvelopes[(offset - 40) / 2].value.toByte()
in 41..63 step 2 -> panEnvelopes[(offset - 41) / 2].offset.index.toByte()
14 -> (loopMode and 3).toByte()
15 -> volEnvSustain.toByte()
16 -> volEnvSustain.ushr(8).toByte()
17 -> panEnvSustain.toByte()
18 -> panEnvSustain.ushr(8).toByte()
19 -> pfEnvSustain.toByte()
20 -> pfEnvSustain.ushr(8).toByte()
in 21..70 -> envPointGet(volEnvelopes, 21, offset)
in 71..120 -> envPointGet(panEnvelopes, 71, offset)
in 121..170 -> envPointGet(pfEnvelopes, 121, offset)
171 -> instGlobalVolume.toByte()
172 -> volumeFadeoutLow.toByte()
173 -> fadeoutHighVibDepth.toByte()
174 -> volumeSwing.toByte()
175 -> vibratoSpeed.toByte()
176 -> vibratoSweep.toByte()
177 -> defaultPan.toByte()
178 -> pitchPanCentre.toByte()
179 -> pitchPanCentre.ushr(8).toByte()
180 -> pitchPanSeparation.toByte()
181 -> panSwing.toByte()
182 -> defaultCutoff.toByte()
183 -> defaultResonance.toByte()
in 184..191 -> reserved[offset - 184]
else -> throw InternalError("Bad offset $offset")
}
fun setByte(offset: Int, byte: Int) = when (offset) {
0 -> { samplePtr = (samplePtr and 0xfff00) or byte }
1 -> { samplePtr = (samplePtr and 0x000ff) or (byte shl 8) }
0 -> { samplePtr = (samplePtr and 0xFFFFFF00.toInt()) or byte }
1 -> { samplePtr = (samplePtr and 0xFFFF00FF.toInt()) or (byte shl 8) }
2 -> { samplePtr = (samplePtr and 0xFF00FFFF.toInt()) or (byte shl 16) }
3 -> { samplePtr = (samplePtr and 0x00FFFFFF) or (byte shl 24) }
2 -> { sampleLength = (sampleLength and 0xff00) or byte }
3 -> { sampleLength = (sampleLength and 0x00ff) or (byte shl 8) }
4 -> { sampleLength = (sampleLength and 0xff00) or byte }
5 -> { sampleLength = (sampleLength and 0x00ff) or (byte shl 8) }
4 -> { samplingRate = (samplingRate and 0xff00) or byte }
5 -> { samplingRate = (samplingRate and 0x00ff) or (byte shl 8) }
6 -> { samplingRate = (samplingRate and 0xff00) or byte }
7 -> { samplingRate = (samplingRate and 0x00ff) or (byte shl 8) }
6 -> { samplePlayStart = (samplePlayStart and 0xff00) or byte }
7 -> { samplePlayStart = (samplePlayStart and 0x00ff) or (byte shl 8) }
8 -> { samplePlayStart = (samplePlayStart and 0xff00) or byte }
9 -> { samplePlayStart = (samplePlayStart and 0x00ff) or (byte shl 8) }
8 -> { sampleLoopStart = (sampleLoopStart and 0xff00) or byte }
9 -> { sampleLoopStart = (sampleLoopStart and 0x00ff) or (byte shl 8) }
10 -> { sampleLoopStart = (sampleLoopStart and 0xff00) or byte }
11 -> { sampleLoopStart = (sampleLoopStart and 0x00ff) or (byte shl 8) }
10 -> { sampleLoopEnd = (sampleLoopEnd and 0xff00) or byte }
11 -> { sampleLoopEnd = (sampleLoopEnd and 0x00ff) or (byte shl 8) }
12 -> { sampleLoopEnd = (sampleLoopEnd and 0xff00) or byte }
13 -> { sampleLoopEnd = (sampleLoopEnd and 0x00ff) or (byte shl 8) }
12 -> {
samplePtr = if (byte and 0b1111_0000 != 0) samplePtr or ((byte ushr 4) shl 16)
else samplePtr and 0x0ffff
loopMode = byte and 3
}
13 -> { volEnvSustain = byte }
14 -> { panEnvSustain = byte }
15 -> { instGlobalVolume = byte and 0xFF }
14 -> { loopMode = byte and 3 }
15 -> { volEnvSustain = (volEnvSustain and 0xff00) or byte }
16 -> { volEnvSustain = (volEnvSustain and 0x00ff) or (byte shl 8) }
17 -> { panEnvSustain = (panEnvSustain and 0xff00) or byte }
18 -> { panEnvSustain = (panEnvSustain and 0x00ff) or (byte shl 8) }
19 -> { pfEnvSustain = (pfEnvSustain and 0xff00) or byte }
20 -> { pfEnvSustain = (pfEnvSustain and 0x00ff) or (byte shl 8) }
in 16..38 step 2 -> volEnvelopes[(offset - 16) / 2].value = byte
in 17..39 step 2 -> volEnvelopes[(offset - 17) / 2].offset = ThreeFiveMiniUfloat(byte)
in 40..62 step 2 -> panEnvelopes[(offset - 40) / 2].value = byte
in 41..63 step 2 -> panEnvelopes[(offset - 41) / 2].offset = ThreeFiveMiniUfloat(byte)
in 21..70 -> envPointSet(volEnvelopes, 21, offset, byte)
in 71..120 -> envPointSet(panEnvelopes, 71, offset, byte)
in 121..170 -> envPointSet(pfEnvelopes, 121, offset, byte)
171 -> { instGlobalVolume = byte and 0xFF }
172 -> { volumeFadeoutLow = byte and 0xFF }
173 -> { fadeoutHighVibDepth = byte and 0xFF }
174 -> { volumeSwing = byte and 0xFF }
175 -> { vibratoSpeed = byte and 0xFF }
176 -> { vibratoSweep = byte and 0xFF }
177 -> { defaultPan = byte and 0xFF }
178 -> { pitchPanCentre = (pitchPanCentre and 0xff00) or byte }
179 -> { pitchPanCentre = (pitchPanCentre and 0x00ff) or (byte shl 8) }
180 -> { pitchPanSeparation = byte.toByte().toInt() } // signed
181 -> { panSwing = byte and 0xFF }
182 -> { defaultCutoff = byte and 0xFF }
183 -> { defaultResonance = byte and 0xFF }
in 184..191 -> { reserved[offset - 184] = byte.toByte() }
else -> throw InternalError("Bad offset $offset")
}
}