Files
tsvm/assets/disk0/tvdos/bin/taut.js
2026-04-26 20:08:02 +09:00

1854 lines
79 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* TSVM Audio Device Tracker
*
* Created by minjaesong on 2026-04-20
*/
const win = require("wintex")
const font = require("font")
const taud = require("taud")
const keys = require("keysym")
font.setLowRom("A:/tvdos/bin/tautfont_low.chr")
font.setHighRom("A:/tvdos/bin/tautfont_high.chr")
const BUILD_DATE = "260424"
const TRACKER_SIGNATURE = "TsvmTaut"+BUILD_DATE // 14-byte string
const MIDDOT = "\u00FA"
const BIGDOT = "\u00F9"
const BULLET = "\u00847u"
const VERT = "\u00B3"
const TWOVERT = "\u00BA"
const sym = {
/* accidentals */
accnull:"\u00A2\u00A3",
demisharp:"\u0080\u0081",
sharp:"\u0082\u0083",
sesquisharp:"\u0084132u\u0085", // refrain from using (not visible on CRT); 0x84 is used as a escape sequence for arbitrary unicode character in TSVM
doublesharp:"\u0086\u0087",
triplesharp:"\u0088\u0089",
quadsharp:"\u008A\u008B",
demiflat:"\u008C\u008D",
flat:"\u008E\u008F",
sesquiflat:"\u0090\u0091",
doubleflat:"\u0092\u0093",
tripleflat:"\u0094\u0095",
quadflat:"\u0096\u0097", // refrain from using (not visible on CRT)
csharp:"\u0098",
cflat:"\u0098",
cdemisharp:"\u009E",
cdemiflat:"\u009F",
uptick:"\u009A",
dntick:"\u009B",
doubleuptick:"\u009C",
doubledntick:"\u009D",
/* special notes */
keyoff:"\u00A0\u00CD\u00CD\u00A1",
notecut:"\u00A4\u00A4\u00A4\u00A4",
/* special effects */
volset:'',//MIDDOT,
volup:"\u008430u",
voldn:"\u008431u",
volfineup:"+",
volfinedn:"-",
panset:'',//MIDDOT,
panle:"\u008417u",
panri:"\u008416u",
panfinele:"\u008427u",
panfineri:"\u008426u",
/* miscellaneous */
unticked:"\u009E",
ticked:"\u009F",
middot:MIDDOT,
doubledot:"\u008419u",
stop:"\u008420u\u008421u",
play:"\u008422u\u008423u",
}
const fxNames = {
'0':"No effect ",
'1':"Mixer config ", // Taud: 1 01xx: set stereo panning law
'2':"UNIMPLEMENTED",
'3':"UNIMPLEMENTED",
'4':"UNIMPLEMENTED",
'5':"UNIMPLEMENTED",
'6':"UNIMPLEMENTED",
'7':"UNIMPLEMENTED",
'8':"UNIMPLEMENTED",
'9':"UNIMPLEMENTED",
A:"Tick speed ",
B:"Jump to order",
C:"Break pattern",
D:"Volume slide ",
E:"Pitch down ",
F:"Pitch up ",
G:"Portamento ",
H:"Vibrato ",
I:"Tremor ",
J:"Arpeggio ",
K:"UNIMPLEMENTED", // Volume slide+Vibrato. Use H0000 and VolEff instead
L:"UNIMPLEMENTED", // Volume slide+Portamento. Use G0000 and VolEff instead
M:"UNIMPLEMENTED", // IT: Set channel volume. Use VolEff instead
N:"UNIMPLEMENTED", // IT: Channel volume slide. Use VolEff instead
O:"Sample offset",
P:"UNIMPLEMENTED", // IT: panning slide. Use PanEff instead
Q:"Retrigger ",
R:"Tremolo ",
S:"Special ",
S0:"UNIMPLEMENTED", // PT: Set audio filter.
S1:"Gliss. ctrl ",
S2:"Sample tune ",
S3:"Vibrato LFO ",
S4:"Tremolo LFO ",
S5:"Panbrello LFO",
S6:"UNIMPLEMENTED", // IT: Fine pattern delay.
S7:"UNIMPLEMENTED", // IT: misc. functions
S8:"Channel pan ", // Taud: 8-bit channel panning.
S9:"UNIMPLEMENTED", // IT: Sound control.
SA:"UNIMPLEMENTED", // SC3: Stereo control. IT: Sample offset high twobyte.
SB:"Pattern loop ",
SC:"Note cut ",
SD:"Note delay ",
SE:"Pattern delay",
SF:"Funk it ",
T:"Tempo ",
U:"Fine vibrato ",
V:"Global volume",
W:"UNIMPLEMENTED", // IT: Global volume slide.
X:"UNIMPLEMENTED", // IT: 8-bit channel panning. Use PanEff or S80xx instead
Y:"Panbrello ",
Z:"UNIMPLEMENTED", // IT: MIDI macro.
}
const panFxNames = {
0:"Set to",
1:"Slide L",
2:"Slide R",
3:"Fine slide",
30:"Fine slide L",
31:"Fine slide R",
999:"--",
}
const volFxNames = {
0:"Set to",
1:"Slide UP",
2:"Slide DN",
3:"Fine slide",
30:"Fine slide DN",
31:"Fine slide UP",
999:"--",
}
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
/* Xenharmonic, equal temperament */
50:{index:50,name:"5-TET", table:[0x0,0x333,0x666,0x99A,0xCCD],
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`]},
70:{index:70,name:"7-TET", table:[0x0,0x249,0x492,0x6DB,0x925,0xB6E,0xDB7],
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`,`B${sym.accnull}`]},
100:{index:100,name:"10-TET", table:[0x0,0x19A,0x333,0x4CD,0x666,0x800,0x99A,0xB33,0xCCD,0xE66],
sym:[`C${sym.accnull}`,`D${sym.flat}`,`D${sym.accnull}`,`E${sym.flat}`,`E${sym.accnull}`,`E${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`]},
150:{index:150,name:"15-TET", table:[0x0,0x111,0x222,0x333,0x444,0x555,0x666,0x777,0x889,0x99A,0xAAB,0xBBC,0xCCD,0xDDE,0xEEF],
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.flat}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.flat}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.flat}`,`B${sym.accnull}`]},
170:{index:170,name:"17-TET", table:[0x0,0xF1,0x1E2,0x2D3,0x3C4,0x4B5,0x5A6,0x697,0x788,0x878,0x969,0xA5A,0xB4B,0xC3C,0xD2D,0xE1E,0xF0F],
sym:[`C${sym.accnull}`,`D${sym.flat}`,`C${sym.sharp}`,`D${sym.accnull}`,`E${sym.flat}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`G${sym.flat}`,`F${sym.sharp}`,`G${sym.accnull}`,`A${sym.flat}`,`G${sym.sharp}`,`A${sym.accnull}`,`B${sym.flat}`,`A${sym.sharp}`,`B${sym.accnull}`]},
190:{index:190,name:"19-TET", table:[0x0,0xD8,0x1AF,0x287,0x35E,0x436,0x50D,0x5E5,0x6BD,0x794,0x86C,0x943,0xA1B,0xAF3,0xBCA,0xCA2,0xD79,0xE51,0xF28],
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.flat}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.flat}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.flat}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.flat}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.flat}`,`B${sym.accnull}`,`B${sym.sharp}`]},
220:{index:220,name:"22-TET", table:[0x0,0xBA,0x174,0x22F,0x2E9,0x3A3,0x45D,0x517,0x5D1,0x68C,0x746,0x800,0x8BA,0x974,0xA2F,0xAE9,0xBA3,0xC5D,0xD17,0xDD1,0xE8C,0xF46],
sym:[`C${sym.accnull}`,`C${sym.demisharp}`,`C${sym.sharp}`,`D${sym.demiflat}`,`D${sym.accnull}`,`D${sym.demisharp}`,`D${sym.sharp}`,`E${sym.demiflat}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.demisharp}`,`F${sym.sharp}`,`G${sym.demiflat}`,`G${sym.accnull}`,`G${sym.demisharp}`,`G${sym.sharp}`,`A${sym.demiflat}`,`A${sym.accnull}`,`A${sym.demisharp}`,`A${sym.sharp}`,`B${sym.demiflat}`,`B${sym.accnull}`]},
240:{index:240,name:"24-TET", table:[0x0,0xAB,0x155,0x200,0x2AB,0x355,0x400,0x4AB,0x555,0x600,0x6AB,0x755,0x800,0x8AB,0x955,0xA00,0xAAB,0xB55,0xC00,0xCAB,0xD55,0xE00,0xEAB,0xF55],
sym:[`C${sym.accnull}`,`C${sym.demisharp}`,`C${sym.sharp}`,`D${sym.demiflat}`,`D${sym.accnull}`,`D${sym.demisharp}`,`D${sym.sharp}`,`E${sym.demiflat}`,`E${sym.accnull}`,`E${sym.demisharp}`,`F${sym.accnull}`,`F${sym.demisharp}`,`F${sym.sharp}`,`G${sym.demiflat}`,`G${sym.accnull}`,`G${sym.demisharp}`,`G${sym.sharp}`,`A${sym.demiflat}`,`A${sym.accnull}`,`A${sym.demisharp}`,`A${sym.sharp}`,`B${sym.demiflat}`,`B${sym.accnull}`,`B${sym.demisharp}`]},
310:{index:310,name:"31-TET", table:[0x0,0x84,0x108,0x18C,0x211,0x295,0x319,0x39D,0x421,0x4A5,0x529,0x5AD,0x632,0x6B6,0x73A,0x7BE,0x842,0x8C6,0x94A,0x9CE,0xA53,0xAD7,0xB5B,0xBDF,0xC63,0xCE7,0xD6B,0xDEF,0xE74,0xEF8,0xF7C],
sym:[`C${sym.accnull}`,`C${sym.demisharp}`,`C${sym.sharp}`,`D${sym.flat}`,`D${sym.demiflat}`,`D${sym.accnull}`,`D${sym.demisharp}`,`D${sym.sharp}`,`E${sym.flat}`,`E${sym.demiflat}`,`E${sym.accnull}`,`E${sym.demisharp}`,`F${sym.demiflat}`,`F${sym.accnull}`,`F${sym.demisharp}`,`F${sym.sharp}`,`G${sym.flat}`,`G${sym.demiflat}`,`G${sym.accnull}`,`G${sym.demisharp}`,`G${sym.sharp}`,`A${sym.flat}`,`A${sym.demiflat}`,`A${sym.accnull}`,`A${sym.demisharp}`,`A${sym.sharp}`,`B${sym.flat}`,`B${sym.demiflat}`,`B${sym.accnull}`,`B${sym.demisharp}`,`C${sym.demiflat}`]},
410:{index:410,name:"41-TET", table:[0x0,0x64,0xC8,0x12C,0x190,0x1F4,0x257,0x2BB,0x31F,0x383,0x3E7,0x44B,0x4AF,0x513,0x577,0x5DB,0x63E,0x6A2,0x706,0x76A,0x7CE,0x832,0x896,0x8FA,0x95E,0x9C2,0xA25,0xA89,0xAED,0xB51,0xBB5,0xC19,0xC7D,0xCE1,0xD45,0xDA9,0xE0C,0xE70,0xED4,0xF38,0xF9C],
sym:[`-C-`,`${sym.uptick}C-`,`${sym.doubledntick}C${sym.csharp}`,`${sym.dntick}C${sym.csharp}`,`-C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.dntick}D-`,`-D-`,`${sym.uptick}D-`,`${sym.doubledntick}D${sym.csharp}`,`${sym.dntick}D${sym.csharp}`,`-D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.dntick}E-`,`-E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`-F-`,`${sym.uptick}F-`,`${sym.doubledntick}F${sym.csharp}`,`${sym.dntick}F${sym.csharp}`,`-F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.dntick}G-`,`-G-`,`${sym.uptick}G-`,`${sym.doubledntick}G${sym.csharp}`,`${sym.dntick}G${sym.csharp}`,`-G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.dntick}A-`,`-A-`,`${sym.uptick}A-`,`${sym.doubledntick}A${sym.csharp}`,`${sym.dntick}A${sym.csharp}`,`-A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.dntick}B-`,`-B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`]},
530:{index:530,name:"53-TET Microtonal Notation", table:[0x0,0x4D,0x9B,0xE8,0x135,0x182,0x1D0,0x21D,0x26A,0x2B8,0x305,0x352,0x39F,0x3ED,0x43A,0x487,0x4D5,0x522,0x56F,0x5BC,0x60A,0x657,0x6A4,0x6F2,0x73F,0x78C,0x7D9,0x827,0x874,0x8C1,0x90E,0x95C,0x9A9,0x9F6,0xA44,0xA91,0xADE,0xB2B,0xB79,0xBC6,0xC13,0xC61,0xCAE,0xCFB,0xD48,0xD96,0xDE3,0xE30,0xE7E,0xECB,0xF18,0xF65,0xFB3],
sym:[`-C-`,`${sym.uptick}C-`,`${sym.doubleuptick}C-`,`${sym.doubledntick}C${sym.csharp}`,`${sym.dntick}C${sym.csharp}`,`-C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.doubledntick}D-`,`${sym.dntick}D-`,`-D-`,`${sym.uptick}D-`,`${sym.doubleuptick}D-`,`${sym.doubledntick}D${sym.csharp}`,`${sym.dntick}D${sym.csharp}`,`-D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.doubledntick}E-`,`${sym.dntick}E-`,`-E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${sym.dntick}F-`,`-F-`,`${sym.uptick}F-`,`${sym.doubleuptick}F-`,`${sym.doubledntick}F${sym.csharp}`,`${sym.dntick}F${sym.csharp}`,`-F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.doubledntick}G-`,`${sym.dntick}G-`,`-G-`,`${sym.uptick}G-`,`${sym.doubleuptick}G-`,`${sym.doubledntick}G${sym.csharp}`,`${sym.dntick}G${sym.csharp}`,`-G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.doubledntick}A-`,`${sym.dntick}A-`,`-A-`,`${sym.uptick}A-`,`${sym.doubleuptick}A-`,`${sym.doubledntick}A${sym.csharp}`,`${sym.dntick}A${sym.csharp}`,`-A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.doubledntick}B-`,`${sym.dntick}B-`,`-B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`,`${sym.dntick}C-`]},
531:{index:531,name:"53-TET Pythagorean Notation", table:[0x0,0x4D,0x9B,0xE8,0x135,0x182,0x1D0,0x21D,0x26A,0x2B8,0x305,0x352,0x39F,0x3ED,0x43A,0x487,0x4D5,0x522,0x56F,0x5BC,0x60A,0x657,0x6A4,0x6F2,0x73F,0x78C,0x7D9,0x827,0x874,0x8C1,0x90E,0x95C,0x9A9,0x9F6,0xA44,0xA91,0xADE,0xB2B,0xB79,0xBC6,0xC13,0xC61,0xCAE,0xCFB,0xD48,0xD96,0xDE3,0xE30,0xE7E,0xECB,0xF18,0xF65,0xFB3],
sym:[`C${sym.accnull}`,`B${sym.sharp}`,`A${sym.triplesharp}`,`E${sym.tripleflat}`,`D${sym.flat}`,`C${sym.sharp}`,`B${sym.doublesharp}`,`F${sym.tripleflat}`,`E${sym.doubleflat}`,`D${sym.accnull}`,`C${sym.doublesharp}`,`B${sym.triplesharp}`,`F${sym.doubleflat}`,`E${sym.flat}`,`D${sym.sharp}`,`C${sym.triplesharp}`,`G${sym.tripleflat}`,`F${sym.flat}`,`E${sym.accnull}`,`D${sym.doublesharp}`,`C${sym.quadsharp}`,`G${sym.doubleflat}`,`F${sym.accnull}`,`E${sym.sharp}`,`D${sym.triplesharp}`,`A${sym.tripleflat}`,`G${sym.flat}`,`F${sym.sharp}`,`E${sym.doublesharp}`,`D${sym.quadsharp}`,`A${sym.doubleflat}`,`G${sym.accnull}`,`F${sym.doublesharp}`,`E${sym.triplesharp}`,`B${sym.tripleflat}`,`A${sym.flat}`,`G${sym.sharp}`,`F${sym.triplesharp}`,`C${sym.tripleflat}`,`B${sym.doubleflat}`,`A${sym.accnull}`,`G${sym.doublesharp}`,`F${sym.quadsharp}`,`C${sym.doubleflat}`,`B${sym.flat}`,`A${sym.sharp}`,`G${sym.triplesharp}`,`D${sym.tripleflat}`,`C${sym.flat}`,`B${sym.accnull}`,`A${sym.doublesharp}`,`G${sym.quadsharp}`,`D${sym.doubleflat}`]},
960:{index:960,name:"96-TET", table:[0x0,0x2B,0x55,0x80,0xAB,0xD5,0x100,0x12B,0x155,0x180,0x1AB,0x1D5,0x200,0x22B,0x255,0x280,0x2AB,0x2D5,0x300,0x32B,0x355,0x380,0x3AB,0x3D5,0x400,0x42B,0x455,0x480,0x4AB,0x4D5,0x500,0x52B,0x555,0x580,0x5AB,0x5D5,0x600,0x62B,0x655,0x680,0x6AB,0x6D5,0x700,0x72B,0x755,0x780,0x7AB,0x7D5,0x800,0x82B,0x855,0x880,0x8AB,0x8D5,0x900,0x92B,0x955,0x980,0x9AB,0x9D5,0xA00,0xA2B,0xA55,0xA80,0xAAB,0xAD5,0xB00,0xB2B,0xB55,0xB80,0xBAB,0xBD5,0xC00,0xC2B,0xC55,0xC80,0xCAB,0xCD5,0xD00,0xD2B,0xD55,0xD80,0xDAB,0xDD5,0xE00,0xE2B,0xE55,0xE80,0xEAB,0xED5,0xF00,0xF2B,0xF55,0xF80,0xFAB,0xFD5],
sym:[`-C-`,`${sym.uptick}C-`,`${sym.doubleuptick}C-`,`${sym.dntick}C${sym.cdemisharp}`,`-C${sym.cdemisharp}`,`${sym.uptick}C${sym.cdemisharp}`,`${sym.doubleuptick}C${sym.cdemisharp}`,`${sym.dntick}C${sym.csharp}`,`-C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.doubleuptick}C${sym.csharp}`,`${sym.dntick}D${sym.cdemiflat}`,`-D${sym.cdemiflat}`,`${sym.uptick}D${sym.cdemiflat}`,`${sym.doubleuptick}D${sym.cdemiflat}`,`${sym.dntick}D-`,`-D-`,`${sym.uptick}D-`,`${sym.doubleuptick}D-`,`${sym.dntick}D${sym.cdemisharp}`,`-D${sym.cdemisharp}`,`${sym.uptick}D${sym.cdemisharp}`,`${sym.doubleuptick}D${sym.cdemisharp}`,`${sym.dntick}D${sym.csharp}`,`-D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.doubleuptick}D${sym.csharp}`,`${sym.dntick}E${sym.cdemiflat}`,`-E${sym.cdemiflat}`,`${sym.uptick}E${sym.cdemiflat}`,`${sym.doubleuptick}E${sym.cdemiflat}`,`${sym.dntick}E-`,`-E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${sym.dntick}E${sym.cdemisharp}`,`-E${sym.cdemisharp}`,`${sym.uptick}E${sym.cdemisharp}`,`${sym.doubleuptick}E${sym.cdemisharp}`,`${sym.dntick}F-`,`-F-`,`${sym.uptick}F-`,`${sym.doubleuptick}F-`,`${sym.dntick}F${sym.cdemisharp}`,`-F${sym.cdemisharp}`,`${sym.uptick}F${sym.cdemisharp}`,`${sym.doubleuptick}F${sym.cdemisharp}`,`${sym.dntick}F${sym.csharp}`,`-F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.doubleuptick}F${sym.csharp}`,`${sym.dntick}G${sym.cdemiflat}`,`-G${sym.cdemiflat}`,`${sym.uptick}G${sym.cdemiflat}`,`${sym.doubleuptick}G${sym.cdemiflat}`,`${sym.dntick}G-`,`-G-`,`${sym.uptick}G-`,`${sym.doubleuptick}G-`,`${sym.dntick}G${sym.cdemisharp}`,`-G${sym.cdemisharp}`,`${sym.uptick}G${sym.cdemisharp}`,`${sym.doubleuptick}G${sym.cdemisharp}`,`${sym.dntick}G${sym.csharp}`,`-G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.doubleuptick}G${sym.csharp}`,`${sym.dntick}A${sym.cdemiflat}`,`-A${sym.cdemiflat}`,`${sym.uptick}A${sym.cdemiflat}`,`${sym.doubleuptick}A${sym.cdemiflat}`,`${sym.dntick}A-`,`-A-`,`${sym.uptick}A-`,`${sym.doubleuptick}A-`,`${sym.dntick}A${sym.cdemisharp}`,`-A${sym.cdemisharp}`,`${sym.uptick}A${sym.cdemisharp}`,`${sym.doubleuptick}A${sym.cdemisharp}`,`${sym.dntick}A${sym.csharp}`,`-A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.doubleuptick}A${sym.csharp}`,`${sym.dntick}B${sym.cdemiflat}`,`-B${sym.cdemiflat}`,`${sym.uptick}B${sym.cdemiflat}`,`${sym.doubleuptick}B${sym.cdemiflat}`,`${sym.dntick}B-`,`-B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`,`${sym.dntick}B${sym.cdemisharp}`,`-B${sym.cdemisharp}`,`${sym.uptick}B${sym.cdemisharp}`,`${sym.doubleuptick}B${sym.cdemisharp}`]},
/* 12-TET variations */
120:{index:120,name:"12-TET", table:[0x0,0x155,0x2AB,0x400,0x555,0x6AB,0x800,0x955,0xAAB,0xC00,0xD55,0xEAB],
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
10121:{index:10121,name:"Pythagorean Diminished Fifth", table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x7D8,0x95C,0xA90,0xC14,0xD48,0xECC],
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
10122:{index:10122,name:"Pythagorean Augmented Fourth", table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x828,0x95C,0xA90,0xC14,0xD48,0xECC],
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
10123:{index:10123,name:"Shi'er lu", table:[0x0,0x184,0x2B8,0x43C,0x570,0x6F4,0x828,0x95C,0xAE0,0xC14,0xD98,0xECC],
sym:[` \u00E0\u00E1`,` \u00E2\u00E3`,` \u00E4\u00E5`,` \u00E6\u00E7`,` \u00E8\u00E9`,` \u00EA\u00EB`,` \u00EC\u00ED`,` \u00EE\u00EF`,` \u00F0\u00F1`,` \u00F2\u00F3`,` \u00F4\u00F5`,` \u00F6\u00F7`]},
}
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 colNote = 254
const colInst = 114
const colVol = 155
const colPan = 219
const colEffOp = 220
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')
}
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.hex1 = function() {
return this.toString(16).toUpperCase()
}
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)
}
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.
*/
function buildRowCell(ptnDat, row) {
const off = 8 * row
const note = ptnDat[off] | (ptnDat[off+1] << 8)
const inst = ptnDat[off+2]
const voleff = ptnDat[off+3]
const voleffarg = voleff & 63
const paneff = ptnDat[off+4]
const paneffarg = paneff & 63
const effop = ptnDat[off+5]
const effarg = ptnDat[off+6] | (ptnDat[off+7] << 8)
const sNote = noteToStr(note)
let sInst = inst.hexD2()
if (inst == 0) sInst = sym.middot.repeat(2)
let sVolEff = volEffSym[voleff >>> 6]
let sVolArg = voleffarg.hexD2()
if (voleff === 0) {
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(1)
}
else if (voleffarg >= 32) {
sVolEff = volEffSym[3]
sVolArg = (voleffarg & 15).hex1()
}
else {
sVolEff = volEffSym[4]
sVolArg = (voleffarg & 15).hex1()
}
}
let sPanEff = panEffSym[paneff >>> 6]
let sPanArg = paneffarg.hexD2()
if (paneff === 0) {
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(1)
}
else if (paneffarg >= 32) {
sPanEff = panEffSym[4]
sPanArg = (paneffarg & 15).hex1()
}
else {
sPanEff = panEffSym[3]
sPanArg = (paneffarg & 15).hex1()
}
}
let sEffOp = (effop > 0) ? effop.toString(36).toUpperCase()[0] : sym.middot
let sEffArg = effarg.hex04()
if (effop === 0 && effarg === 0) {
sEffOp = sym.middot
sEffArg = sym.middot.repeat(4)
}
return { sNote, sInst, sVolEff, sVolArg, sPanEff, sPanArg, sEffOp, sEffArg,
_note: note, _effop: effop, _effarg: effarg, _voleff: voleff, _paneff: paneff }
}
const EMPTY_CELL = {
sNote: sym.middot.repeat(4),
sInst: sym.middot.repeat(3),
sVolEff: '',
sVolArg: sym.middot.repeat(2),
sPanEff: '',
sPanArg: sym.middot.repeat(2),
sEffOp: sym.middot,
sEffArg: sym.middot.repeat(4),
_note: 0xFFFF, _effop: 0, _effarg: 0, _voleff: 0, _paneff: 0
}
function drawCellAt(y, x, cell, back) {
con.move(y, x)
con.color_pair(colNote, back); print(cell.sNote)
con.color_pair(colInst, back); print(cell.sInst)
con.color_pair(colVol, back); print(cell.sVolEff)
con.color_pair(colVol, back); print(cell.sVolArg)
con.color_pair(colPan, back); print(cell.sPanEff)
con.color_pair(colPan, back); print(cell.sPanArg)
con.color_pair(colEffOp, back); print(cell.sEffOp)
con.color_pair(colEffArg, back); print(cell.sEffArg)
}
// Styles: -1 = spaced (dddd ii vv pp effff, 19 chars)
// 0 = compact/current (15 chars)
// 1 = non-NOP preference note/fx + vol/pan (7 chars: 5+2, letters start on border)
// 2 = non-NOP preference note/fx only (5 chars, letters start on border)
function drawCellAtStyled(y, x, cell, back, style) {
if (style === 0) { drawCellAt(y, x, cell, back); return }
if (style === -1) {
con.move(y, x)
con.color_pair(colNote, back); print(cell.sNote)
con.color_pair(colBackPtn, back); print(' ')
con.color_pair(colInst, back); print(cell.sInst)
con.color_pair(colBackPtn, back); print(' ')
con.color_pair(colVol, back); print(cell.sVolEff); print(cell.sVolArg)
con.color_pair(colBackPtn, back); print(' ')
con.color_pair(colPan, back); print(cell.sPanEff); print(cell.sPanArg)
con.color_pair(colBackPtn, back); print(' ')
con.color_pair(colEffOp, back); print(cell.sEffOp)
con.color_pair(colEffArg, back); print(cell.sEffArg)
return
}
// Styles 1 and 2: note-or-fx field (5 chars) starts on the border column [+ vol-or-pan (2 chars)]
const noteEmpty = (cell._note === 0xFFFF)
const fxEmpty = (cell._effop === 0 && cell._effarg === 0)
const volEmpty = (cell._voleff === 0)
const panEmpty = (cell._paneff === 0)
con.move(y, x)
if (!noteEmpty) {
con.color_pair(colBackPtn, back); print(' ')
con.color_pair(colNote, back); print(cell.sNote)
} else if (!fxEmpty) {
con.color_pair(colEffOp, back); print(cell.sEffOp)
con.color_pair(colEffArg, back); print(cell.sEffArg)
} else {
con.color_pair(colNote, back); print(sym.middot.repeat(5))
}
if (style === 1) {
//con.color_pair(colBackPtn, back); print(' ')
if (!volEmpty) {
con.color_pair(colVol, back); print(cell.sVolEff); print(cell.sVolArg)
} else if (!panEmpty) {
con.color_pair(colPan, back); print(cell.sPanEff); print(cell.sPanArg)
} else {
con.color_pair(colVol, back); print(sym.middot.repeat(2))
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// .TAUD FILE LOADER
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
const TAUD_MAGIC = [0x1F,0x54,0x53,0x56,0x4D,0x61,0x75,0x64]
const TAUD_HEADER_SIZE = 32
const TAUD_SONG_ENTRY = 16
const PATTERN_SIZE = 512
const ROWS_PER_PAT = 64
const NUM_CUES = 1024
const CUE_SIZE = 32
const NUM_VOICES = 20
const NUM_INSTRUMENTS = 256
const INSTRUMENT_SIZE = 64
const CUE_EMPTY = 0xFFF
function _peekU32LE(ptr, off) {
return ((sys.peek(ptr+off) & 0xFF) ) |
((sys.peek(ptr+off+1) & 0xFF) << 8 ) |
((sys.peek(ptr+off+2) & 0xFF) << 16 ) |
((sys.peek(ptr+off+3) & 0xFF) * 0x1000000)
}
function loadTaud(filePath, songIndex) {
const fh = files.open(filePath)
if (!fh.exists) throw Error(`taut: file not exists: ${filePath}`)
const fileSize = fh.size
const ptr = sys.malloc(fileSize)
fh.pread(ptr, fileSize, 0)
fh.close()
for (let i = 0; i < 8; i++) {
if ((sys.peek(ptr + i) & 0xFF) !== TAUD_MAGIC[i]) {
sys.free(ptr)
throw Error(`taut: bad magic byte at ${i}`)
}
}
const version = sys.peek(ptr + 8) & 0xFF
const numSongs = sys.peek(ptr + 9) & 0xFF
const compSize = _peekU32LE(ptr, 10)
if (songIndex < 0 || songIndex >= numSongs) {
sys.free(ptr)
throw Error(`taut: song index ${songIndex} out of range (numSongs=${numSongs})`)
}
const songTableOff = TAUD_HEADER_SIZE + compSize
const entryOff = songTableOff + songIndex * TAUD_SONG_ENTRY
const songOff = _peekU32LE(ptr, entryOff)
const numVoices = sys.peek(ptr + entryOff + 4) & 0xFF
const numPats = (sys.peek(ptr + entryOff + 5) & 0xFF) |
((sys.peek(ptr + entryOff + 6) & 0xFF) << 8)
const bpmStored = sys.peek(ptr + entryOff + 7) & 0xFF
const tickRate = sys.peek(ptr + entryOff + 8) & 0xFF
const patterns = new Array(numPats)
for (let p = 0; p < numPats; p++) {
const ptn = new Uint8Array(PATTERN_SIZE)
for (let k = 0; k < PATTERN_SIZE; k++) {
ptn[k] = sys.peek(ptr + songOff + p * PATTERN_SIZE + k) & 0xFF
}
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 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
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] = { ptns, instr }
for (let v = 0; v < NUM_VOICES; v++) {
if (ptns[v] !== CUE_EMPTY) { lastActiveCue = c; break }
}
}
const instrBase = cueBase + NUM_CUES * CUE_SIZE
const instruments = new Array(NUM_INSTRUMENTS)
for (let n = 0; n < NUM_INSTRUMENTS; n++) {
const instr = new Uint8Array(INSTRUMENT_SIZE)
for (let k = 0; k < INSTRUMENT_SIZE; k++) {
instr[k] = sys.peek(ptr + instrBase + n * INSTRUMENT_SIZE + k) & 0xFF
}
instruments[n] = instr
}
sys.free(ptr)
return {
filePath, version, numSongs, numVoices, numPats,
bpm: (bpmStored + 24) & 0xFF, tickRate,
patterns, cues, lastActiveCue, instruments
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// GUI DEFINITION
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
const [SCRH, SCRW] = con.getmaxyx()
const PTNVIEW_OFFSET_X = 3
const PTNVIEW_OFFSET_Y = 9
const PTNVIEW_HEIGHT = SCRH - PTNVIEW_OFFSET_Y
const TIMELINE_COLSIZES = [15, 7, 5]
let timelineRowStyle = 0
let COLSIZE_TIMELINE_FULL = TIMELINE_COLSIZES[0]
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 VIEW_TIMELINE = 0
const VIEW_ORDERS = 1
const VIEW_INSTRUMENT = 2
const VIEW_PATTERN_DETAILS = 3
const colPlayback = 86
const colHighlight = 41
const colColumnSep = 6
const colRowNum = 250
const colRowNumEmph1 = 180
const colStatus = 253
const colVoiceHdr = 230
const colSep = 252
let separatorStyle = 0
const PATEDITOR_LIST_X = 1
const PATEDITOR_SEP1_X = 5
const PATEDITOR_GRID_X = 7
const PATEDITOR_CELL_X = 10
const PATEDITOR_SEP2_X = 30
const PATEDITOR_DETAIL_X = 32
function fillLine(y, c, back) {
con.color_pair(c, back)
for (let x = 1; x <= SCRW; x++) {
con.move(y, x); con.addch(32)
}
}
const TAB_GAP = 2
const PANEL_NAMES = ['Timeline', 'Orders', 'Patterns', 'Samples', 'Instruments', 'Project', 'File']
function drawAlwaysOnElems() {
drawStatusBar()
drawTabIndicator()
}
function drawStatusBar() {
fillLine(1, colStatus, 255)
const maxCue = song.lastActiveCue < 0 ? 0 : song.lastActiveCue
const vHi = Math.min(voiceOff + VOCSIZE_TIMELINE_FULL, song.numVoices)
const txt = `${song.filePath} Cue ${cueIdx.hex03()}/${maxCue.hex03()} Row ${cursorRow.dec02()} V${(voiceOff+1).dec02()}-${vHi.dec02()}/${song.numVoices.dec02()} BPM ${audio.getBPM(PLAYHEAD)} Spd ${audio.getTickRate(PLAYHEAD)} `
con.move(1, 1)
con.color_pair(colStatus, 255)
print(txt)
}
function drawTabIndicator() {
const XOFF = 2
const YOFF = PTNVIEW_OFFSET_Y - 4
// TODO make it fancier
con.move(YOFF, XOFF)
for (let i = 0; i < PANEL_NAMES.length; i++) {
if (i > 0) con.curs_right(TAB_GAP);
let panStr = PANEL_NAMES[i]
print((currentPanel === i) ? `[${panStr}]` : ` ${panStr} `)
}
}
/**
* @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_TIMELINE_FULL - 1; c++) {
for (let y = PTNVIEW_OFFSET_Y - 1; y < PTNVIEW_HEIGHT; y++) {
con.move(y, PTNVIEW_OFFSET_X + COLSIZE_TIMELINE_FULL * (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_TIMELINE_FULL) {
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
let oldBgCol = sys.peek(bgColOffset)
if (oldBgCol == 255) {
sys.poke(bgColOffset, colColumnSep)
}
}
}
con.color_pair(colSep, 255)
}
}
function drawVoiceHeaders() {
fillLine(PTNVIEW_OFFSET_Y - 1, colVoiceHdr, 255)
const cue = song.cues[cueIdx]
for (let c = 0; c < VOCSIZE_TIMELINE_FULL; c++) {
const voice = voiceOff + c
const x = PTNVIEW_OFFSET_X + COLSIZE_TIMELINE_FULL * c
con.move(PTNVIEW_OFFSET_Y - 1, x)
if (voice >= song.numVoices) {
con.color_pair(colVoiceHdr, 255)
print(` `.substring(0, COLSIZE_TIMELINE_FULL))
} else {
const isCursor = (voice === cursorVox)
const isMuted = voiceMutes[voice]
con.color_pair(isMuted ? 249 : colVoiceHdr, isCursor ? colHighlight : 255)
const ptnIdx = cue.ptns[voice]
const vlabel = `V${(voice+1).dec02()}`
const plabel = (ptnIdx === CUE_EMPTY) ? '---' : ptnIdx.hex03()
const label =
(timelineRowStyle == 0) ? ` ${vlabel} ptn ${plabel} ` :
(timelineRowStyle == 1) ? ` ${vlabel.substring(1)}:${plabel}` :
` ${vlabel}`
print((label + ' ').substring(0, COLSIZE_TIMELINE_FULL))
}
}
drawSeparators(separatorStyle)
}
// Sub-field layout for style-0 cells (shared by drawPatternRowAt and drawVoiceColumnAt)
const TL_FIELD_OFFSETS = [0, 4, 6, 8, 10, 11]
const TL_FIELD_FGS = [colNote, colInst, colVol, colPan, colEffOp, colEffArg]
function drawPatternRowAt(viewRow, style = timelineRowStyle) {
const actualRow = scrollRow + viewRow
const y = PTNVIEW_OFFSET_Y + viewRow
const highlight = (actualRow === cursorRow)
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.dec02()
con.move(y, 1); con.prnch(rowstr.charCodeAt(0)); con.move(y, 2); con.prnch(rowstr.charCodeAt(1))
if (timelineRowStyle != 1) {
con.move(y, SCRW-2); con.prnch(rowstr.charCodeAt(0)); con.move(y, SCRW-1); con.prnch(rowstr.charCodeAt(1))
}
}
else {
print(' ')
}
// TODO scroll indicator on x=SCRW?
for (let c = 0; c < VOCSIZE_TIMELINE_FULL; c++) {
const voice = voiceOff + c
const x = PTNVIEW_OFFSET_X + COLSIZE_TIMELINE_FULL * c
let cell = EMPTY_CELL
if (actualRow < ROWS_PER_PAT && voice < song.numVoices) {
const ptnIdx = cue.ptns[voice]
if (ptnIdx !== CUE_EMPTY && ptnIdx < song.numPats) {
cell = buildRowCell(song.patterns[ptnIdx], actualRow)
}
}
drawCellAtStyled(y, x, cell, back, style)
if (style === 0 && highlight && playbackMode === PLAYMODE_NONE && voice === cursorVox) {
const fieldStr = [cell.sNote, cell.sInst, cell.sVolEff+cell.sVolArg,
cell.sPanEff+cell.sPanArg, cell.sEffOp, cell.sEffArg][timelineColCursor]
con.move(y, x + TL_FIELD_OFFSETS[timelineColCursor])
con.color_pair(TL_FIELD_FGS[timelineColCursor], colPlayback)
print(fieldStr)
}
}
drawSeparators(separatorStyle)
}
function drawPatternView(style = timelineRowStyle) {
for (let vr = 0; vr < PTNVIEW_HEIGHT; vr++) drawPatternRowAt(vr, style)
}
function drawControlHint() {
let hintElemTimeline = [
[`\u008428u\u008429u`,'Nav'],
[`Pg\u008418u`,'Cue'],
['sep'],
['Y','Song'],
['U','Cue'],
['I','Row'],
['O/Sp','Stop'],
['sep'],
['m','Mute'],
['s','Solo'],
['sep'],
['Tab','Panel'],
//['q','Quit'],
]
let hintElemOrders = [
[`\u008428u\u008429u`,'Nav'],
[`Ent`,'Go to cue'],
['sep'],
['U','Cue'],
['O/Sp','Stop'],
['sep'],
['Tab','Panel'],
['sep'],
['q','Quit'],
]
let hintElemPatterns = [
[`\u008428u\u008429u`,'Nav'],
[`Pg\u008418u`,'Ptn'],
['sep'],
['U','Ptn'],
['I','Row'],
['O/Sp','Stop'],
['sep'],
['Tab','Panel'],
['sep'],
['q','Quit'],
]
let hintElems = [hintElemTimeline, hintElemOrders, hintElemPatterns]
// erase current line
con.move(SCRH, 1)
print(' '.repeat(SCRW-1))
// start writing
con.move(SCRH, 1)
hintElems[currentPanel].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(isVerticalLayout = false, ptn = null, activeRow = -1, cumState = null) {
// Resolve pattern data: null ptn uses timeline context (cursorVox / cursorRow)
let ptnDat
if (ptn === null) {
const cue = song.cues[cueIdx]
const ptnIdx = cue.ptns[cursorVox]
if (ptnIdx === CUE_EMPTY || ptnIdx >= song.numPats) return
const srcPtn = song.patterns[ptnIdx]
const row = (activeRow >= 0) ? activeRow : cursorRow
const off = 8 * row
ptnDat = srcPtn.slice(off, off + 8)
} else {
const row = (activeRow >= 0) ? activeRow : 0
const off = 8 * row
ptnDat = ptn.slice(off, off + 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)
let fx = effop > 0 ? effop.toString(36).toUpperCase() : '0'
if (fx === 'S') fx += (effarg >>> 12).hex1()
const fxName = fxNames[fx] || '? '
if (!isVerticalLayout) {
con.move(6, 1)
print(`Pitch $${note.hex04()}\tInst $${inst.hex02()}\tVolEff ${voleffop}.$${voleffarg.hex02()}\t` +
`PanEff ${paneffop}.$${paneffarg.hex02()}`)
con.move(7, 1)
print(`\u0084248u ${fxName}\t$${effarg.hex04()} `)
} else {
const dx = PATEDITOR_DETAIL_X
const detailW = SCRW - dx + 1
let voleffop1 = (voleffop == 3) ? 30 + (voleffarg >>> 5) : voleffop
let paneffop1 = (paneffop == 3) ? 30 + (paneffarg >>> 5) : paneffop
let voleffarg1 = '$'+((voleffop == 3) ? voleffarg & 15 : voleffarg).hex02()
let paneffarg1 = '$'+((paneffop == 3) ? paneffarg & 15 : paneffarg).hex02()
if (voleff == 0xC0) { voleffop1 = 999; voleffarg1 = '' }
if (paneff == 0xC0) { paneffop1 = 999; paneffarg1 = '' }
const lines = []
lines.push({ label: 'Note ', value: `${noteToStr(note)} ($${note.hex04()})`, fg: colNote })
lines.push({ label: 'Inst ', value: inst === 0 ? '--' : inst.hex02(), fg: colInst })
lines.push({ label: 'VolEff', value: `${volFxNames[voleffop1]} ${voleffarg1}`, fg: colVol })
lines.push({ label: 'PanEff', value: `${panFxNames[paneffop1]} ${paneffarg1}`, fg: colPan })
lines.push({ label: 'FxOp ', value: fx, fg: colEffOp })
lines.push({ label: 'FxArg ', value: `$${effarg.hex04()}`, fg: colEffArg })
lines.push({ label: 'Fx ', value: fxName.trimEnd(), fg: colEffOp })
if (cumState !== null) {
lines.push({ label: '------', value: '', fg: colSep })
lines.push({ label: 'L.Note', value: noteToStr(cumState.lastNote), fg: colNote })
lines.push({ label: 'L.Inst', value: cumState.lastInst === 0 ? '--' : cumState.lastInst.hex02(), fg: colInst })
lines.push({ label: 'Vol ', value: `$${cumState.volAbs.hex02()}`, fg: colVol })
lines.push({ label: 'Pan ', value: `$${cumState.panAbs.hex02()}`, fg: colPan })
lines.push({ label: 'EF ', value: `$${cumState.memEF.hex04()}`, fg: colEffArg })
lines.push({ label: 'G ', value: `$${cumState.memG.hex04()}`, fg: colEffArg })
lines.push({ label: 'HU ', value: `$${cumState.memHU.speed.hex02()}/$${cumState.memHU.depth.hex02()}`, fg: colEffArg })
lines.push({ label: 'R ', value: `$${cumState.memR.speed.hex02()}/$${cumState.memR.depth.hex02()}`, fg: colEffArg })
lines.push({ label: 'Y ', value: `$${cumState.memY.speed.hex02()}/$${cumState.memY.depth.hex02()}`, fg: colEffArg })
lines.push({ label: 'D ', value: `$${cumState.memD.hex04()}`, fg: colEffArg })
lines.push({ label: 'I ', value: `$${cumState.memI.hex04()}`, fg: colEffArg })
lines.push({ label: 'J ', value: `$${cumState.memJ.hex04()}`, fg: colEffArg })
lines.push({ label: 'O ', value: `$${cumState.memO.hex04()}`, fg: colEffArg })
lines.push({ label: 'Q ', value: `$${cumState.memQ.hex04()}`, fg: colEffArg })
lines.push({ label: 'Tslid ', value: `$${cumState.memTSlide.hex02()}`, fg: colEffArg })
}
const showCount = Math.min(lines.length, PTNVIEW_HEIGHT)
for (let i = 0; i < showCount; i++) {
const y = PTNVIEW_OFFSET_Y + i
const line = lines[i]
con.move(y, dx)
con.color_pair(colNote, 255)
print((line.label + ' ').substring(0, 6) + ' ')
con.color_pair(line.fg, 255)
print((line.value + ' '.repeat(detailW)).substring(0, detailW - 8))
}
for (let i = showCount; i < PTNVIEW_HEIGHT; i++) {
con.move(PTNVIEW_OFFSET_Y + i, dx)
con.color_pair(colBackPtn, 255)
print(' '.repeat(detailW))
}
}
}
function drawAll() {
con.clear()
drawAlwaysOnElems()
drawControlHint()
redrawPanel()
con.move(1, 1)
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// FAST SCROLL — shifts the pattern area in text VRAM so we only redraw newly exposed rows
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Graphics adapter text-area layout (see GraphicsAdapter.kt):
// foreground-colour plane: offset 2
// background-colour plane: offset 2 + 2560
// character plane: offset 2 + 2560 + 2560 = 5122
// Each plane is indexed as y * SCRW + x. Peripheral byte k lives at gpuMem - k.
const GPU_MEM = graphics.getGpuMemBase() - (250880+4+12+1008+2046)
const TEXT_FORE_OFF = 2
const TEXT_BACK_OFF = 2 + 2560
const TEXT_CHAR_OFF = 2 + 2560 + 2560
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
let SALVAGE_HORIZ_LEN = (VOCSIZE_TIMELINE_FULL - 1) * COLSIZE_TIMELINE_FULL
/**
* 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,
* voice headers, or anything outside the pattern viewport.
*/
function shiftPatternArea(dy) {
if (dy === 0) return
const absDy = (dy < 0) ? -dy : dy
if (absDy >= PTNVIEW_HEIGHT) return // nothing to salvage, caller should full-redraw
const srcTopY = (dy > 0) ? PTNVIEW_OFFSET_Y : (PTNVIEW_OFFSET_Y + absDy)
const dstTopY = (dy > 0) ? (PTNVIEW_OFFSET_Y + absDy) : PTNVIEW_OFFSET_Y
const stripBytes = (PTNVIEW_HEIGHT - absDy) * SCRW
for (let p = 0; p < 3; p++) {
const chanOff = TEXT_PLANES[p]
const srcAddr = GPU_MEM - chanOff - (srcTopY - 1) * SCRW
const dstAddr = GPU_MEM - chanOff - (dstTopY - 1) * SCRW
sys.memcpy(srcAddr, SCRATCH_PTR, stripBytes)
sys.memcpy(SCRATCH_PTR, dstAddr, stripBytes)
}
}
/**
* 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_TIMELINE_FULL chars earlier/later.
const srcX = PTNVIEW_OFFSET_X + (dVoice > 0 ? COLSIZE_TIMELINE_FULL : 0)
const dstX = PTNVIEW_OFFSET_X + (dVoice > 0 ? 0 : COLSIZE_TIMELINE_FULL)
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_TIMELINE_FULL-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_TIMELINE_FULL * 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)
}
drawCellAtStyled(y, x, cell, back, timelineRowStyle)
if (timelineRowStyle === 0 && highlight && playbackMode === PLAYMODE_NONE && voice === cursorVox) {
const fieldStr = [cell.sNote, cell.sInst, cell.sVolEff+cell.sVolArg,
cell.sPanEff+cell.sPanArg, cell.sEffOp, cell.sEffArg][timelineColCursor]
con.move(y, x + TL_FIELD_OFFSETS[timelineColCursor])
con.color_pair(TL_FIELD_FGS[timelineColCursor], colPlayback)
print(fieldStr)
}
}
}
function setTimelineRowStyle(style) {
timelineRowStyle = style
COLSIZE_TIMELINE_FULL = TIMELINE_COLSIZES[style]
VOCSIZE_TIMELINE_FULL = Math.floor((SCRW - 3) / COLSIZE_TIMELINE_FULL)
SALVAGE_HORIZ_LEN = (VOCSIZE_TIMELINE_FULL - 1) * COLSIZE_TIMELINE_FULL
clampVoice()
drawAll()
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// APPLICATION STUB
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
con.curs_set(0)
let currentPanel = VIEW_TIMELINE
let cueIdx = 0
let cursorRow = 0
let scrollRow = 0
let voiceOff = 0
let cursorVox = 0
let timelineColCursor = 0 // sub-field within cursorVox (0=note,1=inst,2=vol,3=pan,4=fxop,5=fxarg)
let ordersCursor = 0
let ordersScroll = 0
let ordersColCursor = 0 // 0=Cmd, 1..numVoices=voice columns
let ordersVoiceOff = 0 // horizontal scroll for voice columns
let patternIdx = 0
let patternListScroll = 0
let patternGridRow = 0
let patternGridScroll = 0
let patternGridCol = 0
let simState = null
let simStateKey = ''
if (exec_args[1] === undefined) {
println(`Usage: ${exec_args[0]} path_to.taud`)
return 1
}
const fullPathObj = _G.shell.resolvePathInput(exec_args[1])
if (fullPathObj === undefined) {
println(`taut: cannot resolve path: ${exec_args[1]}`)
return 1
}
const song = loadTaud(fullPathObj.full, 0)
const voiceMutes = new Array(NUM_VOICES).fill(false)
function resetAudioDevice() {
audio.resetParams(PLAYHEAD)
audio.purgeQueue(PLAYHEAD)
audio.stop(PLAYHEAD)
}
function redrawFull() { drawAll() }
function redrawPanel() {
panels[currentPanel].drawContents()
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// PANELS
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
function drawTimelineContents(wo) {
drawVoiceHeaders()
drawPatternView()
drawSeparators(separatorStyle)
drawVoiceDetail()
}
function drawOrdersHeader() {
fillLine(PTNVIEW_OFFSET_Y - 1, colVoiceHdr, 255)
con.move(PTNVIEW_OFFSET_Y - 1, 1)
con.color_pair(colVoiceHdr, 255)
print(' ')
con.color_pair(colVoiceHdr, ordersColCursor === 0 ? colHighlight : 255)
print('Cmd ')
for (let c = 0; c < VOCSIZE_ORDERS; c++) {
const v = ordersVoiceOff + c
con.color_pair(colVoiceHdr, ordersColCursor === v + 1 ? colHighlight : 255)
print(v < song.numVoices ? `V${(v+1).dec02()} ` : ' ')
}
}
function drawOrdersContents(wo) {
drawOrdersHeader()
const maxCue = song.lastActiveCue < 0 ? 0 : song.lastActiveCue
for (let vr = 0; vr < PTNVIEW_HEIGHT; vr++) {
const ci = ordersScroll + vr
const y = PTNVIEW_OFFSET_Y + vr
const isSel = (ci === ordersCursor)
const isCur = playbackMode !== PLAYMODE_NONE && ci === cueIdx
const back = isSel ? (playbackMode !== PLAYMODE_NONE ? colPlayback : colHighlight)
: (isCur ? colPlayback : colBackPtn)
con.move(y, 1)
if (ci > maxCue) {
con.color_pair(colBackPtn, colBackPtn)
print(' '.repeat(SCRW - 1))
} else {
const cue = song.cues[ci]
con.color_pair(ci % 4 === 0 ? colRowNumEmph1 : colRowNum, back)
print(ci.hex03())
con.color_pair(colBackPtn, back)
print(' ')
// CMD column — crosshair highlight at (ordersCursor, col 0)
const cmdBack = (isSel && ordersColCursor === 0) ? colPlayback : back
con.color_pair(cue.instr ? colNote : colSep, cmdBack)
print(cue.instr ? cue.instr.hex02() : '--')
con.color_pair(colBackPtn, back)
print(' ')
// Voice columns
for (let c = 0; c < VOCSIZE_ORDERS; c++) {
const v = ordersVoiceOff + c
const ptn = v < song.numVoices ? cue.ptns[v] : CUE_EMPTY
const vBack = (isSel && ordersColCursor === v + 1) ? colPlayback : back
con.color_pair(ptn === CUE_EMPTY ? colSep : colNote, vBack)
print(ptn === CUE_EMPTY ? '---' : ptn.hex03())
con.color_pair(colBackPtn, back)
print(' ')
}
const endX = ORDERS_VOICE_X + VOCSIZE_ORDERS * 4
if (endX <= SCRW) { con.color_pair(colBackPtn, back); print(' '.repeat(SCRW - endX)) }
}
}
}
function timelineInput(wo, event) {
const keysym = event[1]
const keyJustHit = (1 == event[2])
const shiftDown = (event.includes(59) || event.includes(60))
const moveDelta = shiftDown ? 4 : 1
if (keyJustHit && shiftDown && event.includes(keys.W)) { setTimelineRowStyle(0); return }
if (keyJustHit && shiftDown && event.includes(keys.E)) { setTimelineRowStyle(1); return }
if (keyJustHit && shiftDown && event.includes(keys.R)) { setTimelineRowStyle(2); return }
if (playbackMode !== PLAYMODE_NONE) {
if (keyJustHit && shiftDown && event.includes(keys.Y) || keysym === " ") { stopPlayback(); redrawPanel() }
else if (keysym === "<LEFT>" || keysym === "<RIGHT>") {
const dir = (keysym === "<LEFT>") ? -1 : 1
const oldVoiceOff = voiceOff
cursorVox += dir * moveDelta
timelineColCursor = 0
clampVoice()
const dVoice = voiceOff - oldVoiceOff
if (dVoice !== 0) { shiftPatternAreaHorizontal(dVoice); drawVoiceColumnAt(dVoice > 0 ? VOCSIZE_TIMELINE_FULL - 1 : 0) }
drawVoiceHeaders(); drawSeparators(separatorStyle); drawAlwaysOnElems(); drawVoiceDetail()
}
else if (keyJustHit && !shiftDown && event.includes(keys.M)) { toggleMute(cursorVox) }
else if (keyJustHit && !shiftDown && event.includes(keys.S)) { toggleSolo(cursorVox) }
return
}
if (keyJustHit && shiftDown && event.includes(keys.Y)) { startPlaySong(); redrawPanel(); return }
if (keyJustHit && shiftDown && event.includes(keys.U)) { startPlayCue(); redrawPanel(); return }
if ( shiftDown && event.includes(keys.I)) { startPlayRow(); drawPatternRowAt(cursorRow - scrollRow); return }
if (keyJustHit && shiftDown && event.includes(keys.O) || keysym === " ") { stopPlayback(); return }
const oldCursor = cursorRow
const oldScroll = scrollRow
let rowMove = false
let fullRedraw = false
if (keysym === "<LEFT>" || keysym === "<RIGHT>") {
const dir = (keysym === "<LEFT>") ? -1 : 1
const oldVoiceOff = voiceOff
const prevVox = cursorVox
let triedCross = false
if (shiftDown) {
cursorVox += dir * moveDelta
timelineColCursor = dir > 0 ? 0 : 5
} else {
timelineColCursor += dir
if (timelineColCursor < 0) { timelineColCursor = 5; cursorVox--; triedCross = true }
else if (timelineColCursor > 5) { timelineColCursor = 0; cursorVox++; triedCross = true }
}
clampVoice()
if (triedCross && cursorVox === prevVox) timelineColCursor = dir < 0 ? 0 : 5
const dVoice = voiceOff - oldVoiceOff
if (dVoice !== 0) { shiftPatternAreaHorizontal(dVoice); drawVoiceColumnAt(dVoice > 0 ? VOCSIZE_TIMELINE_FULL - 1 : 0) }
drawVoiceHeaders(); drawSeparators(separatorStyle); drawAlwaysOnElems(); drawVoiceDetail()
drawPatternRowAt(cursorRow - scrollRow)
return
}
if (keyJustHit && !shiftDown && event.includes(keys.M)) { toggleMute(cursorVox); return }
if (keyJustHit && !shiftDown && event.includes(keys.S)) { toggleSolo(cursorVox); return }
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 -= moveDelta; fullRedraw = true }
else if (keysym === "<PAGE_DOWN>") { cueIdx += moveDelta; fullRedraw = true }
else return
clampCursor(); clampVoice(); clampCue()
if (fullRedraw) { drawAll(); return }
if (!rowMove || cursorRow === oldCursor) return
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)
}
drawSeparators(separatorStyle); drawAlwaysOnElems(); drawVoiceDetail()
}
function ordersInput(wo, event) {
const keysym = event[1]
const keyJustHit = (1 == event[2])
const shiftDown = (event.includes(59) || event.includes(60))
const moveDelta = shiftDown ? 4 : 1
const maxCue = song.lastActiveCue < 0 ? 0 : song.lastActiveCue
if (playbackMode !== PLAYMODE_NONE) {
if ((keyJustHit && shiftDown && event.includes(keys.Y)) || keysym === " ") {
stopPlayback(); drawAlwaysOnElems()
}
return
}
if (keyJustHit && shiftDown && event.includes(keys.U)) {
cueIdx = ordersCursor; clampCue(); startPlayCue(); drawAlwaysOnElems(); return
}
if ((keyJustHit && shiftDown && event.includes(keys.O)) || keysym === " ") {
stopPlayback(); drawAlwaysOnElems(); return
}
if (keysym === '<UP>') {
ordersCursor = Math.max(0, ordersCursor - moveDelta)
if (ordersCursor < ordersScroll) ordersScroll = ordersCursor
drawOrdersContents(wo)
} else if (keysym === '<DOWN>') {
ordersCursor = Math.min(maxCue, ordersCursor + moveDelta)
if (ordersCursor >= ordersScroll + PTNVIEW_HEIGHT) ordersScroll = Math.max(0, ordersCursor - PTNVIEW_HEIGHT + 1)
drawOrdersContents(wo)
} else if (keysym === '<PAGE_UP>') {
ordersCursor = Math.max(0, ordersCursor - PTNVIEW_HEIGHT)
ordersScroll = Math.max(0, ordersScroll - PTNVIEW_HEIGHT)
drawOrdersContents(wo)
} else if (keysym === '<PAGE_DOWN>') {
ordersCursor = Math.min(maxCue, ordersCursor + PTNVIEW_HEIGHT)
if (ordersCursor >= ordersScroll + PTNVIEW_HEIGHT) ordersScroll = Math.max(0, ordersCursor - PTNVIEW_HEIGHT + 1)
drawOrdersContents(wo)
} else if (keysym === '<LEFT>' || keysym === '<RIGHT>') {
ordersColCursor += (keysym === '<LEFT>') ? -1 : 1
clampOrdersHoriz()
drawOrdersContents(wo)
} else if (keyJustHit && keysym === '\n') {
cueIdx = ordersCursor
clampCue()
currentPanel = VIEW_TIMELINE
drawAll()
return
} else {
return
}
drawAlwaysOnElems()
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// PATTERN EDITOR PANEL
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Returns the visual width of a TSVM string (handles „Nnu escape sequences)
function visWidth(s) {
let w = 0, i = 0
while (i < s.length) {
if (s.charCodeAt(i) === 0x84) {
i++
while (i < s.length && s[i] !== 'u') i++
i++
w++
} else { i++; w++ }
}
return w
}
function clampPatternIdx() {
if (song.numPats === 0) { patternIdx = 0; patternListScroll = 0; return }
if (patternIdx < 0) patternIdx = 0
if (patternIdx >= song.numPats) patternIdx = song.numPats - 1
if (patternIdx < patternListScroll) patternListScroll = patternIdx
if (patternIdx < patternListScroll + (PTNVIEW_HEIGHT >>> 1) && patternListScroll > 0)
patternListScroll = patternIdx - (PTNVIEW_HEIGHT >>> 1)
if (patternIdx >= patternListScroll + ((PTNVIEW_HEIGHT + 1) >>> 1))
patternListScroll = patternIdx - ((PTNVIEW_HEIGHT + 1) >>> 1) + 1
if (patternListScroll < 0) patternListScroll = 0
if (patternListScroll + PTNVIEW_HEIGHT > song.numPats)
patternListScroll = Math.max(0, song.numPats - PTNVIEW_HEIGHT)
}
function scrollPatternGridTo(row) {
if (row < patternGridScroll) patternGridScroll = row
if (row < patternGridScroll + (PTNVIEW_HEIGHT >>> 1) && patternGridScroll > 0)
patternGridScroll = row - (PTNVIEW_HEIGHT >>> 1)
if (row >= patternGridScroll + ((PTNVIEW_HEIGHT + 1) >>> 1))
patternGridScroll = row - ((PTNVIEW_HEIGHT + 1) >>> 1) + 1
if (patternGridScroll < 0) patternGridScroll = 0
if (patternGridScroll + PTNVIEW_HEIGHT > ROWS_PER_PAT)
patternGridScroll = Math.max(0, ROWS_PER_PAT - PTNVIEW_HEIGHT)
}
function clampPatternGrid() {
if (patternGridRow < 0) patternGridRow = 0
if (patternGridRow >= ROWS_PER_PAT) patternGridRow = ROWS_PER_PAT - 1
scrollPatternGridTo(patternGridRow)
if (patternGridCol < 0) patternGridCol = 0
if (patternGridCol > 5) patternGridCol = 5
}
// Returns the row to use for drawVoiceDetail: pbRow during playback, editor cursor otherwise
function getActiveRowForDetail() {
return (playbackMode !== PLAYMODE_NONE) ? pbRow : patternGridRow
}
// Walk pattern rows 0..uptoRow and accumulate effect-memory cohort state
function simulateRowState(ptnDat, uptoRow) {
const OP_D = 13, OP_E = 14, OP_F = 15, OP_G = 16
const OP_H = 17, OP_I = 18, OP_J = 19, OP_O = 24
const OP_Q = 26, OP_R = 27, OP_T = 29, OP_U = 30, OP_Y = 34
let lastNote = 0xFFFF, lastInst = 0
let volAbs = 0x3F, panAbs = 0x20
let memEF = 0, memG = 0
let memHU = { speed: 0, depth: 0 }
let memR = { speed: 0, depth: 0 }
let memY = { speed: 0, depth: 0 }
let memD = 0, memI = 0, memJ = 0, memO = 0, memQ = 0, memTSlide = 0
const limit = Math.min(uptoRow, ROWS_PER_PAT - 1)
for (let row = 0; row <= limit; row++) {
const off = 8 * row
const note = ptnDat[off] | (ptnDat[off+1] << 8)
const inst = ptnDat[off+2]
const voleff = ptnDat[off+3]
const paneff = ptnDat[off+4]
const effop = ptnDat[off+5]
const effarg = ptnDat[off+6] | (ptnDat[off+7] << 8)
if (note !== 0xFFFF && note !== 0xFFFE) lastNote = note
if (inst !== 0) lastInst = inst
const volop = (voleff >>> 6) & 3
if (voleff !== 0 && volop === 0) volAbs = voleff & 63
const panop = (paneff >>> 6) & 3
if (paneff !== 0 && panop === 0) panAbs = paneff & 63
if (effop !== 0 || effarg !== 0) {
if (effop === OP_E || effop === OP_F) { if (effarg !== 0) memEF = effarg }
else if (effop === OP_G) { if (effarg !== 0) memG = effarg }
else if (effop === OP_H || effop === OP_U) {
const spd = (effarg >>> 8) & 0xFF; const dep = effarg & 0xFF
if (spd !== 0) memHU.speed = spd; if (dep !== 0) memHU.depth = dep
}
else if (effop === OP_R) {
const spd = (effarg >>> 8) & 0xFF; const dep = effarg & 0xFF
if (spd !== 0) memR.speed = spd; if (dep !== 0) memR.depth = dep
}
else if (effop === OP_Y) {
const spd = (effarg >>> 8) & 0xFF; const dep = effarg & 0xFF
if (spd !== 0) memY.speed = spd; if (dep !== 0) memY.depth = dep
}
else if (effop === OP_D) { if (effarg !== 0) memD = effarg }
else if (effop === OP_I) { if (effarg !== 0) memI = effarg }
else if (effop === OP_J) { if (effarg !== 0) memJ = effarg }
else if (effop === OP_O) { if (effarg !== 0) memO = effarg }
else if (effop === OP_Q) { if (effarg !== 0) memQ = effarg }
else if (effop === OP_T) { if ((effarg >>> 8) === 0 && effarg !== 0) memTSlide = effarg }
}
}
return { lastNote, lastInst, volAbs, panAbs,
memEF, memG, memHU, memR, memY,
memD, memI, memJ, memO, memQ, memTSlide }
}
function drawPatternListColumn() {
for (let vr = 0; vr < PTNVIEW_HEIGHT; vr++) {
const pi = patternListScroll + vr
const y = PTNVIEW_OFFSET_Y + vr
const isCur = (pi === patternIdx)
con.move(y, PATEDITOR_LIST_X)
if (pi >= song.numPats) {
con.color_pair(255, colBackPtn)
print(' ')
} else {
con.color_pair(isCur ? colNote : colRowNum, isCur ? colHighlight : 255)
print(pi.hex03())
con.color_pair(colSep, 255)
print(' ')
}
}
}
/**
* @param viewRow which row
*/
function drawPatternGridRowAt(viewRow) {
const actualRow = patternGridScroll + viewRow
const y = PTNVIEW_OFFSET_Y + viewRow
if (actualRow >= ROWS_PER_PAT) {
con.move(y, PATEDITOR_GRID_X)
con.color_pair(colBackPtn, 255)
print(' '.repeat(PATEDITOR_SEP2_X - PATEDITOR_GRID_X))
return
}
const ptn = song.patterns[patternIdx]
const isPbRow = (playbackMode !== PLAYMODE_NONE && actualRow === pbRow)
const isCurRow = (actualRow === patternGridRow)
// Row number gets highlight bg to mark cursor row; playhead takes colPlayback priority
const rowNumBack = isPbRow ? colPlayback : (isCurRow ? colHighlight : colBackPtn)
const cellBack = isPbRow ? colPlayback : colBackPtn
con.color_pair(actualRow % 4 === 0 ? colRowNumEmph1 : colRowNum, rowNumBack)
const rowstr = actualRow.dec02()
con.move(y, PATEDITOR_GRID_X); con.prnch(rowstr.charCodeAt(0))
con.move(y, PATEDITOR_GRID_X+1); con.prnch(rowstr.charCodeAt(1))
con.move(y, PATEDITOR_GRID_X+2)
con.color_pair(colBackPtn, cellBack); con.addch(32)
const cell = buildRowCell(ptn, actualRow)
drawCellAtStyled(y, PATEDITOR_CELL_X, cell, cellBack, -1)
// Overlay sub-field cursor highlight on the cursor row (not playhead).
// Style -1 fixed column offsets from PATEDITOR_CELL_X: 0,5,8,11,14,15
if (isCurRow && !isPbRow) {
const fieldOffsets = [0, 5, 8, 11, 14, 15]
const fieldStrs = [
cell.sNote,
cell.sInst,
cell.sVolEff + cell.sVolArg,
cell.sPanEff + cell.sPanArg,
cell.sEffOp,
cell.sEffArg,
]
const fieldFgs = [colNote, colInst, colVol, colPan, colEffOp, colEffArg]
const col = patternGridCol
con.move(y, PATEDITOR_CELL_X + fieldOffsets[col])
con.color_pair(fieldFgs[col], colHighlight)
print(fieldStrs[col])
}
}
function drawPatternGrid() {
for (let vr = 0; vr < PTNVIEW_HEIGHT; vr++) drawPatternGridRowAt(vr)
}
function drawPatternsHeader() {
fillLine(PTNVIEW_OFFSET_Y - 1, colVoiceHdr, 255)
con.move(PTNVIEW_OFFSET_Y - 1, PATEDITOR_LIST_X)
con.color_pair(colVoiceHdr, 255)
print('Ptn ')
con.move(PTNVIEW_OFFSET_Y - 1, PATEDITOR_GRID_X)
if (song.numPats > 0)
print(`Pattern ${patternIdx.hex03()} Row ${patternGridRow.dec02()}`)
}
function drawPatternsContents(wo) {
drawPatternsHeader()
if (song.numPats === 0) {
con.move(PTNVIEW_OFFSET_Y, 1)
con.color_pair(colStatus, 255)
print('(no patterns)')
return
}
drawPatternListColumn()
drawPatternGrid()
// Column separators
con.color_pair(colSep, 255)
for (let y = PTNVIEW_OFFSET_Y - 1; y < PTNVIEW_OFFSET_Y + PTNVIEW_HEIGHT; y++) {
con.move(y, PATEDITOR_SEP1_X); con.prnch(0xB3)
con.move(y, PATEDITOR_SEP2_X); con.prnch(0xB3)
}
const activeRow = getActiveRowForDetail()
const key = `${patternIdx}:${activeRow}:${playbackMode}`
if (key !== simStateKey) {
simState = simulateRowState(song.patterns[patternIdx], activeRow)
simStateKey = key
}
drawVoiceDetail(true, song.patterns[patternIdx], activeRow, simState)
}
function patternsInput(wo, event) {
const keysym = event[1]
const keyJustHit = (1 == event[2])
const shiftDown = (event.includes(59) || event.includes(60))
const moveDelta = shiftDown ? 4 : 1
if (playbackMode !== PLAYMODE_NONE) {
if ((keyJustHit && shiftDown && event.includes(keys.Y)) || keysym === " ") {
stopPlayback(); simStateKey = ''; drawPatternsContents(wo)
}
return
}
if (keyJustHit && shiftDown && event.includes(keys.U)) { startPlayPattern(); drawPatternsContents(wo); return }
if ( shiftDown && event.includes(keys.I)) { startPlayPatternRow(); drawPatternGrid(); return }
if ((keyJustHit && shiftDown && event.includes(keys.O)) || keysym === " ") { stopPlayback(); return }
if (song.numPats === 0) return
if (keysym === '<UP>' || keysym === '<DOWN>') {
patternGridRow += (keysym === '<UP>') ? -moveDelta : moveDelta
clampPatternGrid()
simStateKey = ''
drawPatternGrid()
con.color_pair(colSep, 255)
for (let y = PTNVIEW_OFFSET_Y - 1; y < PTNVIEW_OFFSET_Y + PTNVIEW_HEIGHT; y++) {
con.move(y, PATEDITOR_SEP1_X); con.prnch(0xB3)
con.move(y, PATEDITOR_SEP2_X); con.prnch(0xB3)
}
const activeRow = getActiveRowForDetail()
const key = `${patternIdx}:${activeRow}:${playbackMode}`
if (key !== simStateKey) { simState = simulateRowState(song.patterns[patternIdx], activeRow); simStateKey = key }
drawVoiceDetail(true, song.patterns[patternIdx], activeRow, simState)
drawPatternsHeader()
return
}
if (keysym === '<HOME>') { patternGridRow = 0; clampPatternGrid(); simStateKey = ''; drawPatternsContents(wo); return }
if (keysym === '<END>') { patternGridRow = ROWS_PER_PAT-1; clampPatternGrid(); simStateKey = ''; drawPatternsContents(wo); return }
if (keysym === '<LEFT>' || keysym === '<RIGHT>') {
patternGridCol += (keysym === '<LEFT>') ? -1 : 1
clampPatternGrid()
drawPatternGridRowAt(patternGridRow - patternGridScroll)
con.color_pair(colSep, 255)
con.move(patternGridRow - patternGridScroll + PTNVIEW_OFFSET_Y, PATEDITOR_SEP1_X); con.prnch(0xB3)
con.move(patternGridRow - patternGridScroll + PTNVIEW_OFFSET_Y, PATEDITOR_SEP2_X); con.prnch(0xB3)
return
}
if (keysym === '<PAGE_UP>' || keysym === '<PAGE_DOWN>') {
patternIdx += (keysym === '<PAGE_UP>') ? -moveDelta : moveDelta
clampPatternIdx()
simStateKey = ''
drawPatternsContents(wo)
return
}
}
const panelTimeline = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, timelineInput, drawTimelineContents, undefined, ()=>{})
const panelOrders = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, ordersInput, drawOrdersContents, undefined, ()=>{})
const panelPatterns = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, patternsInput, drawPatternsContents, undefined, ()=>{})
const panels = [panelTimeline, panelOrders, panelPatterns]
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// PLAYBACK STATE
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
const PLAYHEAD = 0
const PLAYMODE_NONE = 0
const PLAYMODE_SONG = 1
const PLAYMODE_CUE = 2
const PLAYMODE_ROW = 3
// Scratch cue slot used for pattern-only preview; beyond any real cue the song uses
const PREVIEW_CUE_IDX = NUM_CUES - 1
let playbackMode = PLAYMODE_NONE
let playStartCue = 0
let playStartRow = 0
let pbCue = 0
let pbRow = 0
let previewActive = false // true while a pattern-only preview is loaded in PREVIEW_CUE_IDX
// Encode a cue object (from song.cues[]) back to its 32-byte wire format
function encodeCue(cue) {
const bin = new Uint8Array(CUE_SIZE)
for (let i = 0; i < 10; i++) {
const p0 = cue.ptns[i*2], p1 = cue.ptns[i*2+1]
bin[i] = ((p0 & 0xF) << 4) | (p1 & 0xF)
bin[10+i] = (((p0 >> 4) & 0xF) << 4) | ((p1 >> 4) & 0xF)
bin[20+i] = (((p0 >> 8) & 0xF) << 4) | ((p1 >> 8) & 0xF)
}
bin[30] = cue.instr || 0
return bin
}
// Build a preview cue with voice 0 = pidx, all other voices = CUE_EMPTY
function buildPreviewCue(pidx) {
const bin = new Uint8Array(CUE_SIZE)
for (let b = 0; b < 30; b++) bin[b] = 0xFF
bin[0] = ((pidx & 0xF) << 4) | 0xF
bin[10] = (((pidx >> 4) & 0xF) << 4) | 0xF
bin[20] = (((pidx >> 8) & 0xF) << 4) | 0xF
return bin
}
// Restore the scratch cue slot and original BPM/tickRate before full-song playback
function restoreFullSongParams() {
if (!previewActive) return
audio.uploadCue(PREVIEW_CUE_IDX, encodeCue(song.cues[PREVIEW_CUE_IDX]))
audio.setBPM(PLAYHEAD, song.bpm)
audio.setTickRate(PLAYHEAD, song.tickRate)
previewActive = false
}
function startPlaySong() {
restoreFullSongParams()
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() {
restoreFullSongParams()
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(fromRow, fromCue) {
restoreFullSongParams()
if (fromRow === undefined) fromRow = cursorRow
if (fromCue === undefined) fromCue = cueIdx
audio.stop(PLAYHEAD)
audio.setCuePosition(PLAYHEAD, fromCue)
audio.setTrackerRow(PLAYHEAD, fromRow)
playStartCue = fromCue
playStartRow = fromRow
pbCue = fromCue
pbRow = fromRow
playbackMode = PLAYMODE_ROW
audio.play(PLAYHEAD)
}
function startPlayPattern() {
if (song.numPats === 0) return
audio.stop(PLAYHEAD)
audio.setBPM(PLAYHEAD, song.bpm)
audio.setTickRate(PLAYHEAD, song.tickRate)
audio.uploadCue(PREVIEW_CUE_IDX, buildPreviewCue(patternIdx))
audio.setCuePosition(PLAYHEAD, PREVIEW_CUE_IDX)
audio.setTrackerRow(PLAYHEAD, 0)
playStartCue = PREVIEW_CUE_IDX
pbCue = PREVIEW_CUE_IDX
pbRow = 0
playbackMode = PLAYMODE_CUE
previewActive = true
audio.play(PLAYHEAD)
}
function startPlayPatternRow() {
if (song.numPats === 0) return
audio.stop(PLAYHEAD)
audio.setBPM(PLAYHEAD, song.bpm)
audio.setTickRate(PLAYHEAD, song.tickRate)
audio.uploadCue(PREVIEW_CUE_IDX, buildPreviewCue(patternIdx))
audio.setCuePosition(PLAYHEAD, PREVIEW_CUE_IDX)
audio.setTrackerRow(PLAYHEAD, patternGridRow)
playStartCue = PREVIEW_CUE_IDX
playStartRow = patternGridRow
pbCue = PREVIEW_CUE_IDX
pbRow = patternGridRow
playbackMode = PLAYMODE_ROW
previewActive = true
audio.play(PLAYHEAD)
}
function stopPlayback() {
audio.stop(PLAYHEAD)
playbackMode = PLAYMODE_NONE
clampPatternGrid()
}
function updatePlayback() {
if (!audio.isPlaying(PLAYHEAD)) {
playbackMode = PLAYMODE_NONE
clampPatternGrid()
if (currentPanel === VIEW_TIMELINE &&
cursorRow >= scrollRow && cursorRow < scrollRow + PTNVIEW_HEIGHT)
drawPatternRowAt(cursorRow - scrollRow)
else if (currentPanel === 2 && song.numPats > 0) { simStateKey = ''; redrawPanel() }
drawAlwaysOnElems()
return
}
const nowCue = audio.getCuePosition(PLAYHEAD)
const nowRow = audio.getTrackerRow(PLAYHEAD)
if (playbackMode === PLAYMODE_CUE && nowCue !== playStartCue) {
stopPlayback()
if (currentPanel === VIEW_TIMELINE) redrawPanel()
else if (currentPanel === 2 && song.numPats > 0) { simStateKey = ''; redrawPanel() }
return
}
if (playbackMode === PLAYMODE_ROW && (nowRow !== playStartRow || nowCue !== playStartCue)) {
stopPlayback()
if (currentPanel === VIEW_TIMELINE &&
cursorRow >= scrollRow && cursorRow < scrollRow + PTNVIEW_HEIGHT)
drawPatternRowAt(cursorRow - scrollRow)
else if (currentPanel === 2 && song.numPats > 0) { simStateKey = ''; redrawPanel() }
drawAlwaysOnElems()
return
}
if (nowCue === pbCue && nowRow === pbRow) return
pbCue = nowCue
pbRow = nowRow
if (!previewActive && nowCue !== cueIdx) {
cueIdx = nowCue
cursorRow = nowRow
clampCursor()
if (currentPanel === VIEW_TIMELINE) redrawPanel()
else if (currentPanel === 2 && song.numPats > 0) { simStateKey = ''; redrawPanel() }
} else if (previewActive || nowCue === cueIdx) {
const oldCursor = cursorRow
const oldScroll = scrollRow
cursorRow = nowRow
clampCursor()
if (currentPanel === VIEW_TIMELINE) {
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)
}
drawSeparators(separatorStyle)
drawVoiceDetail()
} else if (currentPanel === 2 && song.numPats > 0) {
simStateKey = ''
const activeRow = getActiveRowForDetail()
simState = simulateRowState(song.patterns[patternIdx], activeRow)
simStateKey = `${patternIdx}:${activeRow}:${playbackMode}`
scrollPatternGridTo(pbRow)
drawPatternGrid()
drawVoiceDetail(true, song.patterns[patternIdx], activeRow, simState)
}
drawAlwaysOnElems()
}
}
function clampCursor() {
if (cursorRow < 0) cursorRow = 0
if (cursorRow >= ROWS_PER_PAT) cursorRow = ROWS_PER_PAT - 1
if (cursorRow < scrollRow) scrollRow = cursorRow
// 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
if (scrollRow + PTNVIEW_HEIGHT > ROWS_PER_PAT)
scrollRow = Math.max(0, ROWS_PER_PAT - PTNVIEW_HEIGHT)
}
function clampVoice() {
if (cursorVox < 0) cursorVox = 0
if (cursorVox >= song.numVoices) cursorVox = song.numVoices - 1
if (cursorVox < voiceOff) voiceOff = cursorVox
// keep cursor centred until view reaches an edge (mirrors clampCursor logic)
if (cursorVox < voiceOff + (VOCSIZE_TIMELINE_FULL>>>1) && voiceOff > 0) voiceOff = cursorVox - (VOCSIZE_TIMELINE_FULL>>>1)
if (cursorVox >= voiceOff + ((VOCSIZE_TIMELINE_FULL+1)>>>1)) voiceOff = cursorVox - ((VOCSIZE_TIMELINE_FULL+1)>>>1) + 1
const maxOff = Math.max(0, song.numVoices - VOCSIZE_TIMELINE_FULL)
if (voiceOff < 0) voiceOff = 0
if (voiceOff > maxOff) voiceOff = maxOff
}
function clampCue() {
const maxCue = song.lastActiveCue < 0 ? 0 : song.lastActiveCue
if (cueIdx < 0) cueIdx = 0
if (cueIdx > maxCue) cueIdx = maxCue
}
function clampOrdersHoriz() {
if (ordersColCursor < 0) ordersColCursor = 0
if (ordersColCursor > song.numVoices) ordersColCursor = song.numVoices
if (ordersColCursor >= 1) {
const v = ordersColCursor - 1
if (v < ordersVoiceOff) ordersVoiceOff = v
if (v >= ordersVoiceOff + VOCSIZE_ORDERS) ordersVoiceOff = v - VOCSIZE_ORDERS + 1
if (ordersVoiceOff < 0) ordersVoiceOff = 0
}
}
clampCursor(); clampVoice(); clampCue(); clampOrdersHoriz(); clampPatternIdx(); clampPatternGrid()
drawAll()
resetAudioDevice()
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]
const keyJustHit = (1 == event[2])
const shiftDown = (event.includes(59) || event.includes(60))
if (keysym === "<ESC>" || keysym === "q" || keysym === "Q") {
exitFlag = true
return
}
if (keyJustHit && keysym === "<TAB>") {
currentPanel = (currentPanel + (shiftDown ? -1 : 1))
if (currentPanel < 0) currentPanel += panels.length
currentPanel = currentPanel % panels.length
drawAll()
return
}
panels[currentPanel].processInput(event)
})
if (playbackMode !== PLAYMODE_NONE) updatePlayback()
}
audio.stop(PLAYHEAD)
resetAudioDevice()
sys.free(SCRATCH_PTR)
con.clear()
con.move(1, 1)
con.curs_set(1)
return 0