Compare commits

...

6 Commits

Author SHA1 Message Date
minjaesong
44f11120d8 tightening formats 2026-04-23 14:47:53 +09:00
minjaesong
bc16ffabb4 minor colour change 2026-04-23 14:04:49 +09:00
minjaesong
ad5e5b62bc more tracker gui 2026-04-23 13:59:48 +09:00
minjaesong
887c2fbfba s3m to taud fix (not emitting volcmd on note retrigger) 2026-04-23 13:35:38 +09:00
minjaesong
e58eb2c12b more tracker stuff 2026-04-23 12:43:56 +09:00
minjaesong
3a91edb379 playback ctrl 2026-04-23 09:59:48 +09:00
7 changed files with 459 additions and 71 deletions

View File

@@ -6,10 +6,15 @@
const win = require("wintex")
const font = require("font")
const taud = require("taud")
font.setHighRom("A:/tvdos/bin/tautfont_high.chr")
const MIDDOT = "\u00FA"
const BIGDOT = "\u00F9"
const BULLET = "\u00847u"
const VERT = "\u00B3"
const TWOVERT = "\u00BA"
const sym = {
/* accidentals */
@@ -112,35 +117,55 @@ const colNote = 239
const colInst = 114
const colVol = 117
const colPan = 221
const colEffOp = 208
const colEffOp = 213
const colEffArg = 231
const colBackPtn = 255
Number.prototype.hex02 = function() {
return this.toString(16).toUpperCase().padStart(2,'0')
}
Number.prototype.hex03 = function() {
return this.toString(16).toUpperCase().padStart(3,'0')
}
Number.prototype.hex04 = function() {
return this.toString(16).toUpperCase().padStart(4,'0')
}
Number.prototype.hexD2 = function() {
return this.toString(16).toUpperCase().padStart(2, sym.middot)
}
Number.prototype.dec02 = function() {
return this.toString(10).toUpperCase().padStart(2,'0')
}
Number.prototype.decD2 = function() {
return this.toString(10).toUpperCase().padStart(2, sym.middot)
}
/**
* Builds the coloured string fragments for a single row of pattern data.
*/
function buildRowCell(patternData, row) {
function buildRowCell(ptnDat, row) {
const off = 8 * row
const note = patternData[off] | (patternData[off+1] << 8)
const inst = patternData[off+2]
const voleff = patternData[off+3]
const note = ptnDat[off] | (ptnDat[off+1] << 8)
const inst = ptnDat[off+2]
const voleff = ptnDat[off+3]
const voleffarg = voleff & 63
const paneff = patternData[off+4]
const paneff = ptnDat[off+4]
const paneffarg = paneff & 63
const effop = patternData[off+5]
const effarg = patternData[off+6] | (patternData[off+7] << 8)
const effop = ptnDat[off+5]
const effarg = ptnDat[off+6] | (ptnDat[off+7] << 8)
let sNote = note.toString(16).toUpperCase().padStart(4,'0')
let sNote = note.hex04()
if (note == 0xFFFF) sNote = sym.middot.repeat(4)
else if (note == 0xFFFE) sNote = sym.notecut
else if (note == 0x0000) sNote = sym.keyoff
let sInst = inst.toString(16).toUpperCase().padStart(2, sym.middot)
let sInst = inst.hexD2()
if (inst == 0) sInst = sym.middot.repeat(2)
let sVolEff = volEffSym[voleff >>> 6]
let sVolArg = voleffarg.toString().padStart(2, sym.middot)
let sVolArg = voleffarg.decD2()
if (voleff === 0) {
sVolEff = sym.middot
sVolArg = sym.middot.repeat(2)
@@ -152,16 +177,16 @@ function buildRowCell(patternData, row) {
}
else if (voleffarg >= 32) {
sVolEff = volEffSym[3]
sVolArg = (voleffarg & 31).toString().padStart(2,'0')
sVolArg = (voleffarg & 31).dec02()
}
else {
sVolEff = volEffSym[4]
sVolArg = (voleffarg & 31).toString().padStart(2,'0')
sVolArg = (voleffarg & 31).dec02()
}
}
let sPanEff = panEffSym[paneff >>> 6]
let sPanArg = paneffarg.toString().padStart(2, sym.middot)
let sPanArg = paneffarg.decD2()
if (paneff === 0) {
sPanEff = sym.middot
sPanArg = sym.middot.repeat(2)
@@ -173,16 +198,16 @@ function buildRowCell(patternData, row) {
}
else if (paneffarg >= 32) {
sPanEff = panEffSym[4]
sPanArg = (paneffarg & 31).toString().padStart(2,'0')
sPanArg = (paneffarg & 31).dec02()
}
else {
sPanEff = panEffSym[3]
sPanArg = (paneffarg & 31).toString().padStart(2,'0')
sPanArg = (paneffarg & 31).dec02()
}
}
let sEffOp = (effop > 0) ? effop.toString(36).toUpperCase()[0] : sym.middot
let sEffArg = effarg.toString(16).toUpperCase().padStart(4,'0')
let sEffArg = effarg.hex04()
if (effop === 0 && effarg === 0) {
sEffOp = sym.middot
sEffArg = sym.middot.repeat(4)
@@ -272,30 +297,30 @@ function loadTaud(filePath, songIndex) {
const patterns = new Array(numPats)
for (let p = 0; p < numPats; p++) {
const pat = new Uint8Array(PATTERN_SIZE)
const ptn = new Uint8Array(PATTERN_SIZE)
for (let k = 0; k < PATTERN_SIZE; k++) {
pat[k] = sys.peek(ptr + songOff + p * PATTERN_SIZE + k) & 0xFF
ptn[k] = sys.peek(ptr + songOff + p * PATTERN_SIZE + k) & 0xFF
}
patterns[p] = pat
patterns[p] = ptn
}
const cueBase = songOff + numPats * PATTERN_SIZE
const cues = new Array(NUM_CUES)
let lastActiveCue = -1
for (let c = 0; c < NUM_CUES; c++) {
const pats = new Array(NUM_VOICES)
const ptns = new Array(NUM_VOICES)
for (let i = 0; i < 10; i++) {
const lo = sys.peek(ptr + cueBase + c * CUE_SIZE + i) & 0xFF
const mi = sys.peek(ptr + cueBase + c * CUE_SIZE + 10 + i) & 0xFF
const hi = sys.peek(ptr + cueBase + c * CUE_SIZE + 20 + i) & 0xFF
pats[i*2] = ((hi >> 4) << 8) | ((mi >> 4) << 4) | (lo >> 4)
pats[i*2+1] = ((hi & 0xF) << 8) | ((mi & 0xF) << 4) | (lo & 0xF)
ptns[i*2] = ((hi >> 4) << 8) | ((mi >> 4) << 4) | (lo >> 4)
ptns[i*2+1] = ((hi & 0xF) << 8) | ((mi & 0xF) << 4) | (lo & 0xF)
}
const instr = sys.peek(ptr + cueBase + c * CUE_SIZE + 30) & 0xFF
cues[c] = { pats, instr }
cues[c] = { ptns, instr }
for (let v = 0; v < NUM_VOICES; v++) {
if (pats[v] !== CUE_EMPTY) { lastActiveCue = c; break }
if (ptns[v] !== CUE_EMPTY) { lastActiveCue = c; break }
}
}
@@ -325,7 +350,8 @@ const VIEW_ORDERS = 1
const VIEW_INSTRUMENT = 2
const VIEW_PATTERN_DETAILS = 3
const colHighlight = 81
const colPlayback = 40
const colHighlight = 41
const colRowNum = 249
const colRowNumEmph1 = 180
const colStatus = 253
@@ -343,7 +369,7 @@ function drawStatusBar() {
fillLine(1, colStatus, 255)
const maxCue = song.lastActiveCue < 0 ? 0 : song.lastActiveCue
const vHi = Math.min(voiceOff + VOCSIZE, song.numVoices)
const txt = ` ${song.filePath} Cue ${cueIdx.toString(16).toUpperCase().padStart(3,'0')}/${maxCue.toString(16).toUpperCase().padStart(3,'0')} Row ${cursorRow.toString(16).toUpperCase().padStart(2,'0')} V${(voiceOff+1).toString().padStart(2,'0')}-${vHi.toString().padStart(2,'0')}/${song.numVoices.toString().padStart(2,'0')} BPM ${song.bpm} Spd ${song.tickRate} `
const txt = ` ${song.filePath} Cue ${cueIdx.hex03()}/${maxCue.hex03()} Row ${cursorRow.dec02()} V${(voiceOff+1).dec02()}-${vHi.dec02()}/${song.numVoices.dec02()} BPM ${song.bpm} Spd ${song.tickRate} `
con.move(1, 1)
con.color_pair(colStatus, 255)
print(txt)
@@ -359,18 +385,21 @@ 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 patIdx = cue.pats[voice]
const vlabel = `V${(voice+1).toString().padStart(2,'0')}`
const plabel = (patIdx === CUE_EMPTY) ? '---' : patIdx.toString(16).toUpperCase().padStart(3,'0')
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()
const label = ` ${vlabel} ptn ${plabel} `
print((label + ' ').substring(0, COLSIZE - 1))
}
@@ -383,13 +412,13 @@ function drawPatternRowAt(viewRow) {
const actualRow = scrollRow + viewRow
const y = PTNVIEW_OFFSET_Y + viewRow
const highlight = (actualRow === cursorRow)
const back = highlight ? colHighlight : colBackPtn
const back = highlight ? (playbackMode !== PLAYMODE_NONE ? colPlayback : colHighlight) : colBackPtn
const cue = song.cues[cueIdx]
con.color_pair(colRowNum, back)
if (actualRow < ROWS_PER_PAT) {
if (actualRow % 4 == 0) {con.color_pair(colRowNumEmph1, back)}
let rowstr = actualRow.toString().toUpperCase().padStart(2, '0')
let rowstr = actualRow.dec02()
con.move(y, 1); con.prnch(rowstr.charCodeAt(0)); con.move(y, 2); con.prnch(rowstr.charCodeAt(1))
con.move(y, SCRW-2); con.prnch(rowstr.charCodeAt(0)); con.move(y, SCRW-1); con.prnch(rowstr.charCodeAt(1))
}
@@ -403,9 +432,9 @@ function drawPatternRowAt(viewRow) {
const x = PTNVIEW_OFFSET_X + COLSIZE * c
let cell = EMPTY_CELL
if (actualRow < ROWS_PER_PAT && voice < song.numVoices) {
const patIdx = cue.pats[voice]
if (patIdx !== CUE_EMPTY && patIdx < song.numPats) {
cell = buildRowCell(song.patterns[patIdx], actualRow)
const ptnIdx = cue.ptns[voice]
if (ptnIdx !== CUE_EMPTY && ptnIdx < song.numPats) {
cell = buildRowCell(song.patterns[ptnIdx], actualRow)
}
}
drawCellAt(y, x, cell, back)
@@ -419,10 +448,91 @@ function drawPatternView() {
}
function drawControlHint() {
let hintElem = [
[`\u008424u\u008425u`,'Row'],
[`\u008427u\u008426u`,'Vox'],
[`Pg\u008424u\u008425u`,'Ptn'],
['sep'],
['F5','Song'],
['F6','Cue'],
['F7','Row'],
['F8/Sp','Stop'],
['sep'],
['m','Mute'],
['s','Solo']
]
// erase current line
con.move(SCRH, 1)
print(' '.repeat(SCRW-1))
// start writing
con.move(SCRH, 1)
print(`\u008424u\u008425u Move rows ${MIDDOT} \u008427u\u008426u Move vox ${MIDDOT} Pg\u008424u\u008425u Move Ptns ${MIDDOT} Hm/Ed Init/Last row ${MIDDOT} q Quit ----`)
hintElem.forEach((pair, i, list) => {
con.color_pair(colStatus,255)
if (pair[0] == 'sep') {
print(` ${BIGDOT} `)
}
else {
if (i > 0 && list[i-1][0] != 'sep') print(' ');
con.color_pair(colVoiceHdr,255)
print(pair[0]+' ')
con.color_pair(colStatus,255)
print(pair[1])
}
})
}
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() {
const cue = song.cues[cueIdx]
const ptnIdx = cue.ptns[cursorVox]
if (ptnIdx === CUE_EMPTY || ptnIdx >= song.numPats) return
const ptn = song.patterns[ptnIdx]
const ptnOff = 8 * cursorRow
const ptnDat = ptn.slice(ptnOff, ptnOff + 8)
const note = ptnDat[0] | (ptnDat[1] << 8)
const inst = ptnDat[2]
const voleff = ptnDat[3]
const voleffop = (voleff >>> 6) & 3
const voleffarg = voleff & 63
const paneff = ptnDat[4]
const paneffop = (paneff >>> 6) & 3
const paneffarg = paneff & 63
const effop = ptnDat[5]
const effarg = ptnDat[6] | (ptnDat[7] << 8)
con.move(6,1)
print(`Pattern $${ptnIdx.hex02()}\tRow ${cursorRow.dec02()}\tVoice ${cursorVox}`)
con.move(7,1)
print(`Pitch $${note.hex04()}\tInst $${inst.hex02()}\tVolEff ${voleffop}.${voleffarg.dec02()}\t`+
`PanEff ${paneffop}.${paneffarg.dec02()}\tFx ${effop.toString(36).toUpperCase()}.${effarg.hex04()}`)
}
function drawAll() {
@@ -430,6 +540,7 @@ function drawAll() {
drawStatusBar()
drawVoiceHeaders()
drawPatternView()
drawVoiceDetail()
drawControlHint()
con.move(1, 1)
}
@@ -453,6 +564,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,
@@ -476,6 +594,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
@@ -488,6 +655,7 @@ let cueIdx = 0
let cursorRow = 0
let scrollRow = 0
let voiceOff = 0
let cursorVox = 0
if (exec_args[1] === undefined) {
println(`Usage: ${exec_args[0]} path_to.taud`)
@@ -502,11 +670,134 @@ if (fullPathObj === undefined) {
const song = loadTaud(fullPathObj.full, 0)
const voiceMutes = new Array(NUM_VOICES).fill(false)
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// PLAYBACK STATE
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
const PLAYHEAD = 0
const PLAYMODE_NONE = 0
const PLAYMODE_SONG = 1
const PLAYMODE_CUE = 2
const PLAYMODE_ROW = 3
let playbackMode = PLAYMODE_NONE
let playStartCue = 0
let playStartRow = 0
let pbCue = 0
let pbRow = 0
function startPlaySong() {
audio.stop(PLAYHEAD)
audio.setCuePosition(PLAYHEAD, cueIdx)
audio.setTrackerRow(PLAYHEAD, 0)
cursorRow = 0
clampCursor()
pbCue = cueIdx
pbRow = 0
playbackMode = PLAYMODE_SONG
audio.play(PLAYHEAD)
}
function startPlayCue() {
audio.stop(PLAYHEAD)
audio.setCuePosition(PLAYHEAD, cueIdx)
audio.setTrackerRow(PLAYHEAD, 0)
playStartCue = cueIdx
cursorRow = 0
clampCursor()
pbCue = cueIdx
pbRow = 0
playbackMode = PLAYMODE_CUE
audio.play(PLAYHEAD)
}
function startPlayRow() {
audio.stop(PLAYHEAD)
audio.setCuePosition(PLAYHEAD, cueIdx)
audio.setTrackerRow(PLAYHEAD, cursorRow)
playStartCue = cueIdx
playStartRow = cursorRow
pbCue = cueIdx
pbRow = cursorRow
playbackMode = PLAYMODE_ROW
audio.play(PLAYHEAD)
}
function stopPlayback() {
audio.stop(PLAYHEAD)
playbackMode = PLAYMODE_NONE
}
function updatePlayback() {
if (!audio.isPlaying(PLAYHEAD)) {
playbackMode = PLAYMODE_NONE
if (cursorRow >= scrollRow && cursorRow < scrollRow + PTNVIEW_HEIGHT)
drawPatternRowAt(cursorRow - scrollRow)
drawStatusBar()
return
}
const nowCue = audio.getCuePosition(PLAYHEAD)
const nowRow = audio.getTrackerRow(PLAYHEAD)
if (playbackMode === PLAYMODE_CUE && nowCue !== playStartCue) {
stopPlayback()
drawAll()
return
}
if (playbackMode === PLAYMODE_ROW && (nowRow !== playStartRow || nowCue !== playStartCue)) {
stopPlayback()
if (cursorRow >= scrollRow && cursorRow < scrollRow + PTNVIEW_HEIGHT)
drawPatternRowAt(cursorRow - scrollRow)
drawStatusBar()
return
}
if (nowCue === pbCue && nowRow === pbRow) return
pbCue = nowCue
pbRow = nowRow
if (nowCue !== cueIdx) {
cueIdx = nowCue
cursorRow = nowRow
clampCursor()
drawAll()
} else {
const oldCursor = cursorRow
const oldScroll = scrollRow
cursorRow = nowRow
clampCursor()
const dScroll = scrollRow - oldScroll
if (dScroll === 0) {
drawPatternRowAt(oldCursor - scrollRow)
drawPatternRowAt(cursorRow - scrollRow)
} else if (Math.abs(dScroll) >= PTNVIEW_HEIGHT) {
drawPatternView()
} else {
shiftPatternArea(-dScroll)
if (dScroll > 0) {
for (let i = 0; i < dScroll; i++) drawPatternRowAt(PTNVIEW_HEIGHT - 1 - i)
} else {
for (let i = 0; i < -dScroll; i++) drawPatternRowAt(i)
}
if (oldCursor >= scrollRow && oldCursor < scrollRow + PTNVIEW_HEIGHT)
drawPatternRowAt(oldCursor - scrollRow)
drawPatternRowAt(cursorRow - scrollRow)
}
drawStatusBar()
drawVoiceDetail()
}
}
function clampCursor() {
if (cursorRow < 0) cursorRow = 0
if (cursorRow >= ROWS_PER_PAT) cursorRow = ROWS_PER_PAT - 1
if (cursorRow < scrollRow) scrollRow = cursorRow
// bottom two IF statements will keep the cursor at the centre until viewpoint scroll edge has reached
// these two IF statements will keep the cursor at the centre until viewpoint scroll edge has reached
if (cursorRow < scrollRow + (PTNVIEW_HEIGHT>>>1) && scrollRow > 0) scrollRow = cursorRow - (PTNVIEW_HEIGHT>>>1)
if (cursorRow >= scrollRow + ((PTNVIEW_HEIGHT+1)>>>1)) scrollRow = cursorRow - ((PTNVIEW_HEIGHT+1)>>>1) + 1
if (scrollRow < 0) scrollRow = 0
@@ -515,6 +806,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
@@ -529,27 +824,75 @@ function clampCue() {
clampCursor(); clampVoice(); clampCue()
drawAll()
audio.resetParams(PLAYHEAD)
audio.purgeQueue(PLAYHEAD)
audio.stop(PLAYHEAD)
taud.uploadTaudFile(fullPathObj.full, 0, PLAYHEAD)
audio.setMasterVolume(PLAYHEAD, 255)
audio.setMasterPan(PLAYHEAD, 128)
let exitFlag = false
while (!exitFlag) {
input.withEvent(event => {
if (event[0] !== "key_down") return
const keysym = event[1]
if (keysym === "<ESC>" || keysym === "q" || keysym === "Q") {
exitFlag = true
return
}
if (playbackMode !== PLAYMODE_NONE) {
if (keysym === "<F8>" || keysym === " ") { stopPlayback(); 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
}
if (keysym === "<F5>") { startPlaySong(); drawAll(); return }
if (keysym === "<F6>") { startPlayCue(); drawAll(); return }
if (keysym === "<F7>") { startPlayRow(); drawPatternRowAt(cursorRow - scrollRow); return }
if (keysym === "<F8>" || keysym === " ") { stopPlayback(); return }
const oldCursor = cursorRow
const oldScroll = scrollRow
let rowMove = false // pure row-cursor movement; can be fast-path
let fullRedraw = false // voice/cue change; needs full viewport refresh
if (keysym === "<ESC>" || keysym === "q" || keysym === "Q") {
exitFlag = true
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
}
else if (keysym === "<UP>") { cursorRow -= 1; rowMove = true }
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
@@ -591,9 +934,13 @@ while (!exitFlag) {
}
drawStatusBar()
drawVoiceDetail()
})
if (playbackMode !== PLAYMODE_NONE) updatePlayback()
}
audio.stop(PLAYHEAD)
sys.free(SCRATCH_PTR)
con.clear()
con.move(1, 1)

View File

@@ -16,7 +16,7 @@ const NUM_PATTERNS_MAX = 256
const NUM_CUES = 1024
const CUE_SIZE = 32 // bytes per cue entry (packed 12-bit×20 voices + instruction + pad)
// Signature written into the file (16 bytes, space-padded)
// Signature written into the file (14 bytes, space-padded)
const CAPTURE_SIGNATURE = "LibTaud/TSVM "
// ── Internal helpers ────────────────────────────────────────────────────────
@@ -205,8 +205,8 @@ function captureTrackerDataToFile(outFile) {
(compressedSize >>> 8) & 0xFF,
(compressedSize >>> 16) & 0xFF,
(compressedSize >>> 24) & 0xFF,
// reserved (2)
0x00, 0x00,
// reserved (4)
0x00, 0x00, 0x00, 0x00,
].concat(sigBytes) // 8 + 2 + 4 + 2 + 16 = 32 bytes
// -- 6. Build song-table row (16 bytes) -----------------------------------
@@ -219,7 +219,9 @@ function captureTrackerDataToFile(outFile) {
numPats & 0xFF, (numPats >>> 8) & 0xFF, // numPatterns Uint16 LE
bpmStored, // BPM with 24 bias
tickRate, // initial tick-rate
0,0,0,0,0,0,0, // 7 bytes padding
0x40,0, // basenote
0x13,0xd0,0x82,0x43, // basefreq
0, // padding
]
// -- 7. Write header (creates / truncates file) ---------------------------

View File

@@ -87,7 +87,7 @@ NUM_PATTERNS_MAX = 4095
NUM_CUES = 1024
CUE_SIZE = 32 # packed 12-bit×20 voices + instruction + pad
NUM_VOICES = 20
SIGNATURE = b"s3m2taud/TSVM " # 16 bytes
SIGNATURE = b"s3m2taud/TSVM " # 14 bytes
# Taud note constants
NOTE_NOP = 0xFFFF
@@ -661,6 +661,8 @@ def build_pattern(s3m_grid: list, ch_idx: int, default_pan: int,
out = bytearray(PATTERN_BYTES)
rows = s3m_grid[ch_idx] if ch_idx < len(s3m_grid) else [S3MRow()] * PATTERN_ROWS
last_inst = 0 # 1-based; tracks which instrument is loaded on this channel
last_note = S3M_NOTE_EMPTY # last raw S3M note byte that was a real pitch
last_vol = None # last SEL_SET volume value (0-63), for retrigger recall
for r, row in enumerate(rows[:PATTERN_ROWS]):
note = encode_note(row.note)
inst = row.inst # S3M 1-based → Taud 1-based
@@ -668,6 +670,12 @@ def build_pattern(s3m_grid: list, ch_idx: int, default_pan: int,
if row.inst > 0:
last_inst = row.inst
# ── Instrument-only retrigger ──
# Instrument-only row: recall the last volume without touching the note.
retrigger = (row.inst > 0
and row.note == S3M_NOTE_EMPTY
and last_note not in (S3M_NOTE_EMPTY, S3M_NOTE_OFF))
op, arg, vol_override, pan_override = encode_effect(
row.effect, row.effect_arg, ch_idx, r)
@@ -683,11 +691,21 @@ def build_pattern(s3m_grid: list, ch_idx: int, default_pan: int,
# so prior channel-vol state doesn't bleed through.
vol_sel = SEL_SET
vol_value = inst_vols.get(last_inst, 0x3F)
elif retrigger and last_vol is not None:
# Instrument-only row: re-emit the last known volume so the sample
# restarts at the correct level without an explicit note trigger.
vol_sel, vol_value = SEL_SET, last_vol
elif vol_override is not None:
vol_sel, vol_value = vol_override
else:
vol_sel, vol_value = SEL_FINE, 0 # no-op fine slide
# Track note and volume for future retrigger lookups.
if row.note not in (S3M_NOTE_EMPTY, S3M_NOTE_OFF):
last_note = row.note
if vol_sel == SEL_SET:
last_vol = vol_value
# ── Pan column ──
if pan_override is not None:
pan_sel, pan_value = pan_override
@@ -842,13 +860,13 @@ def assemble_taud(h: S3MHeader, instruments: list, patterns: list) -> bytes:
song_offset = TAUD_HEADER_SIZE + comp_size + TAUD_SONG_ENTRY
num_taud_pats = P * C
# Header (32 bytes): magic(8)+ver(1)+numSongs(1)+compSize(4)+rsvd(2)+sig(16)
sig = (SIGNATURE + b' ' * 16)[:16]
# Header (32 bytes): magic(8)+ver(1)+numSongs(1)+compSize(4)+rsvd(4)+sig(14)
sig = (SIGNATURE + b' ' * 14)[:14]
header = (
TAUD_MAGIC +
bytes([TAUD_VERSION, 1]) +
struct.pack('<I', comp_size) +
b'\x00\x00' +
b'\x00\x00\x00\x00' +
sig
)
assert len(header) == TAUD_HEADER_SIZE
@@ -875,7 +893,7 @@ def assemble_taud(h: S3MHeader, instruments: list, patterns: list) -> bytes:
pat_bin, pat_remap, num_taud_pats = deduplicate_patterns(bytes(pat_bin), orig_count)
vprint(f" patterns: {orig_count}{num_taud_pats} unique ({orig_count - num_taud_pats} deduplicated)")
# Song table row (16 bytes): offset(4)+voices(1)+patsLo(1)+patsHi(1)+bpm(1)+tick(1)+pad(7)
# Song table row (16 bytes): offset(4)+voices(1)+patsLo(1)+patsHi(1)+bpm(1)+tick(1)+basenote(2)+basefreq(4)+pad(1)
# Built after dedup so num_taud_pats reflects the unique count.
num_taud_pats_lo = num_taud_pats & 0xFF
num_taud_pats_hi = (num_taud_pats >> 8) & 0xFF
@@ -886,7 +904,7 @@ def assemble_taud(h: S3MHeader, instruments: list, patterns: list) -> bytes:
num_taud_pats_hi,
bpm_stored,
speed,
) + b'\x00' * 7
) + b'\x40\x00' + b'\x13\xd0\x82\x43' + b'\x00'
assert len(song_table) == TAUD_SONG_ENTRY
# Cue sheet (using remapped pattern indices)

View File

@@ -2200,8 +2200,8 @@ Endianness: Little
Uint8 Format version (always 1)
Uint8 Number of songs in SONG TABLE
Uint32 Compressed size of SAMPLE+INST section (used to calculate offset to SONG TABLE)
Uint16 Reserved for future versions
Byte[16]Tracker/Converter signature
Uint32 Reserved for Taud Project Format. Fill with zero
Byte[14]Tracker/Converter signature
## SONG TABLE
Rows of 16 bytes:
@@ -2210,9 +2210,9 @@ Rows of 16 bytes:
Uint16 Number of patterns (0 is invalid. pattern bin length = numPats * 8 bytes)
Uint8 Initial BPM (bias of -24. 0x00=24, 0xFF=279)
Uint8 Initial Tickrate (0 is invalid)
Uint16 Current Tuning base note (0-4095), assuming octave 3. C3 (the default value) is 0x4000
Float32 Frequency at the base note. Default (A440) is 261.6255653
Byte[7] Reserved for future versions
Uint16 Current Tuning base note (1..65533), assuming octave 3. C3 (the default value) is 0x4000. If zero, assume the default value
Float32 Frequency at the base note. Default (A440) is 261.6255653. If zero, assume the default value
Byte[1] Reserved for future versions
Taud device can queue up to 2 "playdata" in its buffer, which can be interpreted as a song.
@@ -2239,12 +2239,11 @@ Endianness: Little
## Header
Byte[8] Magic
Uint8 Format version (always 129)
Uint8 Format version (always 129; high-bit set and number 0x01)
Uint8 Number of songs in SONG TABLE
Uint32 Compressed size of SAMPLE+INST section (used to calculate offset to SONG TABLE)
Uint16 Offset to Project Data (low twobyte)
Uint32 Offset to Project Data (low twobyte)
Byte[14]Tracker/Converter signature
Uint16 Offset to Project Data (high twobyte)
## Project Data
Byte[8] Magic (\x1E T a u d P r J)
@@ -2268,11 +2267,11 @@ prefixes:
* PCpr. Project copyright string. Encoding: UTF-8
* PNam. Project name. Encoding: UTF-8
* INam. Instrument name table. Strings separated by comma
* INam. Instrument name table. Strings separated by 0x1E
* pNam. Pattern name table. Strings separated by comma
* pNam. Pattern name table. Strings separated by 0x1E
* SNam. Sample name table. Strings separated by comma
* SNam. Sample name table. Strings separated by 0x1E
* sMet. Song metadata table
* Repetition of:
@@ -2288,15 +2287,15 @@ prefixes:
Uint8 Notation index (starting from zero) used by songs
Uint32 Size of this notation following this field
Uint8 Flags
0b nnnn 000t
0b 0000 000t
t: NOT using interval system (you are responsible for defining every notes expressible)
Uint8 Reserved
Float32 Interval size (octave system = 2.0f). If Flag 't' is set, this must be NaN. 0f and Infinity are considered illegal
Uint16 Notes between interval MINUS ONE (or octave); 12-TET will have value 11. 0 is considered illegal
Byte[8] Reserved
Byte[*] Name, null terminated. Encoding: UTF-8
Byte[*] Notation table. Comma-separated and null-terminated. Encoding: raw bytes
Uint16[*] Frequency table. Size of the table is defined by "Notes between interval MINUS ONE". All relative to the base note (Song table will be referred), in 4096-TET note number. Index zero of this table will be 0x0 if you read the spec right
Byte[*] Notation table. 0xFF-separated and null-terminated. Encoding: raw bytes
Uint16[*] Frequency table. Size of the table is defined by "Notes between interval MINUS ONE". This is a lookup table of relative pitch offsets (against the base tuning note) in 4096-TET space. Index zero of this table will be 0x0 if you read the spec right
Note: custom notations will use internal index 65535 down to 65520 (index 0 = 65535, index 15 = 65520)

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

View File

@@ -22,7 +22,7 @@ uniform float noiseMagnitude = 0.0;
// Signal mode: 0 = S-Video, 1 = Composite, 2 = CGA Composite
// Can be changed at runtime without recompilation
uniform int signalMode = 1; // Default should be 1 for composite
uniform int signalMode = 0; // Default should be 1 for composite
// CGA-specific settings
uniform float cgaHue; // Hue adjustment for CGA (default: 0.0, range: -PI to PI)