mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-08 22:34:03 +09:00
IT SusLoop
This commit is contained in:
39
it2taud.py
39
it2taud.py
@@ -84,6 +84,7 @@ IT_SMP_COMPRESSED = 0x08
|
|||||||
IT_SMP_LOOP = 0x10
|
IT_SMP_LOOP = 0x10
|
||||||
IT_SMP_SUS_LOOP = 0x20
|
IT_SMP_SUS_LOOP = 0x20
|
||||||
IT_SMP_PINGPONG = 0x40
|
IT_SMP_PINGPONG = 0x40
|
||||||
|
IT_SMP_PINGPONG_SUS = 0x80
|
||||||
|
|
||||||
# Vol-column byte ranges (inclusive lower, inclusive upper)
|
# Vol-column byte ranges (inclusive lower, inclusive upper)
|
||||||
VC_VOL_LO, VC_VOL_HI = 0, 64
|
VC_VOL_LO, VC_VOL_HI = 0, 64
|
||||||
@@ -438,6 +439,8 @@ def parse_samples(data: bytes, h: ITHeader, decompress: bool) -> list:
|
|||||||
s.length = len(s.sample_data)
|
s.length = len(s.sample_data)
|
||||||
s.loop_beg = min(s.loop_beg, s.length)
|
s.loop_beg = min(s.loop_beg, s.length)
|
||||||
s.loop_end = min(s.loop_end, s.length)
|
s.loop_end = min(s.loop_end, s.length)
|
||||||
|
s.sus_beg = min(s.sus_beg, s.length)
|
||||||
|
s.sus_end = min(s.sus_end, s.length)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
vprint(f" warning: '{s.name}' decompression failed ({e}), silent")
|
vprint(f" warning: '{s.name}' decompression failed ({e}), silent")
|
||||||
else:
|
else:
|
||||||
@@ -452,6 +455,8 @@ def parse_samples(data: bytes, h: ITHeader, decompress: bool) -> list:
|
|||||||
s.length = len(s.sample_data)
|
s.length = len(s.sample_data)
|
||||||
s.loop_beg = min(s.loop_beg, s.length)
|
s.loop_beg = min(s.loop_beg, s.length)
|
||||||
s.loop_end = min(s.loop_end, s.length)
|
s.loop_end = min(s.loop_end, s.length)
|
||||||
|
s.sus_beg = min(s.sus_beg, s.length)
|
||||||
|
s.sus_end = min(s.sus_end, s.length)
|
||||||
samples.append(s)
|
samples.append(s)
|
||||||
return samples
|
return samples
|
||||||
|
|
||||||
@@ -1128,6 +1133,8 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
|||||||
s.length = len(new_data)
|
s.length = len(new_data)
|
||||||
s.loop_beg = max(0, int(s.loop_beg * ratio))
|
s.loop_beg = max(0, int(s.loop_beg * ratio))
|
||||||
s.loop_end = max(0, min(int(s.loop_end * ratio), s.length))
|
s.loop_end = max(0, min(int(s.loop_end * ratio), s.length))
|
||||||
|
s.sus_beg = max(0, int(s.sus_beg * ratio))
|
||||||
|
s.sus_end = max(0, min(int(s.sus_end * ratio), s.length))
|
||||||
s.c5_speed = max(1, int(s.c5_speed * ratio))
|
s.c5_speed = max(1, int(s.c5_speed * ratio))
|
||||||
|
|
||||||
sample_bin = bytearray(SAMPLEBIN_SIZE)
|
sample_bin = bytearray(SAMPLEBIN_SIZE)
|
||||||
@@ -1142,7 +1149,9 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
|||||||
offsets[idx] = pos
|
offsets[idx] = pos
|
||||||
if n < len(s.sample_data):
|
if n < len(s.sample_data):
|
||||||
vprint(f" warning: '{s.name}' truncated {len(s.sample_data)} → {n}")
|
vprint(f" warning: '{s.name}' truncated {len(s.sample_data)} → {n}")
|
||||||
s.length = n; s.loop_end = min(s.loop_end, n)
|
s.length = n
|
||||||
|
s.loop_end = min(s.loop_end, n)
|
||||||
|
s.sus_end = min(s.sus_end, n)
|
||||||
pos += n
|
pos += n
|
||||||
|
|
||||||
# 192-byte instrument layout (terranmon.txt:1997-2070).
|
# 192-byte instrument layout (terranmon.txt:1997-2070).
|
||||||
@@ -1168,15 +1177,33 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
|||||||
ptr = offsets.get(i, 0) & 0xFFFFFFFF
|
ptr = offsets.get(i, 0) & 0xFFFFFFFF
|
||||||
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)
|
# Sustain loop wins over the regular loop because Taud carries one loop
|
||||||
le = min(s.loop_end, 65535)
|
# region. After key-off the engine drops the loop entirely (terranmon.txt:2007).
|
||||||
if s.has_loop and (s.flags & IT_SMP_PINGPONG):
|
if s.flags & IT_SMP_SUS_LOOP:
|
||||||
loop_mode = 2 # backandforth
|
ls = min(s.sus_beg, 65535)
|
||||||
|
le = min(s.sus_end, 65535)
|
||||||
|
sustain_bit = 0x4
|
||||||
|
pingpong_active = bool(s.flags & IT_SMP_PINGPONG_SUS)
|
||||||
|
has_active_loop = True
|
||||||
elif s.has_loop:
|
elif s.has_loop:
|
||||||
|
ls = min(s.loop_beg, 65535)
|
||||||
|
le = min(s.loop_end, 65535)
|
||||||
|
sustain_bit = 0x0
|
||||||
|
pingpong_active = bool(s.flags & IT_SMP_PINGPONG)
|
||||||
|
has_active_loop = True
|
||||||
|
else:
|
||||||
|
ls = min(s.loop_beg, 65535)
|
||||||
|
le = min(s.loop_end, 65535)
|
||||||
|
sustain_bit = 0x0
|
||||||
|
pingpong_active = False
|
||||||
|
has_active_loop = False
|
||||||
|
if has_active_loop and pingpong_active:
|
||||||
|
loop_mode = 2 # backandforth
|
||||||
|
elif has_active_loop:
|
||||||
loop_mode = 1 # forward loop
|
loop_mode = 1 # forward loop
|
||||||
else:
|
else:
|
||||||
loop_mode = 0 # no loop
|
loop_mode = 0 # no loop
|
||||||
flags_byte = loop_mode & 0x3
|
flags_byte = (loop_mode & 0x3) | sustain_bit
|
||||||
|
|
||||||
base = taud_idx * 192
|
base = taud_idx * 192
|
||||||
struct.pack_into('<I', inst_bin, base + 0, ptr)
|
struct.pack_into('<I', inst_bin, base + 0, ptr)
|
||||||
|
|||||||
@@ -2095,7 +2095,8 @@ TODO:
|
|||||||
[x] `S B000` and `S B100` not working as intended -- on first playback it jumps to the next cue same row, on subsequent playbacks the commands are completely ignored
|
[x] `S B000` and `S B100` not working as intended -- on first playback it jumps to the next cue same row, on subsequent playbacks the commands are completely ignored
|
||||||
[x] implement S6x command
|
[x] implement S6x command
|
||||||
[x] implement Wxx command (global volume slide)
|
[x] implement Wxx command (global volume slide)
|
||||||
[ ] implement sample loop sustain
|
[x] implement sample loop sustain
|
||||||
|
"Caveat: on a foreground voice, key-off (row.note == 0x0000) currently sets voice.active = false at AudioAdapter.kt:1713, which silences the channel immediately. Sustain-loop escape therefore only takes effect on background voices spawned by NNA "Note Off" — which matches the IT idiom of layering a new note over a sustained one. Let me know if you also want the foreground key-off to keep the voice playing through fadeout."
|
||||||
[ ] cue and pattern compression of the Taud format (taud_common.py, taud.mjs)
|
[ ] cue and pattern compression of the Taud format (taud_common.py, taud.mjs)
|
||||||
[ ] figure out how IT (8 bits) and FT2 (12 bits) handles volume fadeout numbers, and come up with a compatible Taud spec, then implement
|
[ ] figure out how IT (8 bits) and FT2 (12 bits) handles volume fadeout numbers, and come up with a compatible Taud spec, then implement
|
||||||
[ ] implement bitcrusher (eff sym '8')
|
[ ] implement bitcrusher (eff sym '8')
|
||||||
|
|||||||
@@ -1454,7 +1454,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
|
|
||||||
if (voice.forward) {
|
if (voice.forward) {
|
||||||
voice.samplePos += voice.playbackRate
|
voice.samplePos += voice.playbackRate
|
||||||
when (inst.loopMode) {
|
// When the sustain bit is set, key-off escapes the loop: the sample plays past
|
||||||
|
// loopEnd until it ends naturally (loopMode 0 semantics).
|
||||||
|
val effectiveLoopMode =
|
||||||
|
if (inst.sampleLoopSustain && voice.keyOff) 0 else (inst.loopMode and 3)
|
||||||
|
when (effectiveLoopMode) {
|
||||||
0 -> if (voice.samplePos >= sampleLen) voice.active = false
|
0 -> if (voice.samplePos >= sampleLen) voice.active = false
|
||||||
1 -> if (voice.samplePos >= loopEnd) voice.samplePos -= (loopEnd - loopStart).coerceAtLeast(1.0)
|
1 -> if (voice.samplePos >= loopEnd) voice.samplePos -= (loopEnd - loopStart).coerceAtLeast(1.0)
|
||||||
2 -> if (voice.samplePos >= loopEnd) { voice.samplePos = loopEnd; voice.forward = false }
|
2 -> if (voice.samplePos >= loopEnd) { voice.samplePos = loopEnd; voice.forward = false }
|
||||||
@@ -1836,7 +1840,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
val arg = resolveArg(rawArg, voice.mem.o).also { if (rawArg != 0) voice.mem.o = it }
|
val arg = resolveArg(rawArg, voice.mem.o).also { if (rawArg != 0) voice.mem.o = it }
|
||||||
val inst = instruments[voice.instrumentId]
|
val inst = instruments[voice.instrumentId]
|
||||||
var off = arg
|
var off = arg
|
||||||
if (inst.loopMode != 0 && inst.sampleLoopEnd > inst.sampleLoopStart && off > inst.sampleLoopEnd) {
|
if ((inst.loopMode and 3) != 0 && inst.sampleLoopEnd > inst.sampleLoopStart && off > inst.sampleLoopEnd) {
|
||||||
val loopLen = (inst.sampleLoopEnd - inst.sampleLoopStart).coerceAtLeast(1)
|
val loopLen = (inst.sampleLoopEnd - inst.sampleLoopStart).coerceAtLeast(1)
|
||||||
off = inst.sampleLoopStart + ((off - inst.sampleLoopStart) % loopLen)
|
off = inst.sampleLoopStart + ((off - inst.sampleLoopStart) % loopLen)
|
||||||
}
|
}
|
||||||
@@ -2924,7 +2928,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
var samplePlayStart: Int,
|
var samplePlayStart: Int,
|
||||||
var sampleLoopStart: Int,
|
var sampleLoopStart: Int,
|
||||||
var sampleLoopEnd: Int,
|
var sampleLoopEnd: Int,
|
||||||
var loopMode: Int, // byte 14, low 2 bits
|
var loopMode: Int, // byte 14, low 3 bits (bits 0-1: loop kind, bit 2: sustain)
|
||||||
var volEnvSustain: Int, // bytes 15-16 (16-bit, see flag layout)
|
var volEnvSustain: Int, // bytes 15-16 (16-bit, see flag layout)
|
||||||
var panEnvSustain: Int, // bytes 17-18
|
var panEnvSustain: Int, // bytes 17-18
|
||||||
var pfEnvSustain: Int, // bytes 19-20 (pitch/filter)
|
var pfEnvSustain: Int, // bytes 19-20 (pitch/filter)
|
||||||
@@ -2957,6 +2961,9 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
0, 0, 0, 0
|
0, 0, 0, 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** Sample-flag byte 14 bit 2 — when set, the sample loop is a sustain loop:
|
||||||
|
* it loops while the note is held and is escaped on key-off. */
|
||||||
|
val sampleLoopSustain: Boolean get() = (loopMode and 0x04) != 0
|
||||||
/** New note action — instrumentFlag bits 0-1.
|
/** New note action — instrumentFlag bits 0-1.
|
||||||
* 0=note off, 1=note cut, 2=continue, 3=note fade. */
|
* 0=note off, 1=note cut, 2=continue, 3=note fade. */
|
||||||
val newNoteAction: Int get() = instrumentFlag and 0x03
|
val newNoteAction: Int get() = instrumentFlag and 0x03
|
||||||
@@ -3019,7 +3026,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
12 -> sampleLoopEnd.toByte()
|
12 -> sampleLoopEnd.toByte()
|
||||||
13 -> sampleLoopEnd.ushr(8).toByte()
|
13 -> sampleLoopEnd.ushr(8).toByte()
|
||||||
|
|
||||||
14 -> (loopMode and 3).toByte()
|
14 -> (loopMode and 7).toByte()
|
||||||
15 -> volEnvSustain.toByte()
|
15 -> volEnvSustain.toByte()
|
||||||
16 -> volEnvSustain.ushr(8).toByte()
|
16 -> volEnvSustain.ushr(8).toByte()
|
||||||
17 -> panEnvSustain.toByte()
|
17 -> panEnvSustain.toByte()
|
||||||
@@ -3074,7 +3081,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
12 -> { sampleLoopEnd = (sampleLoopEnd and 0xff00) or byte }
|
12 -> { sampleLoopEnd = (sampleLoopEnd and 0xff00) or byte }
|
||||||
13 -> { sampleLoopEnd = (sampleLoopEnd and 0x00ff) or (byte shl 8) }
|
13 -> { sampleLoopEnd = (sampleLoopEnd and 0x00ff) or (byte shl 8) }
|
||||||
|
|
||||||
14 -> { loopMode = byte and 3 }
|
14 -> { loopMode = byte and 7 }
|
||||||
15 -> { volEnvSustain = (volEnvSustain and 0xff00) or byte }
|
15 -> { volEnvSustain = (volEnvSustain and 0xff00) or byte }
|
||||||
16 -> { volEnvSustain = (volEnvSustain and 0x00ff) or (byte shl 8) }
|
16 -> { volEnvSustain = (volEnvSustain and 0x00ff) or (byte shl 8) }
|
||||||
17 -> { panEnvSustain = (panEnvSustain and 0xff00) or byte }
|
17 -> { panEnvSustain = (panEnvSustain and 0xff00) or byte }
|
||||||
|
|||||||
Reference in New Issue
Block a user