diff --git a/terranmon.txt b/terranmon.txt index 11b4c57..07731e4 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -1986,7 +1986,7 @@ Endianness: little TSVM Audio Adapter is consisted of 4 playheads, each playhead is capable of playing one PCM or Tracker track. -Synchronisation between playheads are not guaranteed. Do not play music in multiple tracks. +Synchronisation between playheads are not guaranteed. Do not play a music across multiple playheads. Memory Space @@ -2799,6 +2799,7 @@ TODO: [ ] midi2taud: toggleable option for disabling filter for percussions [default: on] - Anything on bank 127 and 128 (usually asso siated with ch 10) - GeneralMIDI instruments 113..128 + [ ] midi2taud: instrument fadeout (release) is significantly longer than Fluidsynth [ ] auto-set optimal-ish Tickspeed and RPB using MIDI Time Signature events and note analysis. Break pattern when Time Signature changes. Time Signature @@ -2820,7 +2821,7 @@ TODO: In a format 0 file, the time signatures changes are scattered throughout the one MTrk. In format 1, the very first MTrk should consist of only the time signature (and tempo) events so that it could be read by some device capable of generating a "tempo map". It is best not to place MIDI events in this MTrk. In format 2, each MTrk should begin with at least one initial time signature (and tempo) event. [ ] Taut UI commit - Inst > Gen.1 > sample binding: ~~~....[two doubledots] et al. (n extra samples) - - Inst > Gen.2 > filter: IT/SF mode toggle + - 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 diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index 460c1d7..ccd732b 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -1788,14 +1788,34 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { } else if (idx >= maxIdx) { return env[maxIdx].value / 255.0 } else { - val offset = env[idx].offset.toDouble() - if (offset == 0.0) { + // Advance through zero-duration nodes rather than freezing on them. A node + // whose offset rounds to 0 (sub-4 ms — ThreeFiveMinifloat's smallest non-zero + // step is ≈3.9 ms, so e.g. an SF2 filter mod-env's 1 ms attack stores offset 0) + // is passed instantly, so the envelope must move on to the next node. The old + // code returned here WITHOUT advancing the index, stranding fast-attack filter + // mod-envs at their first node: the filter never opened from its base cutoff to + // the sustain cutoff, so Strings/Flute/Guitar (SF2 base ~600 Hz, sustain ~6 kHz) + // played permanently muffled. The loop stops at a sustain/loop boundary (handled + // by the susEnd branch below and the top-of-function checks) or at maxIdx. + while (idx < maxIdx && !(susOn && idx == susEnd) && env[idx].offset.toDouble() == 0.0) { + idx++ + } + if (susOn && idx == susEnd) { + // Reached the sustain/loop end while skipping: hold (single-node sustain) or + // loop back to susStart, mirroring the top-of-function dispatch. + if (susStart != susEnd) { timeBox[0] = 0.0; idx = susStart } + idxBox[0] = idx return env[idx].value / 255.0 } + idxBox[0] = idx + if (idx >= maxIdx) { + return env[maxIdx].value / 255.0 + } + val offset = env[idx].offset.toDouble() timeBox[0] += tickSec if (timeBox[0] >= offset) { timeBox[0] -= offset - idx = if (susOn && idx == susEnd) susStart else (idx + 1).coerceAtMost(maxIdx) + idx = (idx + 1).coerceAtMost(maxIdx) idxBox[0] = idx return env[idx].value / 255.0 }