Compare commits

..

7 Commits

Author SHA1 Message Date
minjaesong
8d28bde119 taud base note def changed (A3@440Hz) 2026-04-23 23:11:09 +09:00
minjaesong
755afb7df4 reatable fx names; horz scroll fix 2026-04-23 22:23:15 +09:00
minjaesong
539df453ec taut: fix voleff 'v1' rendering as 'v.1' 2026-04-23 21:51:47 +09:00
minjaesong
5e3ffea6d3 taut: better scrolling behav(2) 2026-04-23 21:18:30 +09:00
minjaesong
4bda55d511 taut: better scrolling behav 2026-04-23 21:12:11 +09:00
minjaesong
852c0e6e80 taut: displaying note symbol 2026-04-23 21:03:20 +09:00
minjaesong
25309cf5b6 format revision and tracker GUI 2026-04-23 20:48:40 +09:00
6 changed files with 177 additions and 63 deletions

View File

@@ -679,9 +679,9 @@ on sample byte read during loop playback:
Each cell carries a 6-bit value field plus a 2-bit selector field for the volume column. The four selectors are:
- **`0.$xx` — Set volume** to `$xx` (6-bit, $00..$3F). Equivalent to a note's default volume.
- **`1.$xx` — Volume slide up** by `$xx` per non-first tick (6-bit). Volume clamps at $3F.
- **`2.$xx` — Volume slide down** by `$xx` per non-first tick (6-bit). Volume clamps at $00.
- **`3.$Sx` — Fine volume slide** on tick 0 only. The high bit `$S` of the value selects direction (0 = down, 1 = up); the low 5 bits `$x` ($00..$1F) are the magnitude. Equivalent in scale to `D $xF00` / `D $Fy00` but with a 5-bit cap. Fires once per row regardless of speed.
- **`1.$xx` — Volume slide up** by `$xx` per non-first tick (4-bit). Volume clamps at $3F.
- **`2.$xx` — Volume slide down** by `$xx` per non-first tick (4-bit). Volume clamps at $00.
- **`3.$Sx` — Fine volume slide** on tick 0 only. The high bit `$S` of the value selects direction (0 = down, 1 = up); the low 4 bits `$x` ($0..$F) are the magnitude. Equivalent in scale to `D $xF00` / `D $Fy00` but with a 5-bit cap. Fires once per row regardless of speed.
Volume-column effects do not consume the main effect slot; a cell can carry both (for instance, a tone portamento in the effect slot and a volume slide in the volume column).
@@ -696,8 +696,8 @@ NOTE: **`3.00` — is No-op**
The panning column uses the same 6-bit value + 2-bit selector layout:
- **`0.$xx` — Set pan** (6-bit, $00..$3F mapped onto the channel's 8-bit pan space; $01 = full left, $1F = centre-left, $20 = centre-right, $3F = full right). For 8-bit precision use `S $80xx` instead.
- **`1.$xx` — Pan slide right** by `$xx` per non-first tick.
- **`2.$xx` — Pan slide left** by `$xx` per non-first tick.
- **`1.$xx` — Pan slide right** by `$xx` per non-first tick (4-bit).
- **`2.$xx` — Pan slide left** by `$xx` per non-first tick (4-bit).
- **`3.$Sx` — Fine pan slide** on tick 0 only, same direction-bit encoding as the volume column's selector 3.
NOTE: **`3.00` — is No-op**

View File

@@ -10,6 +10,9 @@ const taud = require("taud")
font.setHighRom("A:/tvdos/bin/tautfont_high.chr")
const BUILD_DATE = "260423"
const TRACKER_SIGNATURE = "TsvmTaut"+BUILD_DATE // 14-byte string
const MIDDOT = "\u00FA"
const BIGDOT = "\u00F9"
const BULLET = "\u00847u"
@@ -47,13 +50,13 @@ keyoff:"\u00A0\u00CD\u00CD\u00A1",
notecut:"\u00A4\u00A4\u00A4\u00A4",
/* special effects */
volset:MIDDOT,
volset:'',//MIDDOT,
volup:"\u008430u",
voldn:"\u008431u",
volfineup:"+",
volfinedn:"-",
panset:MIDDOT,
panset:'',//MIDDOT,
panle:"\u008417u",
panri:"\u008416u",
panfinele:"\u008427u",
@@ -65,6 +68,55 @@ ticked:"\u009F",
middot:MIDDOT
}
const fxNames = {
A:"Set tick speed",
B:"Jump to order",
C:"Break pattern to",
D:"Volume slide",
E:"Pitch down",
F:"Pitch up",
G:"Portamento",
H:"Vibrato",
U:"Fine vibrato",
I:"Tremor",
J:"Arpeggio",
K:"Vibrato + vol slide",
L:"Portamento + vol slide",
O:"Sample offset",
Q:"Retrigger",
R:"Tremolo",
T:"Tempo",
V:"Gloval volume",
S:"Special",
S1:"Glissando ctrl",
S2:"Sample finetune",
S3:"Vibrato LFO",
S4:"Tremolo LFO",
S8:"Channel pan",
SB:"Pattern loop",
SC:"Note cut",
SD:"Note delay",
SE:"Pattern delay",
SF:"Funk it"
}
const panFxNames = {
0:"Set",
1:"Pan slide L",
2:"Pan slide R",
3:"Fine pan slide",
30:"Fine pan slide L",
31:"Fine pan slide R"
}
const volFxNames = {
0:"Set",
1:"Vol slide UP",
2:"Vol slide DN",
3:"Fine vol slide",
30:"Fine vol slide DN",
31:"Fine vol slide UP"
}
const pitchTablePresets = {
// index: pitch table number to be recorded on .taudproj file
0:{index:0,name:"null", table:[], sym:[]}, // when null is specified, hex numbers will be displayed instead
@@ -121,6 +173,8 @@ const colEffOp = 213
const colEffArg = 231
const colBackPtn = 255
const PITCH_PRESET_IDX = 240 // TODO read from the Project Data section of the .taud
Number.prototype.hex02 = function() {
return this.toString(16).toUpperCase().padStart(2,'0')
}
@@ -133,6 +187,9 @@ Number.prototype.hex04 = function() {
Number.prototype.hexD2 = function() {
return this.toString(16).toUpperCase().padStart(2, sym.middot)
}
Number.prototype.hex1 = function() {
return this.toString(16).toUpperCase()
}
Number.prototype.dec02 = function() {
return this.toString(10).toUpperCase().padStart(2,'0')
}
@@ -141,6 +198,24 @@ Number.prototype.decD2 = function() {
}
function noteToStr(note) {
if (note === 0xFFFF) return sym.middot.repeat(4)
if (note === 0xFFFE) return sym.notecut
if (note === 0x0000) return sym.keyoff
const table = pitchTablePresets[PITCH_PRESET_IDX].table
const syms = pitchTablePresets[PITCH_PRESET_IDX].sym
if (table.length === 0) return note.hex04()
const pitchInOct = note & 0xFFF
const octave = (note >> 12) - 1
let best = 0, bestDist = 0x1000
for (let i = 0; i < table.length; i++) {
const d = Math.abs(pitchInOct - table[i])
if (d < bestDist) { bestDist = d; best = i }
}
if ((0x1000 - pitchInOct) < bestDist) return syms[0] + (octave + 1)
return syms[best] + octave
}
/**
* Builds the coloured string fragments for a single row of pattern data.
*/
@@ -156,53 +231,56 @@ function buildRowCell(ptnDat, row) {
const effop = ptnDat[off+5]
const effarg = ptnDat[off+6] | (ptnDat[off+7] << 8)
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
const sNote = noteToStr(note)
let sInst = inst.hexD2()
if (inst == 0) sInst = sym.middot.repeat(2)
let sVolEff = volEffSym[voleff >>> 6]
let sVolArg = voleffarg.decD2()
let sVolArg = voleffarg.hexD2()
if (voleff === 0) {
sVolEff = sym.middot
sVolEff = ''
sVolArg = sym.middot.repeat(2)
}
else if (voleff >>> 6 == 1 || voleff >>> 6 == 2) {
sVolArg = (voleffarg & 15).hex1()
}
else if (voleff >>> 6 == 3) {
if (voleffarg == 0) {
sVolEff = sym.middot
sVolArg = sym.middot.repeat(2)
sVolArg = sym.middot.repeat(1)
}
else if (voleffarg >= 32) {
sVolEff = volEffSym[3]
sVolArg = (voleffarg & 31).dec02()
sVolArg = (voleffarg & 15).hex1()
}
else {
sVolEff = volEffSym[4]
sVolArg = (voleffarg & 31).dec02()
sVolArg = (voleffarg & 15).hex1()
}
}
let sPanEff = panEffSym[paneff >>> 6]
let sPanArg = paneffarg.decD2()
let sPanArg = paneffarg.hexD2()
if (paneff === 0) {
sPanEff = sym.middot
sPanEff = ''
sPanArg = sym.middot.repeat(2)
}
else if (paneff >>> 6 == 1 || paneff >>> 6 == 2) {
sPanArg = (paneffarg & 15).hex1()
}
else if (paneff >>> 6 == 3) {
if (paneffarg == 0) {
sPanEff = sym.middot
sPanArg = sym.middot.repeat(2)
sPanArg = sym.middot.repeat(1)
}
else if (paneffarg >= 32) {
sPanEff = panEffSym[4]
sPanArg = (paneffarg & 31).dec02()
sPanArg = (paneffarg & 15).hex1()
}
else {
sPanEff = panEffSym[3]
sPanArg = (paneffarg & 31).dec02()
sPanArg = (paneffarg & 15).hex1()
}
}
@@ -219,9 +297,9 @@ function buildRowCell(ptnDat, row) {
const EMPTY_CELL = {
sNote: sym.middot.repeat(4),
sInst: sym.middot.repeat(3),
sVolEff: sym.middot,
sVolEff: '',
sVolArg: sym.middot.repeat(2),
sPanEff: sym.middot,
sPanEff: '',
sPanArg: sym.middot.repeat(2),
sEffOp: sym.middot,
sEffArg: sym.middot.repeat(4)
@@ -339,11 +417,11 @@ function loadTaud(filePath, songIndex) {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
const [SCRH, SCRW] = con.getmaxyx()
const PTNVIEW_OFFSET_X = 5
const PTNVIEW_OFFSET_X = 3
const PTNVIEW_OFFSET_Y = 9
const PTNVIEW_HEIGHT = SCRH - PTNVIEW_OFFSET_Y
const COLSIZE = 18
const VOCSIZE = 4
const COLSIZE = 15
const VOCSIZE = 5
const VIEW_TIMELINE = 0
const VIEW_ORDERS = 1
@@ -358,6 +436,8 @@ const colStatus = 253
const colVoiceHdr = 230
const colSep = 252
let separatorStyle = 0
function fillLine(y, c, back) {
con.color_pair(c, back)
for (let x = 1; x <= SCRW; x++) {
@@ -375,13 +455,32 @@ function drawStatusBar() {
print(txt)
}
function drawSeparators(posY, col_size) {
/**
* @param style 0: condensed timeline, 1: vertical bars between voices
*/
function drawSeparators(style) {
if (style == 1) {
con.color_pair(colSep, 255)
for (let c = 0; c < VOCSIZE - 1; c++) {
con.move(posY, PTNVIEW_OFFSET_X + col_size * (c+1) - 1)
for (let y = PTNVIEW_OFFSET_Y - 1; y < PTNVIEW_HEIGHT; y++) {
con.move(y, PTNVIEW_OFFSET_X + COLSIZE * (c+1) - 1)
con.prnch(0xB3)
}
}
}
else {
// paint the first column of pattern view with colour
for (let x = PTNVIEW_OFFSET_X; x < SCRW - 3; x += COLSIZE) {
for (let y = 0; y < PTNVIEW_HEIGHT+1; y++) {
let memOffset = (y+PTNVIEW_OFFSET_Y-2) * SCRW + (x-1)
let bgColOffset = GPU_MEM - TEXT_BACK_OFF - memOffset
sys.poke(bgColOffset, colHighlight)
}
}
con.color_pair(colSep, 255)
}
}
function drawVoiceHeaders() {
fillLine(PTNVIEW_OFFSET_Y - 1, colVoiceHdr, 255)
@@ -405,7 +504,7 @@ function drawVoiceHeaders() {
}
}
drawSeparators(PTNVIEW_OFFSET_Y - 1, COLSIZE)
drawSeparators(separatorStyle)
}
function drawPatternRowAt(viewRow) {
@@ -440,7 +539,7 @@ function drawPatternRowAt(viewRow) {
drawCellAt(y, x, cell, back)
}
drawSeparators(y, COLSIZE)
drawSeparators(separatorStyle)
}
function drawPatternView() {
@@ -449,9 +548,8 @@ function drawPatternView() {
function drawControlHint() {
let hintElem = [
[`\u008424u\u008425u`,'Row'],
[`\u008427u\u008426u`,'Vox'],
[`Pg\u008424u\u008425u`,'Ptn'],
[`\u008427u\u008425u\u008424u\u008426u`,'Ptn'],
[`Pg\u008424u\u008425u`,'Cue'],
['sep'],
['F5','Song'],
['F6','Cue'],
@@ -459,7 +557,9 @@ function drawControlHint() {
['F8/Sp','Stop'],
['sep'],
['m','Mute'],
['s','Solo']
['s','Solo'],
['sep'],
['q','Quit'],
]
// erase current line
@@ -529,10 +629,18 @@ function drawVoiceDetail() {
const effarg = ptnDat[6] | (ptnDat[7] << 8)
con.move(6,1)
print(`Pattern $${ptnIdx.hex02()}\tRow ${cursorRow.dec02()}\tVoice ${cursorVox}`)
print(`Pitch $${note.hex04()}\tInst $${inst.hex02()}\tVolEff ${voleffop}.$${voleffarg.hex02()}\t`+
`PanEff ${paneffop}.$${paneffarg.hex02()}`)
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()}`)
let fx = effop.toString(36).toUpperCase()
if (fx == '0') {
print(`Fx`+' '.repeat(32))
}
else {
if (fx == 'S') fx += (effarg >>> 12).hex1()
let fxName = fxNames[fx]
print(`Fx ${fxName} $${effarg.hex04()} `)
}
}
function drawAll() {
@@ -541,6 +649,7 @@ function drawAll() {
drawVoiceHeaders()
drawPatternView()
drawVoiceDetail()
drawSeparators(separatorStyle)
drawControlHint()
con.move(1, 1)
}
@@ -564,12 +673,8 @@ 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
// Horizontal salvage
const SALVAGE_HORIZ_LEN = (VOCSIZE - 1) * COLSIZE
/**
* Shift the pattern-view rows by `dy` lines (positive = down, negative = up)
@@ -639,7 +744,6 @@ function drawVoiceColumnAt(slot) {
cell = buildRowCell(song.patterns[ptnIdx], actualRow)
}
drawCellAt(y, x, cell, back)
drawSeparators(y, COLSIZE)
}
}
@@ -789,6 +893,7 @@ function updatePlayback() {
drawPatternRowAt(cursorRow - scrollRow)
}
drawStatusBar()
drawSeparators(separatorStyle)
drawVoiceDetail()
}
}
@@ -809,7 +914,9 @@ 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
// keep cursor centred until view reaches an edge (mirrors clampCursor logic)
if (cursorVox < voiceOff + (VOCSIZE>>>1) && voiceOff > 0) voiceOff = cursorVox - (VOCSIZE>>>1)
if (cursorVox >= voiceOff + ((VOCSIZE+1)>>>1)) voiceOff = cursorVox - ((VOCSIZE+1)>>>1) + 1
const maxOff = Math.max(0, song.numVoices - VOCSIZE)
if (voiceOff < 0) voiceOff = 0
if (voiceOff > maxOff) voiceOff = maxOff
@@ -836,6 +943,8 @@ while (!exitFlag) {
input.withEvent(event => {
if (event[0] !== "key_down") return
const keysym = event[1]
const shiftDown = (event.indexOf(59) > 0 || event.indexOf(60) > 0)
const moveDelta = shiftDown ? 4 : 1
if (keysym === "<ESC>" || keysym === "q" || keysym === "Q") {
exitFlag = true
@@ -846,7 +955,7 @@ while (!exitFlag) {
if (keysym === "<F8>" || keysym === " ") { stopPlayback(); drawAll() }
else if (keysym === "<LEFT>" || keysym === "<RIGHT>") {
const oldVoiceOff = voiceOff
cursorVox += (keysym === "<LEFT>") ? -1 : 1
cursorVox += (keysym === "<LEFT>") ? -moveDelta : moveDelta
clampVoice()
const dVoice = voiceOff - oldVoiceOff
if (dVoice !== 0) {
@@ -854,6 +963,7 @@ while (!exitFlag) {
drawVoiceColumnAt(dVoice > 0 ? VOCSIZE - 1 : 0)
}
drawVoiceHeaders()
drawSeparators(separatorStyle)
drawStatusBar()
}
else if (keysym === "m" || keysym === "M") { toggleMute(cursorVox) }
@@ -873,7 +983,7 @@ while (!exitFlag) {
if (keysym === "<LEFT>" || keysym === "<RIGHT>") {
const oldVoiceOff = voiceOff
cursorVox += (keysym === "<LEFT>") ? -1 : 1
cursorVox += (keysym === "<LEFT>") ? -moveDelta : moveDelta
clampVoice()
const dVoice = voiceOff - oldVoiceOff
if (dVoice !== 0) {
@@ -881,6 +991,7 @@ while (!exitFlag) {
drawVoiceColumnAt(dVoice > 0 ? VOCSIZE - 1 : 0)
}
drawVoiceHeaders()
drawSeparators(separatorStyle)
drawStatusBar()
drawVoiceDetail()
return
@@ -889,12 +1000,12 @@ while (!exitFlag) {
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 }
if (keysym === "<UP>") { cursorRow -= moveDelta; rowMove = true }
else if (keysym === "<DOWN>") { cursorRow += moveDelta; rowMove = true }
else if (keysym === "<HOME>") { cursorRow = 0; rowMove = true }
else if (keysym === "<END>") { cursorRow = ROWS_PER_PAT-1; rowMove = true }
else if (keysym === "<PAGE_UP>") { cueIdx -= 1; fullRedraw = true }
else if (keysym === "<PAGE_DOWN>") { cueIdx += 1; fullRedraw = true }
else if (keysym === "<PAGE_UP>") { cueIdx -= moveDelta; fullRedraw = true }
else if (keysym === "<PAGE_DOWN>") { cueIdx += moveDelta; fullRedraw = true }
else return
clampCursor(); clampVoice(); clampCue()
@@ -933,6 +1044,7 @@ while (!exitFlag) {
drawPatternRowAt(cursorRow - scrollRow)
}
drawSeparators(separatorStyle)
drawStatusBar()
drawVoiceDetail()
})

View File

@@ -40,7 +40,8 @@ const COL_HL_EXT = {
"tap": 190,
"txt": 223,
"md": 223,
"log": 223
"log": 223,
"taud":109,
}
const EXEC_FUNS = {
@@ -62,7 +63,8 @@ const EXEC_FUNS = {
"bas": (f) => _G.shell.execute(`basic "${f}"`),
"txt": (f) => _G.shell.execute(`less "${f}"`),
"md": (f) => _G.shell.execute(`less "${f}"`),
"log": (f) => _G.shell.execute(`less "${f}"`)
"log": (f) => _G.shell.execute(`less "${f}"`),
"taud": (f) => _G.shell.execute(`taut "${f}"`),
}
let windowMode = 0 // 0 == left, 1 == right

View File

@@ -219,8 +219,8 @@ function captureTrackerDataToFile(outFile) {
numPats & 0xFF, (numPats >>> 8) & 0xFF, // numPatterns Uint16 LE
bpmStored, // BPM with 24 bias
tickRate, // initial tick-rate
0x40,0, // basenote
0x13,0xd0,0x82,0x43, // basefreq
0x00,0x4C, // basenote (0x4C00 -- A3)
0x00,0x00,0xDC,0x43, // basefreq (440 Hz)
0, // padding
]

View File

@@ -904,7 +904,7 @@ def assemble_taud(h: S3MHeader, instruments: list, patterns: list) -> bytes:
num_taud_pats_hi,
bpm_stored,
speed,
) + b'\x40\x00' + b'\x13\xd0\x82\x43' + b'\x00'
) + b'\x00\x4C' + b'\x00\x00\xDC\x43' + b'\x00'
assert len(song_table) == TAUD_SONG_ENTRY
# Cue sheet (using remapped pattern indices)

View File

@@ -2210,8 +2210,8 @@ 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 (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
Uint16 Current Tuning base note (1..65533). A3 (the default value) is 0x4C00. If zero, assume the default value
Float32 Frequency at the base note. Default (A440) is 440.0. 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.