diff --git a/assets/disk0/tvdos/bin/taut.js b/assets/disk0/tvdos/bin/taut.js index d1f631c..8e06952 100644 --- a/assets/disk0/tvdos/bin/taut.js +++ b/assets/disk0/tvdos/bin/taut.js @@ -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 === "" || keysym === " ") { stopPlayback(); drawAll() } - else if (keysym === "") { voiceOff -= 1; clampVoice(); drawAll() } - else if (keysym === "") { voiceOff += 1; clampVoice(); drawAll() } + else if (keysym === "" || keysym === "") { + const oldVoiceOff = voiceOff + cursorVox += (keysym === "") ? -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 === "" || keysym === "") { + const oldVoiceOff = voiceOff + cursorVox += (keysym === "") ? -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 === "") { cursorRow -= 1; rowMove = true } else if (keysym === "") { cursorRow += 1; rowMove = true } else if (keysym === "") { cursorRow = 0; rowMove = true } else if (keysym === "") { cursorRow = ROWS_PER_PAT-1; rowMove = true } - else if (keysym === "") { voiceOff -= 1; fullRedraw = true } - else if (keysym === "") { voiceOff += 1; fullRedraw = true } else if (keysym === "") { cueIdx -= 1; fullRedraw = true } else if (keysym === "") { cueIdx += 1; fullRedraw = true } else return diff --git a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt index 7a6197a..8004f2c 100644 --- a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt @@ -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 -> diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index 6d47b12..4380006 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -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