more tracker stuff

This commit is contained in:
minjaesong
2026-04-23 12:43:56 +09:00
parent 3a91edb379
commit e58eb2c12b
3 changed files with 149 additions and 8 deletions

View File

@@ -347,7 +347,7 @@ const VIEW_INSTRUMENT = 2
const VIEW_PATTERN_DETAILS = 3
const colPlayback = 6
const colHighlight = 6
const colHighlight = 81
const colRowNum = 249
const colRowNumEmph1 = 180
const colStatus = 253
@@ -381,15 +381,18 @@ function drawSeparators(posY, col_size) {
function drawVoiceHeaders() {
fillLine(PTNVIEW_OFFSET_Y - 1, colVoiceHdr, 255)
con.color_pair(colVoiceHdr, 255)
const cue = song.cues[cueIdx]
for (let c = 0; c < VOCSIZE; c++) {
const voice = voiceOff + c
const x = PTNVIEW_OFFSET_X + COLSIZE * c
con.move(PTNVIEW_OFFSET_Y - 1, x)
if (voice >= song.numVoices) {
con.color_pair(colVoiceHdr, 255)
print(` `.substring(0, COLSIZE - 1))
} else {
const isCursor = (voice === cursorVox)
const isMuted = voiceMutes[voice]
con.color_pair(isMuted ? 249 : colVoiceHdr, isCursor ? colHighlight : 255)
const ptnIdx = cue.ptns[voice]
const vlabel = `V${(voice+1).dec02()}`
const plabel = (ptnIdx === CUE_EMPTY) ? '---' : ptnIdx.hex03()
@@ -444,7 +447,33 @@ function drawControlHint() {
con.move(SCRH, 1)
print(' '.repeat(SCRW-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() {
@@ -502,6 +531,13 @@ const TEXT_PLANES = [TEXT_CHAR_OFF, TEXT_BACK_OFF, TEXT_FORE_OFF]
// One scratch strip, reused across shifts
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)
* 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
@@ -552,6 +637,8 @@ if (fullPathObj === undefined) {
const song = loadTaud(fullPathObj.full, 0)
const voiceMutes = new Array(NUM_VOICES).fill(false)
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// PLAYBACK STATE
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -686,6 +773,10 @@ function clampCursor() {
}
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)
if (voiceOff < 0) voiceOff = 0
if (voiceOff > maxOff) voiceOff = maxOff
@@ -720,8 +811,20 @@ while (!exitFlag) {
if (playbackMode !== PLAYMODE_NONE) {
if (keysym === "<F8>" || keysym === " ") { stopPlayback(); drawAll() }
else if (keysym === "<LEFT>") { voiceOff -= 1; clampVoice(); drawAll() }
else if (keysym === "<RIGHT>") { voiceOff += 1; clampVoice(); drawAll() }
else 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()
}
else if (keysym === "m" || keysym === "M") { toggleMute(cursorVox) }
else if (keysym === "s" || keysym === "S") { toggleSolo(cursorVox) }
return
}
@@ -735,12 +838,28 @@ while (!exitFlag) {
let rowMove = false // pure row-cursor movement; can be fast-path
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 }
else if (keysym === "<DOWN>") { cursorRow += 1; rowMove = true }
else if (keysym === "<HOME>") { cursorRow = 0; 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_DOWN>") { cueIdx += 1; fullRedraw = true }
else return

View File

@@ -89,6 +89,27 @@ class AudioJSR223Delegate(private val vm: VM) {
}
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). */
fun uploadInstrument(slot: Int, bytes: IntArray) {
getFirstSnd()?.instruments?.get(slot and 0xFF)?.let { inst ->

View File

@@ -1708,7 +1708,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
var mixR = 0.0
val gvol = playhead.globalVolume / 255.0
for (voice in ts.voices) {
if (!voice.active) continue
if (!voice.active || voice.muted) continue
val s = fetchTrackerSample(voice, instruments[voice.instrumentId])
val vol = voice.envVolume * voice.rowVolume / 63.0 * gvol * playhead.masterVolume / 255.0
mixL += s * vol * (63 - voice.rowPan) / 63.0
@@ -1857,6 +1857,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
class Voice {
var active = false
var muted = false
var instrumentId = 0
var samplePos = 0.0
var playbackRate = 1.0