mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
Compare commits
3 Commits
f863f6230d
...
1d3b5ce8aa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d3b5ce8aa | ||
|
|
9e8af96c32 | ||
|
|
43e5baadf4 |
@@ -3217,6 +3217,13 @@ const colSmpUsedHdr = colVoiceHdr
|
||||
const colSmpUsedFg = colInst
|
||||
const colSmpWaveLine = 77 // bright cyan-ish; visible on dark bg
|
||||
const colSmpWaveMid = 246 // dim grey for zero-line
|
||||
const colSmpWaveFunk = 221 // orange — loop bytes live-inverted by funk repeat (S$Fx)
|
||||
|
||||
// Funk-repeat introspection API (getVoiceFunkSpeed / getInstrumentFunkMask) ships with this
|
||||
// feature; on an un-rebuilt host VM it's absent and the waveform stays the stored sample.
|
||||
const hasFunkAPI = (typeof audio !== 'undefined' &&
|
||||
typeof audio.getVoiceFunkSpeed === 'function' &&
|
||||
typeof audio.getInstrumentFunkMask === 'function')
|
||||
|
||||
let smpListScroll = 0
|
||||
let smpListCursor = 0
|
||||
@@ -3337,16 +3344,16 @@ function drawSamplesProperties() {
|
||||
let smpUsedScroll = 0
|
||||
|
||||
function drawSamplesUsedBy() {
|
||||
const rightW = SCRW - SMP_RIGHT_X + 1
|
||||
con.move(SMP_USED_Y, SMP_RIGHT_X)
|
||||
con.color_pair(colSmpUsedHdr, colBackPtn)
|
||||
print('Used by instruments:'.padEnd(rightW))
|
||||
|
||||
const s = (samplesCache && samplesCache[smpListCursor]) || null
|
||||
const used = s ? s.usedBy : []
|
||||
const names = (songsMeta && songsMeta.instNames) || []
|
||||
const visible = SMP_USED_LIST_H
|
||||
|
||||
const rightW = SCRW - SMP_RIGHT_X + 1
|
||||
con.move(SMP_USED_Y, SMP_RIGHT_X)
|
||||
con.color_pair(colSmpUsedHdr, colBackPtn)
|
||||
print(`Used by instruments (${used.length}):`.padEnd(rightW))
|
||||
|
||||
if (smpUsedScroll > Math.max(0, used.length - visible))
|
||||
smpUsedScroll = Math.max(0, used.length - visible)
|
||||
if (smpUsedScroll < 0) smpUsedScroll = 0
|
||||
@@ -3372,10 +3379,11 @@ function drawSamplesUsedBy() {
|
||||
}
|
||||
|
||||
// ── Waveform rendering ──────────────────────────────────────────────────────
|
||||
// Renders one sample under the right panel as a min/max envelope, using the
|
||||
// graphics layer. Samples are unsigned 8-bit; bank-switch is required because
|
||||
// only 512 K of the 8 MB pool is mapped at a time. We restore bank 0 (the
|
||||
// playback-expected default) when done.
|
||||
// Renders one sample under the right panel as baseline-filled bars (each bar is
|
||||
// a plotRect anchored at the zero line, extending to the sample amplitude),
|
||||
// using the graphics layer. Samples are unsigned 8-bit; bank-switch is required
|
||||
// because only 512 K of the 8 MB pool is mapped at a time. We restore bank 0
|
||||
// (the playback-expected default) when done.
|
||||
|
||||
// Pixel rect occupied by the waveform inside the Samples viewer. Both the
|
||||
// waveform painter and the leave-Samples cleanup need to reach for the same
|
||||
@@ -3394,6 +3402,46 @@ function clearSampleWaveformArea() {
|
||||
graphics.plotRect(r.x-2, r.y-2, r.w+4, r.h+4, 255) // 255 = transparent
|
||||
}
|
||||
|
||||
// Instrument slot of an active voice that's funk-repeating (S$Fx) one of the sample's `usedBy`
|
||||
// instruments, or -1. Returns -1 when not playing / no funking voice / API absent. Drives the
|
||||
// per-frame repaint cadence only — the *displayed* mask comes from funkMaskForSample, which also
|
||||
// honours masks that persist after the funking voice has gone idle.
|
||||
function findFunkInstForSample(usedBy) {
|
||||
if (!hasFunkAPI) return -1
|
||||
const numVox = (song && song.numVoices) ? song.numVoices : NUM_VOICES
|
||||
for (let v = 0; v < numVox; v++) {
|
||||
if (!audio.getVoiceActive(PLAYHEAD, v)) continue
|
||||
if (audio.getVoiceFunkSpeed(PLAYHEAD, v) <= 0) continue
|
||||
const inst = audio.getVoiceInstrument(PLAYHEAD, v)
|
||||
if (usedBy.indexOf(inst) >= 0) return inst
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Funk XOR mask to DISPLAY for this sample, or null. The per-instrument mask persists in the engine
|
||||
// for the whole playback session (cleared only on stop-and-replay), so once a loop has been
|
||||
// funk-inverted the overlay must stay even after the funking voice goes idle — matching ProTracker,
|
||||
// whose destructive EFx edits never revert until the song is reloaded. Prefer an actively-funking
|
||||
// instrument (its mask is live this frame); otherwise show any usedBy instrument that still carries
|
||||
// a non-empty mask from earlier in the session.
|
||||
function funkMaskForSample(usedBy, activeInst) {
|
||||
if (!hasFunkAPI) return null
|
||||
if (activeInst > 0) {
|
||||
const m = audio.getInstrumentFunkMask(activeInst)
|
||||
if (m && m.length > 0) return m
|
||||
}
|
||||
for (let i = 0; i < usedBy.length; i++) {
|
||||
const m = audio.getInstrumentFunkMask(usedBy[i])
|
||||
if (m && m.length > 0) return m
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Whether a voice was actively funk-repeating the displayed sample on the last paint. Drives the
|
||||
// per-frame repaint cadence in tickFunkWaveform (repaint while the live mask changes, plus one
|
||||
// settling frame after it stops). The painted overlay itself persists — the engine keeps the mask.
|
||||
let funkWaveLast = false
|
||||
|
||||
function drawSampleWaveform() {
|
||||
const r = sampleWaveformRect()
|
||||
const wx0 = r.x, wy0 = r.y, wW = r.w, wH = r.h
|
||||
@@ -3402,53 +3450,104 @@ function drawSampleWaveform() {
|
||||
clearSampleWaveformArea()
|
||||
|
||||
const s = (samplesCache && samplesCache[smpListCursor]) || null
|
||||
if (!s || s.len === 0) return
|
||||
if (!s || s.len === 0) { funkWaveLast = false; return }
|
||||
|
||||
const bankIdxFirst = (s.ptr / TAUT_SBANK_SIZE) | 0
|
||||
const bankOff = s.ptr - bankIdxFirst * TAUT_SBANK_SIZE
|
||||
const memBase = audio.getMemAddr()
|
||||
const prevBank = audio.getSampleBank() || 0
|
||||
|
||||
// Centre line
|
||||
graphics.plotRect(wx0, wy0 + (wH >>> 1), wW, 1, colSmpWaveMid)
|
||||
|
||||
// Walk the sample at one column per output pixel. For each column we read
|
||||
// a chunk and reduce to min/max; vertical extent comes from (max-min).
|
||||
// Bank switching is per-step: each output column may straddle banks.
|
||||
const samplesPerCol = Math.max(1, (s.len / wW) | 0)
|
||||
let pos = 0 // byte offset into the sample, 0..len-1
|
||||
let curBank = -1
|
||||
for (let col = 0; col < wW; col++) {
|
||||
const start = (col * s.len / wW) | 0
|
||||
const end = Math.min(s.len, (((col + 1) * s.len / wW) | 0))
|
||||
if (end <= start) continue
|
||||
|
||||
let mn = 255, mx = 0
|
||||
// Step in coarse strides for speed when samples are long.
|
||||
const step = Math.max(1, ((end - start) / 8) | 0)
|
||||
for (let p = start; p < end; p += step) {
|
||||
const abs = s.ptr + p
|
||||
const bank = (abs / TAUT_SBANK_SIZE) | 0
|
||||
if (bank !== curBank) {
|
||||
audio.setSampleBank(bank)
|
||||
curBank = bank
|
||||
}
|
||||
const off = abs - bank * TAUT_SBANK_SIZE
|
||||
const v = sys.peek(memBase - off) & 0xFF
|
||||
if (v < mn) mn = v
|
||||
if (v > mx) mx = v
|
||||
// Funk-repeat overlay. The per-instrument XOR mask flips loop-region bytes by 0xFF and persists
|
||||
// in the engine until stop-and-replay, so the overlay must remain even after the voice that
|
||||
// funked the sample goes idle — matching ProTracker's destructive EFx, whose inverted bytes
|
||||
// never revert until the song is reloaded. We therefore key the overlay off the persisted mask,
|
||||
// not off a currently-active funking voice. funkLE is clamped to the snapshot mask's coverage so
|
||||
// the bit lookup can never run off the (host) array.
|
||||
let funkMask = null, funkLS = 0, funkLE = 0
|
||||
let activeFunk = false
|
||||
if (playbackMode !== PLAYMODE_NONE && s.loopEnd > s.loopStart) {
|
||||
const activeInst = findFunkInstForSample(s.usedBy)
|
||||
activeFunk = (activeInst > 0)
|
||||
const m = funkMaskForSample(s.usedBy, activeInst)
|
||||
if (m) {
|
||||
funkMask = m
|
||||
funkLS = s.loopStart
|
||||
funkLE = Math.min(s.loopEnd, funkLS + m.length * 8)
|
||||
}
|
||||
}
|
||||
funkWaveLast = activeFunk
|
||||
|
||||
const memBase = audio.getMemAddr()
|
||||
const prevBank = audio.getSampleBank() || 0
|
||||
let curBank = -1
|
||||
|
||||
// Zero line and value→y mapping (unsigned 8-bit: 255 → top, 0 → bottom).
|
||||
const baseY = wy0 + (wH >>> 1)
|
||||
const yOf = (v) => wy0 + (((wH * (255 - v)) / 255) | 0)
|
||||
|
||||
// Read sample byte p (0..len-1) applying the live funk-flip overlay; sets the
|
||||
// shared `flippedAny` flag whenever a byte was inverted by the funk mask.
|
||||
let flippedAny = false
|
||||
const readByte = (p) => {
|
||||
const abs = s.ptr + p
|
||||
const bank = (abs / TAUT_SBANK_SIZE) | 0
|
||||
if (bank !== curBank) { audio.setSampleBank(bank); curBank = bank }
|
||||
let v = sys.peek(memBase - (abs - bank * TAUT_SBANK_SIZE)) & 0xFF
|
||||
if (funkMask !== null && p >= funkLS && p < funkLE) {
|
||||
const k = p - funkLS
|
||||
if ((funkMask[k >>> 3] >>> (k & 7)) & 1) { v ^= 0xFF; flippedAny = true }
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Zero/baseline line
|
||||
graphics.plotRect(wx0, baseY, wW, 1, colSmpWaveMid)
|
||||
|
||||
// Per-sample bar width: how many pixels each sample spans, at least 1px.
|
||||
const rectW = Math.max(1, Math.ceil(wW / s.len))
|
||||
|
||||
if (s.len <= wW) {
|
||||
// Fewer samples than pixels: one baseline-filled bar per sample.
|
||||
for (let i = 0; i < s.len; i++) {
|
||||
flippedAny = false
|
||||
const yv = yOf(readByte(i))
|
||||
const top = Math.min(baseY, yv)
|
||||
graphics.plotRect(wx0 + ((i * wW / s.len) | 0), top, rectW,
|
||||
Math.max(1, Math.abs(baseY - yv)),
|
||||
flippedAny ? colSmpWaveFunk : colSmpWaveLine)
|
||||
}
|
||||
} else {
|
||||
// More samples than pixels: reduce each 1px column to its min/max and
|
||||
// fill from the baseline through the envelope (a solid filled waveform).
|
||||
for (let col = 0; col < wW; col++) {
|
||||
const start = (col * s.len / wW) | 0
|
||||
const end = Math.min(s.len, (((col + 1) * s.len / wW) | 0))
|
||||
if (end <= start) continue
|
||||
const step = Math.max(1, ((end - start) / 8) | 0)
|
||||
let mn = 255, mx = 0
|
||||
flippedAny = false
|
||||
for (let p = start; p < end; p += step) {
|
||||
const v = readByte(p)
|
||||
if (v < mn) mn = v
|
||||
if (v > mx) mx = v
|
||||
}
|
||||
const yTop = Math.min(baseY, yOf(mx))
|
||||
const yBot = Math.max(baseY, yOf(mn))
|
||||
graphics.plotRect(wx0 + col, yTop, 1, Math.max(1, yBot - yTop + 1),
|
||||
flippedAny ? colSmpWaveFunk : colSmpWaveLine)
|
||||
}
|
||||
// unsigned 8-bit → centred around 128
|
||||
const yTop = wy0 + ((wH * (255 - mx)) / 255) | 0
|
||||
const yBot = wy0 + ((wH * (255 - mn)) / 255) | 0
|
||||
const h = Math.max(1, yBot - yTop + 1)
|
||||
graphics.plotRect(wx0 + col, yTop, 1, h, colSmpWaveLine)
|
||||
}
|
||||
|
||||
// Restore bank 0 for playback (engine expects bank 0 as default)
|
||||
audio.setSampleBank(prevBank)
|
||||
}
|
||||
|
||||
// Per-frame driver: while a voice is funk-repeating the displayed sample, repaint the waveform
|
||||
// each frame so the overlay tracks the live mask. One settling repaint fires after funk stops
|
||||
// (funkWaveLast); the persisted overlay then stays until the engine clears the mask on replay.
|
||||
function tickFunkWaveform() {
|
||||
if (currentPanel !== VIEW_SAMPLES) { funkWaveLast = false; return }
|
||||
const s = (samplesCache && samplesCache[smpListCursor]) || null
|
||||
const funking = !!(s && s.len > 0 && playbackMode !== PLAYMODE_NONE &&
|
||||
findFunkInstForSample(s.usedBy) > 0)
|
||||
if (funking || funkWaveLast) drawSampleWaveform()
|
||||
}
|
||||
|
||||
function drawSamplesEditButton() {
|
||||
const y = SMP_BTN_Y
|
||||
con.move(y, SMP_RIGHT_X)
|
||||
@@ -4817,6 +4916,7 @@ function stopPlayback() {
|
||||
// pass ourselves so stale blobs / hairlines don't linger on Samples / Instruments.
|
||||
drawSamplesPlayBlobs()
|
||||
drawInstrumentsPlayBlobs()
|
||||
tickFunkWaveform() // restore the stored waveform now that funk repeat has stopped
|
||||
drawSampleCursor()
|
||||
drawEnvelopeCursor()
|
||||
}
|
||||
@@ -4834,6 +4934,7 @@ function updatePlayback() {
|
||||
// playbackMode is NONE now → these paint a final blob0 / clear-cursor pass.
|
||||
drawSamplesPlayBlobs()
|
||||
drawInstrumentsPlayBlobs()
|
||||
tickFunkWaveform() // restore the stored waveform now that playback has stopped
|
||||
drawSampleCursor()
|
||||
drawEnvelopeCursor()
|
||||
return
|
||||
@@ -4842,6 +4943,7 @@ function updatePlayback() {
|
||||
drawVoiceMeters()
|
||||
drawSamplesPlayBlobs()
|
||||
drawInstrumentsPlayBlobs()
|
||||
tickFunkWaveform() // realtime funk-repeat overlay (no-op unless funking this sample)
|
||||
drawSampleCursor()
|
||||
drawEnvelopeCursor()
|
||||
|
||||
|
||||
32
it2taud.py
32
it2taud.py
@@ -1190,13 +1190,29 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
||||
sample_bin = bytearray(SAMPLEBIN_SIZE)
|
||||
offsets = {}
|
||||
pos = 0
|
||||
# IT use_instruments mode points many Taud instrument slots at the same
|
||||
# underlying sample object (e.g. seven "ChipBass.*" instruments all play
|
||||
# "ChipBass.looped"). Write each distinct sample's PCM into the pool once and
|
||||
# let every referencing slot share the offset, rather than emitting one
|
||||
# identical copy per slot. `pool_order` records the distinct samples in
|
||||
# ascending-offset order — the order taut.js's sample viewer expects SNam to
|
||||
# follow (it dedupes instrument records by (ptr,len), sorts by ptr, and
|
||||
# matches SNam[i+1] positionally — see taut.js buildSampleIndex).
|
||||
written = {} # id(sample) -> pool offset already written
|
||||
pool_order = [] # distinct sample objects, in pool (ascending-offset) order
|
||||
for idx, s in pcm_list:
|
||||
shared = written.get(id(s))
|
||||
if shared is not None:
|
||||
offsets[idx] = shared
|
||||
continue
|
||||
n = min(len(s.sample_data), SAMPLEBIN_SIZE - pos)
|
||||
if n <= 0:
|
||||
vprint(f" warning: sample bin full, dropping '{s.name}'")
|
||||
offsets[idx] = 0; s.length = 0; continue
|
||||
sample_bin[pos:pos+n] = s.sample_data[:n]
|
||||
offsets[idx] = pos
|
||||
written[id(s)] = pos
|
||||
pool_order.append(s)
|
||||
if n < len(s.sample_data):
|
||||
vprint(f" warning: '{s.name}' truncated {len(s.sample_data)} → {n}")
|
||||
s.length = n
|
||||
@@ -1384,7 +1400,7 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
||||
|
||||
vprint(f" instrument[{taud_idx}] '{s.name}' ptr:{ptr} c5spd:{s.c5_speed}")
|
||||
|
||||
return bytes(sample_bin) + bytes(inst_bin), offsets, ratio
|
||||
return bytes(sample_bin) + bytes(inst_bin), offsets, ratio, pool_order
|
||||
|
||||
|
||||
# ── Pattern builder ───────────────────────────────────────────────────────────
|
||||
@@ -1899,7 +1915,7 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
|
||||
'dct': inst.dct,
|
||||
'dca': inst.dca,
|
||||
}
|
||||
sampleinst_raw, _, sample_ratio = build_sample_inst_bin_it(proxy, instr_data_by_slot)
|
||||
sampleinst_raw, _, sample_ratio, pool_order = build_sample_inst_bin_it(proxy, instr_data_by_slot)
|
||||
else:
|
||||
# Samples referenced directly; proxy is samples list (0-based, slot 0 unused)
|
||||
proxy = [None] + list(samples)
|
||||
@@ -1908,7 +1924,7 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
|
||||
for i, s in enumerate(samples)
|
||||
if s is not None
|
||||
}
|
||||
sampleinst_raw, _, sample_ratio = build_sample_inst_bin_it(proxy)
|
||||
sampleinst_raw, _, sample_ratio, pool_order = build_sample_inst_bin_it(proxy)
|
||||
|
||||
assert len(sampleinst_raw) == SAMPLEINST_SIZE
|
||||
|
||||
@@ -1961,8 +1977,14 @@ def assemble_taud(h: ITHeader, samples: list, instruments: list,
|
||||
if with_project_data:
|
||||
inst_names = [''] + [(inst.name if inst is not None else '')
|
||||
for inst in instruments[:255]]
|
||||
smp_names = [''] + [(s.name if s is not None else '')
|
||||
for s in samples[:255]]
|
||||
# SNam mirrors the deduplicated sample pool: one entry per distinct
|
||||
# sample, in pool order, named after the sample itself. taut.js dedupes
|
||||
# instrument records by (ptr,len), sorts ascending by ptr, and matches
|
||||
# SNam[i+1] positionally to that list, so this ordering labels every
|
||||
# sample correctly and a shared sample (e.g. "ChipBass.looped") appears
|
||||
# exactly once instead of once per referencing instrument slot.
|
||||
smp_names = [''] + [(getattr(s, 'name', '') or '')
|
||||
for s in pool_order[:255]]
|
||||
proj_data = build_project_data(
|
||||
project_name=h.title,
|
||||
instrument_names=inst_names,
|
||||
|
||||
22
s3m2taud.py
22
s3m2taud.py
@@ -138,7 +138,11 @@ def parse_instruments(data: bytes, h: S3MHeader) -> list:
|
||||
continue
|
||||
inst = S3MInstrument()
|
||||
inst.itype = data[ptr]
|
||||
inst.filename = data[ptr+1:ptr+13].rstrip(b'\x00').decode('latin-1', errors='replace')
|
||||
# 12-byte DOS filename field; null-terminated with possible trailing
|
||||
# garbage after the terminator (ST3 doesn't zero the tail). Truncate at
|
||||
# the first null. This field carries the per-sample short name (e.g.
|
||||
# 'HIT1') as distinct from the 28-byte title at 0x30.
|
||||
inst.filename = data[ptr+1:ptr+13].split(b'\x00', 1)[0].decode('latin-1', errors='replace')
|
||||
# memseg: 3 bytes at offsets 0x0D,0x0E,0x0F — high byte first (quirk)
|
||||
memseg_hi = data[ptr + 0x0D]
|
||||
memseg_lo = struct.unpack_from('<H', data, ptr + 0x0E)[0]
|
||||
@@ -939,17 +943,21 @@ def assemble_taud(h: S3MHeader, instruments: list, patterns: list,
|
||||
cur_off += len(pat_comp) + len(cue_comp)
|
||||
|
||||
# ── Project Data (optional) ──────────────────────────────────────────────
|
||||
# S3M instruments and samples share the same slot space, so the names go
|
||||
# into both INam and SNam (1-based; slot 0 empty).
|
||||
# S3M instruments and samples share the same slot space, but carry two
|
||||
# distinct name fields: the 28-byte title (inst.name → INam) and the
|
||||
# 12-byte DOS filename (inst.filename → SNam). e.g. WHEN.s3m instrument #1
|
||||
# is titled "(c) Purple Motion / 1994" with sample name 'HIT1'.
|
||||
proj_data = b''
|
||||
proj_off = 0
|
||||
if with_project_data:
|
||||
names = [''] + [(inst.name if inst is not None else '')
|
||||
for inst in instruments[:255]]
|
||||
inst_names = [''] + [(inst.name if inst is not None else '')
|
||||
for inst in instruments[:255]]
|
||||
sample_names = [''] + [(inst.filename if inst is not None else '')
|
||||
for inst in instruments[:255]]
|
||||
proj_data = build_project_data(
|
||||
project_name=h.title,
|
||||
instrument_names=names,
|
||||
sample_names=names,
|
||||
instrument_names=inst_names,
|
||||
sample_names=sample_names,
|
||||
)
|
||||
if proj_data:
|
||||
proj_off = cur_off
|
||||
|
||||
@@ -163,6 +163,24 @@ class AudioJSR223Delegate(private val vm: VM) {
|
||||
return counts
|
||||
}
|
||||
|
||||
/** Funk-repeat (S$Fx) speed currently driving the voice: 0 = off, otherwise the per-tick
|
||||
* accumulator increment. A non-zero value on an active voice means the voice is live-inverting
|
||||
* its instrument's loop region right now — visualisers can use this to gate the funk overlay. */
|
||||
fun getVoiceFunkSpeed(playhead: Int, voice: Int): Int {
|
||||
val v = getPlayhead(playhead)?.trackerState?.voices?.getOrNull(voice.coerceIn(0, 19)) ?: return 0
|
||||
if (!v.active) return 0
|
||||
return v.funkSpeed
|
||||
}
|
||||
|
||||
/** Snapshot of an instrument's funk-repeat XOR mask (one bit per loop-region byte; a set bit
|
||||
* flips that byte by 0xFF during playback). Returns the mask bytes as ints (0..255), or an
|
||||
* empty array when the instrument has never been funk-repeated. The render thread mutates the
|
||||
* live mask, so this returns a copy — the caller gets a stable single-frame view. */
|
||||
fun getInstrumentFunkMask(slot: Int): IntArray {
|
||||
val mask = getFirstSnd()?.instruments?.get(slot and 0xFF)?.funkMask ?: return IntArray(0)
|
||||
return IntArray(mask.size) { mask[it].toInt() and 0xFF }
|
||||
}
|
||||
|
||||
/** Live noteVal (0..65535, 4096-TET) of the foreground voice — the value the mixer is using
|
||||
* *right now* including any in-flight vibrato / arpeggio / portamento delta. Returns 0 for
|
||||
* inactive voices. */
|
||||
|
||||
Reference in New Issue
Block a user