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.
|
- 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
|
||||||
|
|
||||||
### TVDOS Movie Formats
|
### TVDOS Movie Formats
|
||||||
|
|||||||
@@ -4829,10 +4829,19 @@ function nudgeTickRate(delta) {
|
|||||||
drawAlwaysOnElems()
|
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() {
|
function startPlaySong() {
|
||||||
restoreFullSongParams()
|
restoreFullSongParams()
|
||||||
reuploadPatternsIfNeeded()
|
reuploadPatternsIfNeeded()
|
||||||
audio.stop(PLAYHEAD)
|
audio.stop(PLAYHEAD)
|
||||||
|
clearFunkState()
|
||||||
audio.setCuePosition(PLAYHEAD, cueIdx)
|
audio.setCuePosition(PLAYHEAD, cueIdx)
|
||||||
audio.setTrackerRow(PLAYHEAD, 0)
|
audio.setTrackerRow(PLAYHEAD, 0)
|
||||||
cursorRow = 0
|
cursorRow = 0
|
||||||
@@ -4847,6 +4856,7 @@ function startPlayCue() {
|
|||||||
restoreFullSongParams()
|
restoreFullSongParams()
|
||||||
reuploadPatternsIfNeeded()
|
reuploadPatternsIfNeeded()
|
||||||
audio.stop(PLAYHEAD)
|
audio.stop(PLAYHEAD)
|
||||||
|
clearFunkState()
|
||||||
audio.setCuePosition(PLAYHEAD, cueIdx)
|
audio.setCuePosition(PLAYHEAD, cueIdx)
|
||||||
audio.setTrackerRow(PLAYHEAD, 0)
|
audio.setTrackerRow(PLAYHEAD, 0)
|
||||||
playStartCue = cueIdx
|
playStartCue = cueIdx
|
||||||
@@ -4864,6 +4874,7 @@ function startPlayRow(fromRow, fromCue) {
|
|||||||
if (fromRow === undefined) fromRow = cursorRow
|
if (fromRow === undefined) fromRow = cursorRow
|
||||||
if (fromCue === undefined) fromCue = cueIdx
|
if (fromCue === undefined) fromCue = cueIdx
|
||||||
audio.stop(PLAYHEAD)
|
audio.stop(PLAYHEAD)
|
||||||
|
clearFunkState()
|
||||||
audio.setCuePosition(PLAYHEAD, fromCue)
|
audio.setCuePosition(PLAYHEAD, fromCue)
|
||||||
audio.setTrackerRow(PLAYHEAD, fromRow)
|
audio.setTrackerRow(PLAYHEAD, fromRow)
|
||||||
playStartCue = fromCue
|
playStartCue = fromCue
|
||||||
@@ -4880,6 +4891,7 @@ function startPlayPattern() {
|
|||||||
audio.stop(PLAYHEAD)
|
audio.stop(PLAYHEAD)
|
||||||
audio.setBPM(PLAYHEAD, song.bpm)
|
audio.setBPM(PLAYHEAD, song.bpm)
|
||||||
audio.uploadCue(PREVIEW_CUE_IDX, buildPreviewCue(patternIdx))
|
audio.uploadCue(PREVIEW_CUE_IDX, buildPreviewCue(patternIdx))
|
||||||
|
clearFunkState()
|
||||||
audio.setCuePosition(PLAYHEAD, PREVIEW_CUE_IDX)
|
audio.setCuePosition(PLAYHEAD, PREVIEW_CUE_IDX)
|
||||||
audio.setTrackerRow(PLAYHEAD, 0)
|
audio.setTrackerRow(PLAYHEAD, 0)
|
||||||
playStartCue = PREVIEW_CUE_IDX
|
playStartCue = PREVIEW_CUE_IDX
|
||||||
@@ -4896,6 +4908,7 @@ function startPlayPatternRow() {
|
|||||||
audio.stop(PLAYHEAD)
|
audio.stop(PLAYHEAD)
|
||||||
audio.setBPM(PLAYHEAD, song.bpm)
|
audio.setBPM(PLAYHEAD, song.bpm)
|
||||||
audio.uploadCue(PREVIEW_CUE_IDX, buildPreviewCue(patternIdx))
|
audio.uploadCue(PREVIEW_CUE_IDX, buildPreviewCue(patternIdx))
|
||||||
|
clearFunkState()
|
||||||
audio.setCuePosition(PLAYHEAD, PREVIEW_CUE_IDX)
|
audio.setCuePosition(PLAYHEAD, PREVIEW_CUE_IDX)
|
||||||
audio.setTrackerRow(PLAYHEAD, patternGridRow)
|
audio.setTrackerRow(PLAYHEAD, patternGridRow)
|
||||||
playStartCue = PREVIEW_CUE_IDX
|
playStartCue = PREVIEW_CUE_IDX
|
||||||
|
|||||||
@@ -2409,8 +2409,9 @@ TODO:
|
|||||||
[x] expose song table on UI (test with `insaniq2.taud`)
|
[x] expose song table on UI (test with `insaniq2.taud`)
|
||||||
[x] 0x0000 - no-op; 0x0001 - key-off; 0x0002 - note-cut 0x0010..0x001F - INT0..INTF
|
[x] 0x0000 - no-op; 0x0001 - key-off; 0x0002 - note-cut 0x0010..0x001F - INT0..INTF
|
||||||
[ ] establish hooks for the interrupts
|
[ ] 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
|
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:
|
TODO - list of demo songs that MUST ship with Microtone:
|
||||||
* 4THSYM (rename to Fourth Symmetriad) — excellent piece for demonstrating NNAs and filter envelopes
|
* 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()
|
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) {
|
fun purgeQueue(playhead: Int) {
|
||||||
getPlayhead(playhead)?.purgeQueue()
|
getPlayhead(playhead)?.purgeQueue()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2905,7 +2905,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
for (vi in 0 until ts.voices.size) {
|
for (vi in 0 until ts.voices.size) {
|
||||||
val voice = ts.voices[vi]
|
val voice = ts.voices[vi]
|
||||||
if (!voice.active && voice.noteDelayTick < 0) continue
|
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
|
// 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.
|
// 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)
|
maybeSpawnBackgroundForNNA(ts, voice, vi)
|
||||||
triggerNote(voice, voice.delayedNote, voice.delayedInst, voice.delayedVol)
|
triggerNote(voice, voice.delayedNote, voice.delayedInst, voice.delayedVol)
|
||||||
voice.noteDelayTick = -1
|
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) {
|
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() {
|
fun purgeQueue() {
|
||||||
pcmQueue.clear()
|
pcmQueue.clear()
|
||||||
if (isPcmMode) {
|
if (isPcmMode) {
|
||||||
|
|||||||
Reference in New Issue
Block a user