From 515e0268e6665d470b6bcc8b5e03beb42d4fc5de Mon Sep 17 00:00:00 2001 From: minjaesong Date: Thu, 30 Apr 2026 21:54:11 +0900 Subject: [PATCH] taut inst: global volume --- assets/disk0/tvdos/bin/taut.js | 2 +- it2taud.py | 41 ++++++++---- s3m2taud.py | 1 + terranmon.txt | 28 ++++---- .../torvald/tsvm/peripheral/AudioAdapter.kt | 64 ++++++++++++------- 5 files changed, 88 insertions(+), 48 deletions(-) diff --git a/assets/disk0/tvdos/bin/taut.js b/assets/disk0/tvdos/bin/taut.js index 63e10eb..97f03fd 100644 --- a/assets/disk0/tvdos/bin/taut.js +++ b/assets/disk0/tvdos/bin/taut.js @@ -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') : '' diff --git a/it2taud.py b/it2taud.py index ba6deb1..d2825ae 100644 --- a/it2taud.py +++ b/it2taud.py @@ -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: diff --git a/s3m2taud.py b/s3m2taud.py index 02305ce..f46ea5b 100644 --- a/s3m2taud.py +++ b/s3m2taud.py @@ -624,6 +624,7 @@ def build_sample_inst_bin(instruments: list) -> tuple: struct.pack_into('= 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, // 8 points, value 0x00-0x3F var panEnvelopes: Array // 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)