mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
Compare commits
6 Commits
74d94b350c
...
44f11120d8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44f11120d8 | ||
|
|
bc16ffabb4 | ||
|
|
ad5e5b62bc | ||
|
|
887c2fbfba | ||
|
|
e58eb2c12b | ||
|
|
3a91edb379 |
@@ -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)
|
||||
|
||||
@@ -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) ---------------------------
|
||||
|
||||
30
s3m2taud.py
30
s3m2taud.py
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user