diff --git a/assets/disk0/tvdos/include/taud.mjs b/assets/disk0/tvdos/include/taud.mjs index 71aff61..ef4ad19 100644 --- a/assets/disk0/tvdos/include/taud.mjs +++ b/assets/disk0/tvdos/include/taud.mjs @@ -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) diff --git a/it2taud.py b/it2taud.py index dd81852..105a08f 100644 --- a/it2taud.py +++ b/it2taud.py @@ -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(' 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(' 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 diff --git a/mod2taud.py b/mod2taud.py index 4d1ff07..a95c36d 100644 --- a/mod2taud.py +++ b/mod2taud.py @@ -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(' 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 diff --git a/s3m2taud.py b/s3m2taud.py index b9e7833..b53dd36 100644 --- a/s3m2taud.py +++ b/s3m2taud.py @@ -7,7 +7,7 @@ Usage: Limits: - Up to 20 S3M channels (excess disabled; hard error if pattern count × channel count > 4095). - - Sample bin is 770048 bytes; if all samples together exceed this, every + - Sample bin is 737280 bytes; if all samples together exceed this, every sample is globally resampled down (with c2spd adjusted) so pitch is preserved. - AdLib instruments are skipped. @@ -34,7 +34,7 @@ from taud_common import ( TAUD_MAGIC, TAUD_VERSION, TAUD_HEADER_SIZE, TAUD_SONG_ENTRY, SAMPLEBIN_SIZE, INSTBIN_SIZE, SAMPLEINST_SIZE, PATTERN_ROWS, PATTERN_BYTES, NUM_PATTERNS_MAX, NUM_CUES, CUE_SIZE, NUM_VOICES, - NOTE_NOP, NOTE_KEYOFF, NOTE_CUT, TAUD_C3, + NOTE_NOP, NOTE_KEYOFF, NOTE_CUT, TAUD_C4, TOP_NONE, TOP_A, TOP_B, TOP_C, TOP_D, TOP_E, TOP_F, TOP_G, TOP_H, TOP_I, TOP_J, TOP_K, TOP_L, TOP_O, TOP_Q, TOP_R, TOP_S, TOP_T, TOP_U, TOP_V, TOP_Y, SEL_SET, SEL_UP, SEL_DOWN, SEL_FINE, @@ -231,7 +231,7 @@ def encode_note(s3m_note: int) -> int: if pitch > 11: return NOTE_NOP semitones = (octave - 4) * 12 + pitch - val = round(TAUD_C3 + semitones * 4096 / 12) + val = round(TAUD_C4 + semitones * 4096 / 12) return max(1, min(0xFFFD, val)) @@ -455,7 +455,9 @@ def build_sample_inst_bin(instruments: list) -> tuple: inst.loop_end = min(inst.loop_end, n) pos += n - # Build instrument bin (256 × 64 bytes) + # Build instrument bin (256 × 192 bytes) + # New layout (terranmon.txt:1997-2070): u32 sample ptr, ..., 25-point envelopes, + # plus a host of optional fields. S3M doesn't supply most of those — they default to 0. inst_bin = bytearray(INSTBIN_SIZE) for i, inst in enumerate(instruments): taud_idx = i + 1 @@ -463,33 +465,37 @@ def build_sample_inst_bin(instruments: list) -> tuple: break if inst is None or inst.itype != S3M_TYPE_PCM: continue - ptr = offsets.get(i, 0) - ptr_lo = ptr & 0xFFFF - ptr_hi = (ptr >> 16) + ptr = offsets.get(i, 0) & 0xFFFFFFFF s_len = min(inst.length, 65535) c2spd = min(inst.c2spd, 65535) ps = 0 ls = min(inst.loop_begin, 65535) le = min(inst.loop_end, 65535) loop_mode = 1 if (inst.flags & 1) else 0 - flags_byte = (ptr_hi << 4) | (loop_mode & 0x3) # hhhh 00pp + flags_byte = loop_mode & 0x3 # 0b 0000 00pp - base = taud_idx * 64 - struct.pack_into(' 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) diff --git a/taud_common.py b/taud_common.py index 2bcec86..a6592db 100644 --- a/taud_common.py +++ b/taud_common.py @@ -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 diff --git a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt index 89523fc..2e3cc68 100644 --- a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt @@ -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) } } diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index 712fce7..b41b66a 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -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 @@ -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, // 12 points, value 0x00-0x3F - var panEnvelopes: Array // 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, // 25 points + var panEnvelopes: Array, // 25 points + var pfEnvelopes: Array, // 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, 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, 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") } }