mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
taut inst: global volume
This commit is contained in:
@@ -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') :
|
||||
''
|
||||
|
||||
41
it2taud.py
41
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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user