mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
Compare commits
3 Commits
10e577699f
...
f863f6230d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f863f6230d | ||
|
|
d8ac08162c | ||
|
|
e24870ce07 |
@@ -67,20 +67,52 @@ panri:"\u008416u",
|
|||||||
panfinele:"\u008427u",
|
panfinele:"\u008427u",
|
||||||
panfineri:"\u008426u",
|
panfineri:"\u008426u",
|
||||||
|
|
||||||
/* Fx/Vx/Px */
|
|
||||||
fx:'\u00F8',
|
|
||||||
px:'\u00AC',
|
|
||||||
vx:'\u00AD',
|
|
||||||
|
|
||||||
/* transport control */
|
/* transport control */
|
||||||
playall:'\u00E1',
|
playall:'\u00E1',
|
||||||
playcue:'\u00E2',
|
playcue:'\u00E2',
|
||||||
playrow:'\u00E3',
|
playrow:'\u00E3',
|
||||||
stop:'\u00E4',
|
stop:'\u00E4',
|
||||||
|
|
||||||
|
/* GUI stuffs */
|
||||||
|
slider1: '\u00E4', // slider knob fitting in 7px cell snugly
|
||||||
|
slider2: '\u00E5\u00E6', // slider knob fitting in right 6px then 1px to the next cell
|
||||||
|
slider3: '\u00E7\u00E8', // slider knob fitting in right 5px then 2px to the next cell
|
||||||
|
slider4: '\u00E9\u00EA', // slider knob fitting in right 4px then 3px to the next cell
|
||||||
|
slider5: '\u00EB\u00EE', // slider knob fitting in right 3px then 4px to the next cell
|
||||||
|
slider6: '\u00EF\u00F0', // slider knob fitting in right 2px then 5px to the next cell
|
||||||
|
slider7: '\u00F1\u00F2', // slider knob fitting in right 1px then 6px to the next cell
|
||||||
|
|
||||||
|
vhairline1: '\u00AD', // vertical line on left 1px
|
||||||
|
vhairline2: '\u00AE', // vertical line on left 2px
|
||||||
|
vhairline3: '\u00AF', // vertical line on left 3px
|
||||||
|
vhairline4: '\u00DA', // vertical line on the centre
|
||||||
|
vhairline5: '\u00F6', // vertical line on left 5px
|
||||||
|
vhairline6: '\u00F7', // vertical line on left 6px
|
||||||
|
vhairline7: '\u00F8', // vertical line on left 7px
|
||||||
|
|
||||||
|
taut_scrollgutter_top: 0xBA,
|
||||||
|
taut_scrollgutter_mid: 0xBB,
|
||||||
|
taut_scrollgutter_bot: 0xBC,
|
||||||
|
taut_scrollgutter_top_full: 0xBD,
|
||||||
|
taut_scrollgutter_mid_full: 0xBE,
|
||||||
|
taut_scrollgutter_bot_full: 0xBF,
|
||||||
|
|
||||||
|
blob0: '\u00840u',
|
||||||
|
blob1: '\u00841u',
|
||||||
|
blob2: '\u00842u',
|
||||||
|
blob3: '\u00843u',
|
||||||
|
blob4: '\u00844u',
|
||||||
|
blob5: '\u00845u',
|
||||||
|
blob6: '\u00846u',
|
||||||
|
blob7: '\u00847u',
|
||||||
|
blob8: '\u00848u',
|
||||||
|
blob9: '\u00849u',
|
||||||
|
blob10: '\u008410u',
|
||||||
|
|
||||||
|
unticked: '\u009E',
|
||||||
|
ticked: '\u009F',
|
||||||
|
|
||||||
/* miscellaneous */
|
/* miscellaneous */
|
||||||
unticked:"\u00AE",
|
|
||||||
ticked:"\u00AF",
|
|
||||||
middot:MIDDOT,
|
middot:MIDDOT,
|
||||||
doubledot:"\u008419u",
|
doubledot:"\u008419u",
|
||||||
statusstop:"\u008420u\u008421u",
|
statusstop:"\u008420u\u008421u",
|
||||||
@@ -220,7 +252,7 @@ sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym
|
|||||||
10122:{index:10122,name:"Pythagorean aug. 4th",table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x828,0x95C,0xA90,0xC14,0xD48,0xECC],interval:0x1000,t:'d',
|
10122:{index:10122,name:"Pythagorean aug. 4th",table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x828,0x95C,0xA90,0xC14,0xD48,0xECC],interval:0x1000,t:'d',
|
||||||
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
|
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
|
||||||
10123:{index:10123,name:"\u00FC\u00FD\u00FE (shi'er lu)", table:[0x0,0x184,0x2B8,0x43C,0x570,0x6F4,0x828,0x95C,0xAE0,0xC14,0xD98,0xECC],interval:0x1000,t:'d',
|
10123:{index:10123,name:"\u00FC\u00FD\u00FE (shi'er lu)", table:[0x0,0x184,0x2B8,0x43C,0x570,0x6F4,0x828,0x95C,0xAE0,0xC14,0xD98,0xECC],interval:0x1000,t:'d',
|
||||||
sym:[` \u00E0\u00E1`,` \u00E2\u00E3`,` \u00E4\u00E5`,` \u00E6\u00E7`,` \u00E8\u00E9`,` \u00EA\u00EB`,` \u00EC\u00ED`,` \u00EE\u00EF`,` \u00F0\u00F1`,` \u00F2\u00F3`,` \u00F4\u00F5`,` \u00F6\u00F7`]},
|
sym:[` \u00C0\u00C1`,` \u00C2\u00C3`,` \u00C4\u00C5`,` \u00C6\u00C7`,` \u00C8\u00C9`,` \u00CA\u00CB`,` \u00CC\u00CD`,` \u00CE\u00CF`,` \u00D0\u00D1`,` \u00D2\u00D3`,` \u00D4\u00D5`,` \u00D6\u00D7`]},
|
||||||
/* non-octave */
|
/* non-octave */
|
||||||
35130:{index:35130,name:"Equal-Tempered Bohlen-Pierce",table:[0x0,0x1F3,0x3E7,0x5DA,0x7CE,0x9C1,0xBB4,0xDA8,0xF9B,0x118E,0x1382,0x1575,0x1769],interval:0x195C,t:'M',
|
35130:{index:35130,name:"Equal-Tempered Bohlen-Pierce",table:[0x0,0x1F3,0x3E7,0x5DA,0x7CE,0x9C1,0xBB4,0xDA8,0xF9B,0x118E,0x1382,0x1575,0x1769],interval:0x195C,t:'M',
|
||||||
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`H${sym.accnull}`,`H${sym.sharp}`,`J${sym.accnull}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
|
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`H${sym.accnull}`,`H${sym.sharp}`,`J${sym.accnull}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
|
||||||
@@ -1127,7 +1159,7 @@ function drawStatusBar() {
|
|||||||
let beatCursorRow = cursorRow
|
let beatCursorRow = cursorRow
|
||||||
while (beatCursorRow >= beatDivSecondary) { beatCursorRow -= beatDivSecondary }
|
while (beatCursorRow >= beatDivSecondary) { beatCursorRow -= beatDivSecondary }
|
||||||
let beatInd = (playbackMode != PLAYMODE_NONE && beatCursorRow % beatDivPrimary < (beatDivPrimary >>> 1)) ?
|
let beatInd = (playbackMode != PLAYMODE_NONE && beatCursorRow % beatDivPrimary < (beatDivPrimary >>> 1)) ?
|
||||||
((beatCursorRow % beatDivSecondary < (beatDivPrimary >>> 1)) ? '\u00846u' : '\u00847u') :
|
((beatCursorRow % beatDivSecondary < (beatDivPrimary >>> 1)) ? sym.blob8 : sym.blob5) :
|
||||||
''
|
''
|
||||||
|
|
||||||
// cue row
|
// cue row
|
||||||
@@ -3159,7 +3191,7 @@ function refreshSamplesCache() { samplesCache = buildSampleIndex() }
|
|||||||
// Panel area is rows PTNVIEW_OFFSET_Y .. SCRH-1 (the hint bar lives at SCRH).
|
// Panel area is rows PTNVIEW_OFFSET_Y .. SCRH-1 (the hint bar lives at SCRH).
|
||||||
// Columns mirror the Patterns tab: list body | scroll-bar col | VERT separator | right pane.
|
// Columns mirror the Patterns tab: list body | scroll-bar col | VERT separator | right pane.
|
||||||
const SMP_LIST_X = 1
|
const SMP_LIST_X = 1
|
||||||
const SMP_LIST_BODY_W = 26 // text width of one list row
|
const SMP_LIST_BODY_W = 27 // text width of one list row
|
||||||
const SMP_LIST_W = SMP_LIST_BODY_W + 1 // body + 1-col scroll indicator
|
const SMP_LIST_W = SMP_LIST_BODY_W + 1 // body + 1-col scroll indicator
|
||||||
const SMP_LIST_SCROLL_X = SMP_LIST_X + SMP_LIST_BODY_W // scroll-indicator column
|
const SMP_LIST_SCROLL_X = SMP_LIST_X + SMP_LIST_BODY_W // scroll-indicator column
|
||||||
const SMP_LIST_Y = PTNVIEW_OFFSET_Y
|
const SMP_LIST_Y = PTNVIEW_OFFSET_Y
|
||||||
@@ -3229,7 +3261,10 @@ function drawSamplesListColumn() {
|
|||||||
for (let r = 0; r < SMP_LIST_H; r++) {
|
for (let r = 0; r < SMP_LIST_H; r++) {
|
||||||
con.move(SMP_LIST_Y + r, SMP_LIST_SCROLL_X)
|
con.move(SMP_LIST_Y + r, SMP_LIST_SCROLL_X)
|
||||||
con.color_pair(colStatus, colSmpListBg)
|
con.color_pair(colStatus, colSmpListBg)
|
||||||
print(r === indPos ? sym.ticked : sym.unticked)
|
|
||||||
|
let scrollChar = (r == 0) ? sym.taut_scrollgutter_top : (r == SMP_LIST_H - 1) ? sym.taut_scrollgutter_bot : sym.taut_scrollgutter_mid
|
||||||
|
if (r == indPos) scrollChar += 3;
|
||||||
|
con.addch(scrollChar)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (let r = 0; r < SMP_LIST_H; r++) {
|
for (let r = 0; r < SMP_LIST_H; r++) {
|
||||||
@@ -3441,6 +3476,15 @@ function drawSamplesContents(wo) {
|
|||||||
drawSamplesUsedBy()
|
drawSamplesUsedBy()
|
||||||
drawSampleWaveform()
|
drawSampleWaveform()
|
||||||
drawSamplesEditButton()
|
drawSamplesEditButton()
|
||||||
|
// The list column just repainted col 1 with a leading space on every row,
|
||||||
|
// so any prior blob is gone — invalidate the cache, then re-stamp blobs
|
||||||
|
// immediately when playback is live so the user does not see a one-frame gap.
|
||||||
|
invalidateSamplesBlob()
|
||||||
|
if (playbackMode !== PLAYMODE_NONE) drawSamplesPlayBlobs()
|
||||||
|
// Same reasoning for the waveform playhead cursor — drawSampleWaveform just
|
||||||
|
// wiped the area, so its prior column is irrelevant. Re-stamp if playing.
|
||||||
|
invalidateSmpCursor()
|
||||||
|
if (playbackMode !== PLAYMODE_NONE) drawSampleCursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
let pendingEditorLaunch = null // { progName, args[] }
|
let pendingEditorLaunch = null // { progName, args[] }
|
||||||
@@ -3632,7 +3676,7 @@ function refreshInstrumentsCache() { instrumentsCache = buildInstrumentIndex() }
|
|||||||
|
|
||||||
// ── Layout ─────────────────────────────────────────────────────────────────
|
// ── Layout ─────────────────────────────────────────────────────────────────
|
||||||
const INST_LIST_X = 1
|
const INST_LIST_X = 1
|
||||||
const INST_LIST_BODY_W = 26
|
const INST_LIST_BODY_W = 27
|
||||||
const INST_LIST_W = INST_LIST_BODY_W + 1
|
const INST_LIST_W = INST_LIST_BODY_W + 1
|
||||||
const INST_LIST_SCROLL_X = INST_LIST_X + INST_LIST_BODY_W
|
const INST_LIST_SCROLL_X = INST_LIST_X + INST_LIST_BODY_W
|
||||||
const INST_LIST_Y = PTNVIEW_OFFSET_Y
|
const INST_LIST_Y = PTNVIEW_OFFSET_Y
|
||||||
@@ -3713,8 +3757,10 @@ function drawInstrumentsListColumn() {
|
|||||||
for (let r = 0; r < INST_LIST_H; r++) {
|
for (let r = 0; r < INST_LIST_H; r++) {
|
||||||
con.move(INST_LIST_Y + r, INST_LIST_SCROLL_X)
|
con.move(INST_LIST_Y + r, INST_LIST_SCROLL_X)
|
||||||
con.color_pair(colStatus, colInstListBg)
|
con.color_pair(colStatus, colInstListBg)
|
||||||
print(r === indPos ? sym.ticked : sym.unticked)
|
|
||||||
}
|
let scrollChar = (r == 0) ? sym.taut_scrollgutter_top : (r == INST_LIST_H - 1) ? sym.taut_scrollgutter_bot : sym.taut_scrollgutter_mid
|
||||||
|
if (r == indPos) scrollChar += 3;
|
||||||
|
con.addch(scrollChar) }
|
||||||
} else {
|
} else {
|
||||||
for (let r = 0; r < INST_LIST_H; r++) {
|
for (let r = 0; r < INST_LIST_H; r++) {
|
||||||
con.move(INST_LIST_Y + r, INST_LIST_SCROLL_X)
|
con.move(INST_LIST_Y + r, INST_LIST_SCROLL_X)
|
||||||
@@ -3754,7 +3800,17 @@ function drawInstrumentsTabStrip() {
|
|||||||
const pad = Math.max(0, r.w - lbl.length)
|
const pad = Math.max(0, r.w - lbl.length)
|
||||||
const padL = pad >>> 1
|
const padL = pad >>> 1
|
||||||
const padR = pad - padL
|
const padR = pad - padL
|
||||||
print(' '.repeat(padL) + lbl + ' '.repeat(padR))
|
|
||||||
|
let colFore = active ? colTabActive : colTabInactive
|
||||||
|
let colBack = active ? colTabBarBack2 : colTabBarBack
|
||||||
|
let colFore2 = active ? colTabBarBack2 : colTabBarBack
|
||||||
|
let colBack2 = active ? colTabBarBack : colTabBarBack
|
||||||
|
let spcL = active ? sym.leftshade : ' '
|
||||||
|
let spcR = active ? sym.rightshade : ' '
|
||||||
|
|
||||||
|
con.color_pair(colFore2, colBack2); print(spcL)
|
||||||
|
con.color_pair(colFore, colBack); print(' '.repeat(padL-1) + lbl + ' '.repeat(padR-1))
|
||||||
|
con.color_pair(colFore2, colBack2); print(spcR)
|
||||||
}
|
}
|
||||||
// 1-row gap under the tabs
|
// 1-row gap under the tabs
|
||||||
con.move(INST_TAB_Y + 1, INST_RIGHT_X)
|
con.move(INST_TAB_Y + 1, INST_RIGHT_X)
|
||||||
@@ -4146,6 +4202,14 @@ function drawInstrumentsContents(wo) {
|
|||||||
else if (instSubTab === INST_TAB_PAN) drawInstTabPanning(e)
|
else if (instSubTab === INST_TAB_PAN) drawInstTabPanning(e)
|
||||||
else drawInstTabPitch(e)
|
else drawInstTabPitch(e)
|
||||||
drawInstrumentsEditButton()
|
drawInstrumentsEditButton()
|
||||||
|
// List redraw wiped col 1 across every row — invalidate, then re-stamp
|
||||||
|
// immediately while playing so the live indicator isn't blank for a frame.
|
||||||
|
invalidateInstrumentsBlob()
|
||||||
|
if (playbackMode !== PLAYMODE_NONE) drawInstrumentsPlayBlobs()
|
||||||
|
// Envelope-graph cursor: the panel rebuild wiped any prior hairline; invalidate
|
||||||
|
// and re-stamp so the user doesn't see it blink off on tab / inst switches.
|
||||||
|
invalidateEnvCursor()
|
||||||
|
if (playbackMode !== PLAYMODE_NONE) drawEnvelopeCursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
function instrumentsInput(wo, event) {
|
function instrumentsInput(wo, event) {
|
||||||
@@ -4229,6 +4293,364 @@ function registerInstrumentsMouse() {
|
|||||||
// END INSTRUMENTS VIEWER
|
// END INSTRUMENTS VIEWER
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// LIVE-PLAY BLOB (Samples / Instruments column 1)
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Per-row marker painted in column 1 of the Samples / Instruments list while a
|
||||||
|
// voice is actively sounding the corresponding sample / instrument. The glyph
|
||||||
|
// (sym.blob1..blob10) tracks the loudest active voice that references the row;
|
||||||
|
// sym.blob0 wipes the cell. The glyph FOREGROUND is colour-coded by polyphony —
|
||||||
|
// the number of notes (live voices + NNA ghosts) currently sounding the row —
|
||||||
|
// via a green→yellow→orange→red heat ramp. Per-row last-drawn (level, colour
|
||||||
|
// bucket) is cached so the per-frame repaint only redraws rows that changed —
|
||||||
|
// mirrors the bounded-work pattern used by drawVoiceMeters().
|
||||||
|
|
||||||
|
const smpBlobPrev = new Array(SMP_LIST_H).fill(-1)
|
||||||
|
const instBlobPrev = new Array(INST_LIST_H).fill(-1)
|
||||||
|
|
||||||
|
// Polyphony heat ramp for the blob foreground: more simultaneously-sounding notes → hotter.
|
||||||
|
const colBlobPoly1 = 34 // blue — 1 note
|
||||||
|
const colBlobPoly2 = 76 // green — 2 note
|
||||||
|
const colBlobPoly3 = 230 // yellow — 3 notes
|
||||||
|
const colBlobPoly4 = 221 // orange — 4 notes
|
||||||
|
const colBlobPoly5 = 211 // red — 5+ notes
|
||||||
|
const blobPolyCols = [colBlobPoly1, colBlobPoly2, colBlobPoly3, colBlobPoly4, colBlobPoly5]
|
||||||
|
|
||||||
|
// Note count → ramp bucket: 0 (silent), 1..3 verbatim, 4+ saturates at 4.
|
||||||
|
function blobPolyBucket(count) {
|
||||||
|
if (count <= 0) return 0
|
||||||
|
return count >= 5 ? 5 : count
|
||||||
|
}
|
||||||
|
|
||||||
|
// getActiveNoteCounts (the foreground+ghost polyphony API) ships with this feature; on an
|
||||||
|
// un-rebuilt host VM it's absent and blobs fall back to the plain number-column colour.
|
||||||
|
const hasNoteCountAPI = (typeof audio !== 'undefined' && typeof audio.getActiveNoteCounts === 'function')
|
||||||
|
|
||||||
|
function invalidateSamplesBlob() { for (let i = 0; i < smpBlobPrev.length; i++) smpBlobPrev[i] = -1 }
|
||||||
|
function invalidateInstrumentsBlob() { for (let i = 0; i < instBlobPrev.length; i++) instBlobPrev[i] = -1 }
|
||||||
|
|
||||||
|
// Walks the live voice slots and returns {instrumentId → loudest effective volume}.
|
||||||
|
// Silent / inactive voices are skipped. Volumes are 0.0..1.0.
|
||||||
|
function activeInstVolumes() {
|
||||||
|
const out = {}
|
||||||
|
const numVox = (song && song.numVoices) ? song.numVoices : NUM_VOICES
|
||||||
|
for (let v = 0; v < numVox; v++) {
|
||||||
|
if (!audio.getVoiceActive(PLAYHEAD, v)) continue
|
||||||
|
const inst = audio.getVoiceInstrument(PLAYHEAD, v)
|
||||||
|
if (!inst) continue
|
||||||
|
const vol = audio.getVoiceEffectiveVolume(PLAYHEAD, v) || 0
|
||||||
|
if (!(vol > 0)) continue
|
||||||
|
if (!(inst in out) || out[inst] < vol) out[inst] = vol
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0.0 → 0 (clear); (0, 1] → 1..10 via ceil so the quietest audible voice still shows blob1.
|
||||||
|
function blobLevelForVolume(v) {
|
||||||
|
if (!(v > 0)) return 0
|
||||||
|
let lvl = Math.ceil(v * 10)
|
||||||
|
if (lvl < 1) lvl = 1
|
||||||
|
if (lvl > 10) lvl = 10
|
||||||
|
return lvl
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSamplesPlayBlobs() {
|
||||||
|
if (currentPanel !== VIEW_SAMPLES || !samplesCache) return
|
||||||
|
const playing = (playbackMode !== PLAYMODE_NONE)
|
||||||
|
const instVols = playing ? activeInstVolumes() : null
|
||||||
|
const counts = (playing && hasNoteCountAPI) ? audio.getActiveNoteCounts(PLAYHEAD) : null
|
||||||
|
const n = samplesCache.length
|
||||||
|
for (let row = 0; row < SMP_LIST_H; row++) {
|
||||||
|
const idx = smpListScroll + row
|
||||||
|
let level = 0, poly = 0
|
||||||
|
if (playing && idx < n) {
|
||||||
|
const ub = samplesCache[idx].usedBy
|
||||||
|
let m = 0, c = 0
|
||||||
|
for (let j = 0; j < ub.length; j++) {
|
||||||
|
const w = instVols[ub[j]] || 0
|
||||||
|
if (w > m) m = w
|
||||||
|
if (counts) c += counts[ub[j]] || 0
|
||||||
|
}
|
||||||
|
level = blobLevelForVolume(m)
|
||||||
|
poly = blobPolyBucket(c)
|
||||||
|
}
|
||||||
|
// Ghost-only rows have notes sounding but no exposed foreground volume — floor the glyph
|
||||||
|
// to blob1 so the colour-coded marker is still visible.
|
||||||
|
if (poly > 0 && level === 0) level = 1
|
||||||
|
const key = level * 8 + poly // cache combines glyph level + colour bucket
|
||||||
|
if (smpBlobPrev[row] === key) continue
|
||||||
|
const isSel = (idx === smpListCursor)
|
||||||
|
const back = isSel ? colSmpListSel : colSmpListBg
|
||||||
|
const fg = (poly > 0) ? blobPolyCols[poly - 1] : colSmpListNumFg
|
||||||
|
con.move(SMP_LIST_Y + row, SMP_LIST_X)
|
||||||
|
con.color_pair(fg, back)
|
||||||
|
print(sym['blob' + level])
|
||||||
|
smpBlobPrev[row] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawInstrumentsPlayBlobs() {
|
||||||
|
if (currentPanel !== VIEW_INSTRMNT || !instrumentsCache) return
|
||||||
|
const playing = (playbackMode !== PLAYMODE_NONE)
|
||||||
|
const instVols = playing ? activeInstVolumes() : null
|
||||||
|
const counts = (playing && hasNoteCountAPI) ? audio.getActiveNoteCounts(PLAYHEAD) : null
|
||||||
|
const n = instrumentsCache.length
|
||||||
|
for (let row = 0; row < INST_LIST_H; row++) {
|
||||||
|
const idx = instListScroll + row
|
||||||
|
let level = 0, poly = 0
|
||||||
|
if (playing && idx < n) {
|
||||||
|
const slot = instrumentsCache[idx].slot
|
||||||
|
level = blobLevelForVolume(instVols[slot] || 0)
|
||||||
|
poly = blobPolyBucket(counts ? (counts[slot] || 0) : 0)
|
||||||
|
}
|
||||||
|
// Ghost-only rows sound but expose no foreground volume — floor to blob1 so the colour shows.
|
||||||
|
if (poly > 0 && level === 0) level = 1
|
||||||
|
const key = level * 8 + poly
|
||||||
|
if (instBlobPrev[row] === key) continue
|
||||||
|
const isSel = (idx === instListCursor)
|
||||||
|
const back = isSel ? colInstListSel : colInstListBg
|
||||||
|
const fg = (poly > 0) ? blobPolyCols[poly - 1] : colInstListNumFg
|
||||||
|
con.move(INST_LIST_Y + row, INST_LIST_X)
|
||||||
|
con.color_pair(fg, back)
|
||||||
|
print(sym['blob' + level])
|
||||||
|
instBlobPrev[row] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Playback-position cursor (sample waveform + vol/pan/pitch envelope graphs) ───────────────
|
||||||
|
// Vertical hairline glyph painted in the text layer at the column closest to the live
|
||||||
|
// playback position of EVERY voice that's sounding the displayed sample / instrument — one
|
||||||
|
// hairline per voice. Sub-pixel offset within the cell picks between vhairline1..vhairline7
|
||||||
|
// (vhairlineN draws the line N pixels from the cell's left edge; vhairline4 is the cell centre
|
||||||
|
// on a 7-px cell). When several voices want the same text column they are resolved quiet→loud,
|
||||||
|
// so the loudest voice's hairline wins the shared column.
|
||||||
|
|
||||||
|
const colPlayCursor = 215 // same hue used for the timeline play row
|
||||||
|
|
||||||
|
// Last-drawn state so each frame only repaints when something actually moved. Now that we draw
|
||||||
|
// one hairline per voice, *Cols is the list of text columns currently stamped and *Sig is a
|
||||||
|
// signature of the resolved per-column glyphs (so we can detect when any hairline moved).
|
||||||
|
// envCursorPrev{Tab,Inst}: the (tab, instrument-slot) the env hairlines belong to.
|
||||||
|
let envCursorPrevCols = []
|
||||||
|
let envCursorPrevSig = ''
|
||||||
|
let envCursorPrevTab = -1
|
||||||
|
let envCursorPrevInst = -1
|
||||||
|
let smpCursorPrevCols = []
|
||||||
|
let smpCursorPrevSig = ''
|
||||||
|
let smpCursorPrevIdx = -1
|
||||||
|
|
||||||
|
function invalidateEnvCursor() { envCursorPrevCols = []; envCursorPrevSig = ''; envCursorPrevTab = -1; envCursorPrevInst = -1 }
|
||||||
|
function invalidateSmpCursor() { smpCursorPrevCols = []; smpCursorPrevSig = ''; smpCursorPrevIdx = -1 }
|
||||||
|
|
||||||
|
// Map a pixel-space X coordinate to (text-column, vhairline glyph) such that the glyph's
|
||||||
|
// drawn line lands within ±½ a sub-pixel of xPix. Cell pixels are 1-indexed positions
|
||||||
|
// (left edge = pos 1), matching the vhairlineN naming.
|
||||||
|
function pixelToHairline(xPix) {
|
||||||
|
const col0 = Math.floor(xPix / CELL_PW)
|
||||||
|
let pos = xPix - col0 * CELL_PW + 1 // 1..CELL_PW
|
||||||
|
if (pos < 1) pos = 1
|
||||||
|
if (pos > 7) pos = 7
|
||||||
|
return { col: col0 + 1, hair: sym['vhairline' + pos] }
|
||||||
|
}
|
||||||
|
|
||||||
|
// All active voices currently bound to `slot` (1..255), as {voice, vol}. Empty if none.
|
||||||
|
function activeVoicesForInstSlot(slot) {
|
||||||
|
const out = []
|
||||||
|
const numVox = (song && song.numVoices) ? song.numVoices : NUM_VOICES
|
||||||
|
for (let v = 0; v < numVox; v++) {
|
||||||
|
if (!audio.getVoiceActive(PLAYHEAD, v)) continue
|
||||||
|
if (audio.getVoiceInstrument(PLAYHEAD, v) !== slot) continue
|
||||||
|
out.push({ voice: v, vol: audio.getVoiceEffectiveVolume(PLAYHEAD, v) || 0 })
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// All active voices whose instrument is in `usedBy` (the inst-slot list attached to a
|
||||||
|
// samplesCache entry — multiple instruments may share one sample), as {voice, vol}.
|
||||||
|
function activeVoicesForSampleEntry(usedBy) {
|
||||||
|
const out = []
|
||||||
|
const numVox = (song && song.numVoices) ? song.numVoices : NUM_VOICES
|
||||||
|
for (let v = 0; v < numVox; v++) {
|
||||||
|
if (!audio.getVoiceActive(PLAYHEAD, v)) continue
|
||||||
|
const inst = audio.getVoiceInstrument(PLAYHEAD, v)
|
||||||
|
if (usedBy.indexOf(inst) < 0) continue
|
||||||
|
out.push({ voice: v, vol: audio.getVoiceEffectiveVolume(PLAYHEAD, v) || 0 })
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapse a list of {col, hair, vol} hairline hits into a per-column glyph map, resolving
|
||||||
|
// shared columns quiet→loud so the loudest voice's hairline wins. Returns { cols, colMap, sig }
|
||||||
|
// where cols is the sorted column list, colMap maps col→glyph, and sig detects visual changes.
|
||||||
|
function resolveHairlineHits(hits) {
|
||||||
|
hits.sort((a, b) => a.vol - b.vol)
|
||||||
|
const colMap = {}
|
||||||
|
for (let i = 0; i < hits.length; i++) colMap[hits[i].col] = hits[i].hair
|
||||||
|
const cols = Object.keys(colMap).map(Number).sort((a, b) => a - b)
|
||||||
|
let sig = ''
|
||||||
|
for (let i = 0; i < cols.length; i++) sig += cols[i] + ':' + colMap[cols[i]] + ','
|
||||||
|
return { cols, colMap, sig }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull the envelope object + JSR223 getter pair for the active inst sub-tab. Returns null
|
||||||
|
// for tabs without a graph (Gen.1 / Gen.2).
|
||||||
|
function envBundleForCurrentTab(e) {
|
||||||
|
if (instSubTab === INST_TAB_VOL) return { env: e.decoded.volEnv,
|
||||||
|
idxFn: 'getVoiceEnvVolIndex', timeFn: 'getVoiceEnvVolTime' }
|
||||||
|
if (instSubTab === INST_TAB_PAN) return { env: e.decoded.panEnv,
|
||||||
|
idxFn: 'getVoiceEnvPanIndex', timeFn: 'getVoiceEnvPanTime' }
|
||||||
|
if (instSubTab === INST_TAB_PIT) return { env: e.decoded.pfEnv,
|
||||||
|
idxFn: 'getVoiceEnvPitchIndex', timeFn: 'getVoiceEnvPitchTime' }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// First/last text row covered by the inst-tab envelope graph. Mirrors instEnvelopeRect()
|
||||||
|
// in text-coord units so we know which rows to stamp / erase the hairline on.
|
||||||
|
function envGraphTextRows() {
|
||||||
|
const graphRowY = INST_BODY_Y + 7
|
||||||
|
return { y0: graphRowY, y1: INST_BTN_Y - 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same idea for the Samples-tab waveform.
|
||||||
|
function smpWaveTextRows() {
|
||||||
|
return { y0: SMP_WAVE_Y, y1: SMP_BTN_Y - 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintEnvCursorAt(col, hairSym) {
|
||||||
|
const rng = envGraphTextRows()
|
||||||
|
con.color_pair(colPlayCursor, 255)
|
||||||
|
for (let y = rng.y0; y <= rng.y1; y++) {
|
||||||
|
con.move(y, col)
|
||||||
|
print(hairSym)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function eraseEnvCursorIfAny() {
|
||||||
|
if (envCursorPrevCols.length === 0) return
|
||||||
|
const rng = envGraphTextRows()
|
||||||
|
con.color_pair(colInstValue, 255)
|
||||||
|
for (let k = 0; k < envCursorPrevCols.length; k++) {
|
||||||
|
const col = envCursorPrevCols[k]
|
||||||
|
for (let y = rng.y0; y <= rng.y1; y++) {
|
||||||
|
con.move(y, col)
|
||||||
|
print(' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
envCursorPrevCols = []
|
||||||
|
envCursorPrevSig = ''
|
||||||
|
envCursorPrevTab = -1
|
||||||
|
envCursorPrevInst = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintSmpCursorAt(col, hairSym) {
|
||||||
|
const rng = smpWaveTextRows()
|
||||||
|
con.color_pair(colPlayCursor, 255)
|
||||||
|
for (let y = rng.y0; y <= rng.y1; y++) {
|
||||||
|
con.move(y, col)
|
||||||
|
print(hairSym)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function eraseSmpCursorIfAny() {
|
||||||
|
if (smpCursorPrevCols.length === 0) return
|
||||||
|
const rng = smpWaveTextRows()
|
||||||
|
con.color_pair(colSmpPropValue, 255)
|
||||||
|
for (let k = 0; k < smpCursorPrevCols.length; k++) {
|
||||||
|
const col = smpCursorPrevCols[k]
|
||||||
|
for (let y = rng.y0; y <= rng.y1; y++) {
|
||||||
|
con.move(y, col)
|
||||||
|
print(' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
smpCursorPrevCols = []
|
||||||
|
smpCursorPrevSig = ''
|
||||||
|
smpCursorPrevIdx = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawEnvelopeCursor() {
|
||||||
|
if (currentPanel !== VIEW_INSTRMNT) { invalidateEnvCursor(); return }
|
||||||
|
if (!instrumentsCache || instrumentsCache.length === 0) { eraseEnvCursorIfAny(); return }
|
||||||
|
const e = instrumentsCache[instListCursor]
|
||||||
|
if (!e) { eraseEnvCursorIfAny(); return }
|
||||||
|
const bundle = envBundleForCurrentTab(e)
|
||||||
|
// Gen.1 / Gen.2 have no envelope graph — wipe any stale hairline and bail.
|
||||||
|
if (!bundle) { eraseEnvCursorIfAny(); return }
|
||||||
|
const env = bundle.env
|
||||||
|
const lastIdx = (env.terminatorIdx >= 0) ? env.terminatorIdx : (env.nodes.length - 1)
|
||||||
|
if (lastIdx < 0) { eraseEnvCursorIfAny(); return }
|
||||||
|
|
||||||
|
const hits = []
|
||||||
|
if (playbackMode !== PLAYMODE_NONE) {
|
||||||
|
// Cumulative time at each node (mirrors xs[] in drawEnvelopeGraph) — shared by all voices.
|
||||||
|
let acc = 0
|
||||||
|
const xs = new Array(lastIdx + 1)
|
||||||
|
xs[0] = 0
|
||||||
|
for (let i = 1; i <= lastIdx; i++) { acc += env.nodes[i - 1].durSec; xs[i] = acc }
|
||||||
|
const totalTime = Math.max(acc, 1e-6)
|
||||||
|
const r = instEnvelopeRect()
|
||||||
|
const voices = activeVoicesForInstSlot(e.slot)
|
||||||
|
for (let k = 0; k < voices.length; k++) {
|
||||||
|
const v = voices[k].voice
|
||||||
|
const envIdx = audio[bundle.idxFn](PLAYHEAD, v)
|
||||||
|
const envTime = audio[bundle.timeFn](PLAYHEAD, v)
|
||||||
|
if (envIdx < 0) continue
|
||||||
|
const ei = Math.max(0, Math.min(lastIdx, envIdx))
|
||||||
|
const segLen = (ei < lastIdx) ? env.nodes[ei].durSec : 0
|
||||||
|
const tInto = Math.max(0, Math.min(segLen, envTime))
|
||||||
|
const elapsed = xs[ei] + tInto
|
||||||
|
const xPix = r.x + Math.min(r.w - 1, Math.max(0, ((elapsed / totalTime) * (r.w - 1)) | 0))
|
||||||
|
const sel = pixelToHairline(xPix)
|
||||||
|
hits.push({ col: sel.col, hair: sel.hair, vol: voices[k].vol })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const res = resolveHairlineHits(hits)
|
||||||
|
|
||||||
|
if (res.sig === envCursorPrevSig &&
|
||||||
|
envCursorPrevTab === instSubTab &&
|
||||||
|
envCursorPrevInst === e.slot) return
|
||||||
|
|
||||||
|
eraseEnvCursorIfAny()
|
||||||
|
if (res.cols.length > 0) {
|
||||||
|
for (let i = 0; i < res.cols.length; i++) paintEnvCursorAt(res.cols[i], res.colMap[res.cols[i]])
|
||||||
|
envCursorPrevCols = res.cols
|
||||||
|
envCursorPrevSig = res.sig
|
||||||
|
envCursorPrevTab = instSubTab
|
||||||
|
envCursorPrevInst = e.slot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSampleCursor() {
|
||||||
|
if (currentPanel !== VIEW_SAMPLES) { invalidateSmpCursor(); return }
|
||||||
|
if (!samplesCache || samplesCache.length === 0) { eraseSmpCursorIfAny(); return }
|
||||||
|
const s = samplesCache[smpListCursor]
|
||||||
|
if (!s || s.len <= 0) { eraseSmpCursorIfAny(); return }
|
||||||
|
|
||||||
|
const hits = []
|
||||||
|
if (playbackMode !== PLAYMODE_NONE) {
|
||||||
|
const r = sampleWaveformRect()
|
||||||
|
const voices = activeVoicesForSampleEntry(s.usedBy)
|
||||||
|
for (let k = 0; k < voices.length; k++) {
|
||||||
|
const pos = audio.getVoiceSamplePos(PLAYHEAD, voices[k].voice)
|
||||||
|
if (pos < 0) continue
|
||||||
|
const norm = Math.max(0, Math.min(1, pos / s.len))
|
||||||
|
const xPix = r.x + Math.min(r.w - 1, Math.max(0, (norm * (r.w - 1)) | 0))
|
||||||
|
const sel = pixelToHairline(xPix)
|
||||||
|
hits.push({ col: sel.col, hair: sel.hair, vol: voices[k].vol })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const res = resolveHairlineHits(hits)
|
||||||
|
|
||||||
|
if (res.sig === smpCursorPrevSig && smpCursorPrevIdx === smpListCursor) return
|
||||||
|
|
||||||
|
eraseSmpCursorIfAny()
|
||||||
|
if (res.cols.length > 0) {
|
||||||
|
for (let i = 0; i < res.cols.length; i++) paintSmpCursorAt(res.cols[i], res.colMap[res.cols[i]])
|
||||||
|
smpCursorPrevCols = res.cols
|
||||||
|
smpCursorPrevSig = res.sig
|
||||||
|
smpCursorPrevIdx = smpListCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const panelSamples = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, samplesInput, drawSamplesContents, undefined, ()=>{})
|
const panelSamples = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, samplesInput, drawSamplesContents, undefined, ()=>{})
|
||||||
const panelInstrmnt = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, instrumentsInput, drawInstrumentsContents, undefined, ()=>{})
|
const panelInstrmnt = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, instrumentsInput, drawInstrumentsContents, undefined, ()=>{})
|
||||||
const panelProject = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, projectInput, drawProjectContents, undefined, ()=>{})
|
const panelProject = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, projectInput, drawProjectContents, undefined, ()=>{})
|
||||||
@@ -4391,6 +4813,12 @@ function stopPlayback() {
|
|||||||
playbackMode = PLAYMODE_NONE
|
playbackMode = PLAYMODE_NONE
|
||||||
clampPatternGrid()
|
clampPatternGrid()
|
||||||
clearVoiceMeters()
|
clearVoiceMeters()
|
||||||
|
// updatePlayback no longer fires after this point — paint the final clear
|
||||||
|
// pass ourselves so stale blobs / hairlines don't linger on Samples / Instruments.
|
||||||
|
drawSamplesPlayBlobs()
|
||||||
|
drawInstrumentsPlayBlobs()
|
||||||
|
drawSampleCursor()
|
||||||
|
drawEnvelopeCursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePlayback() {
|
function updatePlayback() {
|
||||||
@@ -4403,10 +4831,19 @@ function updatePlayback() {
|
|||||||
else if (currentPanel === VIEW_PATTERN_DETAILS && song.numPats > 0) { simStateKey = ''; redrawPanel() }
|
else if (currentPanel === VIEW_PATTERN_DETAILS && song.numPats > 0) { simStateKey = ''; redrawPanel() }
|
||||||
drawAlwaysOnElems()
|
drawAlwaysOnElems()
|
||||||
clearVoiceMeters()
|
clearVoiceMeters()
|
||||||
|
// playbackMode is NONE now → these paint a final blob0 / clear-cursor pass.
|
||||||
|
drawSamplesPlayBlobs()
|
||||||
|
drawInstrumentsPlayBlobs()
|
||||||
|
drawSampleCursor()
|
||||||
|
drawEnvelopeCursor()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
drawVoiceMeters()
|
drawVoiceMeters()
|
||||||
|
drawSamplesPlayBlobs()
|
||||||
|
drawInstrumentsPlayBlobs()
|
||||||
|
drawSampleCursor()
|
||||||
|
drawEnvelopeCursor()
|
||||||
|
|
||||||
const nowCue = audio.getCuePosition(PLAYHEAD)
|
const nowCue = audio.getCuePosition(PLAYHEAD)
|
||||||
const nowRow = audio.getTrackerRow(PLAYHEAD)
|
const nowRow = audio.getTrackerRow(PLAYHEAD)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -140,6 +140,29 @@ class AudioJSR223Delegate(private val vm: VM) {
|
|||||||
fun getVoiceActive(playhead: Int, voice: Int): Boolean =
|
fun getVoiceActive(playhead: Int, voice: Int): Boolean =
|
||||||
getPlayhead(playhead)?.trackerState?.voices?.getOrNull(voice.coerceIn(0, 19))?.active == true
|
getPlayhead(playhead)?.trackerState?.voices?.getOrNull(voice.coerceIn(0, 19))?.active == true
|
||||||
|
|
||||||
|
/** Active-note counts per instrument id (index 0..255): how many notes are sounding *right
|
||||||
|
* now* for each instrument, counting ~~BOTH~~ the live foreground voices ~~and the NNA background
|
||||||
|
* ghosts in the mixer-private pool~~~. Lets visualisers colour by polyphony. The ghost pool is
|
||||||
|
* mutated by the render thread, so it is read defensively by index and any transient
|
||||||
|
* inconsistency is tolerated (a single best-effort frame). */
|
||||||
|
fun getActiveNoteCounts(playhead: Int): IntArray {
|
||||||
|
val counts = IntArray(256)
|
||||||
|
val ts = getPlayhead(playhead)?.trackerState ?: return counts
|
||||||
|
for (v in ts.voices) {
|
||||||
|
if (v.active) counts[v.instrumentId and 0xFF]++
|
||||||
|
}
|
||||||
|
// disabling NNA for now
|
||||||
|
/*try {
|
||||||
|
val bg = ts.backgroundVoices
|
||||||
|
for (i in 0 until bg.size) {
|
||||||
|
val v = bg.getOrNull(i) ?: continue
|
||||||
|
if (v.active) counts[v.instrumentId and 0xFF]++
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { /* ghost pool mutated mid-read — counts are best-effort */ }
|
||||||
|
*/
|
||||||
|
return counts
|
||||||
|
}
|
||||||
|
|
||||||
/** Live noteVal (0..65535, 4096-TET) of the foreground voice — the value the mixer is using
|
/** 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
|
* *right now* including any in-flight vibrato / arpeggio / portamento delta. Returns 0 for
|
||||||
* inactive voices. */
|
* inactive voices. */
|
||||||
@@ -156,6 +179,54 @@ class AudioJSR223Delegate(private val vm: VM) {
|
|||||||
return v.instrumentId and 0xFF
|
return v.instrumentId and 0xFF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Current sample-frame playback position (fractional double) of the voice. Returns -1.0
|
||||||
|
* when the voice is inactive so visualisers can distinguish "no cursor" from "cursor at 0". */
|
||||||
|
fun getVoiceSamplePos(playhead: Int, voice: Int): Double {
|
||||||
|
val v = getPlayhead(playhead)?.trackerState?.voices?.getOrNull(voice.coerceIn(0, 19)) ?: return -1.0
|
||||||
|
if (!v.active) return -1.0
|
||||||
|
return v.samplePos
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Volume-envelope segment index — i.e. the node the voice is currently moving *away* from
|
||||||
|
* (the next node it will hit is index + 1). Returns -1 when inactive. */
|
||||||
|
fun getVoiceEnvVolIndex(playhead: Int, voice: Int): Int {
|
||||||
|
val v = getPlayhead(playhead)?.trackerState?.voices?.getOrNull(voice.coerceIn(0, 19)) ?: return -1
|
||||||
|
if (!v.active) return -1
|
||||||
|
return v.envIndex
|
||||||
|
}
|
||||||
|
/** Seconds elapsed *into* the current volume-envelope segment (0 ≤ t < segment.offset). */
|
||||||
|
fun getVoiceEnvVolTime(playhead: Int, voice: Int): Double {
|
||||||
|
val v = getPlayhead(playhead)?.trackerState?.voices?.getOrNull(voice.coerceIn(0, 19)) ?: return 0.0
|
||||||
|
if (!v.active) return 0.0
|
||||||
|
return v.envTimeSec
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pan-envelope segment index — see [getVoiceEnvVolIndex]. */
|
||||||
|
fun getVoiceEnvPanIndex(playhead: Int, voice: Int): Int {
|
||||||
|
val v = getPlayhead(playhead)?.trackerState?.voices?.getOrNull(voice.coerceIn(0, 19)) ?: return -1
|
||||||
|
if (!v.active) return -1
|
||||||
|
return v.envPanIndex
|
||||||
|
}
|
||||||
|
/** Seconds elapsed into the current pan-envelope segment. */
|
||||||
|
fun getVoiceEnvPanTime(playhead: Int, voice: Int): Double {
|
||||||
|
val v = getPlayhead(playhead)?.trackerState?.voices?.getOrNull(voice.coerceIn(0, 19)) ?: return 0.0
|
||||||
|
if (!v.active) return 0.0
|
||||||
|
return v.envPanTimeSec
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pitch/filter-envelope segment index — see [getVoiceEnvVolIndex]. */
|
||||||
|
fun getVoiceEnvPitchIndex(playhead: Int, voice: Int): Int {
|
||||||
|
val v = getPlayhead(playhead)?.trackerState?.voices?.getOrNull(voice.coerceIn(0, 19)) ?: return -1
|
||||||
|
if (!v.active) return -1
|
||||||
|
return v.envPfIndex
|
||||||
|
}
|
||||||
|
/** Seconds elapsed into the current pitch/filter-envelope segment. */
|
||||||
|
fun getVoiceEnvPitchTime(playhead: Int, voice: Int): Double {
|
||||||
|
val v = getPlayhead(playhead)?.trackerState?.voices?.getOrNull(voice.coerceIn(0, 19)) ?: return 0.0
|
||||||
|
if (!v.active) return 0.0
|
||||||
|
return v.envPfTimeSec
|
||||||
|
}
|
||||||
|
|
||||||
/** Set the starting row for the next play call, resetting per-row timing and silencing active voices. */
|
/** Set the starting row for the next play call, resetting per-row timing and silencing active voices. */
|
||||||
fun setTrackerRow(playhead: Int, row: Int) {
|
fun setTrackerRow(playhead: Int, row: Int) {
|
||||||
getPlayhead(playhead)?.trackerState?.let { ts ->
|
getPlayhead(playhead)?.trackerState?.let { ts ->
|
||||||
|
|||||||
Reference in New Issue
Block a user