mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
resolving envelope ambiguity
This commit is contained in:
18
it2taud.py
18
it2taud.py
@@ -571,9 +571,10 @@ def _parse_it_envelope(data: bytes, env_ptr: int, kind: str,
|
|||||||
'filter' — IT -32..+32 → Taud 0..255 (0x80 = unity cutoff)
|
'filter' — IT -32..+32 → Taud 0..255 (0x80 = unity cutoff)
|
||||||
|
|
||||||
Word layout (terranmon.txt:2049+ / 2114+):
|
Word layout (terranmon.txt:2049+ / 2114+):
|
||||||
LOOP word: 0b 0000_0sss_ssXcb_eeeee (X = 'p'/'m' for pan/pf, 0 for vol)
|
LOOP word: 0b 00P0_0sss_ssXcb_eeeee (X = 'p'/'m' for pan/pf, 0 for vol)
|
||||||
SUSTAIN word: 0b 0000_0sss_ss00b_eeeee
|
SUSTAIN word: 0b 0000_0sss_ss00b_eeeee
|
||||||
bits 12..8 = start index, bits 4..0 = end index
|
bits 12..8 = start index, bits 4..0 = end index
|
||||||
|
bit 13 = P (envelope present; gates pan/pf evaluation in the engine)
|
||||||
bit 7 = p (pan: use default pan) / m (pf: pitch=0/filter=1) / 0 (vol)
|
bit 7 = p (pan: use default pan) / m (pf: pitch=0/filter=1) / 0 (vol)
|
||||||
bit 6 = c (envelope carry — placed in the LOOP word)
|
bit 6 = c (envelope carry — placed in the LOOP word)
|
||||||
bit 5 = b (enable that region)
|
bit 5 = b (enable that region)
|
||||||
@@ -638,7 +639,10 @@ def _parse_it_envelope(data: bytes, env_ptr: int, kind: str,
|
|||||||
# directly. Bits: 5=b enable, 6=c carry, 7=p (pan default-pan flag) /
|
# directly. Bits: 5=b enable, 6=c carry, 7=p (pan default-pan flag) /
|
||||||
# m (pf filter mode); 12..8=start, 4..0=end. SUSTAIN word never carries
|
# m (pf filter mode); 12..8=start, 4..0=end. SUSTAIN word never carries
|
||||||
# c/p/m — those live in the LOOP word.
|
# c/p/m — those live in the LOOP word.
|
||||||
loop_word = 0
|
# P (bit 13) marks the envelope as present in source, regardless of LOOP/
|
||||||
|
# SUSTAIN enable. We reach this point only when the IT envelope flag bit 0
|
||||||
|
# is set (handled at function top), so P is unconditionally set here.
|
||||||
|
loop_word = 0x2000 # P: envelope present
|
||||||
if has_env_loop and 0 <= it_lpb < 25 and 0 <= it_lpe < 25:
|
if has_env_loop and 0 <= it_lpb < 25 and 0 <= it_lpe < 25:
|
||||||
loop_word |= 0x0020 # b: enable LOOP
|
loop_word |= 0x0020 # b: enable LOOP
|
||||||
loop_word |= (it_lpb & 0x1F) << 8
|
loop_word |= (it_lpb & 0x1F) << 8
|
||||||
@@ -1133,7 +1137,8 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
|||||||
|
|
||||||
# 256-byte instrument layout (terranmon.txt:2001+).
|
# 256-byte instrument layout (terranmon.txt:2001+).
|
||||||
INST_STRIDE = 256
|
INST_STRIDE = 256
|
||||||
USE_ENV_BIT = 0x0020 # b — set whenever the engine should evaluate the envelope
|
USE_ENV_BIT = 0x0020 # b — LOOP wrap enable (legacy; engine still honours)
|
||||||
|
ENV_PRESENT_BIT = 0x2000 # P — envelope present in source (terranmon.txt byte 16/18/20 bit 5)
|
||||||
|
|
||||||
def _write_env(buf: bytearray, base: int, env_pts):
|
def _write_env(buf: bytearray, base: int, env_pts):
|
||||||
"""Write 25 (value, minifloat) pairs starting at `buf[base]`. Pads
|
"""Write 25 (value, minifloat) pairs starting at `buf[base]`. Pads
|
||||||
@@ -1200,7 +1205,8 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
|||||||
# When the source has neither loop nor sustain on the volume envelope
|
# When the source has neither loop nor sustain on the volume envelope
|
||||||
# the engine still needs the b flag so the single-point unit envelope
|
# the engine still needs the b flag so the single-point unit envelope
|
||||||
# is evaluated — synthesise USE_ENV_BIT into the LOOP word as a fallback.
|
# is evaluated — synthesise USE_ENV_BIT into the LOOP word as a fallback.
|
||||||
vol_env_loop = idata.get('vol_env_loop', USE_ENV_BIT)
|
# The P bit is informational for vol but set for consistency.
|
||||||
|
vol_env_loop = idata.get('vol_env_loop', USE_ENV_BIT | ENV_PRESENT_BIT)
|
||||||
vol_env_sus = idata.get('vol_env_sus', 0)
|
vol_env_sus = idata.get('vol_env_sus', 0)
|
||||||
pan_env_loop = idata.get('pan_env_loop', 0)
|
pan_env_loop = idata.get('pan_env_loop', 0)
|
||||||
pan_env_sus = idata.get('pan_env_sus', 0)
|
pan_env_sus = idata.get('pan_env_sus', 0)
|
||||||
@@ -1236,8 +1242,10 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
|||||||
inst_bin[base + 22] = 0
|
inst_bin[base + 22] = 0
|
||||||
# Force engine to use this single point — set the b bit on the LOOP
|
# Force engine to use this single point — set the b bit on the LOOP
|
||||||
# word so the envelope is evaluated even though no wrap region exists.
|
# word so the envelope is evaluated even though no wrap region exists.
|
||||||
|
# P is also set for consistency (vol-env presence is informational
|
||||||
|
# but converters mark it whenever they emit any node data).
|
||||||
cur_loop = struct.unpack_from('<H', inst_bin, base + 15)[0]
|
cur_loop = struct.unpack_from('<H', inst_bin, base + 15)[0]
|
||||||
struct.pack_into('<H', inst_bin, base + 15, cur_loop | USE_ENV_BIT)
|
struct.pack_into('<H', inst_bin, base + 15, cur_loop | USE_ENV_BIT | ENV_PRESENT_BIT)
|
||||||
|
|
||||||
if pan_env:
|
if pan_env:
|
||||||
_write_env(inst_bin, base + 71, pan_env)
|
_write_env(inst_bin, base + 71, pan_env)
|
||||||
|
|||||||
@@ -521,8 +521,10 @@ def build_sample_inst_bin(samples: list) -> tuple:
|
|||||||
# IGV (byte 171) so the envelope must contribute a unit multiplier.
|
# IGV (byte 171) so the envelope must contribute a unit multiplier.
|
||||||
env_vol = 63
|
env_vol = 63
|
||||||
# MOD has no envelopes; vol LOOP word b=1 just so the engine evaluates
|
# MOD has no envelopes; vol LOOP word b=1 just so the engine evaluates
|
||||||
# the unit envelope. Pan/PF stay disabled.
|
# the unit envelope, plus P=1 (envelope present) for consistency with
|
||||||
vol_env_loop = 0x0020 # b enable
|
# the new gate spec (terranmon.txt byte 16/18/20 bit 5). Pan/PF stay
|
||||||
|
# fully zero — the engine sees P=0 there and skips them.
|
||||||
|
vol_env_loop = 0x2020 # P (bit 13) | b (bit 5)
|
||||||
|
|
||||||
base = taud_idx * INST_STRIDE
|
base = taud_idx * INST_STRIDE
|
||||||
struct.pack_into('<I', inst_bin, base + 0, ptr)
|
struct.pack_into('<I', inst_bin, base + 0, ptr)
|
||||||
|
|||||||
@@ -207,9 +207,9 @@ def build_sample_inst_bin() -> bytes:
|
|||||||
struct.pack_into('<H', inst_bin, base + 10, 0) # loop start
|
struct.pack_into('<H', inst_bin, base + 10, 0) # loop start
|
||||||
struct.pack_into('<H', inst_bin, base + 12, len(SQUARE_SAMPLE)) # loop end
|
struct.pack_into('<H', inst_bin, base + 12, len(SQUARE_SAMPLE)) # loop end
|
||||||
inst_bin[base + 14] = 0x01 # forward loop
|
inst_bin[base + 14] = 0x01 # forward loop
|
||||||
struct.pack_into('<H', inst_bin, base + 15, 0x0020) # vol-env enabled
|
struct.pack_into('<H', inst_bin, base + 15, 0x2020) # vol-env: P (bit 13) | b (bit 5)
|
||||||
struct.pack_into('<H', inst_bin, base + 17, 0) # pan-env flags
|
struct.pack_into('<H', inst_bin, base + 17, 0) # pan-env flags (P=0 → mixer skips)
|
||||||
struct.pack_into('<H', inst_bin, base + 19, 0) # pitch-env flags
|
struct.pack_into('<H', inst_bin, base + 19, 0) # pitch-env flags (P=0 → mixer skips)
|
||||||
inst_bin[base + 21] = 63 # vol env pt 0 = full
|
inst_bin[base + 21] = 63 # vol env pt 0 = full
|
||||||
inst_bin[base + 22] = 0
|
inst_bin[base + 22] = 0
|
||||||
inst_bin[base + 171] = 0xA0 # IGV
|
inst_bin[base + 171] = 0xA0 # IGV
|
||||||
|
|||||||
@@ -499,8 +499,11 @@ def build_sample_inst_bin(instruments: list) -> tuple:
|
|||||||
# Volume envelope first point is full-scale; per-sample level is carried
|
# Volume envelope first point is full-scale; per-sample level is carried
|
||||||
# by IGV (byte 171) so the envelope contributes a unit multiplier.
|
# by IGV (byte 171) so the envelope contributes a unit multiplier.
|
||||||
env_vol = 63
|
env_vol = 63
|
||||||
# Vol LOOP word: only b=1 (use envelope) — no actual loop / sustain.
|
# Vol LOOP word: P=1 (envelope present) | b=1 (use envelope) — no actual
|
||||||
vol_env_loop = 0x0020
|
# loop / sustain. P added 2026-05-06 alongside the pan/pf gate spec
|
||||||
|
# change (terranmon.txt byte 16/18/20 bit 5); informational for vol but
|
||||||
|
# set for consistency. Pan/PF stay zero so the engine sees P=0 there.
|
||||||
|
vol_env_loop = 0x2020
|
||||||
|
|
||||||
base = taud_idx * INST_STRIDE
|
base = taud_idx * INST_STRIDE
|
||||||
struct.pack_into('<I', inst_bin, base + 0, ptr) # u32 sample pointer
|
struct.pack_into('<I', inst_bin, base + 0, ptr) # u32 sample pointer
|
||||||
|
|||||||
@@ -2036,6 +2036,36 @@ The b flag is the SOLE enable bit for each region; the historical 't'
|
|||||||
present in this encoding — sustain vs loop is now a structural
|
present in this encoding — sustain vs loop is now a structural
|
||||||
distinction (different word at a different offset), not a flag bit.
|
distinction (different word at a different offset), not a flag bit.
|
||||||
|
|
||||||
|
Envelope PRESENCE — distinct from LOOP/SUSTAIN enable — is signalled by
|
||||||
|
the `P` bit at LOOP-word bit 13 (the high byte's bit 5; offsets 16/18/20
|
||||||
|
bit 5). Added 2026-05-06 to disambiguate two cases that the wrap-enable
|
||||||
|
bits cannot tell apart on their own:
|
||||||
|
P=0: the source had no envelope of this kind. Engine ignores the
|
||||||
|
node array entirely and the mixer skips envelope-driven output
|
||||||
|
for this voice (pan reads from channelPan only, cutoff/pitch
|
||||||
|
reads from sample defaults only). The 25 node slots may still
|
||||||
|
be left as default-fill garbage; nothing reads them.
|
||||||
|
P=1: envelope is defined. Engine evaluates the nodes every tick.
|
||||||
|
Wrap behaviour is independently controlled by LOOP.b and
|
||||||
|
SUSTAIN.b — when both are 0 the envelope walks once forward
|
||||||
|
and holds at its terminator (the IT idiom for envelope-driven
|
||||||
|
decay tails / shaped attacks).
|
||||||
|
The P bit was introduced to fix a gating ambiguity for pan and pitch/
|
||||||
|
filter envelopes: the engine could not distinguish "no envelope at all"
|
||||||
|
(treat as absent) from "envelope present but neither LOOP nor SUSTAIN
|
||||||
|
wrap is enabled" (evaluate and apply, just don't wrap). Volume envelope
|
||||||
|
evaluation has always been unconditional in the engine (a default
|
||||||
|
single-point envelope at value 63 is harmlessly held at unity), so
|
||||||
|
P_vol is currently informational only — converters should still set it
|
||||||
|
when the source defines a volume envelope, for consistency and to
|
||||||
|
support future per-voice gating.
|
||||||
|
|
||||||
|
P is the SOLE presence signal: converters MUST set P=1 whenever they
|
||||||
|
emit envelope nodes, regardless of whether the source enables LOOP or
|
||||||
|
SUSTAIN. Pre-2026-05-06 .taud files predate the P bit and will not have
|
||||||
|
their pan / pf envelopes evaluated by the current engine — re-convert
|
||||||
|
from source.
|
||||||
|
|
||||||
0 Uint32 Sample Pointer
|
0 Uint32 Sample Pointer
|
||||||
4 Uint16 Sample length
|
4 Uint16 Sample length
|
||||||
6 Uint16 Sampling rate at C4 (note number 0x5000)
|
6 Uint16 Sampling rate at C4 (note number 0x5000)
|
||||||
@@ -2061,16 +2091,20 @@ distinction (different word at a different offset), not a flag bit.
|
|||||||
the engine deactivates the voice (player/sndmix.c:493-498). Without this,
|
the engine deactivates the voice (player/sndmix.c:493-498). Without this,
|
||||||
instruments with stored fadeout=0 + envelope ending at 0 would silently
|
instruments with stored fadeout=0 + envelope ending at 0 would silently
|
||||||
hold their voices forever.
|
hold their voices forever.
|
||||||
0b 000_sssss_0cb_eeeee
|
0b 00P_sssss_0cb_eeeee
|
||||||
s (bits 12..8) : loop start index (0..24)
|
s (bits 12..8) : loop start index (0..24)
|
||||||
e (bits 4..0) : loop end index (0..24)
|
e (bits 4..0) : loop end index (0..24)
|
||||||
b (bit 5) : enable the LOOP wrap (0 = envelope walks once to its
|
b (bit 5) : enable the LOOP wrap (0 = envelope walks once to its
|
||||||
terminator and holds; non-zero loops between s and e)
|
terminator and holds; non-zero loops between s and e)
|
||||||
c (bit 6) : envelope carry (cross-trigger envelope position carry)
|
c (bit 6) : envelope carry (cross-trigger envelope position carry)
|
||||||
(bits 7, 13..15 reserved — set to 0)
|
P (bit 13) : envelope present in source (informational for vol —
|
||||||
|
engine evaluates vol env unconditionally; converters
|
||||||
|
should set P=1 when emitting nodes for consistency
|
||||||
|
with pan/pf envelopes, see file-header preamble)
|
||||||
|
(bits 7, 14..15 reserved — set to 0)
|
||||||
17 Bit16 Panning envelope LOOP word
|
17 Bit16 Panning envelope LOOP word
|
||||||
* Always-active wrap region for the pan envelope.
|
* Always-active wrap region for the pan envelope.
|
||||||
0b 000_sssss_pcb_eeeee
|
0b 00P_sssss_pcb_eeeee
|
||||||
s (bits 12..8) : loop start index
|
s (bits 12..8) : loop start index
|
||||||
e (bits 4..0) : loop end index
|
e (bits 4..0) : loop end index
|
||||||
b (bit 5) : enable the LOOP
|
b (bit 5) : enable the LOOP
|
||||||
@@ -2079,16 +2113,28 @@ distinction (different word at a different offset), not a flag bit.
|
|||||||
Independent of LOOP enable; the engine reads this bit
|
Independent of LOOP enable; the engine reads this bit
|
||||||
from the LOOP word as the canonical home for envelope-
|
from the LOOP word as the canonical home for envelope-
|
||||||
level meta flags.
|
level meta flags.
|
||||||
(bits 13..15 reserved)
|
P (bit 13) : envelope present in source. Gates whether the mixer
|
||||||
|
applies envelope-driven pan at all. P=0 ⇒ mixer uses
|
||||||
|
channelPan only and the node array is ignored. P=1 ⇒
|
||||||
|
evaluate every tick, even when both LOOP.b and SUSTAIN.b
|
||||||
|
are 0 (envelope walks once and holds — IT pan-env
|
||||||
|
flag=0x01 idiom).
|
||||||
|
(bits 14..15 reserved)
|
||||||
19 Bit16 Pitch/Filter envelope LOOP word
|
19 Bit16 Pitch/Filter envelope LOOP word
|
||||||
* Always-active wrap region for the pitch/filter envelope.
|
* Always-active wrap region for the pitch/filter envelope.
|
||||||
0b 000_sssss_mcb_eeeee
|
0b 00P_sssss_mcb_eeeee
|
||||||
s (bits 12..8) : loop start index
|
s (bits 12..8) : loop start index
|
||||||
e (bits 4..0) : loop end index
|
e (bits 4..0) : loop end index
|
||||||
b (bit 5) : enable the LOOP
|
b (bit 5) : enable the LOOP
|
||||||
c (bit 6) : envelope carry
|
c (bit 6) : envelope carry
|
||||||
m (bit 7) : mode — 0 = pitch envelope, 1 = filter envelope
|
m (bit 7) : mode — 0 = pitch envelope, 1 = filter envelope
|
||||||
(bits 13..15 reserved)
|
P (bit 13) : envelope present in source. Same semantics as the
|
||||||
|
pan envelope's P bit: gates whether the mixer applies
|
||||||
|
envelope-driven pitch / cutoff at all. P=0 ⇒ no
|
||||||
|
envelope contribution (sample plays at its own pitch /
|
||||||
|
default cutoff). P=1 ⇒ evaluate every tick regardless
|
||||||
|
of LOOP.b / SUSTAIN.b.
|
||||||
|
(bits 14..15 reserved)
|
||||||
21 Bit16x25 Volume envelopes
|
21 Bit16x25 Volume envelopes
|
||||||
Byte 1: Volume (00..3F)
|
Byte 1: Volume (00..3F)
|
||||||
Byte 2: Time until the next point, in seconds (3.5 Unsigned Minifloat). 0 = hold at this point indefinitely.
|
Byte 2: Time until the next point, in seconds (3.5 Unsigned Minifloat). 0 = hold at this point indefinitely.
|
||||||
@@ -2270,25 +2316,17 @@ TODO:
|
|||||||
skipped because vEnvActive required either b bit. Now evaluation
|
skipped because vEnvActive required either b bit. Now evaluation
|
||||||
is gated only by voice.volEnvOn (matches CHN_VOLENV in Schism).
|
is gated only by voice.volEnvOn (matches CHN_VOLENV in Schism).
|
||||||
See byte 15 spec for the LOOP word.
|
See byte 15 spec for the LOOP word.
|
||||||
[ ] Same gate fix needed for pan and pitch/filter envelopes? Currently
|
[x] Same gate fix needed for pan and pitch/filter envelopes.
|
||||||
advanceEnvelope/advancePfEnvelope still require LOOP-b OR SUSTAIN-b
|
Resolution (2026-05-06): added P (envelope present) bit at LOOP-word
|
||||||
before evaluating, AND the same condition feeds voice.hasPanEnv /
|
bit 13 (offsets 16/18/20 bit 5) for all three envelopes. Engine
|
||||||
voice.hasPfEnv which the mixer uses to decide whether to apply
|
gates pan/pf envelope evaluation on P alone; converters set P=1
|
||||||
envelope-driven pan / cutoff at all. The simple "drop the gate"
|
whenever they emit envelope nodes, regardless of LOOP/SUSTAIN
|
||||||
treatment that worked for vol env doesn't transfer cleanly: an
|
enable, so an enabled-no-wrap envelope (IT pan-env flag=0x01)
|
||||||
absent pan/pf envelope (FT2 default, no env at all) needs to look
|
animates correctly. Mixer's hasPanEnv/hasPfEnv read the same gate,
|
||||||
different from an enabled-no-wrap envelope so the mixer can ignore
|
so absent envelopes still bypass envelope-driven output. Pre-
|
||||||
the absent case. Options:
|
2026-05-06 .taud files predate the P bit and need re-conversion
|
||||||
(a) Distinguish via a new format bit (e.g. byte 15/17/19 bit 7
|
for pan/pf envelopes to play. See byte 15/17/19 spec for the LOOP
|
||||||
for vol/pan, but bit 7 of pf already carries 'm' filter mode).
|
word bit layout.
|
||||||
(b) Content-based detection at note trigger: envelope is "present"
|
|
||||||
if any node has non-default value or non-zero offset.
|
|
||||||
(c) Make the converters write a dedicated "envelope present"
|
|
||||||
sentinel (e.g. start>end in the LOOP word) that the engine
|
|
||||||
recognises as evaluate-but-don't-wrap.
|
|
||||||
Until decided, IT pan/pf envelopes with flags=0x01 will not animate
|
|
||||||
between rows. Workaround: enable IT's envelope loop or sustain bit
|
|
||||||
in source so the converter sets the LOOP/SUSTAIN b bit.
|
|
||||||
[ ] implement extended tone mode (MONOTONE compat)
|
[ ] implement extended tone mode (MONOTONE compat)
|
||||||
[ ] pattern loops stops working after processed once (test with slumberjack.xm)
|
[ ] pattern loops stops working after processed once (test with slumberjack.xm)
|
||||||
[ ] milkytracker-style volume ramping (on sample-end only)
|
[ ] milkytracker-style volume ramping (on sample-end only)
|
||||||
|
|||||||
@@ -1241,6 +1241,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Envelope-present test (terranmon.txt byte 15/17/19, P bit at LOOP word bit 13).
|
||||||
|
// The P bit is the sole presence signal — converters set it whenever they emit
|
||||||
|
// envelope nodes. Pre-2026-05-06 .taud files without P will not have pan/pf
|
||||||
|
// envelopes evaluated; re-convert from source.
|
||||||
|
private inline fun envPresent(loopWord: Int): Boolean = ((loopWord ushr 13) and 1) != 0
|
||||||
|
|
||||||
// Reusable per-envelope wrap-range scratch (avoid per-tick allocation).
|
// Reusable per-envelope wrap-range scratch (avoid per-tick allocation).
|
||||||
private val volWrap = IntArray(2)
|
private val volWrap = IntArray(2)
|
||||||
private val panWrap = IntArray(2)
|
private val panWrap = IntArray(2)
|
||||||
@@ -1305,10 +1311,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pan envelope (only when active for this instrument)
|
// Pan envelope. Presence is decided once per trigger and stored on the voice
|
||||||
|
// (voice.hasPanEnv is keyed on LOOP.P — see triggerNote). Like the volume
|
||||||
|
// envelope above, evaluation is no longer gated by the wrap-enable bits: an
|
||||||
|
// envelope marked "present but no wrap" still walks forward, matching the IT
|
||||||
|
// idiom (pan-env flag=0x01) and Schism player/sndmix.c:470-502.
|
||||||
if (!voice.hasPanEnv || !voice.panEnvOn) return
|
if (!voice.hasPanEnv || !voice.panEnvOn) return
|
||||||
val pEnvActive = (((inst.panEnvLoop ushr 5) and 1) or ((inst.panEnvSustainWord ushr 5) and 1)) != 0
|
|
||||||
if (!pEnvActive) return
|
|
||||||
resolveEnvWrap(inst.panEnvLoop, inst.panEnvSustainWord, voice.keyOff, panWrap)
|
resolveEnvWrap(inst.panEnvLoop, inst.panEnvSustainWord, voice.keyOff, panWrap)
|
||||||
val pStart = panWrap[0]
|
val pStart = panWrap[0]
|
||||||
val pEnd = panWrap[1]
|
val pEnd = panWrap[1]
|
||||||
@@ -1348,10 +1356,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
* as advanceEnvelope. Result is stored in `voice.envPfValue` (0.0..1.0; 0.5 = unity).
|
* as advanceEnvelope. Result is stored in `voice.envPfValue` (0.0..1.0; 0.5 = unity).
|
||||||
*/
|
*/
|
||||||
private fun advancePfEnvelope(voice: Voice, inst: TaudInst, tickSec: Double) {
|
private fun advancePfEnvelope(voice: Voice, inst: TaudInst, tickSec: Double) {
|
||||||
|
// Same gate semantics as the pan envelope above: presence (voice.hasPfEnv) is
|
||||||
|
// latched at trigger time from LOOP.P; evaluation is unconditional once
|
||||||
|
// present, so an enabled-no-wrap envelope animates.
|
||||||
if (!voice.hasPfEnv || !voice.pfEnvOn) return
|
if (!voice.hasPfEnv || !voice.pfEnvOn) return
|
||||||
val maxIdx = 24
|
val maxIdx = 24
|
||||||
val pEnvActive = (((inst.pfEnvLoop ushr 5) and 1) or ((inst.pfEnvSustainWord ushr 5) and 1)) != 0
|
|
||||||
if (!pEnvActive) return
|
|
||||||
resolveEnvWrap(inst.pfEnvLoop, inst.pfEnvSustainWord, voice.keyOff, pfWrap)
|
resolveEnvWrap(inst.pfEnvLoop, inst.pfEnvSustainWord, voice.keyOff, pfWrap)
|
||||||
val pSusStart = pfWrap[0]
|
val pSusStart = pfWrap[0]
|
||||||
val pSusEnd = pfWrap[1]
|
val pSusEnd = pfWrap[1]
|
||||||
@@ -1623,10 +1632,14 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
voice.envPanIndex = 0
|
voice.envPanIndex = 0
|
||||||
voice.envPanTimeSec = 0.0
|
voice.envPanTimeSec = 0.0
|
||||||
voice.envPan = inst.panEnvelopes[0].value / 255.0
|
voice.envPan = inst.panEnvelopes[0].value / 255.0
|
||||||
// Pan envelope is active when EITHER the LOOP word's b bit OR the SUSTAIN word's b bit is set.
|
// Envelope-present gate (added 2026-05-06). Driven by the P bit at LOOP-word
|
||||||
voice.hasPanEnv = (((inst.panEnvLoop ushr 5) and 1) or ((inst.panEnvSustainWord ushr 5) and 1)) != 0
|
// bit 13 (high byte's bit 5; offsets 16/18/20 bit 5), set by converters
|
||||||
|
// whenever they emit envelope nodes. See terranmon.txt at byte 15/17/19 for
|
||||||
|
// the bit layout and the file-header preamble for the presence-vs-wrap
|
||||||
|
// distinction.
|
||||||
|
voice.hasPanEnv = envPresent(inst.panEnvLoop)
|
||||||
// Pitch/filter envelope state.
|
// Pitch/filter envelope state.
|
||||||
voice.hasPfEnv = (((inst.pfEnvLoop ushr 5) and 1) or ((inst.pfEnvSustainWord ushr 5) and 1)) != 0
|
voice.hasPfEnv = envPresent(inst.pfEnvLoop)
|
||||||
// The pf 'm' mode bit (pitch=0, filter=1) lives in the LOOP word at bit 7.
|
// The pf 'm' mode bit (pitch=0, filter=1) lives in the LOOP word at bit 7.
|
||||||
voice.envPfIsFilter = (inst.pfEnvLoop ushr 7) and 1 != 0
|
voice.envPfIsFilter = (inst.pfEnvLoop ushr 7) and 1 != 0
|
||||||
voice.envPfIndex = 0
|
voice.envPfIndex = 0
|
||||||
|
|||||||
14
xm2taud.py
14
xm2taud.py
@@ -819,7 +819,11 @@ def _xm_envelope_to_taud(env_pts: list, num_pts: int, env_type: int,
|
|||||||
# LOOP word (offsets 15/17/19): b=enable, bits 12..8=start, 4..0=end.
|
# LOOP word (offsets 15/17/19): b=enable, bits 12..8=start, 4..0=end.
|
||||||
# SUSTAIN word (offsets 189/191/193): same bit layout; FT2 single-point
|
# SUSTAIN word (offsets 189/191/193): same bit layout; FT2 single-point
|
||||||
# sustain is encoded with start == end (engine wraps that index → itself).
|
# sustain is encoded with start == end (engine wraps that index → itself).
|
||||||
loop_word = 0x0020 # b: use envelope (vol always; even with no loop the engine evaluates it)
|
# P (bit 13) marks the envelope as present in source — this branch is only
|
||||||
|
# reached when XM_ENV_ON is set, so P is unconditionally 1 here. P gates
|
||||||
|
# whether the engine evaluates pan envelope at all (terranmon.txt byte
|
||||||
|
# 16/18/20 bit 5); for vol it is informational.
|
||||||
|
loop_word = 0x2020 # P (bit 13) | b (bit 5)
|
||||||
if has_loop:
|
if has_loop:
|
||||||
loop_word |= (loop_start & 0x1F) << 8
|
loop_word |= (loop_start & 0x1F) << 8
|
||||||
loop_word |= (loop_end & 0x1F)
|
loop_word |= (loop_end & 0x1F)
|
||||||
@@ -931,7 +935,8 @@ def build_sample_inst_bin_xm(proxies: list) -> tuple:
|
|||||||
s.loop_end = min(s.loop_end, n)
|
s.loop_end = min(s.loop_end, n)
|
||||||
pos += n
|
pos += n
|
||||||
|
|
||||||
USE_ENV_BIT = 0x0020 # b: engine should evaluate the envelope
|
USE_ENV_BIT = 0x0020 # b: engine should evaluate the envelope (LOOP wrap enable)
|
||||||
|
ENV_PRESENT_BIT = 0x2000 # P: envelope present in source (terranmon.txt byte 16/18/20 bit 5)
|
||||||
INST_STRIDE = 256
|
INST_STRIDE = 256
|
||||||
|
|
||||||
def _write_env(buf: bytearray, base: int, env_pts, pad_value: int) -> None:
|
def _write_env(buf: bytearray, base: int, env_pts, pad_value: int) -> None:
|
||||||
@@ -962,13 +967,14 @@ def build_sample_inst_bin_xm(proxies: list) -> tuple:
|
|||||||
|
|
||||||
# Resolve envelope LOOP / SUSTAIN words from the proxy. When XM has no
|
# Resolve envelope LOOP / SUSTAIN words from the proxy. When XM has no
|
||||||
# envelope, fall back to a single-point unit envelope (vol LOOP word
|
# envelope, fall back to a single-point unit envelope (vol LOOP word
|
||||||
# b=1 only) and rely on IGV for level.
|
# b=1 plus P=1 for consistency) and rely on IGV for level. Pan stays
|
||||||
|
# zero so the engine sees P=0 there and skips envelope-driven pan.
|
||||||
if s.vol_env_pts is not None:
|
if s.vol_env_pts is not None:
|
||||||
vol_env_loop = s.vol_env_loop_word
|
vol_env_loop = s.vol_env_loop_word
|
||||||
vol_env_sus = s.vol_env_sus_word
|
vol_env_sus = s.vol_env_sus_word
|
||||||
vol_env = s.vol_env_pts
|
vol_env = s.vol_env_pts
|
||||||
else:
|
else:
|
||||||
vol_env_loop = USE_ENV_BIT
|
vol_env_loop = USE_ENV_BIT | ENV_PRESENT_BIT
|
||||||
vol_env_sus = 0
|
vol_env_sus = 0
|
||||||
vol_env = None
|
vol_env = None
|
||||||
if s.pan_env_pts is not None:
|
if s.pan_env_pts is not None:
|
||||||
|
|||||||
Reference in New Issue
Block a user