From 038db60b593ce18fcac7769b2b514f6e2c1c6cd5 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sat, 30 May 2026 09:05:28 +0900 Subject: [PATCH] fix: taud note with SDx not firing due to unbound inst --- CLAUDE.md | 8 +++++++ assets/disk0/tvdos/bin/taut.js | 13 +++++++++++ terranmon.txt | 3 ++- .../net/torvald/tsvm/AudioJSR223Delegate.kt | 7 ++++++ .../torvald/tsvm/peripheral/AudioAdapter.kt | 23 ++++++++++++++++++- 5 files changed, 52 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 50d317c..f704286 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -165,6 +165,14 @@ Peripheral memories can be accessed using `vm.peek()` and `vm.poke()` functions, - The 'gzip' namespace in TSVM's JS programs is a misnomer: the actual 'gzip' functions (defined in CompressorDelegate.kt) call Zstd functions. +## Taud Tracker Engine + +The Taud playback engine lives in `tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt`. + +### Critical Implementation Notes + +**Re-bind the local `inst` after any mid-tick `triggerNote`.** `applyTrackerTick` binds `var inst = instruments[voice.instrumentId]` once at the top of the per-voice loop. When the note-delay (`S$Dx`) deferred trigger fires mid-tick, `triggerNote` swaps the voice's `instrumentId` — but the rest of that tick (playback-rate recompute at the `computePlaybackRate(inst, finalPitch)` line, `advanceEnvelope`, `advancePfEnvelope`, `advanceAutoVibrato`, and the fadeout / filter-env reads of `inst.*`) keeps using the captured binding. The damage on a **never-triggered voice** (`instrumentId == 0` → stale `inst = instruments[0]`, whose `samplingRate == 0`) is that `playbackRate` is overwritten with `0.0`, freezing the sample at its start for the trigger tick — perceived as "the first delayed note on a fresh channel doesn't fire" (canonical: WHEN.taud cue 0 voice 13 pattern 0x0A row 16, inst `0x11` SD2 on a fresh play). On a warm voice the stale `inst` is a real instrument with non-zero rate, so the note sounds (at the wrong rate for one tick — a sub-perceptual glitch). Re-bind `inst = instruments[voice.instrumentId]` immediately after the note-delay fire block. Any future in-tick trigger paths (currently only S$Dx) must do the same. + ## TVDOS ### TVDOS Movie Formats diff --git a/assets/disk0/tvdos/bin/taut.js b/assets/disk0/tvdos/bin/taut.js index 931f373..69b4e15 100644 --- a/assets/disk0/tvdos/bin/taut.js +++ b/assets/disk0/tvdos/bin/taut.js @@ -4829,10 +4829,19 @@ function nudgeTickRate(delta) { drawAlwaysOnElems() } +// Drop accumulated funk-repeat (S$Fx) run-state and loop-inversion masks so a fresh play +// starts deterministic instead of inheriting a prior session's funkSpeed / inverted bytes. +// Engine still keeps PT2-style persistence across a natural loop; older runtimes lacking the +// API simply retain the inversions. +function clearFunkState() { + if (typeof audio.resetFunkState === 'function') audio.resetFunkState(PLAYHEAD) +} + function startPlaySong() { restoreFullSongParams() reuploadPatternsIfNeeded() audio.stop(PLAYHEAD) + clearFunkState() audio.setCuePosition(PLAYHEAD, cueIdx) audio.setTrackerRow(PLAYHEAD, 0) cursorRow = 0 @@ -4847,6 +4856,7 @@ function startPlayCue() { restoreFullSongParams() reuploadPatternsIfNeeded() audio.stop(PLAYHEAD) + clearFunkState() audio.setCuePosition(PLAYHEAD, cueIdx) audio.setTrackerRow(PLAYHEAD, 0) playStartCue = cueIdx @@ -4864,6 +4874,7 @@ function startPlayRow(fromRow, fromCue) { if (fromRow === undefined) fromRow = cursorRow if (fromCue === undefined) fromCue = cueIdx audio.stop(PLAYHEAD) + clearFunkState() audio.setCuePosition(PLAYHEAD, fromCue) audio.setTrackerRow(PLAYHEAD, fromRow) playStartCue = fromCue @@ -4880,6 +4891,7 @@ function startPlayPattern() { audio.stop(PLAYHEAD) audio.setBPM(PLAYHEAD, song.bpm) audio.uploadCue(PREVIEW_CUE_IDX, buildPreviewCue(patternIdx)) + clearFunkState() audio.setCuePosition(PLAYHEAD, PREVIEW_CUE_IDX) audio.setTrackerRow(PLAYHEAD, 0) playStartCue = PREVIEW_CUE_IDX @@ -4896,6 +4908,7 @@ function startPlayPatternRow() { audio.stop(PLAYHEAD) audio.setBPM(PLAYHEAD, song.bpm) audio.uploadCue(PREVIEW_CUE_IDX, buildPreviewCue(patternIdx)) + clearFunkState() audio.setCuePosition(PLAYHEAD, PREVIEW_CUE_IDX) audio.setTrackerRow(PLAYHEAD, patternGridRow) playStartCue = PREVIEW_CUE_IDX diff --git a/terranmon.txt b/terranmon.txt index f7688eb..90fcae4 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -2409,8 +2409,9 @@ TODO: [x] expose song table on UI (test with `insaniq2.taud`) [x] 0x0000 - no-op; 0x0001 - key-off; 0x0002 - note-cut 0x0010..0x001F - INT0..INTF [ ] establish hooks for the interrupts - [ ] Samples and Instruments view (viewer on taut.js; editor on separate .js) + [x] Samples and Instruments view (viewer on taut.js; editor on separate .js) follow the ImpulseTracker design first, then improve from there + [ ] Sample desig for instrument in Pitch-Volume space (one rectangle = one patch). If undefined, the old sample pointer falls thru 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/AudioJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt index 3703690..4a49d27 100644 --- a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt @@ -315,6 +315,13 @@ class AudioJSR223Delegate(private val vm: VM) { getPlayhead(playhead)?.resetParams() } + /** Clear funk-repeat (S$Fx) state (per-voice run-state + per-instrument loop-inversion masks) + * without disturbing tempo / volume / position. Call on a fresh play-from-start so stale funk + * state from a prior playback doesn't bleed into the replay. */ + fun resetFunkState(playhead: Int) { + getPlayhead(playhead)?.resetFunkState() + } + fun purgeQueue(playhead: Int) { getPlayhead(playhead)?.purgeQueue() } diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index f17a7aa..3f96538 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -2905,7 +2905,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { for (vi in 0 until ts.voices.size) { val voice = ts.voices[vi] if (!voice.active && voice.noteDelayTick < 0) continue - val inst = instruments[voice.instrumentId] + var inst = instruments[voice.instrumentId] // Note cut. Zero noteVolume / rowVolume (silence this note) but leave channelVolume // alone — IT's note cut stops the sample, it doesn't reset chan->global_volume. @@ -2921,6 +2921,13 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { maybeSpawnBackgroundForNNA(ts, voice, vi) triggerNote(voice, voice.delayedNote, voice.delayedInst, voice.delayedVol) voice.noteDelayTick = -1 + // triggerNote may have swapped in a new instrument; re-bind so the rest of this + // tick's per-voice work (playbackRate at L3090, envelope/fadeout/auto-vibrato) + // uses the instrument that just fired, not the one the voice held on entry. On a + // never-triggered voice the stale binding is instruments[0] (samplingRate 0), + // which would zero playbackRate and freeze the sample — the "first note on a + // fresh channel via S$Dx is silent" bug. + inst = instruments[voice.instrumentId] } if (!voice.active) { @@ -4094,6 +4101,20 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { } } + /** Clear funk-repeat (S$Fx) state only — per-voice run-state plus the per-instrument + * loop-inversion masks — without touching tempo / volume / position. taut calls this on + * every fresh play-from-start so accumulated inversions and a stale funkSpeed don't bleed + * from a prior session into the replay; full resetParams would also clobber bpm / tickRate / + * volume, which a replay must preserve. Masks still persist across a natural song loop. */ + fun resetFunkState() { + trackerState?.voices?.forEach { + it.funkSpeed = 0 + it.funkAccumulator = 0 + it.funkWritePos = 0 + } + parent.instruments.forEach { it.funkMask = null } + } + fun purgeQueue() { pcmQueue.clear() if (isPcmMode) {