mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 13:38:30 +09:00
reflecting spec changes
This commit is contained in:
@@ -10,7 +10,7 @@ const TAUD_MAGIC = [0x1F,0x54,0x53,0x56,0x4D,0x61,0x75,0x64] // \x1F TSV
|
|||||||
const TAUD_VERSION = 1
|
const TAUD_VERSION = 1
|
||||||
const TAUD_HEADER_SIZE = 32 // magic(8) + version(1) + numSongs(1) + compSize(4) + rsvd(2) + sig(16)
|
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 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 PATTERN_SIZE = 512 // bytes per pattern (64 rows × 8 bytes)
|
||||||
const NUM_PATTERNS_MAX = 256
|
const NUM_PATTERNS_MAX = 256
|
||||||
const NUM_CUES = 1024
|
const NUM_CUES = 1024
|
||||||
@@ -95,6 +95,7 @@ function uploadTaudFile(inFile, songIndex, playhead) {
|
|||||||
// Write decompressed data to peripheral memory (backwards addressing:
|
// Write decompressed data to peripheral memory (backwards addressing:
|
||||||
// peripheral byte k lives at memBase - k).
|
// peripheral byte k lives at memBase - k).
|
||||||
for (let i = 0; i < SAMPLEINST_SIZE; i++) {
|
for (let i = 0; i < SAMPLEINST_SIZE; i++) {
|
||||||
|
// TODO use sys.memcpy
|
||||||
sys.poke(memBase - i, sys.peek(decompPtr + i))
|
sys.poke(memBase - i, sys.peek(decompPtr + i))
|
||||||
}
|
}
|
||||||
sys.free(decompPtr)
|
sys.free(decompPtr)
|
||||||
|
|||||||
165
it2taud.py
165
it2taud.py
@@ -47,7 +47,7 @@ from taud_common import (
|
|||||||
TAUD_MAGIC, TAUD_VERSION, TAUD_HEADER_SIZE, TAUD_SONG_ENTRY,
|
TAUD_MAGIC, TAUD_VERSION, TAUD_HEADER_SIZE, TAUD_SONG_ENTRY,
|
||||||
SAMPLEBIN_SIZE, INSTBIN_SIZE, SAMPLEINST_SIZE,
|
SAMPLEBIN_SIZE, INSTBIN_SIZE, SAMPLEINST_SIZE,
|
||||||
PATTERN_ROWS, PATTERN_BYTES, NUM_PATTERNS_MAX, NUM_CUES, CUE_SIZE, NUM_VOICES,
|
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_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,
|
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,
|
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:
|
if it_note == IT_NOTE_CUT:
|
||||||
return NOTE_CUT
|
return NOTE_CUT
|
||||||
if 0 <= it_note <= 119:
|
if 0 <= it_note <= 119:
|
||||||
# IT middle C is C-5 (note 60); Taud reference is C-3 (TAUD_C3 = 0x4000).
|
# IT middle C is C-5 (note 60); Taud reference is C-4 (TAUD_C4 = 0x5000).
|
||||||
# IT C-5 anchors to Taud C-3, so offset = it_note - 60.
|
# IT C-5 anchors to Taud C-4, so offset = it_note - 60.
|
||||||
semis = 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 max(1, min(0xFFFD, val))
|
||||||
return NOTE_NOP
|
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)
|
s.length = n; s.loop_end = min(s.loop_end, n)
|
||||||
pos += 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)
|
inst_bin = bytearray(INSTBIN_SIZE)
|
||||||
for i, s in enumerate(samples_or_proxy):
|
for i, s in enumerate(samples_or_proxy):
|
||||||
taud_idx = i # samples_or_proxy is 0-based here; slot 0 unused
|
taud_idx = i # samples_or_proxy is 0-based here; slot 0 unused
|
||||||
if i == 0 or i >= 256 or s is None:
|
if i == 0 or i >= 256 or s is None:
|
||||||
continue
|
continue
|
||||||
ptr = offsets.get(i, 0)
|
ptr = offsets.get(i, 0) & 0xFFFFFFFF
|
||||||
ptr_lo = ptr & 0xFFFF
|
|
||||||
ptr_hi = ptr >> 16
|
|
||||||
s_len = min(s.length, 65535)
|
s_len = min(s.length, 65535)
|
||||||
c2spd = min(s.c5_speed, 65535)
|
c2spd = min(s.c5_speed, 65535)
|
||||||
ls = min(s.loop_beg, 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
|
loop_mode = 1 # forward loop
|
||||||
else:
|
else:
|
||||||
loop_mode = 0 # no loop
|
loop_mode = 0 # no loop
|
||||||
flags_byte = (ptr_hi << 4) | (loop_mode & 0x3)
|
flags_byte = loop_mode & 0x3
|
||||||
|
|
||||||
base = taud_idx * 64
|
base = taud_idx * 192
|
||||||
struct.pack_into('<H', inst_bin, base + 0, ptr_lo)
|
struct.pack_into('<I', inst_bin, base + 0, ptr)
|
||||||
struct.pack_into('<H', inst_bin, base + 2, s_len)
|
struct.pack_into('<H', inst_bin, base + 4, s_len)
|
||||||
struct.pack_into('<H', inst_bin, base + 4, c2spd)
|
struct.pack_into('<H', inst_bin, base + 6, c2spd)
|
||||||
struct.pack_into('<H', inst_bin, base + 6, 0)
|
struct.pack_into('<H', inst_bin, base + 8, 0) # play start
|
||||||
struct.pack_into('<H', inst_bin, base + 8, ls)
|
struct.pack_into('<H', inst_bin, base + 10, ls)
|
||||||
struct.pack_into('<H', inst_bin, base + 10, le)
|
struct.pack_into('<H', inst_bin, base + 12, le)
|
||||||
inst_bin[base + 12] = flags_byte
|
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
|
env_data = envelopes_by_slot.get(taud_idx) if envelopes_by_slot else None
|
||||||
if env_data and env_data[0]:
|
if env_data and env_data[0]:
|
||||||
vol_env, vol_sus, pan_env, pan_sus, inst_gv = env_data
|
vol_env, vol_sus, pan_env, pan_sus, inst_gv = env_data
|
||||||
inst_bin[base + 13] = vol_sus & 0xFF
|
# Old caller passed an 8-bit sustain byte (0b ut eee sss for 12-point indices).
|
||||||
inst_bin[base + 14] = pan_sus & 0xFF
|
# Convert to new 16-bit layout (5-bit sus indices in bits 12..8 / 4..0).
|
||||||
inst_bin[base + 15] = inst_gv & 0xFF
|
def _convert_old_sus(b: int, has_env: bool) -> int:
|
||||||
for k, (val, mf) in enumerate(vol_env[:12]):
|
if not has_env:
|
||||||
inst_bin[base + 16 + k*2] = val & 0xFF
|
return 0
|
||||||
inst_bin[base + 16 + k*2 + 1] = mf & 0xFF
|
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:
|
if pan_env:
|
||||||
for k, (val, mf) in enumerate(pan_env[:12]):
|
for k, (val, mf) in enumerate(pan_env[:25]):
|
||||||
inst_bin[base + 40 + k*2] = val & 0xFF
|
inst_bin[base + 71 + k*2] = val & 0xFF
|
||||||
inst_bin[base + 40 + k*2 + 1] = mf & 0xFF
|
inst_bin[base + 71 + k*2 + 1] = mf & 0xFF
|
||||||
else:
|
else:
|
||||||
for k in range(12):
|
for k in range(25):
|
||||||
inst_bin[base + 40 + k*2] = 0x80 # pan centre
|
inst_bin[base + 71 + k*2] = 0x80 # pan centre
|
||||||
inst_bin[base + 40 + k*2 + 1] = 0x00 # hold
|
inst_bin[base + 71 + k*2 + 1] = 0x00 # hold
|
||||||
else:
|
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_gv = env_data[4] if env_data else 255
|
||||||
inst_bin[base + 15] = inst_gv & 0xFF
|
struct.pack_into('<H', inst_bin, base + 15, USE_ENV_BIT)
|
||||||
inst_bin[base + 16] = min(s.vol, 63) # value 0-63
|
struct.pack_into('<H', inst_bin, base + 17, 0)
|
||||||
inst_bin[base + 17] = 0 # offset 0 = hold
|
struct.pack_into('<H', inst_bin, base + 19, 0)
|
||||||
for k in range(12):
|
inst_bin[base + 171] = inst_gv & 0xFF
|
||||||
inst_bin[base + 40 + k*2] = 0x80 # pan centre
|
inst_bin[base + 21] = min(s.vol, 63) # value 0-63
|
||||||
inst_bin[base + 40 + k*2 + 1] = 0x00 # hold
|
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}")
|
vprint(f" instrument[{taud_idx}] '{s.name}' ptr:{ptr} c5spd:{s.c5_speed}")
|
||||||
|
|
||||||
return bytes(sample_bin) + bytes(inst_bin), offsets
|
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 ─────────────────────────────────────────────────────────────
|
# ── 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,
|
def find_initial_bpm_speed(patterns_rows: list, order_list: list,
|
||||||
default_speed: int, default_tempo: int) -> tuple:
|
default_speed: int, default_tempo: int) -> tuple:
|
||||||
speed = default_speed or 6
|
speed = default_speed or 6
|
||||||
@@ -1604,6 +1693,10 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
|
|||||||
old_effects=h.old_effects,
|
old_effects=h.old_effects,
|
||||||
initial_global_vol=h.global_vol)
|
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) ─────────────────────────────────
|
# ── Check SBx chunk crossing (warn only) ─────────────────────────────────
|
||||||
for pi, (grid, rows) in enumerate(patterns_rows):
|
for pi, (grid, rows) in enumerate(patterns_rows):
|
||||||
if rows <= PATTERN_ROWS: continue
|
if rows <= PATTERN_ROWS: continue
|
||||||
|
|||||||
101
mod2taud.py
101
mod2taud.py
@@ -7,7 +7,7 @@ Usage:
|
|||||||
Limits:
|
Limits:
|
||||||
- Up to 20 MOD channels (excess disabled; hard error if pattern count
|
- Up to 20 MOD channels (excess disabled; hard error if pattern count
|
||||||
× channel count > 4095).
|
× 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
|
sample is globally resampled down (with c2spd adjusted) so pitch is
|
||||||
preserved.
|
preserved.
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ from taud_common import (
|
|||||||
TAUD_MAGIC, TAUD_VERSION, TAUD_HEADER_SIZE, TAUD_SONG_ENTRY,
|
TAUD_MAGIC, TAUD_VERSION, TAUD_HEADER_SIZE, TAUD_SONG_ENTRY,
|
||||||
SAMPLEBIN_SIZE, INSTBIN_SIZE, SAMPLEINST_SIZE,
|
SAMPLEBIN_SIZE, INSTBIN_SIZE, SAMPLEINST_SIZE,
|
||||||
PATTERN_ROWS, PATTERN_BYTES, NUM_PATTERNS_MAX, NUM_CUES, CUE_SIZE, NUM_VOICES,
|
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_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,
|
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,
|
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
|
SIGNATURE = b"mod2taud/TSVM " # 14 bytes
|
||||||
|
|
||||||
# PT period 428 (PT "C-2") corresponds to OpenMPT/IT C-4 which s3m2taud
|
# 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.
|
# share a pitch reference.
|
||||||
PT_REFERENCE_PERIOD = 428.0
|
PT_REFERENCE_PERIOD = 428.0
|
||||||
|
|
||||||
@@ -224,7 +224,7 @@ def _signed4(nibble: int) -> int:
|
|||||||
def period_to_taud_note(period: int) -> int:
|
def period_to_taud_note(period: int) -> int:
|
||||||
if period <= 0:
|
if period <= 0:
|
||||||
return NOTE_NOP
|
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))
|
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)
|
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:
|
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.
|
"""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)
|
s.loop_end = min(s.loop_end, n)
|
||||||
pos += n
|
pos += n
|
||||||
|
|
||||||
|
# New 192-byte instrument layout (terranmon.txt:1997-2070).
|
||||||
inst_bin = bytearray(INSTBIN_SIZE)
|
inst_bin = bytearray(INSTBIN_SIZE)
|
||||||
for i, s in enumerate(samples):
|
for i, s in enumerate(samples):
|
||||||
taud_idx = i + 1 # 1-based instrument number
|
taud_idx = i + 1 # 1-based instrument number
|
||||||
@@ -434,29 +490,31 @@ def build_sample_inst_bin(samples: list) -> tuple:
|
|||||||
break
|
break
|
||||||
if not s.sample_data:
|
if not s.sample_data:
|
||||||
continue
|
continue
|
||||||
ptr = offsets.get(i, 0)
|
ptr = offsets.get(i, 0) & 0xFFFFFFFF
|
||||||
ptr_lo = ptr & 0xFFFF
|
|
||||||
ptr_hi = (ptr >> 16)
|
|
||||||
s_len = min(s.length, 65535)
|
s_len = min(s.length, 65535)
|
||||||
c2spd = min(s.c2spd, 65535)
|
c2spd = min(s.c2spd, 65535)
|
||||||
ps = 0
|
ps = 0
|
||||||
ls = min(s.loop_begin, 65535)
|
ls = min(s.loop_begin, 65535)
|
||||||
le = min(s.loop_end, 65535)
|
le = min(s.loop_end, 65535)
|
||||||
loop_mode = 1 if (s.flags & 1) else 0
|
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
|
base = taud_idx * 192
|
||||||
struct.pack_into('<H', inst_bin, base + 0, ptr_lo)
|
struct.pack_into('<I', inst_bin, base + 0, ptr)
|
||||||
struct.pack_into('<H', inst_bin, base + 2, s_len)
|
struct.pack_into('<H', inst_bin, base + 4, s_len)
|
||||||
struct.pack_into('<H', inst_bin, base + 4, c2spd)
|
struct.pack_into('<H', inst_bin, base + 6, c2spd)
|
||||||
struct.pack_into('<H', inst_bin, base + 6, ps)
|
struct.pack_into('<H', inst_bin, base + 8, ps)
|
||||||
struct.pack_into('<H', inst_bin, base + 8, ls)
|
struct.pack_into('<H', inst_bin, base + 10, ls)
|
||||||
struct.pack_into('<H', inst_bin, base + 10, le)
|
struct.pack_into('<H', inst_bin, base + 12, le)
|
||||||
inst_bin[base + 12] = flags_byte
|
inst_bin[base + 14] = flags_byte
|
||||||
inst_bin[base + 15] = 0xFF # global volume — full
|
struct.pack_into('<H', inst_bin, base + 15, vol_env_flags)
|
||||||
env_vol = min(s.volume, 63)
|
struct.pack_into('<H', inst_bin, base + 17, 0)
|
||||||
inst_bin[base + 16] = env_vol # envelope hold value
|
struct.pack_into('<H', inst_bin, base + 19, 0)
|
||||||
inst_bin[base + 17] = 0 # offset minifloat = 0 → hold
|
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} "
|
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'})")
|
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…")
|
vprint(" resolving PT per-effect recalls…")
|
||||||
resolve_pt_recalls(patterns, order_list, n_channels)
|
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…")
|
vprint(" building sample/instrument bin…")
|
||||||
sampleinst_raw, _offsets = build_sample_inst_bin(samples)
|
sampleinst_raw, _offsets = build_sample_inst_bin(samples)
|
||||||
assert len(sampleinst_raw) == SAMPLEINST_SIZE
|
assert len(sampleinst_raw) == SAMPLEINST_SIZE
|
||||||
|
|||||||
107
s3m2taud.py
107
s3m2taud.py
@@ -7,7 +7,7 @@ Usage:
|
|||||||
Limits:
|
Limits:
|
||||||
- Up to 20 S3M channels (excess disabled; hard error if pattern count
|
- Up to 20 S3M channels (excess disabled; hard error if pattern count
|
||||||
× channel count > 4095).
|
× 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
|
sample is globally resampled down (with c2spd adjusted) so pitch is
|
||||||
preserved.
|
preserved.
|
||||||
- AdLib instruments are skipped.
|
- AdLib instruments are skipped.
|
||||||
@@ -34,7 +34,7 @@ from taud_common import (
|
|||||||
TAUD_MAGIC, TAUD_VERSION, TAUD_HEADER_SIZE, TAUD_SONG_ENTRY,
|
TAUD_MAGIC, TAUD_VERSION, TAUD_HEADER_SIZE, TAUD_SONG_ENTRY,
|
||||||
SAMPLEBIN_SIZE, INSTBIN_SIZE, SAMPLEINST_SIZE,
|
SAMPLEBIN_SIZE, INSTBIN_SIZE, SAMPLEINST_SIZE,
|
||||||
PATTERN_ROWS, PATTERN_BYTES, NUM_PATTERNS_MAX, NUM_CUES, CUE_SIZE, NUM_VOICES,
|
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_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,
|
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,
|
SEL_SET, SEL_UP, SEL_DOWN, SEL_FINE,
|
||||||
@@ -231,7 +231,7 @@ def encode_note(s3m_note: int) -> int:
|
|||||||
if pitch > 11:
|
if pitch > 11:
|
||||||
return NOTE_NOP
|
return NOTE_NOP
|
||||||
semitones = (octave - 4) * 12 + pitch
|
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))
|
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)
|
inst.loop_end = min(inst.loop_end, n)
|
||||||
pos += 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)
|
inst_bin = bytearray(INSTBIN_SIZE)
|
||||||
for i, inst in enumerate(instruments):
|
for i, inst in enumerate(instruments):
|
||||||
taud_idx = i + 1
|
taud_idx = i + 1
|
||||||
@@ -463,33 +465,37 @@ def build_sample_inst_bin(instruments: list) -> tuple:
|
|||||||
break
|
break
|
||||||
if inst is None or inst.itype != S3M_TYPE_PCM:
|
if inst is None or inst.itype != S3M_TYPE_PCM:
|
||||||
continue
|
continue
|
||||||
ptr = offsets.get(i, 0)
|
ptr = offsets.get(i, 0) & 0xFFFFFFFF
|
||||||
ptr_lo = ptr & 0xFFFF
|
|
||||||
ptr_hi = (ptr >> 16)
|
|
||||||
s_len = min(inst.length, 65535)
|
s_len = min(inst.length, 65535)
|
||||||
c2spd = min(inst.c2spd, 65535)
|
c2spd = min(inst.c2spd, 65535)
|
||||||
ps = 0
|
ps = 0
|
||||||
ls = min(inst.loop_begin, 65535)
|
ls = min(inst.loop_begin, 65535)
|
||||||
le = min(inst.loop_end, 65535)
|
le = min(inst.loop_end, 65535)
|
||||||
loop_mode = 1 if (inst.flags & 1) else 0
|
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
|
# Volume envelope: hold at instrument volume (clamped to 0x3F).
|
||||||
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)
|
|
||||||
env_vol = min(inst.volume, 63)
|
env_vol = min(inst.volume, 63)
|
||||||
inst_bin[base + 16] = env_vol # volume
|
# Vol env-flags: enable use-envelope bit (b=1) so engine reads the single point.
|
||||||
inst_bin[base + 17] = 0 # offset minifloat = 0 → hold
|
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:
|
if inst.c2spd > 65535:
|
||||||
vprint(f" warning: sampling rate of '{inst.name}' exceeds 65535 (got '{inst.c2spd}')")
|
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)
|
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,
|
def find_initial_bpm_speed(patterns: list, order_list: list,
|
||||||
default_speed: int, default_tempo: int) -> tuple:
|
default_speed: int, default_tempo: int) -> tuple:
|
||||||
"""Scan first pattern in order for Axx/Txx in row 0 of any channel."""
|
"""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)
|
resolve_st3_recalls(patterns, h.order_list, 32)
|
||||||
warn_st3_quirks(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
|
# Build sample+instrument bin
|
||||||
vprint(" building sample/instrument bin…")
|
vprint(" building sample/instrument bin…")
|
||||||
sampleinst_raw, _offsets = build_sample_inst_bin(instruments)
|
sampleinst_raw, _offsets = build_sample_inst_bin(instruments)
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ TAUD_MAGIC = bytes([0x1F,0x54,0x53,0x56,0x4D,0x61,0x75,0x64])
|
|||||||
TAUD_VERSION = 1
|
TAUD_VERSION = 1
|
||||||
TAUD_HEADER_SIZE = 32 # magic(8)+ver(1)+numSongs(1)+compSize(4)+rsvd(4)+sig(14)
|
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)
|
TAUD_SONG_ENTRY = 16 # offset(4)+voices(1)+pats(2)+bpm(1)+tick(1)+basenote(2)+basefreq(4)+flags(1)
|
||||||
SAMPLEBIN_SIZE = 770048
|
SAMPLEBIN_SIZE = 737280
|
||||||
INSTBIN_SIZE = 16384 # 256 instruments × 64 bytes
|
INSTBIN_SIZE = 49152 # 256 instruments × 192 bytes
|
||||||
SAMPLEINST_SIZE = SAMPLEBIN_SIZE + INSTBIN_SIZE
|
SAMPLEINST_SIZE = SAMPLEBIN_SIZE + INSTBIN_SIZE
|
||||||
PATTERN_ROWS = 64
|
PATTERN_ROWS = 64
|
||||||
PATTERN_BYTES = PATTERN_ROWS * 8 # 512
|
PATTERN_BYTES = PATTERN_ROWS * 8 # 512
|
||||||
@@ -44,7 +44,7 @@ NUM_VOICES = 20
|
|||||||
NOTE_NOP = 0xFFFF
|
NOTE_NOP = 0xFFFF
|
||||||
NOTE_KEYOFF = 0x0000
|
NOTE_KEYOFF = 0x0000
|
||||||
NOTE_CUT = 0xFFFE
|
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)
|
# Taud effect opcodes (base-36: 0..9 → 0x00..0x09, A..Z → 0x0A..0x23)
|
||||||
TOP_NONE = 0x00
|
TOP_NONE = 0x00
|
||||||
|
|||||||
@@ -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) {
|
fun uploadInstrument(slot: Int, bytes: IntArray) {
|
||||||
getFirstSnd()?.instruments?.get(slot and 0xFF)?.let { inst ->
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,16 +124,17 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
internal val DBGPRN = false
|
internal val DBGPRN = false
|
||||||
const val SAMPLING_RATE = 32000
|
const val SAMPLING_RATE = 32000
|
||||||
const val TRACKER_CHUNK = 512
|
const val TRACKER_CHUNK = 512
|
||||||
const val TRACKER_C3 = 0x4000
|
const val TRACKER_C3 = 0x4000 // legacy alias (one octave below the new reference)
|
||||||
// Amiga period at TRACKER_C3 for a standard 8363 Hz instrument (NTSC clock 3579545 Hz).
|
const val TRACKER_C4 = 0x5000 // reference C for instrument samplingRate (terranmon.txt:2000)
|
||||||
// Used to implement Amiga-mode pitch slides (effect '1' f-bit or song-table flag).
|
// Amiga period at TRACKER_C4 for a standard 8363 Hz instrument (NTSC clock 3579545 Hz).
|
||||||
const val AMIGA_BASE_PERIOD = 214.0
|
// 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.
|
// 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.
|
// Taud coarse unit = round(ST3_unit × 64/3), so the inverse is × 3/64.
|
||||||
const val AMIGA_PERIOD_SCALE = 3.0 / 64.0
|
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 instruments = Array(256) { TaudInst(it) }
|
||||||
internal val playdata = Array(4096) { Array(64) { TaudPlayData(0xFFFF, 0, 0, 0, 32, 0, 0, 0) } }
|
internal val playdata = Array(4096) { Array(64) { TaudPlayData(0xFFFF, 0, 0, 0, 32, 0, 0, 0) } }
|
||||||
internal val playheads: Array<Playhead>
|
internal val playheads: Array<Playhead>
|
||||||
@@ -305,8 +306,8 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
|
|
||||||
override fun peek(addr: Long): Byte {
|
override fun peek(addr: Long): Byte {
|
||||||
return when (val adi = addr.toInt()) {
|
return when (val adi = addr.toInt()) {
|
||||||
in 0..770047 -> sampleBin[addr]
|
in 0..737279 -> sampleBin[addr]
|
||||||
in 770048..786431 -> (adi - 770048).let { instruments[it / 64].getByte(it % 64) }
|
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 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 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)
|
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 adi = addr.toInt()
|
||||||
val bi = byte.toUint()
|
val bi = byte.toUint()
|
||||||
when (adi) {
|
when (adi) {
|
||||||
in 0..770047 -> { sampleBin[addr] = byte }
|
in 0..737279 -> { sampleBin[addr] = byte }
|
||||||
in 770048..786431 -> (adi - 770048).let { instruments[it / 64].setByte(it % 64, bi) }
|
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 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 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
|
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 =
|
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
|
// 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).
|
// 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.
|
// The Taud coarse-slide value is converted back to Amiga period units via AMIGA_PERIOD_SCALE.
|
||||||
private fun amigaSlide(noteVal: Int, slideArg: Int): Int {
|
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.
|
// Negate slideArg: pitch down (slideArg < 0) → period up, pitch up (slideArg > 0) → period down.
|
||||||
val newPeriod = (period - slideArg * AMIGA_PERIOD_SCALE).coerceAtLeast(1.0)
|
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) {
|
private fun advanceEnvelope(voice: Voice, inst: TaudInst, tickSec: Double) {
|
||||||
// Volume envelope
|
// 16-bit envelope-flag layout (terranmon.txt:2007-2030):
|
||||||
// sustain byte: bit7=enable (u), bit6=sustain (t: 1=breaks on key-off,
|
// 0b 0ut sssss pcb eeeee
|
||||||
// 0=loops forever), bits[5:3]=end_idx, bits[2:0]=start_idx
|
// bit 14 = u (enable sustain/loop)
|
||||||
val vSus = inst.volEnvSustain
|
// bit 13 = t (sustain — 1=breaks on key-off, 0=loops forever)
|
||||||
val vEnabled = (vSus and 0x80) != 0
|
// bits 12..8 = sustain/loop start index (0..24)
|
||||||
val vIsSustain = (vSus and 0x40) != 0
|
// bit 7 = p (channel-specific flag — fadeout zero / use default pan)
|
||||||
// Loop is "active" when enabled AND (it's a forever-loop OR key not yet released)
|
// bit 6 = c (envelope carry)
|
||||||
val vSusOn = vEnabled && (!vIsSustain || !voice.keyOff)
|
// bit 5 = b (use envelope at all)
|
||||||
val vSusStart = vSus and 7
|
// bits 4..0 = sustain/loop end index (0..24)
|
||||||
val vSusEnd = (vSus ushr 3) and 7
|
val maxIdx = 24
|
||||||
|
|
||||||
if (vSusOn && voice.envIndex == vSusEnd && vSusStart == vSusEnd) {
|
// Volume envelope
|
||||||
// slb == sle: hold at this node until key-off (no cycling)
|
val vSus = inst.volEnvSustain
|
||||||
voice.envVolume = (inst.volEnvelopes[voice.envIndex].value / 63.0).coerceIn(0.0, 1.0)
|
val vUseEnv = (vSus ushr 5) and 1 != 0
|
||||||
} else if (vSusOn && voice.envIndex == vSusEnd) {
|
if (vUseEnv) {
|
||||||
// At sustain-loop end: snap back to start regardless of stored offset.
|
val vEnabled = (vSus ushr 14) and 1 != 0
|
||||||
voice.envTimeSec = 0.0
|
val vIsSustain = (vSus ushr 13) and 1 != 0
|
||||||
voice.envIndex = vSusStart
|
val vSusOn = vEnabled && (!vIsSustain || !voice.keyOff)
|
||||||
voice.envVolume = (inst.volEnvelopes[voice.envIndex].value / 63.0).coerceIn(0.0, 1.0)
|
val vSusStart = (vSus ushr 8) and 0x1F
|
||||||
} else if (voice.envIndex >= 11) {
|
val vSusEnd = vSus and 0x1F
|
||||||
voice.envVolume = (inst.volEnvelopes[11].value / 63.0).coerceIn(0.0, 1.0)
|
|
||||||
} else {
|
if (vSusOn && voice.envIndex == vSusEnd && vSusStart == vSusEnd) {
|
||||||
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)
|
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 {
|
} else {
|
||||||
voice.envTimeSec += tickSec
|
val vOffset = inst.volEnvelopes[voice.envIndex].offset.toDouble()
|
||||||
if (voice.envTimeSec >= vOffset) {
|
if (vOffset == 0.0) {
|
||||||
voice.envTimeSec -= vOffset
|
|
||||||
val nextIdx = if (vSusOn && voice.envIndex == vSusEnd) vSusStart
|
|
||||||
else (voice.envIndex + 1).coerceAtMost(11)
|
|
||||||
voice.envIndex = nextIdx
|
|
||||||
voice.envVolume = (inst.volEnvelopes[voice.envIndex].value / 63.0).coerceIn(0.0, 1.0)
|
voice.envVolume = (inst.volEnvelopes[voice.envIndex].value / 63.0).coerceIn(0.0, 1.0)
|
||||||
} else {
|
} else {
|
||||||
val cur = (inst.volEnvelopes[voice.envIndex].value / 63.0).coerceIn(0.0, 1.0)
|
voice.envTimeSec += tickSec
|
||||||
val nxt = (inst.volEnvelopes[(voice.envIndex + 1).coerceAtMost(11)].value / 63.0).coerceIn(0.0, 1.0)
|
if (voice.envTimeSec >= vOffset) {
|
||||||
voice.envVolume = cur + (nxt - cur) * (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)
|
// Pan envelope (only when active for this instrument)
|
||||||
if (!voice.hasPanEnv) return
|
if (!voice.hasPanEnv) return
|
||||||
val pSus = inst.panEnvSustain
|
val pSus = inst.panEnvSustain
|
||||||
val pEnabled = (pSus and 0x80) != 0
|
val pUseEnv = (pSus ushr 5) and 1 != 0
|
||||||
val pIsSustain = (pSus and 0x40) != 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 pSusOn = pEnabled && (!pIsSustain || !voice.keyOff)
|
||||||
val pSusStart = pSus and 7
|
val pSusStart = (pSus ushr 8) and 0x1F
|
||||||
val pSusEnd = (pSus ushr 3) and 7
|
val pSusEnd = pSus and 0x1F
|
||||||
|
|
||||||
if (pSusOn && voice.envPanIndex == pSusEnd && pSusStart == pSusEnd) {
|
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
|
voice.envPan = inst.panEnvelopes[voice.envPanIndex].value / 255.0
|
||||||
} else if (pSusOn && voice.envPanIndex == pSusEnd) {
|
} 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.envPanTimeSec = 0.0
|
||||||
voice.envPanIndex = pSusStart
|
voice.envPanIndex = pSusStart
|
||||||
voice.envPan = inst.panEnvelopes[voice.envPanIndex].value / 255.0
|
voice.envPan = inst.panEnvelopes[voice.envPanIndex].value / 255.0
|
||||||
} else if (voice.envPanIndex >= 11) {
|
} else if (voice.envPanIndex >= maxIdx) {
|
||||||
voice.envPan = inst.panEnvelopes[11].value / 255.0
|
voice.envPan = inst.panEnvelopes[maxIdx].value / 255.0
|
||||||
} else {
|
} else {
|
||||||
val pOffset = inst.panEnvelopes[voice.envPanIndex].offset.toDouble()
|
val pOffset = inst.panEnvelopes[voice.envPanIndex].offset.toDouble()
|
||||||
if (pOffset == 0.0) {
|
if (pOffset == 0.0) {
|
||||||
@@ -1253,12 +1262,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
if (voice.envPanTimeSec >= pOffset) {
|
if (voice.envPanTimeSec >= pOffset) {
|
||||||
voice.envPanTimeSec -= pOffset
|
voice.envPanTimeSec -= pOffset
|
||||||
val nextIdx = if (pSusOn && voice.envPanIndex == pSusEnd) pSusStart
|
val nextIdx = if (pSusOn && voice.envPanIndex == pSusEnd) pSusStart
|
||||||
else (voice.envPanIndex + 1).coerceAtMost(11)
|
else (voice.envPanIndex + 1).coerceAtMost(maxIdx)
|
||||||
voice.envPanIndex = nextIdx
|
voice.envPanIndex = nextIdx
|
||||||
voice.envPan = inst.panEnvelopes[voice.envPanIndex].value / 255.0
|
voice.envPan = inst.panEnvelopes[voice.envPanIndex].value / 255.0
|
||||||
} else {
|
} else {
|
||||||
val cur = inst.panEnvelopes[voice.envPanIndex].value / 255.0
|
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)
|
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 sampleLen = inst.sampleLength.coerceAtLeast(1)
|
||||||
val loopStart = inst.sampleLoopStart.toDouble()
|
val loopStart = inst.sampleLoopStart.toDouble()
|
||||||
val loopEnd = inst.sampleLoopEnd.toDouble().coerceAtLeast(1.0)
|
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 i0 = voice.samplePos.toInt().coerceIn(0, sampleLen - 1)
|
||||||
val i1 = (i0 + 1).coerceAtMost(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.envPanIndex = 0
|
||||||
voice.envPanTimeSec = 0.0
|
voice.envPanTimeSec = 0.0
|
||||||
voice.envPan = inst.panEnvelopes[0].value / 255.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.noteVal = noteVal
|
||||||
voice.basePitch = noteVal
|
voice.basePitch = noteVal
|
||||||
voice.playbackRate = computePlaybackRate(inst, 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)
|
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(
|
data class TaudInst(
|
||||||
var index: Int,
|
var index: Int,
|
||||||
|
|
||||||
var samplePtr: Int, // 20-bit number
|
var samplePtr: Int, // 32-bit sample bin offset
|
||||||
var sampleLength: Int,
|
var sampleLength: Int,
|
||||||
var samplingRate: Int,
|
var samplingRate: Int, // rate at TRACKER_C4
|
||||||
var samplePlayStart: Int,
|
var samplePlayStart: Int,
|
||||||
var sampleLoopStart: Int,
|
var sampleLoopStart: Int,
|
||||||
var sampleLoopEnd: Int,
|
var sampleLoopEnd: Int,
|
||||||
// flags
|
var loopMode: Int, // byte 14, low 2 bits
|
||||||
var loopMode: Int,
|
var volEnvSustain: Int, // bytes 15-16 (16-bit, see flag layout)
|
||||||
var volEnvSustain: Int, // byte 13: ut eee sss (u=enable, t=sustain (1=breaks on key-off, 0=loops forever))
|
var panEnvSustain: Int, // bytes 17-18
|
||||||
var panEnvSustain: Int, // byte 14: ut eee sss (u=enable, t=sustain (1=breaks on key-off, 0=loops forever))
|
var pfEnvSustain: Int, // bytes 19-20 (pitch/filter)
|
||||||
var instGlobalVolume: Int, // byte 15: instrument global volume (0..255, 255 = unity)
|
var instGlobalVolume: Int, // byte 171
|
||||||
var volEnvelopes: Array<TaudInstEnvPoint>, // 12 points, value 0x00-0x3F
|
var volEnvelopes: Array<TaudInstEnvPoint>, // 25 points
|
||||||
var panEnvelopes: Array<TaudInstEnvPoint> // 12 points, value 0x00-0xFF (0x80 = centre)
|
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,
|
constructor(index: Int) : this(
|
||||||
Array(12) { TaudInstEnvPoint(0x3F, ThreeFiveMiniUfloat(0)) },
|
index, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF,
|
||||||
Array(12) { TaudInstEnvPoint(0x80, ThreeFiveMiniUfloat(0)) })
|
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.
|
// 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.
|
// 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
|
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) {
|
fun getByte(offset: Int): Byte = when (offset) {
|
||||||
0 -> samplePtr.toByte()
|
0 -> samplePtr.toByte()
|
||||||
1 -> samplePtr.ushr(8).toByte()
|
1 -> samplePtr.ushr(8).toByte()
|
||||||
|
2 -> samplePtr.ushr(16).toByte()
|
||||||
|
3 -> samplePtr.ushr(24).toByte()
|
||||||
|
|
||||||
2 -> sampleLength.toByte()
|
4 -> sampleLength.toByte()
|
||||||
3 -> sampleLength.ushr(8).toByte()
|
5 -> sampleLength.ushr(8).toByte()
|
||||||
|
|
||||||
4 -> samplingRate.toByte()
|
6 -> samplingRate.toByte()
|
||||||
5 -> samplingRate.ushr(8).toByte()
|
7 -> samplingRate.ushr(8).toByte()
|
||||||
|
|
||||||
6 -> samplePlayStart.toByte()
|
8 -> samplePlayStart.toByte()
|
||||||
7 -> samplePlayStart.ushr(8).toByte()
|
9 -> samplePlayStart.ushr(8).toByte()
|
||||||
|
|
||||||
8 -> sampleLoopStart.toByte()
|
10 -> sampleLoopStart.toByte()
|
||||||
9 -> sampleLoopStart.ushr(8).toByte()
|
11 -> sampleLoopStart.ushr(8).toByte()
|
||||||
|
|
||||||
10 -> sampleLoopEnd.toByte()
|
12 -> sampleLoopEnd.toByte()
|
||||||
11 -> sampleLoopEnd.ushr(8).toByte()
|
13 -> sampleLoopEnd.ushr(8).toByte()
|
||||||
|
|
||||||
12 -> (samplePtr.ushr(16).and(15).shl(4) or loopMode.and(3)).toByte()
|
14 -> (loopMode and 3).toByte()
|
||||||
13 -> volEnvSustain.toByte()
|
15 -> volEnvSustain.toByte()
|
||||||
14 -> panEnvSustain.toByte()
|
16 -> volEnvSustain.ushr(8).toByte()
|
||||||
15 -> instGlobalVolume.toByte()
|
17 -> panEnvSustain.toByte()
|
||||||
in 16..38 step 2 -> volEnvelopes[(offset - 16) / 2].value.toByte()
|
18 -> panEnvSustain.ushr(8).toByte()
|
||||||
in 17..39 step 2 -> volEnvelopes[(offset - 17) / 2].offset.index.toByte()
|
19 -> pfEnvSustain.toByte()
|
||||||
in 40..62 step 2 -> panEnvelopes[(offset - 40) / 2].value.toByte()
|
20 -> pfEnvSustain.ushr(8).toByte()
|
||||||
in 41..63 step 2 -> panEnvelopes[(offset - 41) / 2].offset.index.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")
|
else -> throw InternalError("Bad offset $offset")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setByte(offset: Int, byte: Int) = when (offset) {
|
fun setByte(offset: Int, byte: Int) = when (offset) {
|
||||||
0 -> { samplePtr = (samplePtr and 0xfff00) or byte }
|
0 -> { samplePtr = (samplePtr and 0xFFFFFF00.toInt()) or byte }
|
||||||
1 -> { samplePtr = (samplePtr and 0x000ff) or (byte shl 8) }
|
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 }
|
4 -> { sampleLength = (sampleLength and 0xff00) or byte }
|
||||||
3 -> { sampleLength = (sampleLength and 0x00ff) or (byte shl 8) }
|
5 -> { sampleLength = (sampleLength and 0x00ff) or (byte shl 8) }
|
||||||
|
|
||||||
4 -> { samplingRate = (samplingRate and 0xff00) or byte }
|
6 -> { samplingRate = (samplingRate and 0xff00) or byte }
|
||||||
5 -> { samplingRate = (samplingRate and 0x00ff) or (byte shl 8) }
|
7 -> { samplingRate = (samplingRate and 0x00ff) or (byte shl 8) }
|
||||||
|
|
||||||
6 -> { samplePlayStart = (samplePlayStart and 0xff00) or byte }
|
8 -> { samplePlayStart = (samplePlayStart and 0xff00) or byte }
|
||||||
7 -> { samplePlayStart = (samplePlayStart and 0x00ff) or (byte shl 8) }
|
9 -> { samplePlayStart = (samplePlayStart and 0x00ff) or (byte shl 8) }
|
||||||
|
|
||||||
8 -> { sampleLoopStart = (sampleLoopStart and 0xff00) or byte }
|
10 -> { sampleLoopStart = (sampleLoopStart and 0xff00) or byte }
|
||||||
9 -> { sampleLoopStart = (sampleLoopStart and 0x00ff) or (byte shl 8) }
|
11 -> { sampleLoopStart = (sampleLoopStart and 0x00ff) or (byte shl 8) }
|
||||||
|
|
||||||
10 -> { sampleLoopEnd = (sampleLoopEnd and 0xff00) or byte }
|
12 -> { sampleLoopEnd = (sampleLoopEnd and 0xff00) or byte }
|
||||||
11 -> { sampleLoopEnd = (sampleLoopEnd and 0x00ff) or (byte shl 8) }
|
13 -> { sampleLoopEnd = (sampleLoopEnd and 0x00ff) or (byte shl 8) }
|
||||||
|
|
||||||
12 -> {
|
14 -> { loopMode = byte and 3 }
|
||||||
samplePtr = if (byte and 0b1111_0000 != 0) samplePtr or ((byte ushr 4) shl 16)
|
15 -> { volEnvSustain = (volEnvSustain and 0xff00) or byte }
|
||||||
else samplePtr and 0x0ffff
|
16 -> { volEnvSustain = (volEnvSustain and 0x00ff) or (byte shl 8) }
|
||||||
loopMode = byte and 3
|
17 -> { panEnvSustain = (panEnvSustain and 0xff00) or byte }
|
||||||
}
|
18 -> { panEnvSustain = (panEnvSustain and 0x00ff) or (byte shl 8) }
|
||||||
13 -> { volEnvSustain = byte }
|
19 -> { pfEnvSustain = (pfEnvSustain and 0xff00) or byte }
|
||||||
14 -> { panEnvSustain = byte }
|
20 -> { pfEnvSustain = (pfEnvSustain and 0x00ff) or (byte shl 8) }
|
||||||
15 -> { instGlobalVolume = byte and 0xFF }
|
|
||||||
|
|
||||||
in 16..38 step 2 -> volEnvelopes[(offset - 16) / 2].value = byte
|
in 21..70 -> envPointSet(volEnvelopes, 21, offset, byte)
|
||||||
in 17..39 step 2 -> volEnvelopes[(offset - 17) / 2].offset = ThreeFiveMiniUfloat(byte)
|
in 71..120 -> envPointSet(panEnvelopes, 71, offset, byte)
|
||||||
in 40..62 step 2 -> panEnvelopes[(offset - 40) / 2].value = byte
|
in 121..170 -> envPointSet(pfEnvelopes, 121, offset, byte)
|
||||||
in 41..63 step 2 -> panEnvelopes[(offset - 41) / 2].offset = ThreeFiveMiniUfloat(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")
|
else -> throw InternalError("Bad offset $offset")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user