From af8dd6aea8cee5c8f68b2db188be09e63efb9ac5 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sun, 14 Jun 2026 19:35:07 +0900 Subject: [PATCH] taut: IT note fade --- it2taud.py | 11 +++++++--- taud_common.py | 2 +- terranmon.txt | 9 +++++++- .../net/torvald/tsvm/AudioJSR223Delegate.kt | 3 ++- .../torvald/tsvm/peripheral/AudioAdapter.kt | 22 +++++++++++++++++-- 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/it2taud.py b/it2taud.py index 7b46fc1..0511075 100644 --- a/it2taud.py +++ b/it2taud.py @@ -44,7 +44,7 @@ from taud_common import ( TAUD_MAGIC, TAUD_VERSION, TAUD_HEADER_SIZE, TAUD_SONG_ENTRY, SAMPLEBIN_SIZE, INSTBIN_SIZE, SAMPLEINST_SIZE, SAMPLE_LEN_LIMIT, PATTERN_ROWS, PATTERN_BYTES, NUM_PATTERNS_MAX, NUM_CUES, CUE_SIZE, NUM_VOICES, - NOTE_NOP, NOTE_KEYOFF, NOTE_CUT, TAUD_C4, + NOTE_NOP, NOTE_KEYOFF, NOTE_CUT, NOTE_NOTEFADE, TAUD_C4, TOP_NONE, TOP_A, TOP_B, TOP_C, TOP_D, TOP_E, TOP_F, TOP_G, TOP_H, TOP_I, TOP_J, TOP_K, TOP_L, TOP_M, TOP_N, TOP_O, TOP_P, TOP_Q, TOP_R, TOP_S, TOP_T, TOP_U, TOP_V, TOP_W, TOP_Y, SEL_SET, SEL_UP, SEL_DOWN, SEL_FINE, @@ -69,7 +69,7 @@ IT_INST_MAGIC = b'IMPI' IT_NOTE_OFF = 255 IT_NOTE_CUT = 254 -IT_NOTE_FADE = 246 # treated as key-off +IT_NOTE_FADE = 246 # → Taud Note Fade (0x0003): fade by instrument fadeout, sustain kept IT_ORD_END = 255 IT_ORD_SKIP = 254 @@ -709,8 +709,13 @@ def parse_patterns(data: bytes, h: ITHeader) -> list: # ── Note encoding (IT linear 0-119 → Taud pitch units) ─────────────────────── def encode_note_it(it_note: int) -> int: - if it_note == IT_NOTE_OFF or it_note == IT_NOTE_FADE: + if it_note == IT_NOTE_OFF: return NOTE_KEYOFF + if it_note == IT_NOTE_FADE: + # IT "~~~" Note Fade: CHN_NOTEFADE — begins the instrument's volume fadeout + # without releasing the sustain loop (Schism effects.c:1505-1509). Distinct from + # key-off (0x0001), which lifts sustain. Engine handles it via voice.noteFading. + return NOTE_NOTEFADE if it_note == IT_NOTE_CUT: return NOTE_CUT if 0 <= it_note <= 119: diff --git a/taud_common.py b/taud_common.py index 7f7634c..6ef48dd 100644 --- a/taud_common.py +++ b/taud_common.py @@ -99,7 +99,7 @@ SAMPLE_LEN_LIMIT = 65535 NOTE_NOP = 0x0000 NOTE_KEYOFF = 0x0001 NOTE_CUT = 0x0002 -# 0x0003 reserved for Impulse Tracker Note Fade. +NOTE_NOTEFADE = 0x0003 # IT Note Fade ("~~~"): CHN_NOTEFADE — fade by instrument fadeout, sustain kept NOTE_FASTFADE = 0x0004 # ~0.3 s note-fade (SF2 exclusiveClass choke; fluid_voice_kill_excl) TAUD_C4 = 0x5000 # The audio engine's Middle C diff --git a/terranmon.txt b/terranmon.txt index d309f59..c4e9cea 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -2832,7 +2832,14 @@ TODO: - Inst > Gen.2 > filter: IT/SF mode toggle (which also need to redefine slider range and their writebacks as IT takes 8-bit and SF takes 16-bit values) - Samples playblobs: only active for actually playing samples - Samples playcursor: true cursors for actually playing samples - [ ] implement note-fade (0x0003) and wire it to it2taud + [x] implement note-fade (0x0003) and wire it to it2taud + * DONE 2026-06-14. Engine: note word 0x0003 sets voice.noteFading (IT CHN_NOTEFADE, + Schism effects.c:1505-1509) — the instrument's own activeFadeoutStep drives + fadeoutVolume to 0 in the line ~3676 fade path while sustain loop + vol envelope + keep running; no applyKeyLift (sustain kept), no rate override (unlike fast fade + 0x0004); a zero fadeout rings on, matching IT. Honours sub-row S$Dx delay (delayed + dispatch L3453). it2taud: IT_NOTE_FADE (246) now emits NOTE_NOTEFADE (0x0003) + instead of collapsing to key-off; NOTE_NOTEFADE added to taud_common.py. [x] taut.js quit with TypeError: Cannot read property 'terminatorIdx' of undefined: TypeError: Cannot read property 'terminatorIdx' of undefined at drawEnvelopeCursor (:5312:22) diff --git a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt index 53d8773..9687ef3 100644 --- a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt @@ -24,7 +24,8 @@ import net.torvald.tsvm.peripheral.MP2Env * * Note values: 0x4000 = C3 (sample's native pitch), 4096 steps per octave. * Empty row: note = 0x0000 (no trigger). Note sentinels (0x0000..0x001F): 0x0000 = no-op, - * 0x0001 = key-off, 0x0002 = note cut, 0x0010..0x001F = Int0..IntF (reserved interrupts). + * 0x0001 = key-off, 0x0002 = note cut, 0x0003 = note fade (IT-style, by instrument fadeout), + * 0x0004 = fast fade, 0x0010..0x001F = Int0..IntF (reserved interrupts). * Valid playable notes are 0x0020..0xFFFF. All 256 instrument slots (0-255) are valid. * * ## How to upload PCM audio into a playhead diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index ccd732b..536465e 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -2959,8 +2959,25 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { startFastFade(voice, playhead) } } - // 0x0003 (IT-style slow note fade, "~~~~") not yet implemented; 0x0005..0x000F reserved. - in 0x0003..0x000F -> { /* reserved sentinel range, no engine handler */ } + // IT-style note fade ("~~~~"): set the Note-Fade flag (Schism CHN_NOTEFADE, + // effects.c:1505-1509) — the voice's own fadeout step (activeFadeoutStep, the + // instrument's volume fadeout) drives fadeoutVolume to 0 in the line ~3676 fade + // path, while the sustain loop and volume envelope keep running. Unlike KEY_OFF + // (0x0001) it does NOT release sustain (no applyKeyLift); unlike the fast fade + // (0x0004) it does NOT override the fadeout rate. If the instrument's fadeout is + // 0 the note rings on — matches IT, where CHN_NOTEFADE with a zero fadeout + // subtracts nothing. Honours a sub-row S$Dx delay like KEY_OFF / note-cut do. + 0x0003 -> { + val dTick = if ((row.effect == EffectOp.OP_S) && ((row.effectArg ushr 12) and 0xF) == 0xD) + (row.effectArg ushr 8) and 0xF else 0 + if (dTick > 0) { + voice.noteDelayTick = dTick; voice.delayedNote = 0x0003 + voice.delayedInst = 0; voice.delayedVol = -1 + } else { + voice.noteFading = true + } + } + in 0x0005..0x000F -> { /* reserved sentinel range, no engine handler */ } in 0x0010..0x001F -> { /* Int0..IntF: reserved interrupt slots, no engine handler yet */ } else -> { if (toneG && voice.active) { @@ -3451,6 +3468,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { applyKeyLift(voice, instruments[voice.instrumentId]) } 0x0002 -> { voice.active = false; cutLayerChildren(ts, vi) } // delayed note cut + 0x0003 -> voice.noteFading = true // delayed note fade (IT CHN_NOTEFADE) 0x0004 -> startFastFade(voice, playhead) // delayed fast fade else -> { applyDuplicateCheck(ts, vi, voice.delayedInst, voice.delayedNote)