From ddeab1c78254162fed074e9184d6f768c2528383 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sat, 9 May 2026 21:17:49 +0900 Subject: [PATCH] taud bugfix --- mod2taud.py | 20 ++++++++++++++ terranmon.txt | 4 +-- .../torvald/tsvm/peripheral/AudioAdapter.kt | 27 +++++++++++++++---- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/mod2taud.py b/mod2taud.py index c2fe453..0c43b1a 100644 --- a/mod2taud.py +++ b/mod2taud.py @@ -182,6 +182,26 @@ def parse_mod(data: bytes): inst = (b0 & 0xF0) | ((b2 >> 4) & 0x0F) effect = b2 & 0x0F arg = b3 + # MT-style PT-strict cell rewrites (LoaderMOD.cpp:354-365): + # PT does not recall arg for portamento up/down (1xx, 2xx) or + # volume slide (Axx); the literal arg is read every tick. The + # vol-slide nibbles in 5xx/6xx likewise take literal args, with + # the recalled state living in the porta/vibrato side. So a + # zero-arg cell decays to a no-slide variant: 1/2/A drop to + # no-op, 5 collapses to bare tone-porta (3), 6 to bare vibrato + # (4). Without this, resolve_pt_recalls would back-fill these + # zero args from the cohort memory and produce a continuous + # slide where PT plays a single-row swell (canonical bug: + # GSLINGER ord 0x03 ch1 — `5 01` on r30/r38 with `5 00` on the + # rest, was fading 24→0 in 5 rows instead of stair-stepping + # 24→14 across 16 rows). + if arg == 0: + if effect in (0x1, 0x2, 0xA): + effect = 0x0 + elif effect == 0x5: + effect = 0x3 + elif effect == 0x6: + effect = 0x4 cell = grid[ch][r] cell.period = period cell.inst = inst diff --git a/terranmon.txt b/terranmon.txt index 6459e28..1e8dd9c 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -2394,8 +2394,8 @@ TODO: when no V column is present. Engine + all four `*2taud` converters updated; legacy `.taud` files (byte 196 == 0) fall back to the previous "row volume default = 63" behaviour. - [ ] Physical Presence order 0x1F chn 2: note cuts unexpectedly fast? - GSLINGER order 0x03 chn 1: L 0100 fades unexpectedly fast? + [x] physical_presence order 0x1F chn 2: note cuts unexpectedly fast — engine fix + [x] GSLINGER order 0x03 chn 1: L 0100 fades unexpectedly fast? — converter fix TODO - list of demo songs that MUST ship with Microtone: * 4THSYM (rename to Fourth Symmetriad) — excellent piece for demonstrating NNAs and filter envelopes diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index 179829f..b4a34d2 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -2145,11 +2145,28 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { // mirroring G's behaviour — the L command continues the porta started by an earlier G. val toneG = (row.effect == EffectOp.OP_G || row.effect == EffectOp.OP_L) when (row.note) { - // No note but an instrument byte is present: latch the instrument so - // the *next* note-only trigger picks up the right sample. Trackers - // call this an "instrument-only retrigger"; in MOD/S3M/IT the sample - // keeps playing, but the channel's instrument reference advances. - 0xFFFF -> { if (row.instrment != 0) voice.instrumentId = row.instrment } + // No note but an instrument byte is present: latch the instrument and + // re-seed the channel volume from the new sample's Default Note Volume. + // PT, FT2, IT and Schism all do this — pt2_replayer.c:1086 writes + // ch->n_volume = s->volume on every sample-byte row regardless of note; + // ft2_replayer.c:1431-1434 calls resetVolumes(ch) when (note==0 && inst>0); + // schism csf_instrument_change writes chan->volume = psmp->volume whenever + // inst_column is set. Without this, a MOD pattern that runs continuous + // volume slides while re-asserting the sample byte each row (e.g. + // physical_presence ord 0x1F ch2: every row carries `... 1E A0F/A09/A02`) + // silences after the first row because the slide saturates at 0 and there's + // nothing to lift the volume back up before the next slide starts. + 0xFFFF -> { + if (row.instrment != 0) { + voice.instrumentId = row.instrment + val seedVol = rowVolumeFromDefault(instruments[voice.instrumentId]) + voice.channelVolume = seedVol + voice.rowVolume = seedVol + voice.keyOff = false + voice.noteFading = false + voice.fadeoutVolume = 1.0 + } + } // Key-off: release sustain; envelope walks past the sustain point and the fadeout // begins (foreground-voice fade path at line ~2380). The voice deactivates when // fadeoutVolume reaches 0, or immediately if FT2-mode fadeStep == 0. Setting