mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
fix: taud note with SDx not firing due to unbound inst
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user