taut: IT note fade

This commit is contained in:
minjaesong
2026-06-14 19:35:07 +09:00
parent 62fe227b6b
commit af8dd6aea8
5 changed files with 39 additions and 8 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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 (<eval>:5312:22)

View File

@@ -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

View File

@@ -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)