mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
more tracker stuff
This commit is contained in:
@@ -347,7 +347,7 @@ const VIEW_INSTRUMENT = 2
|
|||||||
const VIEW_PATTERN_DETAILS = 3
|
const VIEW_PATTERN_DETAILS = 3
|
||||||
|
|
||||||
const colPlayback = 6
|
const colPlayback = 6
|
||||||
const colHighlight = 6
|
const colHighlight = 81
|
||||||
const colRowNum = 249
|
const colRowNum = 249
|
||||||
const colRowNumEmph1 = 180
|
const colRowNumEmph1 = 180
|
||||||
const colStatus = 253
|
const colStatus = 253
|
||||||
@@ -381,15 +381,18 @@ function drawSeparators(posY, col_size) {
|
|||||||
|
|
||||||
function drawVoiceHeaders() {
|
function drawVoiceHeaders() {
|
||||||
fillLine(PTNVIEW_OFFSET_Y - 1, colVoiceHdr, 255)
|
fillLine(PTNVIEW_OFFSET_Y - 1, colVoiceHdr, 255)
|
||||||
con.color_pair(colVoiceHdr, 255)
|
|
||||||
const cue = song.cues[cueIdx]
|
const cue = song.cues[cueIdx]
|
||||||
for (let c = 0; c < VOCSIZE; c++) {
|
for (let c = 0; c < VOCSIZE; c++) {
|
||||||
const voice = voiceOff + c
|
const voice = voiceOff + c
|
||||||
const x = PTNVIEW_OFFSET_X + COLSIZE * c
|
const x = PTNVIEW_OFFSET_X + COLSIZE * c
|
||||||
con.move(PTNVIEW_OFFSET_Y - 1, x)
|
con.move(PTNVIEW_OFFSET_Y - 1, x)
|
||||||
if (voice >= song.numVoices) {
|
if (voice >= song.numVoices) {
|
||||||
|
con.color_pair(colVoiceHdr, 255)
|
||||||
print(` `.substring(0, COLSIZE - 1))
|
print(` `.substring(0, COLSIZE - 1))
|
||||||
} else {
|
} else {
|
||||||
|
const isCursor = (voice === cursorVox)
|
||||||
|
const isMuted = voiceMutes[voice]
|
||||||
|
con.color_pair(isMuted ? 249 : colVoiceHdr, isCursor ? colHighlight : 255)
|
||||||
const ptnIdx = cue.ptns[voice]
|
const ptnIdx = cue.ptns[voice]
|
||||||
const vlabel = `V${(voice+1).dec02()}`
|
const vlabel = `V${(voice+1).dec02()}`
|
||||||
const plabel = (ptnIdx === CUE_EMPTY) ? '---' : ptnIdx.hex03()
|
const plabel = (ptnIdx === CUE_EMPTY) ? '---' : ptnIdx.hex03()
|
||||||
@@ -444,7 +447,33 @@ function drawControlHint() {
|
|||||||
con.move(SCRH, 1)
|
con.move(SCRH, 1)
|
||||||
print(' '.repeat(SCRW-1))
|
print(' '.repeat(SCRW-1))
|
||||||
con.move(SCRH, 1)
|
con.move(SCRH, 1)
|
||||||
print(`\u008424u\u008425u Row ${MIDDOT} \u008427u\u008426u Vox ${MIDDOT} Pg\u008424u\u008425u Ptn ${MIDDOT} Hm/Ed Row ${MIDDOT} F5 Song F6 Cue F7 Row F8/Spc Stop`)
|
print(`\u008424u\u008425u Row ${MIDDOT} \u008427u\u008426u Vox ${MIDDOT} Pg\u008424u\u008425u Ptn ${MIDDOT} F5 Song F6 Cue F7 Row F8/Spc Stop ${MIDDOT} m Mute s Solo`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMute(vox) {
|
||||||
|
voiceMutes[vox] = !voiceMutes[vox]
|
||||||
|
audio.setVoiceMute(PLAYHEAD, vox, voiceMutes[vox])
|
||||||
|
drawVoiceHeaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSolo(vox) {
|
||||||
|
let inSolo = true
|
||||||
|
for (let i = 0; i < song.numVoices; i++) {
|
||||||
|
if (i !== vox && !voiceMutes[i]) { inSolo = false; break }
|
||||||
|
}
|
||||||
|
if (inSolo) {
|
||||||
|
for (let i = 0; i < song.numVoices; i++) {
|
||||||
|
voiceMutes[i] = false
|
||||||
|
audio.setVoiceMute(PLAYHEAD, i, false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < song.numVoices; i++) {
|
||||||
|
const m = (i !== vox)
|
||||||
|
voiceMutes[i] = m
|
||||||
|
audio.setVoiceMute(PLAYHEAD, i, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawVoiceHeaders()
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawVoiceDetail() {
|
function drawVoiceDetail() {
|
||||||
@@ -502,6 +531,13 @@ const TEXT_PLANES = [TEXT_CHAR_OFF, TEXT_BACK_OFF, TEXT_FORE_OFF]
|
|||||||
// One scratch strip, reused across shifts
|
// One scratch strip, reused across shifts
|
||||||
const SCRATCH_PTR = sys.malloc(SCRW * PTNVIEW_HEIGHT)
|
const SCRATCH_PTR = sys.malloc(SCRW * PTNVIEW_HEIGHT)
|
||||||
|
|
||||||
|
// Horizontal salvage: 3 carried voice columns minus the missing trailing separator.
|
||||||
|
// For shift-left: source x=23..75 (old cols 1,2,3); dest x=5..57 (new cols 0,1,2).
|
||||||
|
// For shift-right: source x=5..57 (old cols 0,1,2); dest x=23..75 (new cols 1,2,3).
|
||||||
|
// The separator at the boundary of the exposed column is already in place after
|
||||||
|
// the shift (it was never overwritten), so no extra separator fix-up is needed.
|
||||||
|
const SALVAGE_HORIZ_LEN = (VOCSIZE - 1) * COLSIZE - 1 // 53 chars
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shift the pattern-view rows by `dy` lines (positive = down, negative = up)
|
* Shift the pattern-view rows by `dy` lines (positive = down, negative = up)
|
||||||
* using bulk peri→main→peri memcpy for speed. Does not touch status bar,
|
* using bulk peri→main→peri memcpy for speed. Does not touch status bar,
|
||||||
@@ -525,6 +561,55 @@ function shiftPatternArea(dy) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shift the voice columns left (dVoice > 0) or right (dVoice < 0) by one column
|
||||||
|
* using per-row peri→main→peri memcpy. Only the pattern-view rows are touched;
|
||||||
|
* voice headers and status bar must be redrawn by the caller.
|
||||||
|
*/
|
||||||
|
function shiftPatternAreaHorizontal(dVoice) {
|
||||||
|
// Column of the first char to copy (1-indexed); dest is COLSIZE chars earlier/later.
|
||||||
|
const srcX = PTNVIEW_OFFSET_X + (dVoice > 0 ? COLSIZE : 0)
|
||||||
|
const dstX = PTNVIEW_OFFSET_X + (dVoice > 0 ? 0 : COLSIZE)
|
||||||
|
const srcOff = srcX - 1 // 0-indexed offset from column 1 for address arithmetic
|
||||||
|
const dstOff = dstX - 1
|
||||||
|
|
||||||
|
for (let p = 0; p < 3; p++) {
|
||||||
|
const chanOff = TEXT_PLANES[p]
|
||||||
|
for (let vr = 0; vr < PTNVIEW_HEIGHT; vr++) {
|
||||||
|
const rowBase = GPU_MEM - chanOff - (PTNVIEW_OFFSET_Y + vr - 1) * SCRW
|
||||||
|
sys.memcpy(rowBase - srcOff, SCRATCH_PTR, SALVAGE_HORIZ_LEN)
|
||||||
|
sys.memcpy(SCRATCH_PTR, rowBase - dstOff, SALVAGE_HORIZ_LEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redraw every row of one voice column (slot 0..VOCSIZE-1) after a horizontal shift.
|
||||||
|
* Also redraws separators for the whole row so any separator at the exposed boundary
|
||||||
|
* (which the VRAM shift left correct) is confirmed visually consistent.
|
||||||
|
*/
|
||||||
|
function drawVoiceColumnAt(slot) {
|
||||||
|
const voice = voiceOff + slot
|
||||||
|
const x = PTNVIEW_OFFSET_X + COLSIZE * slot
|
||||||
|
const cue = song.cues[cueIdx]
|
||||||
|
const ptnIdx = (voice < song.numVoices) ? cue.ptns[voice] : CUE_EMPTY
|
||||||
|
|
||||||
|
for (let vr = 0; vr < PTNVIEW_HEIGHT; vr++) {
|
||||||
|
const actualRow = scrollRow + vr
|
||||||
|
const y = PTNVIEW_OFFSET_Y + vr
|
||||||
|
const highlight = (actualRow === cursorRow)
|
||||||
|
const back = highlight ? (playbackMode !== PLAYMODE_NONE ? colPlayback : colHighlight) : colBackPtn
|
||||||
|
|
||||||
|
let cell = EMPTY_CELL
|
||||||
|
if (actualRow < ROWS_PER_PAT && voice < song.numVoices &&
|
||||||
|
ptnIdx !== CUE_EMPTY && ptnIdx < song.numPats) {
|
||||||
|
cell = buildRowCell(song.patterns[ptnIdx], actualRow)
|
||||||
|
}
|
||||||
|
drawCellAt(y, x, cell, back)
|
||||||
|
drawSeparators(y, COLSIZE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// APPLICATION STUB
|
// APPLICATION STUB
|
||||||
@@ -552,6 +637,8 @@ if (fullPathObj === undefined) {
|
|||||||
|
|
||||||
const song = loadTaud(fullPathObj.full, 0)
|
const song = loadTaud(fullPathObj.full, 0)
|
||||||
|
|
||||||
|
const voiceMutes = new Array(NUM_VOICES).fill(false)
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// PLAYBACK STATE
|
// PLAYBACK STATE
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -686,6 +773,10 @@ function clampCursor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clampVoice() {
|
function clampVoice() {
|
||||||
|
if (cursorVox < 0) cursorVox = 0
|
||||||
|
if (cursorVox >= song.numVoices) cursorVox = song.numVoices - 1
|
||||||
|
if (cursorVox < voiceOff) voiceOff = cursorVox
|
||||||
|
if (cursorVox >= voiceOff + VOCSIZE) voiceOff = cursorVox - VOCSIZE + 1
|
||||||
const maxOff = Math.max(0, song.numVoices - VOCSIZE)
|
const maxOff = Math.max(0, song.numVoices - VOCSIZE)
|
||||||
if (voiceOff < 0) voiceOff = 0
|
if (voiceOff < 0) voiceOff = 0
|
||||||
if (voiceOff > maxOff) voiceOff = maxOff
|
if (voiceOff > maxOff) voiceOff = maxOff
|
||||||
@@ -720,8 +811,20 @@ while (!exitFlag) {
|
|||||||
|
|
||||||
if (playbackMode !== PLAYMODE_NONE) {
|
if (playbackMode !== PLAYMODE_NONE) {
|
||||||
if (keysym === "<F8>" || keysym === " ") { stopPlayback(); drawAll() }
|
if (keysym === "<F8>" || keysym === " ") { stopPlayback(); drawAll() }
|
||||||
else if (keysym === "<LEFT>") { voiceOff -= 1; clampVoice(); drawAll() }
|
else if (keysym === "<LEFT>" || keysym === "<RIGHT>") {
|
||||||
else if (keysym === "<RIGHT>") { voiceOff += 1; clampVoice(); drawAll() }
|
const oldVoiceOff = voiceOff
|
||||||
|
cursorVox += (keysym === "<LEFT>") ? -1 : 1
|
||||||
|
clampVoice()
|
||||||
|
const dVoice = voiceOff - oldVoiceOff
|
||||||
|
if (dVoice !== 0) {
|
||||||
|
shiftPatternAreaHorizontal(dVoice)
|
||||||
|
drawVoiceColumnAt(dVoice > 0 ? VOCSIZE - 1 : 0)
|
||||||
|
}
|
||||||
|
drawVoiceHeaders()
|
||||||
|
drawStatusBar()
|
||||||
|
}
|
||||||
|
else if (keysym === "m" || keysym === "M") { toggleMute(cursorVox) }
|
||||||
|
else if (keysym === "s" || keysym === "S") { toggleSolo(cursorVox) }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,12 +838,28 @@ while (!exitFlag) {
|
|||||||
let rowMove = false // pure row-cursor movement; can be fast-path
|
let rowMove = false // pure row-cursor movement; can be fast-path
|
||||||
let fullRedraw = false // voice/cue change; needs full viewport refresh
|
let fullRedraw = false // voice/cue change; needs full viewport refresh
|
||||||
|
|
||||||
|
if (keysym === "<LEFT>" || keysym === "<RIGHT>") {
|
||||||
|
const oldVoiceOff = voiceOff
|
||||||
|
cursorVox += (keysym === "<LEFT>") ? -1 : 1
|
||||||
|
clampVoice()
|
||||||
|
const dVoice = voiceOff - oldVoiceOff
|
||||||
|
if (dVoice !== 0) {
|
||||||
|
shiftPatternAreaHorizontal(dVoice)
|
||||||
|
drawVoiceColumnAt(dVoice > 0 ? VOCSIZE - 1 : 0)
|
||||||
|
}
|
||||||
|
drawVoiceHeaders()
|
||||||
|
drawStatusBar()
|
||||||
|
drawVoiceDetail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keysym === "m" || keysym === "M") { toggleMute(cursorVox); return }
|
||||||
|
if (keysym === "s" || keysym === "S") { toggleSolo(cursorVox); return }
|
||||||
|
|
||||||
if (keysym === "<UP>") { cursorRow -= 1; rowMove = true }
|
if (keysym === "<UP>") { cursorRow -= 1; rowMove = true }
|
||||||
else if (keysym === "<DOWN>") { cursorRow += 1; rowMove = true }
|
else if (keysym === "<DOWN>") { cursorRow += 1; rowMove = true }
|
||||||
else if (keysym === "<HOME>") { cursorRow = 0; rowMove = true }
|
else if (keysym === "<HOME>") { cursorRow = 0; rowMove = true }
|
||||||
else if (keysym === "<END>") { cursorRow = ROWS_PER_PAT-1; rowMove = true }
|
else if (keysym === "<END>") { cursorRow = ROWS_PER_PAT-1; rowMove = true }
|
||||||
else if (keysym === "<LEFT>") { voiceOff -= 1; fullRedraw = true }
|
|
||||||
else if (keysym === "<RIGHT>") { voiceOff += 1; fullRedraw = true }
|
|
||||||
else if (keysym === "<PAGE_UP>") { cueIdx -= 1; fullRedraw = true }
|
else if (keysym === "<PAGE_UP>") { cueIdx -= 1; fullRedraw = true }
|
||||||
else if (keysym === "<PAGE_DOWN>") { cueIdx += 1; fullRedraw = true }
|
else if (keysym === "<PAGE_DOWN>") { cueIdx += 1; fullRedraw = true }
|
||||||
else return
|
else return
|
||||||
|
|||||||
@@ -89,6 +89,27 @@ class AudioJSR223Delegate(private val vm: VM) {
|
|||||||
}
|
}
|
||||||
fun getCuePosition(playhead: Int) = getPlayhead(playhead)?.position
|
fun getCuePosition(playhead: Int) = getPlayhead(playhead)?.position
|
||||||
|
|
||||||
|
fun getTrackerRow(playhead: Int) = getPlayhead(playhead)?.trackerState?.rowIndex ?: 0
|
||||||
|
|
||||||
|
fun setVoiceMute(playhead: Int, voice: Int, muted: Boolean) {
|
||||||
|
getPlayhead(playhead)?.trackerState?.voices?.getOrNull(voice.coerceIn(0, 19))?.muted = muted
|
||||||
|
}
|
||||||
|
fun getVoiceMute(playhead: Int, voice: Int): Boolean =
|
||||||
|
getPlayhead(playhead)?.trackerState?.voices?.getOrNull(voice.coerceIn(0, 19))?.muted ?: false
|
||||||
|
|
||||||
|
/** Set the starting row for the next play call, resetting per-row timing and silencing active voices. */
|
||||||
|
fun setTrackerRow(playhead: Int, row: Int) {
|
||||||
|
getPlayhead(playhead)?.trackerState?.let { ts ->
|
||||||
|
ts.rowIndex = row.coerceIn(0, 63)
|
||||||
|
ts.tickInRow = 0
|
||||||
|
ts.samplesIntoTick = 0.0
|
||||||
|
ts.firstRow = true
|
||||||
|
ts.pendingOrderJump = -1
|
||||||
|
ts.pendingRowJump = -1
|
||||||
|
ts.voices.forEach { it.active = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Upload 64 bytes defining instrument `slot` (0-255). */
|
/** Upload 64 bytes defining instrument `slot` (0-255). */
|
||||||
fun uploadInstrument(slot: Int, bytes: IntArray) {
|
fun uploadInstrument(slot: Int, bytes: IntArray) {
|
||||||
getFirstSnd()?.instruments?.get(slot and 0xFF)?.let { inst ->
|
getFirstSnd()?.instruments?.get(slot and 0xFF)?.let { inst ->
|
||||||
|
|||||||
@@ -1708,7 +1708,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
var mixR = 0.0
|
var mixR = 0.0
|
||||||
val gvol = playhead.globalVolume / 255.0
|
val gvol = playhead.globalVolume / 255.0
|
||||||
for (voice in ts.voices) {
|
for (voice in ts.voices) {
|
||||||
if (!voice.active) continue
|
if (!voice.active || voice.muted) continue
|
||||||
val s = fetchTrackerSample(voice, instruments[voice.instrumentId])
|
val s = fetchTrackerSample(voice, instruments[voice.instrumentId])
|
||||||
val vol = voice.envVolume * voice.rowVolume / 63.0 * gvol * playhead.masterVolume / 255.0
|
val vol = voice.envVolume * voice.rowVolume / 63.0 * gvol * playhead.masterVolume / 255.0
|
||||||
mixL += s * vol * (63 - voice.rowPan) / 63.0
|
mixL += s * vol * (63 - voice.rowPan) / 63.0
|
||||||
@@ -1857,6 +1857,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
|
|
||||||
class Voice {
|
class Voice {
|
||||||
var active = false
|
var active = false
|
||||||
|
var muted = false
|
||||||
var instrumentId = 0
|
var instrumentId = 0
|
||||||
var samplePos = 0.0
|
var samplePos = 0.0
|
||||||
var playbackRate = 1.0
|
var playbackRate = 1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user