taut inst: global volume

This commit is contained in:
minjaesong
2026-04-30 21:54:11 +09:00
parent 606fa736af
commit 515e0268e6
5 changed files with 88 additions and 48 deletions

View File

@@ -675,7 +675,7 @@ function drawStatusBar() {
// beat indicator
let beatCursorRow = cursorRow
while (beatCursorRow > beatDivSecondary) { beatCursorRow -= beatDivSecondary } // test this behaviour with primary=4, secondary=22 or something
while (beatCursorRow >= beatDivSecondary) { beatCursorRow -= beatDivSecondary }
let beatInd = (playbackMode != PLAYMODE_NONE && beatCursorRow % beatDivPrimary < (beatDivPrimary >>> 1)) ?
((beatCursorRow % beatDivSecondary < (beatDivPrimary >>> 1)) ? '\u00846u' : '\u00847u') :
''

View File

@@ -559,12 +559,13 @@ def _parse_it_envelope(data: bytes, env_ptr: int, is_pan: bool,
Returns (points_list, sustain_byte) where points_list is a list of
8 (value, minifloat_idx) tuples, or None if envelope not enabled.
sustain_byte: bit7=enabled, bits[5:3]=end_idx, bits[2:0]=start_idx; 0=disabled.
sustain_byte: bit7=enabled (u), bit6=sustain (t: 1=breaks on key-off,
0=loops forever), bits[5:3]=end_idx, bits[2:0]=start_idx; 0=disabled.
IT has two loop types: envelope loop (always on) and sustain loop (breaks on
key-off). Taud only has sustain loop semantics. Priority: sustain > env loop.
When slb==sle the AudioAdapter holds at that node (no cycling); for slb!=sle
it cycles between them.
IT has two loop types: envelope loop (continues forever) and sustain loop
(breaks on key-off). Taud distinguishes them via the 't' flag. Priority
when both exist: sustain (because IT plays sustain while held, then env
loop after release; Taud can only express one).
"""
if env_ptr + 82 > len(data):
return None, 0
@@ -580,17 +581,20 @@ def _parse_it_envelope(data: bytes, env_ptr: int, is_pan: bool,
has_env_loop = bool(flags & 0x02)
has_sus_loop = bool(flags & 0x04)
# Choose which IT loop to map to Taud sustain (priority: sus > env)
# Choose which IT loop to map to Taud (priority: sus > env). The 't' flag
# distinguishes them: t=1 for sustain (breaks on key-off), t=0 for env loop.
if has_sus_loop:
use_lb, use_le = it_slb, it_sle
has_loop = True
is_sustain = True
elif has_env_loop:
use_lb, use_le = it_lpb, it_lpe
has_loop = True
vprint(f" envelope loop mapped as sustain loop (approximation: breaks on key-off)")
is_sustain = False
else:
use_lb = use_le = -1
has_loop = False
is_sustain = False
# Read IT nodes: (int8 value, uint16 tick_pos LE)
nodes = []
@@ -640,10 +644,12 @@ def _parse_it_envelope(data: bytes, env_ptr: int, is_pan: bool,
mf_idx = 0
points.append((taud_val, mf_idx))
# Build sustain byte: bit7=enabled, bits[5:3]=end_idx, bits[2:0]=start_idx.
# 0 = disabled (no bit7). All (slb, sle) pairs including (0,0) are valid when bit7=1.
# Build sustain byte: bit7=enable (u), bit6=sustain (t), bits[5:3]=end,
# bits[2:0]=start. 0=disabled. t=1 → breaks on key-off (IT sustain loop);
# t=0 → loops forever (IT envelope loop).
if taud_slb >= 0 and taud_sle >= 0:
sus_byte = 0x80 | ((taud_sle & 7) << 3) | (taud_slb & 7)
t_bit = 0x40 if is_sustain else 0x00
sus_byte = 0x80 | t_bit | ((taud_sle & 7) << 3) | (taud_slb & 7)
else:
sus_byte = 0
@@ -1126,8 +1132,9 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
envelopes_by_slot: dict = None) -> tuple:
"""samples_or_proxy: list of ITSample | None, indexed 1-based (index 0 unused).
envelopes_by_slot: optional dict mapping taud_slot → (vol_env, vol_sus, pan_env, pan_sus)
where vol_env/pan_env are lists of 8 (value, minifloat_idx) tuples (or None).
envelopes_by_slot: optional dict mapping taud_slot → (vol_env, vol_sus, pan_env, pan_sus, inst_gv)
where vol_env/pan_env are lists of 8 (value, minifloat_idx) tuples (or None),
and inst_gv is instrument global volume (0..255, byte 15).
Returns (bin_bytes[SAMPLEINST_SIZE], offsets_dict).
"""
@@ -1194,9 +1201,10 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
# Write envelope data (new 8-point format)
env_data = envelopes_by_slot.get(taud_idx) if envelopes_by_slot else None
if env_data and env_data[0]:
vol_env, vol_sus, pan_env, pan_sus = env_data
vol_env, vol_sus, pan_env, pan_sus, inst_gv = env_data
inst_bin[base + 13] = vol_sus & 0xFF
inst_bin[base + 14] = pan_sus & 0xFF
inst_bin[base + 15] = inst_gv & 0xFF
for k, (val, mf) in enumerate(vol_env[:8]):
inst_bin[base + 16 + k*2] = val & 0xFF
inst_bin[base + 16 + k*2 + 1] = mf & 0xFF
@@ -1209,7 +1217,9 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
inst_bin[base + 32 + k*2] = 0x80 # pan centre
inst_bin[base + 32 + k*2 + 1] = 0x00 # hold
else:
# No instrument envelope: single-point vol, neutral pan
# No instrument envelope: single-point vol, neutral pan, full gv
inst_gv = env_data[4] if env_data else 255
inst_bin[base + 15] = inst_gv & 0xFF
inst_bin[base + 16] = min(s.vol, 63) # value 0-63
inst_bin[base + 17] = 0 # offset 0 = hold
for k in range(8):
@@ -1484,9 +1494,12 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
proxy[taud_slot] = samples[si]
vol64 = min(inst.canonical_volume, 64)
inst_vols[taud_slot] = min(vol64, 0x3F)
# IT global volume range is 0..128; rescale to Taud's 0..255.
inst_gv_255 = min(255, round(inst.gv * 255 / 128))
envelopes_by_slot[taud_slot] = (
inst.vol_envelope, inst.vol_env_sustain,
inst.pan_envelope, inst.pan_env_sustain,
inst_gv_255,
)
sampleinst_raw, _ = build_sample_inst_bin_it(proxy, envelopes_by_slot)
else:

View File

@@ -624,6 +624,7 @@ def build_sample_inst_bin(instruments: list) -> tuple:
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)
inst_bin[base + 16] = env_vol # volume

View File

@@ -2005,17 +2005,23 @@ Instrument bin: Registry for 256 instruments, formatted as:
0b hhhh 00pp
h: sample pointer high bit
pp: loop mode. 0-no loop, 1-loop, 2-backandforth, 3-oneshot (ignores note length unless overridden by other notes)
Bit8 Volume envelope sustain loops
0b u0 eee sss
s: sustain loop start index
e: sustain loop end index
u: set to enable the loop
Bit8 Panning envelope sustain loops
0b u0 eee sss
s: sustain loop start index
e: sustain loop end index
u: set to enable the loop
Bit8 Reserved
Bit8 Volume envelope sustain/loops
* Sustain is implemented by enabling 't' flag. FastTracker has no 'Sus Loop' but only 'Sus Point'; use same value for start and end index
0b ut eee sss
s: sustain/loop start index
e: sustain/loop end index
t: the loop must sustain (key-off escapes the loop)
u: set to enable the sustain/loop
Bit8 Panning envelope sustain/loops
* Sustain is implemented by enabling 't' flag
0b ut eee sss
s: sustain/loop start index
e: sustain/loop end index
t: the loop must sustain (key-off escapes the loop)
u: set to enable the sustain/loop
Uint8 Instrument Global Volume (0..255)
* ImpulseTracker has range of 0..128; multiply by (255/128) then round to int
* FastTracker2 has range of 0..64; multiply by (255/64) then round to int
Bit16x8 Volume envelopes
Byte 1: Volume (00..3F)
Byte 2: Time until the next point, in seconds (3.5 Unsigned Minifloat). 0 = hold at this point indefinitely.

View File

@@ -1184,17 +1184,26 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
private fun advanceEnvelope(voice: Voice, inst: TaudInst, tickSec: Double) {
// Volume envelope
// sustain byte: bit7=enabled, bits[5:3]=end_idx, bits[2:0]=start_idx
val vSus = inst.volEnvSustain
val vSusOn = (vSus and 0x80) != 0 && !voice.keyOff
val vSusStart = vSus and 7
val vSusEnd = (vSus ushr 3) and 7
// 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
if (voice.envIndex >= 7) {
voice.envVolume = (inst.volEnvelopes[7].value / 63.0).coerceIn(0.0, 1.0)
} else if (vSusOn && voice.envIndex == vSusEnd && vSusStart == vSusEnd) {
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 >= 7) {
voice.envVolume = (inst.volEnvelopes[7].value / 63.0).coerceIn(0.0, 1.0)
} else {
val vOffset = inst.volEnvelopes[voice.envIndex].offset.toDouble()
if (vOffset == 0.0) {
@@ -1217,16 +1226,24 @@ 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 pSusOn = (pSus and 0x80) != 0 && !voice.keyOff
val pSusStart = pSus and 7
val pSusEnd = (pSus ushr 3) and 7
val pSus = inst.panEnvSustain
val pEnabled = (pSus and 0x80) != 0
val pIsSustain = (pSus and 0x40) != 0
val pSusOn = pEnabled && (!pIsSustain || !voice.keyOff)
val pSusStart = pSus and 7
val pSusEnd = (pSus ushr 3) and 7
if (voice.envPanIndex >= 7) {
voice.envPan = inst.panEnvelopes[7].value / 255.0
} else 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
} 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 >= 7) {
voice.envPan = inst.panEnvelopes[7].value / 255.0
} else {
val pOffset = inst.panEnvelopes[voice.envPanIndex].offset.toDouble()
if (pOffset == 0.0) {
@@ -1816,8 +1833,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
val gvol = playhead.globalVolume / 255.0
for (voice in ts.voices) {
if (!voice.active || voice.muted) continue
val s = fetchTrackerSample(voice, instruments[voice.instrumentId])
val vol = voice.envVolume * voice.rowVolume / 63.0 * gvol * playhead.masterVolume / 255.0
val voiceInst = instruments[voice.instrumentId]
val s = fetchTrackerSample(voice, voiceInst)
val instGv = voiceInst.instGlobalVolume / 255.0
val vol = voice.envVolume * voice.rowVolume / 63.0 * gvol * instGv * playhead.masterVolume / 255.0
val pan = if (voice.hasPanEnv) {
val envPanRaw = (voice.envPan * 255.0).roundToInt().coerceIn(0, 255)
(voice.channelPan + envPanRaw - 128).coerceIn(0, 255)
@@ -2314,12 +2333,13 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
var sampleLoopEnd: Int,
// flags
var loopMode: Int,
var volEnvSustain: Int, // byte 13: 00 eee sss (0 = no sustain loop)
var panEnvSustain: Int, // byte 14: 00 eee sss (0 = no sustain loop)
var volEnvSustain: Int, // byte 13: ut eee sss (u=enable, t=sustain (1=breaks on key-off, 0=loops forever))
var panEnvSustain: Int, // byte 14: ut eee sss (u=enable, t=sustain (1=breaks on key-off, 0=loops forever))
var instGlobalVolume: Int, // byte 15: instrument global volume (0..255, 255 = unity)
var volEnvelopes: Array<TaudInstEnvPoint>, // 8 points, value 0x00-0x3F
var panEnvelopes: Array<TaudInstEnvPoint> // 8 points, value 0x00-0xFF (0x80 = centre)
) {
constructor(index: Int) : this(index, 0, 0, 0, 0, 0, 0, 0, 0, 0,
constructor(index: Int) : this(index, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF,
Array(8) { TaudInstEnvPoint(0x3F, ThreeFiveMiniUfloat(0)) },
Array(8) { TaudInstEnvPoint(0x80, ThreeFiveMiniUfloat(0)) })
@@ -2361,7 +2381,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
12 -> (samplePtr.ushr(16).and(15).shl(4) or loopMode.and(3)).toByte()
13 -> volEnvSustain.toByte()
14 -> panEnvSustain.toByte()
15 -> 0
15 -> instGlobalVolume.toByte()
in 16..30 step 2 -> volEnvelopes[(offset - 16) / 2].value.toByte()
in 17..31 step 2 -> volEnvelopes[(offset - 17) / 2].offset.index.toByte()
in 32..46 step 2 -> panEnvelopes[(offset - 32) / 2].value.toByte()
@@ -2396,7 +2416,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
}
13 -> { volEnvSustain = byte }
14 -> { panEnvSustain = byte }
15 -> {}
15 -> { instGlobalVolume = byte and 0xFF }
in 16..30 step 2 -> volEnvelopes[(offset - 16) / 2].value = byte
in 17..31 step 2 -> volEnvelopes[(offset - 17) / 2].offset = ThreeFiveMiniUfloat(byte)