mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 13:38:30 +09:00
Compare commits
2 Commits
0124b062d0
...
937d3e27ed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
937d3e27ed | ||
|
|
e64e335db3 |
@@ -146,11 +146,11 @@ Z:"UNIMPLEMENTED", // IT: MIDI macro
|
||||
}
|
||||
const panFxNames = {
|
||||
0:"Set to",
|
||||
1:"Slide L",
|
||||
2:"Slide R",
|
||||
1:"Slide R",
|
||||
2:"Slide L",
|
||||
3:"Fine slide",
|
||||
30:"Fine slide L",
|
||||
31:"Fine slide R",
|
||||
30:"Fine slide R",
|
||||
31:"Fine slide L",
|
||||
999:"---",
|
||||
}
|
||||
const volFxNames = {
|
||||
@@ -209,7 +209,7 @@ sym:[` \u00E0\u00E1`,` \u00E2\u00E3`,` \u00E4\u00E5`,` \u00E6\u00E7`,` \u00E8\u0
|
||||
|
||||
|
||||
const volEffSym = [sym.volset, sym.volup, sym.voldn, sym.volfineup, sym.volfinedn]
|
||||
const panEffSym = [sym.panset, sym.panle, sym.panri, sym.panfinele, sym.panfineri]
|
||||
const panEffSym = [sym.panset, sym.panri, sym.panle, sym.panfineri, sym.panfinele]
|
||||
|
||||
const colNote = 239
|
||||
const colInst = 114
|
||||
@@ -521,7 +521,7 @@ function loadTaud(filePath, songIndex) {
|
||||
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(cueSheetPtr + c * CUE_SIZE + 30) & 0xFF
|
||||
const instr = (sys.peek(cueSheetPtr + c * CUE_SIZE + 30) << 8) | sys.peek(cueSheetPtr + c * CUE_SIZE + 31)
|
||||
cues[c] = { ptns, instr }
|
||||
|
||||
for (let v = 0; v < NUM_VOICES; v++) {
|
||||
@@ -557,7 +557,7 @@ let VOCSIZE_TIMELINE_FULL = Math.floor((SCRW - 3) / COLSIZE_TIMELINE_FULL)
|
||||
|
||||
const ORDERS_CMD_X = 5
|
||||
const ORDERS_VOICE_X = 9
|
||||
const VOCSIZE_ORDERS = Math.floor((SCRW - 8) / 4)
|
||||
const VOCSIZE_ORDERS = Math.floor((SCRW - 10) / 4)
|
||||
|
||||
const VIEW_TIMELINE = 0
|
||||
const VIEW_CUES = 1
|
||||
@@ -863,7 +863,9 @@ function drawControlHint() {
|
||||
['n','Solo'],
|
||||
['m','Mute'],
|
||||
['sep'],
|
||||
['tab','Panel']
|
||||
['tab','Panel'],
|
||||
['sep'],
|
||||
['!','Help'],
|
||||
// ['sep'],
|
||||
// ['q','Quit'],
|
||||
]
|
||||
@@ -874,6 +876,8 @@ function drawControlHint() {
|
||||
['sp','Edit'],
|
||||
['sep'],
|
||||
['tab','Panel'],
|
||||
['sep'],
|
||||
['!','Help'],
|
||||
// ['sep'],
|
||||
// ['q','Quit'],
|
||||
]
|
||||
@@ -885,6 +889,8 @@ function drawControlHint() {
|
||||
['sp','Edit'],
|
||||
['sep'],
|
||||
['tab','Panel'],
|
||||
['sep'],
|
||||
['!','Help'],
|
||||
// ['sep'],
|
||||
// ['q','Quit'],
|
||||
]
|
||||
@@ -902,6 +908,8 @@ function drawControlHint() {
|
||||
['sep'],
|
||||
['=','KOff'],
|
||||
['^','KCut'],
|
||||
['sep'],
|
||||
['!','Help'],
|
||||
// ['sep'],
|
||||
// ['Sp','ExitEdit'],
|
||||
]
|
||||
@@ -911,18 +919,22 @@ function drawControlHint() {
|
||||
['sep'],
|
||||
[`0${sym.doubledot}9 A${sym.doubledot}F`,'Instrument'],
|
||||
['sep'],
|
||||
['sp','ExitEdit'],
|
||||
['!','Help'],
|
||||
// ['sep'],
|
||||
// ['sp','ExitEdit'],
|
||||
]
|
||||
let hintElemEditVolEff = [
|
||||
[`\u008428u\u008429u`,'Nav'],
|
||||
[`pg\u008418u`,'Cue'],
|
||||
['sep'],
|
||||
['h','Set'],
|
||||
['j','SlideDn'],
|
||||
['k','SlideUp'],
|
||||
['u','FineDn'],
|
||||
['i','FineUp'],
|
||||
['.','Set'],
|
||||
['v','SlideUp'],
|
||||
['^','SlideDn'],
|
||||
['-','FineDn'],
|
||||
['=','FineUp'],
|
||||
[`0${sym.doubledot}9 A${sym.doubledot}F`,'Val'],
|
||||
['sep'],
|
||||
['!','Help'],
|
||||
// ['sep'],
|
||||
// ['Sp','ExitEdit'],
|
||||
]
|
||||
@@ -930,11 +942,11 @@ function drawControlHint() {
|
||||
[`\u008428u\u008429u`,'Nav'],
|
||||
[`pg\u008418u`,'Cue'],
|
||||
['sep'],
|
||||
['h','Set'],
|
||||
['j','SlideL'],
|
||||
['k','SlideR'],
|
||||
['u','FineL'],
|
||||
['i','FineR'],
|
||||
['.','Set'],
|
||||
['<','SlideL'],
|
||||
['>','SlideR'],
|
||||
['-','FineL'],
|
||||
['=','FineR'],
|
||||
[`0${sym.doubledot}9 A${sym.doubledot}F`,'Val'],
|
||||
// ['sep'],
|
||||
// ['Sp','ExitEdit'],
|
||||
@@ -945,7 +957,9 @@ function drawControlHint() {
|
||||
['sep'],
|
||||
[`0${sym.doubledot}9 A${sym.doubledot}F`,`FxSym`],
|
||||
['sep'],
|
||||
['sp','ExitEdit'],
|
||||
['!','Help'],
|
||||
// ['sep'],
|
||||
// ['sp','ExitEdit'],
|
||||
]
|
||||
let hintElemEditFxVal = [
|
||||
[`\u008428u\u008429u`,'Nav'],
|
||||
@@ -953,10 +967,12 @@ function drawControlHint() {
|
||||
['sep'],
|
||||
[`0${sym.doubledot}9 A${sym.doubledot}F`,`FxVal`],
|
||||
['sep'],
|
||||
['sp','ExitEdit'],
|
||||
['!','Help'],
|
||||
// ['sep'],
|
||||
// ['sp','ExitEdit'],
|
||||
]
|
||||
|
||||
const hintElemExternal = [['Tab','Panel']]
|
||||
const hintElemExternal = [['Tab','Panel'],['sep'],['!','Help']]
|
||||
let hintElems = [hintElemTimeline, hintElemOrders, hintElemPatterns, hintElemExternal, hintElemExternal, hintElemExternal, hintElemExternal]
|
||||
let hintElemPat = [hintElemEditNoteValue, hintElemEditInstValue, hintElemEditVolEff, hintElemEditPanEff, hintElemEditFxSym, hintElemEditFxVal]
|
||||
|
||||
@@ -1383,7 +1399,7 @@ function drawOrdersHeader() {
|
||||
con.color_pair(colVoiceHdr, 255)
|
||||
print(' ')
|
||||
con.color_pair(colVoiceHdr, ordersColCursor === 0 ? colHighlight : 255)
|
||||
print('Cmd ')
|
||||
print('Comand ')
|
||||
for (let c = 0; c < VOCSIZE_ORDERS; c++) {
|
||||
const v = ordersVoiceOff + c
|
||||
con.color_pair(colVoiceHdr, ordersColCursor === v + 1 ? colHighlight : 255)
|
||||
@@ -1416,9 +1432,9 @@ function drawOrdersContents(wo) {
|
||||
// CMD column — crosshair highlight at (ordersCursor, col 0)
|
||||
const cmdBack = (isSel && ordersColCursor === 0) ? colPlayback : back
|
||||
con.color_pair(cue.instr ? colStatus : colSep, cmdBack)
|
||||
print(cue.instr ? cue.instr.hex02() : '--')
|
||||
print(cue.instr ? cueInstToStr(cue.instr) : '------')
|
||||
con.color_pair(colBackPtn, back)
|
||||
print(' ')
|
||||
print(' ')
|
||||
// Voice columns
|
||||
for (let c = 0; c < VOCSIZE_ORDERS; c++) {
|
||||
const v = ordersVoiceOff + c
|
||||
@@ -1435,6 +1451,35 @@ function drawOrdersContents(wo) {
|
||||
}
|
||||
}
|
||||
|
||||
function cueInstToStr(inst) {
|
||||
let foreword = (inst >>> 12) & 15
|
||||
let preamble = (inst >>> 8) & 15
|
||||
let arg12 = inst & 0xFFF
|
||||
let arg8 = inst & 0xFF
|
||||
let fallback = `?${inst.hex04()}?`
|
||||
switch (foreword) {
|
||||
case 0b1000:
|
||||
return "BAK" + arg12.hex03()
|
||||
case 0b1001:
|
||||
return "FWD" + arg12.hex03()
|
||||
case 0b1111:
|
||||
return "JMP" + arg12.hex03()
|
||||
case 0b0000:
|
||||
switch (preamble) {
|
||||
case 0b0010:
|
||||
return "LEN " + arg8.dec02()
|
||||
case 0b0001:
|
||||
return arg8 ? ("FADE" + arg8.dec02()) : "HALT "
|
||||
case 0b0000:
|
||||
return "NO-OP "
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
function timelineInput(wo, event) {
|
||||
const keysym = event[1]
|
||||
const keyJustHit = (1 == event[2])
|
||||
@@ -2404,6 +2449,93 @@ function clampOrdersHoriz() {
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// HELP POPUP
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const HELP_POPUP_W = SCRW - 8
|
||||
const HELP_POPUP_X = ((SCRW - HELP_POPUP_W) / 2 | 0) + 1
|
||||
const HELP_POPUP_Y = 5
|
||||
const HELP_POPUP_H = SCRH - HELP_POPUP_Y - 1
|
||||
const HELP_CONTENT_X = HELP_POPUP_X + 2
|
||||
const HELP_CONTENT_Y = HELP_POPUP_Y + 2
|
||||
const HELP_CONTENT_W = HELP_POPUP_W - 6
|
||||
const HELP_CONTENT_H = HELP_POPUP_H - 3
|
||||
|
||||
// Pre-typeset every panel's help text. taut_helpmsg.js reads HELPMSG_WIDTH for
|
||||
// the wrap width and stores ready-to-print display strings into MSG_BY_TABS.
|
||||
_G.TAUT.HELPMSG_WIDTH = HELP_CONTENT_W
|
||||
_G.shell.execute("taut_helpmsg")
|
||||
|
||||
function openHelpPopup() {
|
||||
const helpmsg = _G.TAUT.HELPMSG || {}
|
||||
const lines = (helpmsg.MSG_BY_TABS && helpmsg.MSG_BY_TABS[currentPanel]) || ['']
|
||||
const colText = helpmsg.COL_TEXT || colWHITE
|
||||
|
||||
const popup = new win.WindowObject(
|
||||
HELP_POPUP_X, HELP_POPUP_Y, HELP_POPUP_W, HELP_POPUP_H,
|
||||
()=>{}, ()=>{}, `Help: ${PANEL_NAMES[currentPanel]}`, popupDrawFrame
|
||||
)
|
||||
popup.isHighlighted = true
|
||||
popup.titleBack = colPopupBack
|
||||
|
||||
let scroll = 0
|
||||
const maxScroll = Math.max(0, lines.length - HELP_CONTENT_H)
|
||||
|
||||
const repaint = () => {
|
||||
con.color_pair(230, colPopupBack)
|
||||
popup.drawFrame()
|
||||
|
||||
// popupDrawFrame leaves the bottom row unpainted; fill it ourselves.
|
||||
con.color_pair(colText, colPopupBack)
|
||||
con.move(HELP_POPUP_Y + HELP_POPUP_H - 1, HELP_POPUP_X)
|
||||
print(' '.repeat(HELP_POPUP_W))
|
||||
|
||||
for (let r = 0; r < HELP_CONTENT_H; r++) {
|
||||
con.move(HELP_CONTENT_Y + r, HELP_CONTENT_X)
|
||||
con.color_pair(colText, colPopupBack)
|
||||
const line = lines[scroll + r]
|
||||
print((line === undefined) ? ' '.repeat(HELP_CONTENT_W) : line)
|
||||
}
|
||||
|
||||
// scroll indicator on the right inner edge
|
||||
if (lines.length > HELP_CONTENT_H) {
|
||||
const trackH = HELP_CONTENT_H
|
||||
const indPos = (maxScroll === 0) ? 0 : ((scroll * (trackH - 1) / maxScroll) | 0)
|
||||
for (let r = 0; r < trackH; r++) {
|
||||
con.move(HELP_CONTENT_Y + r, HELP_POPUP_X + HELP_POPUP_W - 2)
|
||||
con.color_pair(colPushBtnBack, colPopupBack)
|
||||
print(r === indPos ? '\u00DB' : '\u00B3')
|
||||
}
|
||||
}
|
||||
|
||||
con.color_pair(colStatus, 255)
|
||||
}
|
||||
|
||||
repaint()
|
||||
|
||||
let done = false
|
||||
let eventJustReceived = true
|
||||
while (!done) {
|
||||
input.withEvent(ev => {
|
||||
if (ev[0] !== 'key_down') return
|
||||
//if (1 !== ev[2]) return // allow continuous scroll by key repeat
|
||||
if (eventJustReceived) { eventJustReceived = false; return }
|
||||
const ks = ev[1]
|
||||
|
||||
if (ks === '<ESC>' || ks === '!' || ks === 'q' || ks === '\n') { done = true }
|
||||
else if (ks === '<UP>') { if (scroll > 0) { scroll -= 1; repaint() } }
|
||||
else if (ks === '<DOWN>') { if (scroll < maxScroll) { scroll += 1; repaint() } }
|
||||
else if (ks === '<PAGE_UP>') { scroll = Math.max(0, scroll - HELP_CONTENT_H); repaint() }
|
||||
else if (ks === '<PAGE_DOWN>') { scroll = Math.min(maxScroll, scroll + HELP_CONTENT_H); repaint() }
|
||||
else if (ks === '<HOME>') { scroll = 0; repaint() }
|
||||
else if (ks === '<END>') { scroll = maxScroll; repaint() }
|
||||
})
|
||||
}
|
||||
|
||||
drawAll()
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// GOTO POPUP
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -2613,6 +2745,11 @@ while (!exitFlag) {
|
||||
return
|
||||
}
|
||||
|
||||
if (keyJustHit && keysym === '!') {
|
||||
openHelpPopup()
|
||||
return
|
||||
}
|
||||
|
||||
panels[currentPanel].processInput(event)
|
||||
})
|
||||
|
||||
|
||||
@@ -9,10 +9,11 @@ Tags:
|
||||
<c> - centre the line. If the line spans multiple lines, centre each line
|
||||
<r> - align right
|
||||
<l> - align left
|
||||
µtone; - replace with the brand string
|
||||
&bul; - replace with bullet (\u00847u)
|
||||
<o> - create virtual typesetting box. Left anchor: where the text cursor is. Right anchor: end of the line
|
||||
µtone; - replace with the brand string (<col 211>Micro</col><col 239>tone</col>)
|
||||
&bul; - replace with bullet (\u00F9)
|
||||
&ddot; - replace with double-dot (\u008419u)
|
||||
&mdot; - replace with BIGDOT (\u00F9)
|
||||
&mdot; - replace with BIGDOT (\u00FA)
|
||||
&updn; - up-down arrow (\u008418u)
|
||||
&udlr; - four direction arrow (\u008428u\u008429u)
|
||||
&keyoffsym; - pattern view key-off symbol (\u00A0\u00CD\u00CD\u00A1)
|
||||
@@ -22,63 +23,319 @@ Tags:
|
||||
default alignment: fully justified
|
||||
*/
|
||||
|
||||
help.notation = `<c>CONTROL NOTATON</c>
|
||||
let helpNotation = `<c>CONTROL NOTATON</c>
|
||||
|
||||
µtone; shortcuts differentiate normal and shifted shortcuts.
|
||||
&bul;a&ddot;z : alphabet without shift-in
|
||||
&bul;A&ddot;Z : alphabet with shift-in
|
||||
&bul;^ : control key`
|
||||
µtone; <O>shortcuts differentiate normal and shifted shortcuts.</O>
|
||||
&bul;<b>a</b>&ddot;<b>z</b> : <O>alphabet without shift-in</O>
|
||||
&bul;<b>A</b>&ddot;<b>Z</b> : <O>alphabet with shift-in</O>
|
||||
&bul;<b>^q</b> : <O>hit 'q' with control key</O>
|
||||
&bul;<b>^Q</b> : <O>hit 'q' with control and shift key</O>`
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
help.jam = `<c>NOTE JAMMING</c>
|
||||
let helpJam = `<c>NOTE JAMMING</c>
|
||||
|
||||
Push keys to play or insert notes.
|
||||
w e t y u i
|
||||
w e t y u
|
||||
a s d f g h j k`
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
help.common = `<c>COMMON CONTROLS</c>
|
||||
let helpCommon = `<c>COMMON CONTROLS</c>
|
||||
|
||||
&bul;Y : play the entire song from the current cue
|
||||
&bul;U : play the current cue then stop
|
||||
&bul;I : play the current row
|
||||
&bul;O : stop the playback
|
||||
&bul;tab : switch forward a tab
|
||||
&bul;TAB : switch backward a tab
|
||||
&bul;q : close µtone;`
|
||||
&bul;<b>!</b> : <O>show this help message</O>
|
||||
&bul;<b>Y</b> : <O>play the entire song from the current cue</O>
|
||||
&bul;<b>U</b> : <O>play the current cue then stop</O>
|
||||
&bul;<b>I</b> : <O>play the current row</O>
|
||||
&bul;<b>O</b> : <O>stop the playback</O>
|
||||
&bul;<b>tab</b> : <O>switch forward a tab</O>
|
||||
&bul;<b>TAB</b> : <O>switch backward a tab</O>
|
||||
&bul;<b>q</b> : <O>close µtone;</O>`
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
help.timeline = `<c>TIMELINE VIEW</c>
|
||||
let helpTimeline = `<c>TIMELINE VIEW</c>
|
||||
|
||||
Timeline has two distinct modes: view and edit mode. Two modes are toggled using the space bar.
|
||||
|
||||
<b>View mode</b>
|
||||
&bul;Note jamming : plays the note
|
||||
&bul;&udlr; : move the viewing cursor by voices and rows
|
||||
&bul;pg&updn; : go to previous/next cue
|
||||
&bul;W&mdot;E&mdot;R : toggle timeline view mode. W-most detailed, R-most abridged
|
||||
&bul;n : toggle soloing of the selected voice
|
||||
&bul;m : toggle muting of the selected voice
|
||||
<b>VIEW MODE</b>
|
||||
&bul;Note jamming : <O>plays the note</O>
|
||||
&bul;<b>&udlr;</b> : <O>move the viewing cursor by voices and rows</O>
|
||||
&bul;<b>pg&updn;</b> : <O>go to previous/next cue</O>
|
||||
&bul;<b>W</b>&mdot;<b>E</b>&mdot;<b>R</b> : <O>toggle timeline view mode. W-most detailed, R-most abridged</O>
|
||||
&bul;<b>n</b> : <O>toggle soloing of the selected voice</O>
|
||||
&bul;<b>m</b> : <O>toggle muting of the selected voice</O>
|
||||
|
||||
<b>Edit mode</b>
|
||||
&bul;Note jammping : (note column) inserts the note
|
||||
&bul;{&mdot;} : (note column) lower/raise a note by one octave (or period)
|
||||
&bul;[&mdot;] : (note column) lower/raise a note by one unit
|
||||
&bul;= : (note column) insert a key-off &keyoffsym;
|
||||
&bul;^ : (note column) insert a note-cut ¬ecutsym;
|
||||
&bul;. : remove a symbol on the selected column
|
||||
&bul;bksp : delete one character on the selected column
|
||||
&bul;0&ddot;9 a&ddot;f : inserts a (hexa)decimal number
|
||||
&bul;^&mdot;v : (volume column) slide up/down
|
||||
&bul;<&mdot;> : (panning column) slide left/right
|
||||
&bul;-&mdot;= : (vol/pan col) fine slide down/up
|
||||
&bul;&udlr; : move the viewing cursor by columns and rows
|
||||
&bul;pg&updn; : go to previous/next cue`
|
||||
<b>EDIT MODE</b>
|
||||
&bul;Note jamming : <O>(note column) inserts the note</O>
|
||||
&bul;<b>{</b>&mdot;<b>}</b> : <O>(note column) lower/raise a note by one octave (or period)</O>
|
||||
&bul;<b>[</b>&mdot;<b>]</b> : <O>(note column) lower/raise a note by one unit</O>
|
||||
&bul;<b>=</b> : <O>(note column) insert a key-off &keyoffsym;</O>
|
||||
&bul;<b>^</b> : <O>(note column) insert a note-cut ¬ecutsym;</O>
|
||||
&bul;<b>.</b> : <O>remove a symbol on the selected column</O>
|
||||
&bul;<b>bksp</b> : <O>delete one character on the selected column</O>
|
||||
&bul;<b>0</b>&ddot;<b>9</b> <b>a</b>&ddot;<b>f</b> : <O>inserts a (hexa)decimal number</O>
|
||||
&bul;<b>^</b>&mdot;<b>v</b> : <O>(volume column) slide up/down</O>
|
||||
&bul;<b><</b>&mdot;<b>></b>: <O>(panning column) slide left/right</O>
|
||||
&bul;<b>-</b>&mdot;<b>=</b> : <O>(vol/pan col) fine slide down/up</O>
|
||||
&bul;<b>&udlr;</b> : <O>move the viewing cursor by columns and rows</O>
|
||||
&bul;<b>pg&updn;</b> : <O>go to previous/next cue</O>`
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
if (!_G.TAUT.HELPMSG) _G.TAUT.HELPMSG=help;
|
||||
// assemble help text pieces to complete help message
|
||||
|
||||
const SCRW = con.getmaxyx()[1]
|
||||
|
||||
// Display-command palette. taut.js's popup uses (HELP_COL_TEXT on background) as the
|
||||
// default colour pair, so embedded `\x1B[38;5;Nm` codes switch foreground only.
|
||||
const HELP_COL_TEXT = 239 // popup body default (== colWHITE)
|
||||
const HELP_COL_EMPH = 230 // <b>...</b> highlight (== colVoiceHdr)
|
||||
const HELP_COL_BRAND = 211 // first half of "Microtone"
|
||||
const HELP_COL_BRAND_DIM = 239 // second half of "Microtone"
|
||||
|
||||
const fgEsc = (n) => `\x1B[38;5;${n}m`
|
||||
const ESC_DEFAULT = fgEsc(HELP_COL_TEXT)
|
||||
const ESC_EMPH = fgEsc(HELP_COL_EMPH)
|
||||
const MICROTONE = `${fgEsc(HELP_COL_BRAND)}Micro${fgEsc(HELP_COL_BRAND_DIM)}tone${ESC_DEFAULT}`
|
||||
|
||||
// Replace &xxx; entities with their final printable representations.
|
||||
function expandEntities(s) {
|
||||
return s
|
||||
.replaceAll('µtone;', MICROTONE)
|
||||
.replaceAll('&bul;', '\u00F9')
|
||||
.replaceAll('&ddot;', '\u008419u')
|
||||
.replaceAll('&mdot;', '\u00FA')
|
||||
.replaceAll('&updn;', '\u008418u')
|
||||
.replaceAll('&udlr;', '\u008428u\u008429u')
|
||||
.replaceAll('&keyoffsym;', '\u00A0\u00CD\u00CD\u00A1')
|
||||
.replaceAll('¬ecutsym;', '\u00A4\u00A4\u00A4\u00A4')
|
||||
.replaceAll(' ', '\u007F')
|
||||
.replaceAll('­', '')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
}
|
||||
|
||||
// Tokenise a (post-entity-expansion) line. Returns an array of:
|
||||
// {type:'word', text:String, w:int} - non-breakable run of visible chars (may carry ANSI escapes)
|
||||
// {type:'sp'} - a single soft space (eligible for break/expansion)
|
||||
// {type:'anchor', open:Boolean} - <o>/</o> markers (zero width)
|
||||
//
|
||||
// Width accounting:
|
||||
// - ANSI escapes (`\x1B[...m`) : 0 visible chars
|
||||
// - TSVM unicode escapes (`..u`) : 1 visible char
|
||||
// - non-breaking space ( ) : 1 visible char (consumed as part of a word)
|
||||
// - soft hyphen () : dropped (not implemented as a break point)
|
||||
// - everything else : 1 visible char
|
||||
function tokenise(line) {
|
||||
const tokens = []
|
||||
let buf = ''
|
||||
let bufW = 0
|
||||
let i = 0
|
||||
|
||||
const flushWord = () => {
|
||||
if (buf.length > 0) {
|
||||
tokens.push({type: 'word', text: buf, w: bufW})
|
||||
buf = ''
|
||||
bufW = 0
|
||||
}
|
||||
}
|
||||
|
||||
while (i < line.length) {
|
||||
// inline tags (case-sensitive for <b>, case-insensitive for <o>)
|
||||
if (line.slice(i, i + 3) === '<b>') { buf += ESC_EMPH; i += 3; continue }
|
||||
if (line.slice(i, i + 4) === '</b>') { buf += ESC_DEFAULT; i += 4; continue }
|
||||
const head3 = line.slice(i, i + 3).toLowerCase()
|
||||
const head4 = line.slice(i, i + 4).toLowerCase()
|
||||
if (head3 === '<o>') { flushWord(); tokens.push({type: 'anchor', open: true}); i += 3; continue }
|
||||
if (head4 === '</o>') { flushWord(); tokens.push({type: 'anchor', open: false}); i += 4; continue }
|
||||
|
||||
const c = line[i]
|
||||
const cc = line.charCodeAt(i)
|
||||
|
||||
if (cc === 0x1B) {
|
||||
// pre-existing ANSI escape - copy verbatim, zero visible width
|
||||
const m = line.indexOf('m', i)
|
||||
const end = (m < 0) ? line.length : m + 1
|
||||
buf += line.slice(i, end)
|
||||
i = end
|
||||
}
|
||||
else if (cc === 0x84) {
|
||||
// TSVM <digits>u escape - copy verbatim, one visible char
|
||||
const u = line.indexOf('u', i)
|
||||
const end = (u < 0) ? line.length : u + 1
|
||||
buf += line.slice(i, end)
|
||||
bufW += 1
|
||||
i = end
|
||||
}
|
||||
else if (c === ' ') {
|
||||
flushWord()
|
||||
tokens.push({type: 'sp'})
|
||||
i += 1
|
||||
}
|
||||
else if (cc === 0x00AD) {
|
||||
// soft hyphen: drop (no break-point handling for now)
|
||||
i += 1
|
||||
}
|
||||
else {
|
||||
buf += c
|
||||
bufW += 1
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
flushWord()
|
||||
return tokens
|
||||
}
|
||||
|
||||
// Build wrapped lines from a token stream then format each one according to alignment.
|
||||
// Returns an array of strings, each exactly `width` visible chars wide (padded with
|
||||
// trailing spaces) so the caller can blit them without further math.
|
||||
function wrapAndAlign(tokens, width, alignment) {
|
||||
const lines = [] // each: {tokens, indent, contentW}
|
||||
let curTokens = []
|
||||
let curW = 0
|
||||
let curIndent = 0
|
||||
let nextIndent = 0 // indent the *next* flushed line should use
|
||||
|
||||
const flushLine = () => {
|
||||
// strip trailing soft spaces
|
||||
while (curTokens.length > 0 && curTokens[curTokens.length - 1].type === 'sp') {
|
||||
curTokens.pop()
|
||||
curW -= 1
|
||||
}
|
||||
lines.push({tokens: curTokens, indent: curIndent, contentW: curW})
|
||||
curTokens = []
|
||||
curW = 0
|
||||
curIndent = nextIndent
|
||||
}
|
||||
|
||||
for (const tok of tokens) {
|
||||
if (tok.type === 'anchor') {
|
||||
// anchor opens at the current visible column (accounting for indent)
|
||||
if (tok.open) nextIndent = curIndent + curW
|
||||
else nextIndent = 0
|
||||
continue
|
||||
}
|
||||
|
||||
if (tok.type === 'sp') {
|
||||
// ignore leading soft spaces on a fresh line
|
||||
if (curW === 0) continue
|
||||
// hard wrap if the line is already at the right edge
|
||||
if (curIndent + curW + 1 > width) { flushLine(); continue }
|
||||
curTokens.push(tok)
|
||||
curW += 1
|
||||
continue
|
||||
}
|
||||
|
||||
// word
|
||||
const tw = tok.w
|
||||
if (curIndent + curW + tw > width) {
|
||||
flushLine()
|
||||
// word too wide for the wrapped line: emit it on its own row (possibly clipped by terminal)
|
||||
if (curIndent + tw > width) {
|
||||
curTokens.push(tok)
|
||||
curW += tw
|
||||
flushLine()
|
||||
continue
|
||||
}
|
||||
}
|
||||
curTokens.push(tok)
|
||||
curW += tw
|
||||
}
|
||||
|
||||
if (curTokens.length > 0 || lines.length === 0) flushLine()
|
||||
|
||||
return lines.map((line, i) => formatLine(line, width, alignment, i === lines.length - 1))
|
||||
}
|
||||
|
||||
function formatLine(line, totalWidth, alignment, isLast) {
|
||||
if (line.tokens.length === 0) return ' '.repeat(totalWidth)
|
||||
|
||||
const indent = ' '.repeat(line.indent)
|
||||
const remaining = totalWidth - line.indent - line.contentW
|
||||
const pad = (n) => (n > 0) ? ' '.repeat(n) : ''
|
||||
const flatText = () => line.tokens.map(t => (t.type === 'sp') ? ' ' : t.text).join('')
|
||||
|
||||
if (alignment === 'c') {
|
||||
const left = remaining >> 1
|
||||
return indent + pad(left) + flatText() + pad(remaining - left)
|
||||
}
|
||||
if (alignment === 'r') return indent + pad(remaining) + flatText()
|
||||
if (alignment === 'l') return indent + flatText() + pad(remaining)
|
||||
|
||||
// justified: only expand spaces when there's slack and we're not on the
|
||||
// last (or single) wrapped line
|
||||
if (isLast || remaining <= 0) return indent + flatText() + pad(remaining)
|
||||
|
||||
const spaceCount = line.tokens.reduce((n, t) => n + (t.type === 'sp' ? 1 : 0), 0)
|
||||
if (spaceCount === 0) return indent + flatText() + pad(remaining)
|
||||
|
||||
const baseExtra = (remaining / spaceCount) | 0
|
||||
let leftover = remaining - baseExtra * spaceCount
|
||||
|
||||
let out = indent
|
||||
for (const tok of line.tokens) {
|
||||
if (tok.type === 'sp') {
|
||||
const extra = baseExtra + (leftover > 0 ? 1 : 0)
|
||||
if (leftover > 0) leftover -= 1
|
||||
out += ' '.repeat(1 + extra)
|
||||
} else {
|
||||
out += tok.text
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Process a single source line: peel a leading <c>/<r>/<l> alignment tag (if present),
|
||||
// strip its matching close tag, then tokenise + wrap.
|
||||
function typesetSourceLine(line, width) {
|
||||
if (line.length === 0) return [' '.repeat(width)]
|
||||
|
||||
let alignment = 'j' // justified default
|
||||
const startMatch = line.match(/^<([crl])>/i)
|
||||
if (startMatch) {
|
||||
alignment = startMatch[1].toLowerCase()
|
||||
line = line.slice(startMatch[0].length)
|
||||
const closeRe = new RegExp(`</${alignment}>$`, 'i')
|
||||
line = line.replace(closeRe, '')
|
||||
}
|
||||
|
||||
const tokens = tokenise(line)
|
||||
return wrapAndAlign(tokens, width, alignment)
|
||||
}
|
||||
|
||||
function typesetText(text, width) {
|
||||
text = expandEntities(text)
|
||||
const out = []
|
||||
for (const srcLine of text.split('\n')) {
|
||||
for (const outLine of typesetSourceLine(srcLine, width)) out.push(outLine)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function typeset(text, customWidth) {
|
||||
let typesetWidth = customWidth
|
||||
if (typesetWidth === undefined) typesetWidth = _G.TAUT.HELPMSG_WIDTH
|
||||
if (typesetWidth === undefined) {
|
||||
const currentPosX = con.getyx()[1] // 1-indexed
|
||||
typesetWidth = SCRW - currentPosX + 1
|
||||
}
|
||||
return typesetText(text, typesetWidth)
|
||||
}
|
||||
|
||||
let helpMessages = [ // index: taut.js PANEL_NAMES
|
||||
[helpJam, helpTimeline, helpCommon, helpNotation].join('\n\n'),
|
||||
[helpCommon, helpNotation].join('\n\n'), // placeholder
|
||||
[helpCommon, helpNotation].join('\n\n'), // placeholder
|
||||
[helpCommon, helpNotation].join('\n\n'), // placeholder
|
||||
[helpCommon, helpNotation].join('\n\n'), // placeholder
|
||||
[helpCommon, helpNotation].join('\n\n'), // placeholder
|
||||
[helpCommon, helpNotation].join('\n\n'), // placeholder
|
||||
]
|
||||
|
||||
help.MSG_BY_TABS = helpMessages.map(it => typeset(it))
|
||||
help.typeset = typeset
|
||||
help.COL_TEXT = HELP_COL_TEXT
|
||||
help.COL_EMPH = HELP_COL_EMPH
|
||||
|
||||
if (!_G.TAUT.HELPMSG) _G.TAUT.HELPMSG=help;
|
||||
|
||||
Binary file not shown.
Binary file not shown.
18
it2taud.py
18
it2taud.py
@@ -571,9 +571,10 @@ def _parse_it_envelope(data: bytes, env_ptr: int, kind: str,
|
||||
'filter' — IT -32..+32 → Taud 0..255 (0x80 = unity cutoff)
|
||||
|
||||
Word layout (terranmon.txt:2049+ / 2114+):
|
||||
LOOP word: 0b 0000_0sss_ssXcb_eeeee (X = 'p'/'m' for pan/pf, 0 for vol)
|
||||
LOOP word: 0b 00P0_0sss_ssXcb_eeeee (X = 'p'/'m' for pan/pf, 0 for vol)
|
||||
SUSTAIN word: 0b 0000_0sss_ss00b_eeeee
|
||||
bits 12..8 = start index, bits 4..0 = end index
|
||||
bit 13 = P (envelope present; gates pan/pf evaluation in the engine)
|
||||
bit 7 = p (pan: use default pan) / m (pf: pitch=0/filter=1) / 0 (vol)
|
||||
bit 6 = c (envelope carry — placed in the LOOP word)
|
||||
bit 5 = b (enable that region)
|
||||
@@ -638,7 +639,10 @@ def _parse_it_envelope(data: bytes, env_ptr: int, kind: str,
|
||||
# directly. Bits: 5=b enable, 6=c carry, 7=p (pan default-pan flag) /
|
||||
# m (pf filter mode); 12..8=start, 4..0=end. SUSTAIN word never carries
|
||||
# c/p/m — those live in the LOOP word.
|
||||
loop_word = 0
|
||||
# P (bit 13) marks the envelope as present in source, regardless of LOOP/
|
||||
# SUSTAIN enable. We reach this point only when the IT envelope flag bit 0
|
||||
# is set (handled at function top), so P is unconditionally set here.
|
||||
loop_word = 0x2000 # P: envelope present
|
||||
if has_env_loop and 0 <= it_lpb < 25 and 0 <= it_lpe < 25:
|
||||
loop_word |= 0x0020 # b: enable LOOP
|
||||
loop_word |= (it_lpb & 0x1F) << 8
|
||||
@@ -1133,7 +1137,8 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
||||
|
||||
# 256-byte instrument layout (terranmon.txt:2001+).
|
||||
INST_STRIDE = 256
|
||||
USE_ENV_BIT = 0x0020 # b — set whenever the engine should evaluate the envelope
|
||||
USE_ENV_BIT = 0x0020 # b — LOOP wrap enable (legacy; engine still honours)
|
||||
ENV_PRESENT_BIT = 0x2000 # P — envelope present in source (terranmon.txt byte 16/18/20 bit 5)
|
||||
|
||||
def _write_env(buf: bytearray, base: int, env_pts):
|
||||
"""Write 25 (value, minifloat) pairs starting at `buf[base]`. Pads
|
||||
@@ -1200,7 +1205,8 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
||||
# When the source has neither loop nor sustain on the volume envelope
|
||||
# the engine still needs the b flag so the single-point unit envelope
|
||||
# is evaluated — synthesise USE_ENV_BIT into the LOOP word as a fallback.
|
||||
vol_env_loop = idata.get('vol_env_loop', USE_ENV_BIT)
|
||||
# The P bit is informational for vol but set for consistency.
|
||||
vol_env_loop = idata.get('vol_env_loop', USE_ENV_BIT | ENV_PRESENT_BIT)
|
||||
vol_env_sus = idata.get('vol_env_sus', 0)
|
||||
pan_env_loop = idata.get('pan_env_loop', 0)
|
||||
pan_env_sus = idata.get('pan_env_sus', 0)
|
||||
@@ -1236,8 +1242,10 @@ def build_sample_inst_bin_it(samples_or_proxy: list,
|
||||
inst_bin[base + 22] = 0
|
||||
# Force engine to use this single point — set the b bit on the LOOP
|
||||
# word so the envelope is evaluated even though no wrap region exists.
|
||||
# P is also set for consistency (vol-env presence is informational
|
||||
# but converters mark it whenever they emit any node data).
|
||||
cur_loop = struct.unpack_from('<H', inst_bin, base + 15)[0]
|
||||
struct.pack_into('<H', inst_bin, base + 15, cur_loop | USE_ENV_BIT)
|
||||
struct.pack_into('<H', inst_bin, base + 15, cur_loop | USE_ENV_BIT | ENV_PRESENT_BIT)
|
||||
|
||||
if pan_env:
|
||||
_write_env(inst_bin, base + 71, pan_env)
|
||||
|
||||
@@ -521,8 +521,10 @@ def build_sample_inst_bin(samples: list) -> tuple:
|
||||
# IGV (byte 171) so the envelope must contribute a unit multiplier.
|
||||
env_vol = 63
|
||||
# MOD has no envelopes; vol LOOP word b=1 just so the engine evaluates
|
||||
# the unit envelope. Pan/PF stay disabled.
|
||||
vol_env_loop = 0x0020 # b enable
|
||||
# the unit envelope, plus P=1 (envelope present) for consistency with
|
||||
# the new gate spec (terranmon.txt byte 16/18/20 bit 5). Pan/PF stay
|
||||
# fully zero — the engine sees P=0 there and skips them.
|
||||
vol_env_loop = 0x2020 # P (bit 13) | b (bit 5)
|
||||
|
||||
base = taud_idx * INST_STRIDE
|
||||
struct.pack_into('<I', inst_bin, base + 0, ptr)
|
||||
|
||||
@@ -207,9 +207,9 @@ def build_sample_inst_bin() -> bytes:
|
||||
struct.pack_into('<H', inst_bin, base + 10, 0) # loop start
|
||||
struct.pack_into('<H', inst_bin, base + 12, len(SQUARE_SAMPLE)) # loop end
|
||||
inst_bin[base + 14] = 0x01 # forward loop
|
||||
struct.pack_into('<H', inst_bin, base + 15, 0x0020) # vol-env enabled
|
||||
struct.pack_into('<H', inst_bin, base + 17, 0) # pan-env flags
|
||||
struct.pack_into('<H', inst_bin, base + 19, 0) # pitch-env flags
|
||||
struct.pack_into('<H', inst_bin, base + 15, 0x2020) # vol-env: P (bit 13) | b (bit 5)
|
||||
struct.pack_into('<H', inst_bin, base + 17, 0) # pan-env flags (P=0 → mixer skips)
|
||||
struct.pack_into('<H', inst_bin, base + 19, 0) # pitch-env flags (P=0 → mixer skips)
|
||||
inst_bin[base + 21] = 63 # vol env pt 0 = full
|
||||
inst_bin[base + 22] = 0
|
||||
inst_bin[base + 171] = 0xA0 # IGV
|
||||
|
||||
@@ -499,8 +499,11 @@ def build_sample_inst_bin(instruments: list) -> tuple:
|
||||
# Volume envelope first point is full-scale; per-sample level is carried
|
||||
# by IGV (byte 171) so the envelope contributes a unit multiplier.
|
||||
env_vol = 63
|
||||
# Vol LOOP word: only b=1 (use envelope) — no actual loop / sustain.
|
||||
vol_env_loop = 0x0020
|
||||
# Vol LOOP word: P=1 (envelope present) | b=1 (use envelope) — no actual
|
||||
# loop / sustain. P added 2026-05-06 alongside the pan/pf gate spec
|
||||
# change (terranmon.txt byte 16/18/20 bit 5); informational for vol but
|
||||
# set for consistency. Pan/PF stay zero so the engine sees P=0 there.
|
||||
vol_env_loop = 0x2020
|
||||
|
||||
base = taud_idx * INST_STRIDE
|
||||
struct.pack_into('<I', inst_bin, base + 0, ptr) # u32 sample pointer
|
||||
|
||||
@@ -2036,6 +2036,36 @@ The b flag is the SOLE enable bit for each region; the historical 't'
|
||||
present in this encoding — sustain vs loop is now a structural
|
||||
distinction (different word at a different offset), not a flag bit.
|
||||
|
||||
Envelope PRESENCE — distinct from LOOP/SUSTAIN enable — is signalled by
|
||||
the `P` bit at LOOP-word bit 13 (the high byte's bit 5; offsets 16/18/20
|
||||
bit 5). Added 2026-05-06 to disambiguate two cases that the wrap-enable
|
||||
bits cannot tell apart on their own:
|
||||
P=0: the source had no envelope of this kind. Engine ignores the
|
||||
node array entirely and the mixer skips envelope-driven output
|
||||
for this voice (pan reads from channelPan only, cutoff/pitch
|
||||
reads from sample defaults only). The 25 node slots may still
|
||||
be left as default-fill garbage; nothing reads them.
|
||||
P=1: envelope is defined. Engine evaluates the nodes every tick.
|
||||
Wrap behaviour is independently controlled by LOOP.b and
|
||||
SUSTAIN.b — when both are 0 the envelope walks once forward
|
||||
and holds at its terminator (the IT idiom for envelope-driven
|
||||
decay tails / shaped attacks).
|
||||
The P bit was introduced to fix a gating ambiguity for pan and pitch/
|
||||
filter envelopes: the engine could not distinguish "no envelope at all"
|
||||
(treat as absent) from "envelope present but neither LOOP nor SUSTAIN
|
||||
wrap is enabled" (evaluate and apply, just don't wrap). Volume envelope
|
||||
evaluation has always been unconditional in the engine (a default
|
||||
single-point envelope at value 63 is harmlessly held at unity), so
|
||||
P_vol is currently informational only — converters should still set it
|
||||
when the source defines a volume envelope, for consistency and to
|
||||
support future per-voice gating.
|
||||
|
||||
P is the SOLE presence signal: converters MUST set P=1 whenever they
|
||||
emit envelope nodes, regardless of whether the source enables LOOP or
|
||||
SUSTAIN. Pre-2026-05-06 .taud files predate the P bit and will not have
|
||||
their pan / pf envelopes evaluated by the current engine — re-convert
|
||||
from source.
|
||||
|
||||
0 Uint32 Sample Pointer
|
||||
4 Uint16 Sample length
|
||||
6 Uint16 Sampling rate at C4 (note number 0x5000)
|
||||
@@ -2061,16 +2091,20 @@ distinction (different word at a different offset), not a flag bit.
|
||||
the engine deactivates the voice (player/sndmix.c:493-498). Without this,
|
||||
instruments with stored fadeout=0 + envelope ending at 0 would silently
|
||||
hold their voices forever.
|
||||
0b 000_sssss_0cb_eeeee
|
||||
0b 00P_sssss_0cb_eeeee
|
||||
s (bits 12..8) : loop start index (0..24)
|
||||
e (bits 4..0) : loop end index (0..24)
|
||||
b (bit 5) : enable the LOOP wrap (0 = envelope walks once to its
|
||||
terminator and holds; non-zero loops between s and e)
|
||||
c (bit 6) : envelope carry (cross-trigger envelope position carry)
|
||||
(bits 7, 13..15 reserved — set to 0)
|
||||
P (bit 13) : envelope present in source (informational for vol —
|
||||
engine evaluates vol env unconditionally; converters
|
||||
should set P=1 when emitting nodes for consistency
|
||||
with pan/pf envelopes, see file-header preamble)
|
||||
(bits 7, 14..15 reserved — set to 0)
|
||||
17 Bit16 Panning envelope LOOP word
|
||||
* Always-active wrap region for the pan envelope.
|
||||
0b 000_sssss_pcb_eeeee
|
||||
0b 00P_sssss_pcb_eeeee
|
||||
s (bits 12..8) : loop start index
|
||||
e (bits 4..0) : loop end index
|
||||
b (bit 5) : enable the LOOP
|
||||
@@ -2079,16 +2113,28 @@ distinction (different word at a different offset), not a flag bit.
|
||||
Independent of LOOP enable; the engine reads this bit
|
||||
from the LOOP word as the canonical home for envelope-
|
||||
level meta flags.
|
||||
(bits 13..15 reserved)
|
||||
P (bit 13) : envelope present in source. Gates whether the mixer
|
||||
applies envelope-driven pan at all. P=0 ⇒ mixer uses
|
||||
channelPan only and the node array is ignored. P=1 ⇒
|
||||
evaluate every tick, even when both LOOP.b and SUSTAIN.b
|
||||
are 0 (envelope walks once and holds — IT pan-env
|
||||
flag=0x01 idiom).
|
||||
(bits 14..15 reserved)
|
||||
19 Bit16 Pitch/Filter envelope LOOP word
|
||||
* Always-active wrap region for the pitch/filter envelope.
|
||||
0b 000_sssss_mcb_eeeee
|
||||
0b 00P_sssss_mcb_eeeee
|
||||
s (bits 12..8) : loop start index
|
||||
e (bits 4..0) : loop end index
|
||||
b (bit 5) : enable the LOOP
|
||||
c (bit 6) : envelope carry
|
||||
m (bit 7) : mode — 0 = pitch envelope, 1 = filter envelope
|
||||
(bits 13..15 reserved)
|
||||
P (bit 13) : envelope present in source. Same semantics as the
|
||||
pan envelope's P bit: gates whether the mixer applies
|
||||
envelope-driven pitch / cutoff at all. P=0 ⇒ no
|
||||
envelope contribution (sample plays at its own pitch /
|
||||
default cutoff). P=1 ⇒ evaluate every tick regardless
|
||||
of LOOP.b / SUSTAIN.b.
|
||||
(bits 14..15 reserved)
|
||||
21 Bit16x25 Volume envelopes
|
||||
Byte 1: Volume (00..3F)
|
||||
Byte 2: Time until the next point, in seconds (3.5 Unsigned Minifloat). 0 = hold at this point indefinitely.
|
||||
@@ -2270,25 +2316,17 @@ TODO:
|
||||
skipped because vEnvActive required either b bit. Now evaluation
|
||||
is gated only by voice.volEnvOn (matches CHN_VOLENV in Schism).
|
||||
See byte 15 spec for the LOOP word.
|
||||
[ ] Same gate fix needed for pan and pitch/filter envelopes? Currently
|
||||
advanceEnvelope/advancePfEnvelope still require LOOP-b OR SUSTAIN-b
|
||||
before evaluating, AND the same condition feeds voice.hasPanEnv /
|
||||
voice.hasPfEnv which the mixer uses to decide whether to apply
|
||||
envelope-driven pan / cutoff at all. The simple "drop the gate"
|
||||
treatment that worked for vol env doesn't transfer cleanly: an
|
||||
absent pan/pf envelope (FT2 default, no env at all) needs to look
|
||||
different from an enabled-no-wrap envelope so the mixer can ignore
|
||||
the absent case. Options:
|
||||
(a) Distinguish via a new format bit (e.g. byte 15/17/19 bit 7
|
||||
for vol/pan, but bit 7 of pf already carries 'm' filter mode).
|
||||
(b) Content-based detection at note trigger: envelope is "present"
|
||||
if any node has non-default value or non-zero offset.
|
||||
(c) Make the converters write a dedicated "envelope present"
|
||||
sentinel (e.g. start>end in the LOOP word) that the engine
|
||||
recognises as evaluate-but-don't-wrap.
|
||||
Until decided, IT pan/pf envelopes with flags=0x01 will not animate
|
||||
between rows. Workaround: enable IT's envelope loop or sustain bit
|
||||
in source so the converter sets the LOOP/SUSTAIN b bit.
|
||||
[x] Same gate fix needed for pan and pitch/filter envelopes.
|
||||
Resolution (2026-05-06): added P (envelope present) bit at LOOP-word
|
||||
bit 13 (offsets 16/18/20 bit 5) for all three envelopes. Engine
|
||||
gates pan/pf envelope evaluation on P alone; converters set P=1
|
||||
whenever they emit envelope nodes, regardless of LOOP/SUSTAIN
|
||||
enable, so an enabled-no-wrap envelope (IT pan-env flag=0x01)
|
||||
animates correctly. Mixer's hasPanEnv/hasPfEnv read the same gate,
|
||||
so absent envelopes still bypass envelope-driven output. Pre-
|
||||
2026-05-06 .taud files predate the P bit and need re-conversion
|
||||
for pan/pf envelopes to play. See byte 15/17/19 spec for the LOOP
|
||||
word bit layout.
|
||||
[ ] implement extended tone mode (MONOTONE compat)
|
||||
[ ] pattern loops stops working after processed once (test with slumberjack.xm)
|
||||
[ ] milkytracker-style volume ramping (on sample-end only)
|
||||
@@ -2420,8 +2458,8 @@ Play Head Flags
|
||||
1001xxxx yyyyyyyy (FWD000) - Skip forward 0bxxxxyyyyyyyy patterns
|
||||
1111xxxx yyyyyyyy (JMP000) - Go to absolute pattern number 0bxxxxyyyyyyyy
|
||||
00000010 00xxxxxx (LEN 00) - Pattern length for this cue (0..63), where 0: 1 row, 63: 64 rows (decoded by AudioAdapter as of 2026-05-05; emitted by xm2taud / it2taud for non-multiple-of-64 source patterns)
|
||||
00000001 00000000 - Halt (HALT )
|
||||
00000001 00111111 - Fadeout (FADOUT) - Gradually decrease global volume such that at row 63 it reaches zero
|
||||
00000001 00000000 - Halt (HALT ) - Play the full length of the pattern then stop the playback
|
||||
00000001 00xxxxxx - Fadeout (FADOUT) - Gradually decrease global volume such that at row 0bxxxxxx it reaches zero, then stop the playback
|
||||
00000000 - No operation
|
||||
|
||||
65536..131071 RW: PCM Sample buffer
|
||||
|
||||
@@ -1241,6 +1241,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
}
|
||||
}
|
||||
|
||||
// Envelope-present test (terranmon.txt byte 15/17/19, P bit at LOOP word bit 13).
|
||||
// The P bit is the sole presence signal — converters set it whenever they emit
|
||||
// envelope nodes. Pre-2026-05-06 .taud files without P will not have pan/pf
|
||||
// envelopes evaluated; re-convert from source.
|
||||
private inline fun envPresent(loopWord: Int): Boolean = ((loopWord ushr 13) and 1) != 0
|
||||
|
||||
// Reusable per-envelope wrap-range scratch (avoid per-tick allocation).
|
||||
private val volWrap = IntArray(2)
|
||||
private val panWrap = IntArray(2)
|
||||
@@ -1305,10 +1311,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
}
|
||||
}
|
||||
|
||||
// Pan envelope (only when active for this instrument)
|
||||
// Pan envelope. Presence is decided once per trigger and stored on the voice
|
||||
// (voice.hasPanEnv is keyed on LOOP.P — see triggerNote). Like the volume
|
||||
// envelope above, evaluation is no longer gated by the wrap-enable bits: an
|
||||
// envelope marked "present but no wrap" still walks forward, matching the IT
|
||||
// idiom (pan-env flag=0x01) and Schism player/sndmix.c:470-502.
|
||||
if (!voice.hasPanEnv || !voice.panEnvOn) return
|
||||
val pEnvActive = (((inst.panEnvLoop ushr 5) and 1) or ((inst.panEnvSustainWord ushr 5) and 1)) != 0
|
||||
if (!pEnvActive) return
|
||||
resolveEnvWrap(inst.panEnvLoop, inst.panEnvSustainWord, voice.keyOff, panWrap)
|
||||
val pStart = panWrap[0]
|
||||
val pEnd = panWrap[1]
|
||||
@@ -1348,10 +1356,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
* as advanceEnvelope. Result is stored in `voice.envPfValue` (0.0..1.0; 0.5 = unity).
|
||||
*/
|
||||
private fun advancePfEnvelope(voice: Voice, inst: TaudInst, tickSec: Double) {
|
||||
// Same gate semantics as the pan envelope above: presence (voice.hasPfEnv) is
|
||||
// latched at trigger time from LOOP.P; evaluation is unconditional once
|
||||
// present, so an enabled-no-wrap envelope animates.
|
||||
if (!voice.hasPfEnv || !voice.pfEnvOn) return
|
||||
val maxIdx = 24
|
||||
val pEnvActive = (((inst.pfEnvLoop ushr 5) and 1) or ((inst.pfEnvSustainWord ushr 5) and 1)) != 0
|
||||
if (!pEnvActive) return
|
||||
resolveEnvWrap(inst.pfEnvLoop, inst.pfEnvSustainWord, voice.keyOff, pfWrap)
|
||||
val pSusStart = pfWrap[0]
|
||||
val pSusEnd = pfWrap[1]
|
||||
@@ -1623,10 +1632,14 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
||||
voice.envPanIndex = 0
|
||||
voice.envPanTimeSec = 0.0
|
||||
voice.envPan = inst.panEnvelopes[0].value / 255.0
|
||||
// Pan envelope is active when EITHER the LOOP word's b bit OR the SUSTAIN word's b bit is set.
|
||||
voice.hasPanEnv = (((inst.panEnvLoop ushr 5) and 1) or ((inst.panEnvSustainWord ushr 5) and 1)) != 0
|
||||
// Envelope-present gate (added 2026-05-06). Driven by the P bit at LOOP-word
|
||||
// bit 13 (high byte's bit 5; offsets 16/18/20 bit 5), set by converters
|
||||
// whenever they emit envelope nodes. See terranmon.txt at byte 15/17/19 for
|
||||
// the bit layout and the file-header preamble for the presence-vs-wrap
|
||||
// distinction.
|
||||
voice.hasPanEnv = envPresent(inst.panEnvLoop)
|
||||
// Pitch/filter envelope state.
|
||||
voice.hasPfEnv = (((inst.pfEnvLoop ushr 5) and 1) or ((inst.pfEnvSustainWord ushr 5) and 1)) != 0
|
||||
voice.hasPfEnv = envPresent(inst.pfEnvLoop)
|
||||
// The pf 'm' mode bit (pitch=0, filter=1) lives in the LOOP word at bit 7.
|
||||
voice.envPfIsFilter = (inst.pfEnvLoop ushr 7) and 1 != 0
|
||||
voice.envPfIndex = 0
|
||||
|
||||
14
xm2taud.py
14
xm2taud.py
@@ -819,7 +819,11 @@ def _xm_envelope_to_taud(env_pts: list, num_pts: int, env_type: int,
|
||||
# LOOP word (offsets 15/17/19): b=enable, bits 12..8=start, 4..0=end.
|
||||
# SUSTAIN word (offsets 189/191/193): same bit layout; FT2 single-point
|
||||
# sustain is encoded with start == end (engine wraps that index → itself).
|
||||
loop_word = 0x0020 # b: use envelope (vol always; even with no loop the engine evaluates it)
|
||||
# P (bit 13) marks the envelope as present in source — this branch is only
|
||||
# reached when XM_ENV_ON is set, so P is unconditionally 1 here. P gates
|
||||
# whether the engine evaluates pan envelope at all (terranmon.txt byte
|
||||
# 16/18/20 bit 5); for vol it is informational.
|
||||
loop_word = 0x2020 # P (bit 13) | b (bit 5)
|
||||
if has_loop:
|
||||
loop_word |= (loop_start & 0x1F) << 8
|
||||
loop_word |= (loop_end & 0x1F)
|
||||
@@ -931,7 +935,8 @@ def build_sample_inst_bin_xm(proxies: list) -> tuple:
|
||||
s.loop_end = min(s.loop_end, n)
|
||||
pos += n
|
||||
|
||||
USE_ENV_BIT = 0x0020 # b: engine should evaluate the envelope
|
||||
USE_ENV_BIT = 0x0020 # b: engine should evaluate the envelope (LOOP wrap enable)
|
||||
ENV_PRESENT_BIT = 0x2000 # P: envelope present in source (terranmon.txt byte 16/18/20 bit 5)
|
||||
INST_STRIDE = 256
|
||||
|
||||
def _write_env(buf: bytearray, base: int, env_pts, pad_value: int) -> None:
|
||||
@@ -962,13 +967,14 @@ def build_sample_inst_bin_xm(proxies: list) -> tuple:
|
||||
|
||||
# Resolve envelope LOOP / SUSTAIN words from the proxy. When XM has no
|
||||
# envelope, fall back to a single-point unit envelope (vol LOOP word
|
||||
# b=1 only) and rely on IGV for level.
|
||||
# b=1 plus P=1 for consistency) and rely on IGV for level. Pan stays
|
||||
# zero so the engine sees P=0 there and skips envelope-driven pan.
|
||||
if s.vol_env_pts is not None:
|
||||
vol_env_loop = s.vol_env_loop_word
|
||||
vol_env_sus = s.vol_env_sus_word
|
||||
vol_env = s.vol_env_pts
|
||||
else:
|
||||
vol_env_loop = USE_ENV_BIT
|
||||
vol_env_loop = USE_ENV_BIT | ENV_PRESENT_BIT
|
||||
vol_env_sus = 0
|
||||
vol_env = None
|
||||
if s.pan_env_pts is not None:
|
||||
|
||||
Reference in New Issue
Block a user