Compare commits

..

9 Commits

Author SHA1 Message Date
minjaesong
1d28c89937 taut: global flag editor 2026-05-17 23:18:06 +09:00
minjaesong
61524b3685 TVDOS: userconfigpath and zfmrc 2026-05-17 00:25:40 +09:00
minjaesong
e6f77c4789 Taud: sentinel values moved to negative octave range 2026-05-16 19:33:17 +09:00
minjaesong
00c0e18c1a TVDOS: minor improvements 2026-05-16 12:26:45 +09:00
minjaesong
135c7b9c4e TVDOS: NodeJS style libfs 2026-05-15 23:36:14 +09:00
minjaesong
295c1f7fe2 TVDOS: path for require() 2026-05-15 23:35:38 +09:00
minjaesong
e74a373605 taut.js: minor UI improvements 2026-05-15 23:35:12 +09:00
minjaesong
b1a0a9f801 TerranBASIC to JS compiler that needs TVDOS 2026-05-15 20:15:07 +09:00
minjaesong
bdc2578072 new coreutils which/where 2026-05-15 20:13:50 +09:00
27 changed files with 2913 additions and 182 deletions

View File

@@ -779,7 +779,7 @@ if V.dittoActive and armRow <= N <= V.dittoEndRow:
srcRow = V.dittoSourceStart + ((N - V.dittoSourceStart) mod V.dittoLength) srcRow = V.dittoSourceStart + ((N - V.dittoSourceStart) mod V.dittoLength)
src = patternRows[V.pattern][srcRow] src = patternRows[V.pattern][srcRow]
cell.note = (raw.note != 0xFFFF) ? raw.note : src.note cell.note = (raw.note != 0x0000) ? raw.note : src.note
cell.instrument = (raw.instrument != 0) ? raw.instrument : src.instrument cell.instrument = (raw.instrument != 0) ? raw.instrument : src.instrument
# SEL_FINE / 0 is the canonical no-op encoding for the vol- and pan-columns; # SEL_FINE / 0 is the canonical no-op encoding for the vol- and pan-columns;

View File

@@ -1 +1 @@
let p=_BIOS.FIRST_BOOTABLE_PORT;com.sendMessage(p[0],"DEVRST\x17");com.sendMessage(p[0],'OPENR"tvdos/hyve.SYS",'+p[1]);let r=com.getStatusCode(p[0]);if(0==r)if(com.sendMessage(p[0],"READ"),r=com.getStatusCode(p[0]),0==r){let g=com.pullMessage(p[0]);eval(g)}else println("I/O Error");else println("TVDOS.SYS not found");println("Shutting down...");println("It is now safe to turn off the power") let p=_BIOS.FIRST_BOOTABLE_PORT;com.sendMessage(p[0],"DEVRST\x17");com.sendMessage(p[0],'OPENR"tvdos/TVDOS.SYS",'+p[1]);let r=com.getStatusCode(p[0]);if(0==r)if(com.sendMessage(p[0],"READ"),r=com.getStatusCode(p[0]),0==r){let g=com.pullMessage(p[0]);eval(g)}else println("I/O Error");else println("TVDOS.SYS not found");println("Shutting down...");println("It is now safe to turn off the power")

View File

@@ -1,7 +1,9 @@
echo "Starting TVDOS..." echo "Starting TVDOS..."
rem put set-xxx commands here: rem put set-xxx commands here:
set PATH=\tvdos\installer;\tvdos\tuidev;$PATH set PATH=\tvdos\installer;\tvdos\tuidev;\tbas;\hopper\bin;$PATH
set INCLPATH=\hopper\include;$INCLPATH
set HELPPATH=\hopper\help;$HELPPATH
set KEYBOARD=us_colemak set KEYBOARD=us_colemak
rem this line specifies which shell to be presented after the boot precess: rem this line specifies which shell to be presented after the boot precess:

View File

@@ -1,4 +1,4 @@
Copyright (c) 2020-2024 CuriousTorvald Copyright (c) 2020-2026 CuriousTorvald
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,3 +1,3 @@
TVDOS (c) 2020-2024 CuriousTorvald TVDOS (c) 2020-2026 CuriousTorvald
TVDOS is provided "as is", without warranty of any kind; in no event shall the authors or copyright holders be liable for any claim, damages or other liabilities. Run 'less COPYING' for more information. TVDOS is provided "as is", without warranty of any kind; in no event shall the authors or copyright holders be liable for any claim, damages or other liabilities. Run 'less COPYING' for more information.

View File

@@ -3,3 +3,4 @@
* Created by CuriousTorvald on 2026-04-16 * Created by CuriousTorvald on 2026-04-16
*/ */
println("Hopper - Package manager for TSVM")

View File

@@ -32,7 +32,7 @@ if (exec_args !== undefined && exec_args[1] !== undefined && exec_args[1].starts
return 0 return 0
} }
const THEVERSION = "1.2.1" const THEVERSION = "1.2.2"
const PROD = true const PROD = true
let INDEX_BASE = 0 let INDEX_BASE = 0
@@ -4197,7 +4197,7 @@ bF.load = function(args) { // LOAD function
if (args[1] === undefined) throw lang.missingOperand if (args[1] === undefined) throw lang.missingOperand
var fileOpened = fs.open(args[1], "R") var fileOpened = fs.open(args[1], "R")
serial.printerr('load '+args[1])
if (replUsrConfirmed || cmdbuf.length == 0) { if (replUsrConfirmed || cmdbuf.length == 0) {
if (!fileOpened) { if (!fileOpened) {
fileOpened = fs.open(args[1]+".BAS", "R") fileOpened = fs.open(args[1]+".BAS", "R")
@@ -4241,7 +4241,7 @@ bF.yes = function() {
} }
} }
bF.catalog = function(args) { // CATALOG function bF.catalog = function(args) { // CATALOG function
if (args[1] === undefined) args[1] = "\\" if (args[1] === undefined) args[1] = BASIC_HOME_PATH
var pathOpened = fs.open(args[1], 'R') var pathOpened = fs.open(args[1], 'R')
if (!pathOpened) { if (!pathOpened) {
throw lang.noSuchFile throw lang.noSuchFile
@@ -4251,6 +4251,57 @@ bF.catalog = function(args) { // CATALOG function
com.sendMessage(port, "LIST") com.sendMessage(port, "LIST")
println(com.pullMessage(port)) println(com.pullMessage(port))
} }
// Load a file by absolute disk path (bypasses BASIC_HOME_PATH).
// Used by COMPILE to fetch /tbas/compile.js.
bF._slurpAbsolute = function(path) {
var port = _BIOS.FIRST_BOOTABLE_PORT
com.sendMessage(port[0], "FLUSH")
com.sendMessage(port[0], "CLOSE")
com.sendMessage(port[0], 'OPENR"' + path + '",' + port[1])
if (com.getStatusCode(port[0]) != 0) return undefined
com.sendMessage(port[0], "READ")
if (com.getStatusCode(port[0]) >= 128) return undefined
var s = com.pullMessage(port[0])
com.sendMessage(port[0], "FLUSH"); com.sendMessage(port[0], "CLOSE")
return s
}
bF.compile = function(args) { // COMPILE "OUT.JS" -- transpile cmdbuf to JS
if (args[1] === undefined) {
println("Usage: COMPILE \"out.js\""); return
}
if (cmdbuf.length === 0) {
println("No program loaded"); return
}
if (bS._compileImpl === undefined) {
// Lazy-load compile.js from /tbas/compile.js
var src = bF._slurpAbsolute("/tbas/compile.js")
if (src === undefined) {
println("Cannot load /tbas/compile.js")
return
}
try { eval(src) } catch (e) {
println("Failed to load compiler: " + e); return
}
if (bS._compileImpl === undefined) {
println("compile.js loaded but did not define bS._compileImpl"); return
}
}
var outpath = args[1]
// Strip surrounding quotes if any
if ((outpath.charAt(0) === '"' || outpath.charAt(0) === "'") &&
outpath.charAt(outpath.length - 1) === outpath.charAt(0)) {
outpath = outpath.substring(1, outpath.length - 1)
}
// Default to .js extension if missing
if (!/\.[A-Za-z0-9]+$/.test(outpath)) outpath += ".js"
try {
var n = bS._compileImpl(outpath)
println("Wrote " + n + " bytes to " + outpath)
} catch (e) {
serial.printerr(e + "\n" + (e.stack || ""))
println("Compile error: " + e)
}
}
Object.freeze(bF) Object.freeze(bF)
if (exec_args !== undefined && exec_args[1] !== undefined) { if (exec_args !== undefined && exec_args[1] !== undefined) {

View File

@@ -0,0 +1,564 @@
// Terran BASIC -> JavaScript compiler
// Loaded into basic.js's context by `bF.compile`. Re-uses bF._interpretLine
// (tokeniser + elaborator + parser + pruner) verbatim and emits a self-
// contained JS program that does its work via `let bS = require("tbas")`.
//
// On load, attaches `bS._compileImpl` to the live bS object.
;(function() {
// ---------- helpers ----------------------------------------------------------
function isValidJsId(s) {
return /^[A-Z_][A-Z0-9_]*$/i.test(s)
}
function varRef(name) {
const u = String(name).toUpperCase()
return isValidJsId(u) ? `bS.__state.vars.${u}` : `bS.__state.vars[${JSON.stringify(u)}]`
}
function jsLit(v) { return JSON.stringify(v) }
// Resolve a literal AST node down to a raw JS value at compile time. Used
// for harvesting DATA constants. Only constant-propagatable types are
// permitted; otherwise compile-time evaluation fails.
function literalValue(node) {
if (!node) return undefined
switch (node.astType) {
case "num": return Number(node.astValue)
case "string": return String(node.astValue)
case "bool": return Boolean(node.astValue)
case "null": return undefined
case "lit": return String(node.astValue) // bare identifier in DATA: keep as string
default:
throw Error("DATA: unsupported literal node type: " + node.astType)
}
}
// Returns the maximum varIndex used at the immediate scope of a lambda body,
// hence its arity.
function lambdaArity(body) {
let maxIdx = -1
function walk(t, level) {
if (!t || !t.astType) return
if (t.astType === "defun_args" && t.astValue[0] === level) {
if (t.astValue[1] > maxIdx) maxIdx = t.astValue[1]
}
// descend into nested usrdefun (its body lives in astValue, not leaves)
if (t.astType === "usrdefun" && t.astValue && t.astValue.astLeaves !== undefined) {
walk(t.astValue, level + 1)
}
// generic descent
if (t.astLeaves) {
for (let i = 0; i < t.astLeaves.length; i++) walk(t.astLeaves[i], level)
}
}
walk(body, 0)
return maxIdx + 1
}
// ---------- expression lowering ---------------------------------------------
// `depth` tracks the number of enclosing lambdas during emission. When we
// emit a lambda we increment it; defun_args [d, i] becomes _aN_i where
// N = depth - 1 - d (the absolute lambda index of the binding scope).
function compileExpr(tree, depth) {
if (tree === undefined || tree === null) return "undefined"
// Empty parens / wrapper node: descend into the single child
if (tree.astType === "null") {
if (tree.astLeaves && tree.astLeaves[0] !== undefined) return compileExpr(tree.astLeaves[0], depth)
return "undefined"
}
if (tree.astValue === undefined && tree.astLeaves && tree.astLeaves.length === 1) {
return compileExpr(tree.astLeaves[0], depth)
}
switch (tree.astType) {
case "num": return String(Number(tree.astValue))
case "string": return jsLit(String(tree.astValue))
case "bool": return tree.astValue ? "true" : "false"
case "lit": return compileLit(tree)
case "defun_args": {
const d = tree.astValue[0], i = tree.astValue[1]
const scope = depth - 1 - d
if (scope < 0) throw Error("defun_args refers to a scope outside the program (depth=" + depth + ", d=" + d + ")")
return "_a" + scope + "_" + i
}
case "usrdefun": return compileLambdaExpr(tree, depth)
case "array": return compileArrayRef(tree, depth)
case "function": return compileFunctionExpr(tree, depth)
case "op": return compileOpExpr(tree, depth)
default:
throw Error("Cannot compile expression node of type: " + tree.astType + " (value=" + tree.astValue + ")")
}
}
function compileLit(tree) {
const name = String(tree.astValue).toUpperCase()
// Built-in zero-arg / pass-as-value functions: when a builtin name is
// referenced as a value (e.g. assigned to a variable for later use as a
// higher-order arg), emit a JS function reference. For a plain variable
// read, emit the vars table lookup.
// Heuristic: if the name matches a builtin we know about, prefer the
// function; otherwise, vars lookup.
if (RUNTIME_BUILTINS.has(name)) {
return "bS." + (isValidJsId(name) ? name : `[${jsLit(name)}]`)
}
return varRef(name)
}
function compileArrayRef(tree, depth) {
// tree.astValue = array variable name; tree.astLeaves = index expressions
if (!tree.astLeaves || tree.astLeaves.length === 0) {
return varRef(tree.astValue)
}
const indices = tree.astLeaves.map(l => compileExpr(l, depth))
return `bS.__arrGet(${varRef(tree.astValue)}, [${indices.join(",")}])`
}
function compileFunctionExpr(tree, depth) {
const name = String(tree.astValue).toUpperCase()
if (name === "PRINT" || name === "EMIT") {
// PRINT/EMIT used as expression — emit as IIFE returning undefined
return "(" + compilePrintLike(tree, name, depth) + ", undefined)"
}
// user function call by name: <varname>(args) — when astType is "function"
// and astValue is a string that matches a variable, the parser may have
// generated this. Treat it as: invoke the var.
if (!RUNTIME_BUILTINS.has(name)) {
// Not a known builtin: treat as a user defined function call
const args = (tree.astLeaves || []).map(l => compileExpr(l, depth))
return `bS.__runFn(${varRef(name)}, [${args.join(",")}])`
}
const args = (tree.astLeaves || []).map(l => compileExpr(l, depth))
return `bS.${isValidJsId(name) ? name : `[${jsLit(name)}]`}(${args.join(",")})`
}
const ARITH_OP = {
"+": (l,r) => `bS.__add(${l},${r})`,
"-": (l,r) => `((${l})-(${r}))`,
"*": (l,r) => `((${l})*(${r}))`,
"/": (l,r) => `bS.__div(${l},${r})`,
"\\": (l,r) => `bS.__intdiv(${l},${r})`,
"MOD":(l,r) => `bS.__mod(${l},${r})`,
"^": (l,r) => `bS.__pow(${l},${r})`,
"==": (l,r) => `((${l})==(${r}))`,
"<>": (l,r) => `((${l})!=(${r}))`,
"><": (l,r) => `((${l})!=(${r}))`,
"<": (l,r) => `((${l})<(${r}))`,
">": (l,r) => `((${l})>(${r}))`,
"<=": (l,r) => `((${l})<=(${r}))`,
"=<": (l,r) => `((${l})<=(${r}))`,
">=": (l,r) => `((${l})>=(${r}))`,
"=>": (l,r) => `((${l})>=(${r}))`,
"AND":(l,r) => `bS.AND(${l},${r})`,
"OR": (l,r) => `bS.OR(${l},${r})`,
"<<": (l,r) => `((${l})<<(${r}))`,
">>": (l,r) => `((${l})>>>(${r}))`,
"BAND":(l,r) => `((${l})&(${r}))`,
"BOR": (l,r) => `((${l})|(${r}))`,
"BXOR":(l,r) => `((${l})^(${r}))`,
}
const UNARY_OP = {
"UNARYMINUS": (a) => `(-(${a}))`,
"UNARYPLUS": (a) => `(+(${a}))`,
"UNARYLOGICNOT":(a) => `(!(${a}))`,
"UNARYBNOT": (a) => `(~(${a}))`,
}
function compileOpExpr(tree, depth) {
const op = String(tree.astValue)
const leaves = tree.astLeaves || []
// Unary
if (UNARY_OP[op] && (leaves.length === 1 || leaves[1] === undefined)) {
return UNARY_OP[op](compileExpr(leaves[0], depth))
}
// Binary arithmetic / comparison / logic
if (ARITH_OP[op] && leaves.length === 2) {
return ARITH_OP[op](compileExpr(leaves[0], depth), compileExpr(leaves[1], depth))
}
// Generator / range
if (op === "TO" && leaves.length === 2) {
return `new bS.__ForGen(${compileExpr(leaves[0], depth)}, ${compileExpr(leaves[1], depth)}, 1)`
}
if (op === "STEP" && leaves.length === 2) {
return `bS.STEP(${compileExpr(leaves[0], depth)}, ${compileExpr(leaves[1], depth)})`
}
// List ops
if ((op === "!" || op === "~" || op === "#") && leaves.length === 2) {
const fn = (op === "!") ? "['!']" : (op === "~") ? "['~']" : "['#']"
return `bS${fn}(${compileExpr(leaves[0], depth)}, ${compileExpr(leaves[1], depth)})`
}
// Assignment as expression — returns the assigned value
if (op === "=" && leaves.length === 2) {
return "(" + compileAssignExpr(tree, depth) + ")"
}
if (op === "IN" && leaves.length === 2) {
// Used inside FOR/FOREACH; compileFor unwraps these. As a value, treat
// as { asgnVarName, asgnValue } so a stray IN still works.
const name = jsLit(String(leaves[0].astValue).toUpperCase())
const rhs = compileExpr(leaves[1], depth)
return `({asgnVarName: ${name}, asgnValue: ${rhs}})`
}
// Functional / monad ops
if ((op === ">>=" || op === ">>~" || op === "." || op === "$" ||
op === "&" || op === "~<" || op === "<*>" || op === "<$>" ||
op === "<~>") && leaves.length === 2) {
return `bS[${jsLit(op)}](${compileExpr(leaves[0], depth)}, ${compileExpr(leaves[1], depth)})`
}
if (op === "@" && leaves.length === 1) {
// Monad return as prefix
return `bS.MRET(${compileExpr(leaves[0], depth)})`
}
if (op === "~>") {
throw Error("Compiler: bare ~> survived prune (should be usrdefun)")
}
throw Error("Cannot compile op '" + op + "' with " + leaves.length + " operand(s)")
}
function compileLambdaExpr(tree, depth) {
// tree.astType === "usrdefun"; tree.astValue holds the body AST; if
// tree.astLeaves is non-empty, this is an immediate application.
const body = tree.astValue
if (!body || !body.astType) throw Error("Malformed usrdefun")
const arity = lambdaArity(body)
const newDepth = depth + 1
const params = []
for (let i = 0; i < arity; i++) params.push("_a" + (newDepth - 1) + "_" + i)
const bodyJs = compileExpr(body, newDepth)
const arrow = `((${params.join(",")}) => (${bodyJs}))`
if (tree.astLeaves && tree.astLeaves.length > 0) {
const args = tree.astLeaves.map(l => compileExpr(l, depth))
return `${arrow}(${args.join(",")})`
}
return arrow
}
function compileAssignExpr(tree, depth) {
// op "=" with leaves[0] as target, leaves[1] as RHS
const lhs = tree.astLeaves[0]
const rhs = compileExpr(tree.astLeaves[1], depth)
if (lhs.astType === "lit") {
const name = String(lhs.astValue).toUpperCase()
return `(${varRef(name)} = ${rhs})`
}
// The parser emits "function" or "array" for `A(i,j) = ...` — both mean
// "store into element of A".
if (lhs.astType === "array" || lhs.astType === "function") {
const indices = lhs.astLeaves.map(l => compileExpr(l, depth))
return `(bS.__arrSet(${varRef(lhs.astValue)}, [${indices.join(",")}], ${rhs}), ${rhs})`
}
throw Error("Cannot assign to LHS of type " + lhs.astType)
}
// ---------- statement lowering ----------------------------------------------
function compilePrintLike(tree, fname, depth) {
const leaves = (tree.astLeaves || []).slice()
const seps = (tree.astSeps || []).slice()
let suppressNewline = false
if (leaves.length > 0 && leaves[leaves.length - 1] !== undefined &&
leaves[leaves.length - 1].astType === "null") {
suppressNewline = true
leaves.pop()
}
const valueExprs = leaves.map(l => compileExpr(l, depth))
if (suppressNewline) valueExprs.push("bS.__PRINT_NONL")
const sepArr = seps.slice(0, leaves.length - 1)
return `bS.${fname}([${valueExprs.join(", ")}], ${jsLit(sepArr)})`
}
function setPc(pc) {
if (pc[0] === Infinity) return "pc=[Infinity,0];"
return "pc=[" + pc[0] + "," + pc[1] + "];"
}
function compileStatement(tree, lnum, stmt, nextPc) {
if (!tree) return setPc(nextPc)
if (tree.astType === "null" && tree.astLeaves && tree.astLeaves[0]) {
return compileStatement(tree.astLeaves[0], lnum, stmt, nextPc)
}
const isFn = (tree.astType === "function" || tree.astType === "op")
const fname = isFn ? String(tree.astValue).toUpperCase() : null
switch (fname) {
case "GOTO": {
const target = compileGotoTarget(tree.astLeaves[0])
return `pc=${target};`
}
case "GOSUB": {
const target = compileGotoTarget(tree.astLeaves[0])
return `gosubStack.push([${nextPc[0]},${nextPc[1]}]); pc=${target};`
}
case "RETURN":
return `pc=gosubStack.pop(); if(!pc) throw new Error("RETURN without GOSUB");`
case "END":
return "pc=[Infinity,0];"
case "IF":
return compileIf(tree, lnum, stmt, nextPc)
case "ON":
return compileOn(tree, lnum, stmt, nextPc)
case "FOR":
case "FOREACH":
return compileFor(tree, lnum, stmt, nextPc, fname === "FOREACH")
case "NEXT":
return compileNext(tree, lnum, stmt, nextPc)
case "READ": {
const target = tree.astLeaves[0]
if (target.astType !== "lit") throw Error("READ: target must be a variable")
return `${varRef(target.astValue)}=bS.__readData(); ${setPc(nextPc)}`
}
case "RESTORE":
return `bS.__state.dataCursor=0; ${setPc(nextPc)}`
case "DATA":
case "LABEL":
return setPc(nextPc) // harvested at compile time
case "DIM":
return compileDim(tree, lnum, stmt, nextPc)
case "PRINT":
case "EMIT":
return `${compilePrintLike(tree, fname, 0)}; ${setPc(nextPc)}`
case "OPTIONBASE":
return `bS.OPTIONBASE(${compileExpr(tree.astLeaves[0], 0)}); ${setPc(nextPc)}`
case "OPTIONDEBUG":
return `bS.OPTIONDEBUG(${compileExpr(tree.astLeaves[0], 0)}); ${setPc(nextPc)}`
case "OPTIONTRACE":
return `bS.OPTIONTRACE(${compileExpr(tree.astLeaves[0], 0)}); ${setPc(nextPc)}`
case "INPUT": {
// INPUT <var> -> read into var
const target = tree.astLeaves[tree.astLeaves.length - 1]
if (target.astType !== "lit") throw Error("INPUT: target must be a variable")
return `${varRef(target.astValue)}=bS.INPUT(); ${setPc(nextPc)}`
}
case "=":
return `${compileAssignExpr(tree, 0)}; ${setPc(nextPc)}`
case "IN":
// bare IN as a statement is unusual but harmless
return `${compileExpr(tree, 0)}; ${setPc(nextPc)}`
case "REM":
return setPc(nextPc)
}
// Default: evaluate as an expression for side effect, then advance
return `${compileExpr(tree, 0)}; ${setPc(nextPc)}`
}
function compileGotoTarget(leaf) {
// Always route through __resolveTarget so non-existent line numbers snap
// upward to the next existing line — matching basic.js's main loop,
// which increments lnum until it finds a populated cmdbuf entry.
if (leaf.astType === "num") return `bS.__resolveTarget(${Number(leaf.astValue)})`
if (leaf.astType === "string") return `bS.__resolveTarget(${jsLit(leaf.astValue)})`
if (leaf.astType === "lit") {
const name = String(leaf.astValue)
return `bS.__resolveTarget(bS.__state.gotoLabels[${jsLit(name)}]!==undefined ? ${jsLit(name)} : ${varRef(name)})`
}
return `bS.__resolveTarget(${compileExpr(leaf, 0)})`
}
function compileIf(tree, lnum, stmt, nextPc) {
const test = compileExpr(tree.astLeaves[0], 0)
const thenStmt = compileStatement(tree.astLeaves[1], lnum, stmt, nextPc)
const elseStmt = (tree.astLeaves[2])
? compileStatement(tree.astLeaves[2], lnum, stmt, nextPc)
: setPc(nextPc)
return `if(bS.__test(${test})){${thenStmt}}else{${elseStmt}}`
}
function compileOn(tree, lnum, stmt, nextPc) {
// children: testExpr, jumpFnLit, target0, target1, ...
const testExpr = compileExpr(tree.astLeaves[0], 0)
const jmpFn = String(tree.astLeaves[1].astValue).toUpperCase()
const targets = tree.astLeaves.slice(2)
const cases = targets.map((t, i) => {
const tgt = compileGotoTarget(t)
if (jmpFn === "GOSUB") {
return `case ${i}: gosubStack.push([${nextPc[0]},${nextPc[1]}]); pc=${tgt}; break;`
}
return `case ${i}: pc=${tgt}; break;`
})
return `{const _o=(${testExpr})-bS.__state.indexBase; switch(_o){${cases.join(" ")} default: ${setPc(nextPc)}}}`
}
function compileFor(tree, lnum, stmt, nextPc, isForEach) {
const child = tree.astLeaves[0]
if (child.astType !== "op" || (child.astValue !== "=" && child.astValue !== "IN")) {
throw Error("FOR/FOREACH: expected = or IN, got " + child.astType + ":" + child.astValue)
}
const varname = String(child.astLeaves[0].astValue).toUpperCase()
let iter = compileExpr(child.astLeaves[1], 0)
if (isForEach) {
// ensure we coerce generators into arrays for FOREACH semantics
iter = `(function(_x){return bS.__isGenerator(_x)?bS.__genToArray(_x):_x})(${iter})`
}
// Pass nextPc — the PC of the loop body's first statement — so NEXT can
// jump straight back without relying on fall-through.
return `bS.__forSetup(${jsLit(varname)}, ${iter}, ${nextPc[0]}, ${nextPc[1]}); ${setPc(nextPc)}`
}
function compileNext(tree, lnum, stmt, nextPc) {
let argExpr = "undefined"
const leaves = tree.astLeaves || []
if (leaves.length === 1 && leaves[0] && leaves[0].astType === "lit") {
argExpr = jsLit(String(leaves[0].astValue).toUpperCase())
}
return `{const _n=bS.__forNext(${argExpr}); if(_n){pc=_n;}else{${setPc(nextPc)}}}`
}
function compileDim(tree, lnum, stmt, nextPc) {
// tree.astLeaves contains array constructor calls: each leaf is either
// an `array` node OR a `function` node (the parser doesn't distinguish
// `A(5)` from a function call until runtime). astValue is the variable
// name and astLeaves are the dimension expressions.
const stmts = []
for (let i = 0; i < tree.astLeaves.length; i++) {
const leaf = tree.astLeaves[i]
if (leaf.astType !== "array" && leaf.astType !== "function") {
throw Error("DIM: expected array decl, got " + leaf.astType)
}
const name = String(leaf.astValue).toUpperCase()
const dims = leaf.astLeaves.map(l => compileExpr(l, 0))
stmts.push(`${varRef(name)}=bS.__dim([${dims.join(",")}]);`)
}
return stmts.join(" ") + " " + setPc(nextPc)
}
// ---------- top-level entry --------------------------------------------------
// Set of builtin names exposed by tbas.mjs. Used to decide whether a `lit`
// in expression position is a variable or a function reference.
const RUNTIME_BUILTINS = new Set([
"PRINT","EMIT","INPUT","CIN",
"ABS","SGN","INT","FLOOR","CEIL","FIX","ROUND","SQR","CBR",
"SIN","COS","TAN","ASN","ACO","ATN","SINH","COSH","TANH",
"EXP","LOG","MIN","MAX","RND",
"SPC","LEFT","RIGHT","MID","CHR",
"LEN","HEAD","TAIL","INIT","LAST","MAP","FOLD","FILTER","ARRAY",
"CLS","CLPX","PLOT","GOTOYX","TEXTFORE","TEXTBACK",
"POKE","PEEK","GETKEYSDOWN","CPUT","CGET","CSTA",
"TYPEOF","OPTIONBASE","OPTIONDEBUG","OPTIONTRACE",
"MRET","MLIST","MJOIN",
"AND","OR","NOT",
"DO","CLEAR","END","TO","STEP",
"FOR","FOREACH","NEXT","IF","ON","GOTO","GOSUB","RETURN",
"DIM","DATA","READ","RESTORE","LABEL","REM",
"TEST",
])
bS._compileImpl = function(outpath) {
if (typeof cmdbuf === "undefined") throw Error("compile.js: cmdbuf not available")
if (typeof bF === "undefined") throw Error("compile.js: bF not available")
if (typeof bF._interpretLine !== "function") throw Error("compile.js: bF._interpretLine not available")
// Reset parser-side state so we don't pollute the live interpreter
if (typeof lambdaBoundVars !== "undefined") lambdaBoundVars.length = 0
const savedPrescan = (typeof prescan !== "undefined") ? prescan : false
if (typeof prescan !== "undefined") prescan = true // suppress execution of LABEL/DATA prescan side-effects
// ---- pass 1: parse every line ----
const programTrees = [] // [lnum] -> array of statements
for (let lnum = 0; lnum < cmdbuf.length; lnum++) {
const linestr = cmdbuf[lnum]
if (linestr === undefined) continue
const trees = bF._interpretLine(lnum, String(linestr).trim())
if (trees !== undefined) programTrees[lnum] = trees
}
if (typeof prescan !== "undefined") prescan = savedPrescan
// ---- pass 2: ordered list of populated lnums and successor table ----
const linenums = []
for (let lnum = 0; lnum < programTrees.length; lnum++) {
if (programTrees[lnum] !== undefined) linenums.push(lnum)
}
function nextPcOf(idx, stmtIdx) {
const lnum = linenums[idx]
const stmts = programTrees[lnum]
if (stmtIdx + 1 < stmts.length) return [lnum, stmtIdx + 1]
if (idx + 1 < linenums.length) return [linenums[idx + 1], 0]
return [Infinity, 0]
}
// ---- pass 3: harvest DATA constants and LABEL definitions ----
const dataConsts = []
const labelMap = {}
for (let i = 0; i < linenums.length; i++) {
const lnum = linenums[i]
const stmts = programTrees[lnum]
for (let s = 0; s < stmts.length; s++) {
const t = stmts[s]
if (!t) continue
if (t.astValue === "DATA") {
for (let k = 0; k < t.astLeaves.length; k++) {
dataConsts.push(literalValue(t.astLeaves[k]))
}
} else if (t.astValue === "LABEL") {
const lblNode = t.astLeaves[0]
if (!lblNode) throw Error("LABEL with no name on line " + lnum)
const lblName = String(lblNode.astValue)
labelMap[lblName] = [lnum, s]
}
}
}
// ---- pass 4: emit case bodies ----
const cases = []
for (let i = 0; i < linenums.length; i++) {
const lnum = linenums[i]
const stmts = programTrees[lnum]
for (let s = 0; s < stmts.length; s++) {
const next = nextPcOf(i, s)
const body = compileStatement(stmts[s], lnum, s, next)
cases.push(` case ${lnum}*32+${s}: { ${body} break; }`)
}
}
// ---- pass 5: assemble final output ----
const firstPc = (linenums.length > 0) ? `[${linenums[0]},0]` : `[Infinity,0]`
const labelMapJs = "{" + Object.keys(labelMap).map(k =>
`${jsLit(k)}: [${labelMap[k][0]}, ${labelMap[k][1]}]`
).join(", ") + "}"
const out =
`// Compiled by Terran BASIC -> JS compiler (assets/disk0/tbas/compile.js)
// Source line count: ${linenums.length}
let bS = require("tbas")
bS.__reset()
bS.__data(${jsLit(dataConsts)})
bS.__labels(${labelMapJs})
bS.__setLines(${jsLit(linenums)})
let pc = ${firstPc}
const gosubStack = []
while (pc[0] !== Infinity) {
switch (pc[0]*32 + pc[1]) {
${cases.join("\n")}
default: pc = [Infinity, 0]; break;
}
}
`
// ---- write to disk via basic.js's fs (writes under BASIC_HOME_PATH) ----
const opened = fs.open(outpath, "W")
if (!opened) throw Error("Cannot open " + outpath + " for writing")
fs.write(out)
return out.length
}
})();

View File

@@ -19,9 +19,9 @@ var Note = (function() {
if (flats[s] !== names[s]) t[flats[s] + oct] = n(oct, s); if (flats[s] !== names[s]) t[flats[s] + oct] = n(oct, s);
} }
} }
t.OFF = 0x0000; // key-off t.NOP = 0x0000; // no-op (empty row)
t.CUT = 0xFFFE; // note cut (immediate) t.OFF = 0x0001; // key-off
t.NOP = 0xFFFF; // no-op (empty row) t.CUT = 0x0002; // note cut (immediate)
return t; return t;
}()); }());

View File

@@ -147,10 +147,12 @@ _TVDOS.variables = {
LANG: "EN", LANG: "EN",
KEYBOARD: "us_qwerty", KEYBOARD: "us_qwerty",
PATH: "\\tvdos\\bin;\\home", PATH: "\\tvdos\\bin;\\home",
INCLPATH: "\\tvdos\\include;\\home",
PATHEXT: ".com;.bat;.app;.js;.alias", PATHEXT: ".com;.bat;.app;.js;.alias",
HELPPATH: "\\tvdos\\help", HELPPATH: "\\tvdos\\help",
OS_NAME: "TSVM Disk Operating System", OS_NAME: "TSVM Disk Operating System",
OS_VERSION: _TVDOS.VERSION OS_VERSION: _TVDOS.VERSION,
USERCONFIGPATH: "\\home\\config",
}; };
Object.freeze(_TVDOS); Object.freeze(_TVDOS);
@@ -1405,9 +1407,6 @@ let requireFromMemory = (ptr) => {
}*/ }*/
var GL = require("A:/tvdos/include/gl.mjs")
// @param cmdsrc JS source code // @param cmdsrc JS source code
// @param args arguments for the program, must be Array, and args[0] is always the name of the program, e.g. // @param args arguments for the program, must be Array, and args[0] is always the name of the program, e.g.
// for command line 'echo foo bar', args[0] must be 'echo' // for command line 'echo foo bar', args[0] must be 'echo'
@@ -1420,7 +1419,7 @@ var execApp = (cmdsrc, args, appname) => {
`var ${appname}=function(exec_args){${injectIntChk(cmdsrc, intchkFunName)}\n};` + `var ${appname}=function(exec_args){${injectIntChk(cmdsrc, intchkFunName)}\n};` +
`${appname}`); // making 'exec_args' a app-level global `${appname}`); // making 'exec_args' a app-level global
execAppPrg(args); return execAppPrg(args);
} }

View File

@@ -577,6 +577,59 @@ shell.coreutils = {
ver: function(args) { ver: function(args) {
println(welcome_text) println(welcome_text)
}, },
which: function(args) {
if (args[1] === undefined) {
printerrln(`Usage: ${args[0].toUpperCase()} program_name`)
return 1
}
let cmd = args[1]
if (shell.coreutils[cmd.toLowerCase()] !== undefined) {
println(`${cmd}: shell built-in command`)
return 0
}
var fileExists = false
var searchFile
var searchPath = ""
if (shell.isValidDriveLetter(cmd[0]) && cmd[1] == ':') {
searchFile = files.open(cmd)
searchPath = trimStartRevSlash(searchFile.path)
fileExists = searchFile.exists
}
else {
var searchDir = (cmd.startsWith("/")) ? [""] : ["/"+shell_pwd.join("/")].concat(_TVDOS.getPath())
var pathExt = []
if (cmd.split(".")[1] === undefined)
_TVDOS.variables.PATHEXT.split(';').forEach(function(it) { pathExt.push(it); pathExt.push(it.toUpperCase()); })
else
pathExt.push("")
searchLoop:
for (var i = 0; i < searchDir.length; i++) {
for (var j = 0; j < pathExt.length; j++) {
let search = searchDir[i]; if (!search.endsWith('\\')) search += '\\'
searchPath = trimStartRevSlash(search + cmd + pathExt[j])
searchFile = files.open(`${CURRENT_DRIVE}:\\${searchPath}`)
if (searchFile.exists) {
fileExists = true
break searchLoop
}
}
}
}
if (!fileExists) {
printerrln(`${cmd}: not found`)
return 1
}
println(searchFile.fullPath)
return 0
},
panic: function(args) { panic: function(args) {
throw Error("Panicking command.js") throw Error("Panicking command.js")
} }
@@ -590,6 +643,7 @@ shell.coreutils.ls = shell.coreutils.dir
shell.coreutils.time = shell.coreutils.date shell.coreutils.time = shell.coreutils.date
shell.coreutils.md = shell.coreutils.mkdir shell.coreutils.md = shell.coreutils.mkdir
shell.coreutils.move = shell.coreutils.mv shell.coreutils.move = shell.coreutils.mv
shell.coreutils.where = shell.coreutils.which
// end of command aliases // end of command aliases
Object.freeze(shell.coreutils) Object.freeze(shell.coreutils)
shell.stdio = { shell.stdio = {
@@ -614,13 +668,25 @@ require = function(path) {
if (path[1] == ":") return shell.require(path) if (path[1] == ":") return shell.require(path)
else { else {
// if the path starts with ".", look for the current directory // if the path starts with ".", look for the current directory
// if the path starts with [A-Za-z0-9], look for the DOSDIR/includes // if the path starts with [A-Za-z0-9], search through INCLPATH
if (path[0] == '.') return shell.require(shell.resolvePathInput(path).full + ".mjs") if (path[0] == '.') return shell.require(shell.resolvePathInput(path).full + ".mjs")
else return shell.require(`A:${_TVDOS.variables.DOSDIR}/include/${path}.mjs`) else {
let inclDirs = (_TVDOS.variables.INCLPATH || "").split(';').filter(function(it) { return it.length > 0 })
for (let i = 0; i < inclDirs.length; i++) {
let dir = inclDirs[i]
if (!dir.endsWith('\\') && !dir.endsWith('/')) dir += '\\'
let candidate = `${CURRENT_DRIVE}:${dir}${path}.mjs`
if (files.open(candidate).exists) return shell.require(candidate)
}
// no match found; defer to shell.require with the first entry so the error mentions a sensible path
let firstDir = inclDirs[0] || `${_TVDOS.variables.DOSDIR}\\include`
if (!firstDir.endsWith('\\') && !firstDir.endsWith('/')) firstDir += '\\'
return shell.require(`${CURRENT_DRIVE}:${firstDir}${path}.mjs`)
}
} }
} }
shell.execute = function(line) { shell.execute = function(line, nameOverride) {
if (0 == line.size) return if (0 == line.size) return
let parsedTokens = shell.parse(line) // echo, "hai", |, less let parsedTokens = shell.parse(line) // echo, "hai", |, less
let statements = [] // [[echo, "hai"], [less]] let statements = [] // [[echo, "hai"], [less]]
@@ -757,19 +823,28 @@ shell.execute = function(line) {
// parse alias // parse alias
// $0: all arguments // $0: all arguments
// $1..9: specific arguments // $1..9: specific arguments
// Tokens that contain whitespace or shell metacharacters must be re-quoted
// before re-execution, otherwise the re-parse splits them on spaces.
var quoteAliasArg = function(s) {
if (s === undefined || s === null) return ""
s = ''+s
if (s.length === 0) return ""
if (/[\s"|><&]/.test(s)) return '"' + s.replaceAll('"', '^"') + '"'
return s
}
var lines = programCode.split('\n').filter(function(it) { return it.length > 0 }) // this return is not shell's return! var lines = programCode.split('\n').filter(function(it) { return it.length > 0 }) // this return is not shell's return!
lines.forEach(function(line) { lines.forEach(function(line) {
var newLine = line var newLine = line
// replace $1..$9 // replace $1..$9
for (let j = 1; j < 9; j++) { for (let j = 1; j <= 9; j++) {
newLine = newLine.replaceAll('$'+j, tokens[j]) newLine = newLine.replaceAll('$'+j, quoteAliasArg(tokens[j]))
} }
// replace $0 // replace $0
newLine = newLine.replaceAll('$0', tokens.slice(1).join(' ')) newLine = newLine.replaceAll('$0', tokens.slice(1).map(quoteAliasArg).join(' '))
shell.execute(newLine) shell.execute(newLine, cmd)
}) })
} }
else if ("APP" == extension) { else if ("APP" == extension) {
@@ -786,6 +861,10 @@ shell.execute = function(line) {
errorlevel = 0 // reset the number errorlevel = 0 // reset the number
if (_G.shellProgramTitles === undefined) _G.shellProgramTitles = [] if (_G.shellProgramTitles === undefined) _G.shellProgramTitles = []
if (nameOverride !== undefined) {
tokens[0] = (''+nameOverride)
cmd = tokens[0]
}
_G.shellProgramTitles.push(cmd.toUpperCase()) _G.shellProgramTitles.push(cmd.toUpperCase())
sendLcdMsg(_G.shellProgramTitles[_G.shellProgramTitles.length - 1]) sendLcdMsg(_G.shellProgramTitles[_G.shellProgramTitles.length - 1])
//serial.println(_G.shellProgramTitles) //serial.println(_G.shellProgramTitles)
@@ -885,6 +964,18 @@ _G.shell = shell
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ensure USERCONFIGPATH directory exists
try {
let userConfigPath = `${CURRENT_DRIVE}:${_TVDOS.variables.USERCONFIGPATH}`
let userConfigDir = files.open(userConfigPath)
if (!userConfigDir.exists) {
debugprintln(`command.js > creating USERCONFIGPATH at ${userConfigPath}`)
userConfigDir.mkDir()
}
} catch (e) {
debugprintln("command.js > USERCONFIGPATH creation failed: " + e.message)
}
if (exec_args[1] !== undefined) { if (exec_args[1] !== undefined) {
// only meaningful switches would be either -c or -k anyway // only meaningful switches would be either -c or -k anyway
var firstSwitch = exec_args[1].toLowerCase() var firstSwitch = exec_args[1].toLowerCase()

View File

@@ -307,7 +307,7 @@ for (let i = 0; i < cueElements.length; i++) {
// Execute the player with modified environment // Execute the player with modified environment
exec_args[1] = targetPath exec_args[1] = targetPath
if (playerFile) { if (playerFile) {
let playerPath = `A:\\tvdos\\bin\\${playerFile}.js` let playerPath = `A:${_TVDOS.variables.DOSDIR}/bin/${playerFile}.js`
if (files.open(playerPath).exists) { if (files.open(playerPath).exists) {
eval(files.readText(playerPath)) eval(files.readText(playerPath))
} else { } else {
@@ -334,7 +334,7 @@ for (let i = 0; i < cueElements.length; i++) {
} }
// Execute the appropriate player // Execute the appropriate player
let playerPath = `A:\\tvdos\\bin\\${playerFile}.js` let playerPath = `A:${_TVDOS.variables.DOSDIR}/bin/${playerFile}.js`
if (!files.open(playerPath).exists) { if (!files.open(playerPath).exists) {
serial.println(`Warning: Player script not found: ${playerPath}`) serial.println(`Warning: Player script not found: ${playerPath}`)
continue continue

View File

@@ -169,61 +169,63 @@ const volFxNames = {
const pitchTablePresets = { const pitchTablePresets = {
// index: pitch table number to be recorded on .taudproj file // index: pitch table number to be recorded on .taudproj file
0:{index:0,name:"Raw format",table:[],interval:0x1000,sym:[]}, // when null is specified, hex numbers will be displayed instead // t: type of the tuning. M - Macrotonal, m - microtonal, d - 12-tone
0:{index:0,name:"Raw format",table:[],interval:0x1000,t:'',sym:[]}, // when null is specified, hex numbers will be displayed instead
/* Xenharmonic, equal temperament */ /* Xenharmonic, equal temperament */
10:{index:10,name:"Octave only", table:[0x0],interval:0x1000, 10:{index:10,name:"Octave only",table:[0x0],interval:0x1000,t:'M',
sym:[`C${sym.accnull}`]}, sym:[`C${sym.accnull}`]},
20:{index:20,name:"2-TET", table:[0x0,0x800],interval:0x1000, 20:{index:20,name:"2-TET",table:[0x0,0x800],interval:0x1000,t:'M',
sym:[`C${sym.accnull}`,`F${sym.sharp}`]}, sym:[`C${sym.accnull}`,`F${sym.sharp}`]},
30:{index:30,name:"3-TET", table:[0x0,0x555,0xAAB],interval:0x1000, 30:{index:30,name:"3-TET",table:[0x0,0x555,0xAAB],interval:0x1000,t:'M',
sym:[`C${sym.accnull}`,`E${sym.accnull}`,`G${sym.sharp}`]}, sym:[`C${sym.accnull}`,`E${sym.accnull}`,`G${sym.sharp}`]},
40:{index:40,name:"4-TET", table:[0x0,0x400,0x800,0xC00],interval:0x1000, 40:{index:40,name:"4-TET",table:[0x0,0x400,0x800,0xC00],interval:0x1000,t:'M',
sym:[`C${sym.accnull}`,`D${sym.sharp}`,`F${sym.sharp}`,`A${sym.accnull}`]}, sym:[`C${sym.accnull}`,`D${sym.sharp}`,`F${sym.sharp}`,`A${sym.accnull}`]},
50:{index:50,name:"5-TET", table:[0x0,0x333,0x666,0x99A,0xCCD],interval:0x1000, 50:{index:50,name:"5-TET",table:[0x0,0x333,0x666,0x99A,0xCCD],interval:0x1000,t:'M',
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`]}, sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`]},
60:{index:60,name:"6-TET", table:[0x0,0x2AB,0x555,0x800,0xAAB,0xD55],interval:0x1000, 60:{index:60,name:"6-TET",table:[0x0,0x2AB,0x555,0x800,0xAAB,0xD55],interval:0x1000,t:'M',
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.sharp}`,`G${sym.sharp}`,`A${sym.sharp}`]}, sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.sharp}`,`G${sym.sharp}`,`A${sym.sharp}`]},
70:{index:70,name:"7-TET", table:[0x0,0x249,0x492,0x6DB,0x925,0xB6E,0xDB7],interval:0x1000, 70:{index:70,name:"7-TET",table:[0x0,0x249,0x492,0x6DB,0x925,0xB6E,0xDB7],interval:0x1000,t:'M',
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`,`B${sym.accnull}`]}, sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`,`B${sym.accnull}`]},
80:{index:80,name:"8-TET", table:[0x0,0x200,0x400,0x600,0x800,0xA00,0xC00,0xE00],interval:0x1000, 80:{index:80,name:"8-TET",table:[0x0,0x200,0x400,0x600,0x800,0xA00,0xC00,0xE00],interval:0x1000,t:'M',
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.sharp}`,`A${sym.accnull}`,`B${sym.accnull}`]}, sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.sharp}`,`A${sym.accnull}`,`B${sym.accnull}`]},
90:{index:90,name:"9-TET", table:[0x0,0x1C7,0x38E,0x555,0x71C,0x8E4,0xAAB,0xC72,0xE39],interval:0x1000, 90:{index:90,name:"9-TET",table:[0x0,0x1C7,0x38E,0x555,0x71C,0x8E4,0xAAB,0xC72,0xE39],interval:0x1000,t:'M',
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`,`B${sym.accnull}`,`B${sym.sharp}`]}, sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`,`B${sym.accnull}`,`B${sym.sharp}`]},
100:{index:100,name:"10-TET", table:[0x0,0x19A,0x333,0x4CD,0x666,0x800,0x99A,0xB33,0xCCD,0xE66],interval:0x1000, 100:{index:100,name:"10-TET",table:[0x0,0x19A,0x333,0x4CD,0x666,0x800,0x99A,0xB33,0xCCD,0xE66],interval:0x1000,t:'M',
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}`]}, 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],interval:0x1000, 150:{index:150,name:"15-TET",table:[0x0,0x111,0x222,0x333,0x444,0x555,0x666,0x777,0x889,0x99A,0xAAB,0xBBC,0xCCD,0xDDE,0xEEF],interval:0x1000,t:'m',
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}`]}, 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}`]},
160:{index:160,name:"16-TET", table:[0x0,0x100,0x200,0x300,0x400,0x500,0x600,0x700,0x800,0x900,0xA00,0xB00,0xC00,0xD00,0xE00,0xF00],interval:0x1000, 160:{index:160,name:"16-TET",table:[0x0,0x100,0x200,0x300,0x400,0x500,0x600,0x700,0x800,0x900,0xA00,0xB00,0xC00,0xD00,0xE00,0xF00],interval:0x1000,t:'m',
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.flat}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`,`B${sym.sharp}`,`C${sym.flat}`]}, sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.flat}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`,`B${sym.sharp}`,`C${sym.flat}`]},
170:{index:170,name:"17-TET", table:[0x0,0xF1,0x1E2,0x2D3,0x3C4,0x4B5,0x5A6,0x697,0x788,0x878,0x969,0xA5A,0xB4B,0xC3C,0xD2D,0xE1E,0xF0F],interval:0x1000, 170:{index:170,name:"17-TET",table:[0x0,0xF1,0x1E2,0x2D3,0x3C4,0x4B5,0x5A6,0x697,0x788,0x878,0x969,0xA5A,0xB4B,0xC3C,0xD2D,0xE1E,0xF0F],interval:0x1000,t:'m',
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}`]}, 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],interval:0x1000, 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],interval:0x1000,t:'m',
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}`]}, 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],interval:0x1000, 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],interval:0x1000,t:'m',
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}`]}, 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],interval:0x1000, 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],interval:0x1000,t:'m',
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}`]}, 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],interval:0x1000, 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],interval:0x1000,t:'m',
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}`]}, 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 (Kite)", 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],interval:0x1000, 410:{index:410,name:"41-TET (Kite)",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],interval:0x1000,t:'m',
sym:[`${BIGDOT}C-`,`${sym.uptick}C-`,`${sym.doubledntick}C${sym.csharp}`,`${sym.dntick}C${sym.csharp}`,`${BIGDOT}C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.dntick}D-`,`${BIGDOT}D-`,`${sym.uptick}D-`,`${sym.doubledntick}D${sym.csharp}`,`${sym.dntick}D${sym.csharp}`,`${BIGDOT}D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.dntick}E-`,`${BIGDOT}E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${BIGDOT}F-`,`${sym.uptick}F-`,`${sym.doubledntick}F${sym.csharp}`,`${sym.dntick}F${sym.csharp}`,`${BIGDOT}F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.dntick}G-`,`${BIGDOT}G-`,`${sym.uptick}G-`,`${sym.doubledntick}G${sym.csharp}`,`${sym.dntick}G${sym.csharp}`,`${BIGDOT}G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.dntick}A-`,`${BIGDOT}A-`,`${sym.uptick}A-`,`${sym.doubledntick}A${sym.csharp}`,`${sym.dntick}A${sym.csharp}`,`${BIGDOT}A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.dntick}B-`,`${BIGDOT}B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`]}, sym:[`${BIGDOT}C-`,`${sym.uptick}C-`,`${sym.doubledntick}C${sym.csharp}`,`${sym.dntick}C${sym.csharp}`,`${BIGDOT}C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.dntick}D-`,`${BIGDOT}D-`,`${sym.uptick}D-`,`${sym.doubledntick}D${sym.csharp}`,`${sym.dntick}D${sym.csharp}`,`${BIGDOT}D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.dntick}E-`,`${BIGDOT}E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${BIGDOT}F-`,`${sym.uptick}F-`,`${sym.doubledntick}F${sym.csharp}`,`${sym.dntick}F${sym.csharp}`,`${BIGDOT}F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.dntick}G-`,`${BIGDOT}G-`,`${sym.uptick}G-`,`${sym.doubledntick}G${sym.csharp}`,`${sym.dntick}G${sym.csharp}`,`${BIGDOT}G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.dntick}A-`,`${BIGDOT}A-`,`${sym.uptick}A-`,`${sym.doubledntick}A${sym.csharp}`,`${sym.dntick}A${sym.csharp}`,`${BIGDOT}A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.dntick}B-`,`${BIGDOT}B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`]},
530:{index:530,name:"53-TET (Kite)", 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],interval:0x1000, 530:{index:530,name:"53-TET (Kite)",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],interval:0x1000,t:'m',
sym:[`${BIGDOT}C-`,`${sym.uptick}C-`,`${sym.doubleuptick}C-`,`${sym.doubledntick}C${sym.csharp}`,`${sym.dntick}C${sym.csharp}`,`${BIGDOT}C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.doubledntick}D-`,`${sym.dntick}D-`,`${BIGDOT}D-`,`${sym.uptick}D-`,`${sym.doubleuptick}D-`,`${sym.doubledntick}D${sym.csharp}`,`${sym.dntick}D${sym.csharp}`,`${BIGDOT}D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.doubledntick}E-`,`${sym.dntick}E-`,`${BIGDOT}E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${sym.dntick}F-`,`${BIGDOT}F-`,`${sym.uptick}F-`,`${sym.doubleuptick}F-`,`${sym.doubledntick}F${sym.csharp}`,`${sym.dntick}F${sym.csharp}`,`${BIGDOT}F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.doubledntick}G-`,`${sym.dntick}G-`,`${BIGDOT}G-`,`${sym.uptick}G-`,`${sym.doubleuptick}G-`,`${sym.doubledntick}G${sym.csharp}`,`${sym.dntick}G${sym.csharp}`,`${BIGDOT}G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.doubledntick}A-`,`${sym.dntick}A-`,`${BIGDOT}A-`,`${sym.uptick}A-`,`${sym.doubleuptick}A-`,`${sym.doubledntick}A${sym.csharp}`,`${sym.dntick}A${sym.csharp}`,`${BIGDOT}A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.doubledntick}B-`,`${sym.dntick}B-`,`${BIGDOT}B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`,`${sym.dntick}C-`]}, sym:[`${BIGDOT}C-`,`${sym.uptick}C-`,`${sym.doubleuptick}C-`,`${sym.doubledntick}C${sym.csharp}`,`${sym.dntick}C${sym.csharp}`,`${BIGDOT}C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.doubledntick}D-`,`${sym.dntick}D-`,`${BIGDOT}D-`,`${sym.uptick}D-`,`${sym.doubleuptick}D-`,`${sym.doubledntick}D${sym.csharp}`,`${sym.dntick}D${sym.csharp}`,`${BIGDOT}D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.doubledntick}E-`,`${sym.dntick}E-`,`${BIGDOT}E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${sym.dntick}F-`,`${BIGDOT}F-`,`${sym.uptick}F-`,`${sym.doubleuptick}F-`,`${sym.doubledntick}F${sym.csharp}`,`${sym.dntick}F${sym.csharp}`,`${BIGDOT}F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.doubledntick}G-`,`${sym.dntick}G-`,`${BIGDOT}G-`,`${sym.uptick}G-`,`${sym.doubleuptick}G-`,`${sym.doubledntick}G${sym.csharp}`,`${sym.dntick}G${sym.csharp}`,`${BIGDOT}G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.doubledntick}A-`,`${sym.dntick}A-`,`${BIGDOT}A-`,`${sym.uptick}A-`,`${sym.doubleuptick}A-`,`${sym.doubledntick}A${sym.csharp}`,`${sym.dntick}A${sym.csharp}`,`${BIGDOT}A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.doubledntick}B-`,`${sym.dntick}B-`,`${BIGDOT}B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`,`${sym.dntick}C-`]},
531:{index:531,name:"53-TET (Pythagorean)", 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],interval:0x1000, 531:{index:531,name:"53-TET (Pythagorean)",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],interval:0x1000,t:'m',
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}`]}, 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 (Kite)", 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],interval:0x1000, 960:{index:960,name:"96-TET (Kite)",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],interval:0x1000,t:'m',
sym:[`${BIGDOT}C-`,`${sym.uptick}C-`,`${sym.doubleuptick}C-`,`${sym.dntick}C${sym.cdemisharp}`,`${BIGDOT}C${sym.cdemisharp}`,`${sym.uptick}C${sym.cdemisharp}`,`${sym.doubleuptick}C${sym.cdemisharp}`,`${sym.dntick}C${sym.csharp}`,`${BIGDOT}C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.doubleuptick}C${sym.csharp}`,`${sym.dntick}D${sym.cdemiflat}`,`${BIGDOT}D${sym.cdemiflat}`,`${sym.uptick}D${sym.cdemiflat}`,`${sym.doubleuptick}D${sym.cdemiflat}`,`${sym.dntick}D-`,`${BIGDOT}D-`,`${sym.uptick}D-`,`${sym.doubleuptick}D-`,`${sym.dntick}D${sym.cdemisharp}`,`${BIGDOT}D${sym.cdemisharp}`,`${sym.uptick}D${sym.cdemisharp}`,`${sym.doubleuptick}D${sym.cdemisharp}`,`${sym.dntick}D${sym.csharp}`,`${BIGDOT}D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.doubleuptick}D${sym.csharp}`,`${sym.dntick}E${sym.cdemiflat}`,`${BIGDOT}E${sym.cdemiflat}`,`${sym.uptick}E${sym.cdemiflat}`,`${sym.doubleuptick}E${sym.cdemiflat}`,`${sym.dntick}E-`,`${BIGDOT}E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${sym.dntick}E${sym.cdemisharp}`,`${BIGDOT}E${sym.cdemisharp}`,`${sym.uptick}E${sym.cdemisharp}`,`${sym.doubleuptick}E${sym.cdemisharp}`,`${sym.dntick}F-`,`${BIGDOT}F-`,`${sym.uptick}F-`,`${sym.doubleuptick}F-`,`${sym.dntick}F${sym.cdemisharp}`,`${BIGDOT}F${sym.cdemisharp}`,`${sym.uptick}F${sym.cdemisharp}`,`${sym.doubleuptick}F${sym.cdemisharp}`,`${sym.dntick}F${sym.csharp}`,`${BIGDOT}F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.doubleuptick}F${sym.csharp}`,`${sym.dntick}G${sym.cdemiflat}`,`${BIGDOT}G${sym.cdemiflat}`,`${sym.uptick}G${sym.cdemiflat}`,`${sym.doubleuptick}G${sym.cdemiflat}`,`${sym.dntick}G-`,`${BIGDOT}G-`,`${sym.uptick}G-`,`${sym.doubleuptick}G-`,`${sym.dntick}G${sym.cdemisharp}`,`${BIGDOT}G${sym.cdemisharp}`,`${sym.uptick}G${sym.cdemisharp}`,`${sym.doubleuptick}G${sym.cdemisharp}`,`${sym.dntick}G${sym.csharp}`,`${BIGDOT}G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.doubleuptick}G${sym.csharp}`,`${sym.dntick}A${sym.cdemiflat}`,`${BIGDOT}A${sym.cdemiflat}`,`${sym.uptick}A${sym.cdemiflat}`,`${sym.doubleuptick}A${sym.cdemiflat}`,`${sym.dntick}A-`,`${BIGDOT}A-`,`${sym.uptick}A-`,`${sym.doubleuptick}A-`,`${sym.dntick}A${sym.cdemisharp}`,`${BIGDOT}A${sym.cdemisharp}`,`${sym.uptick}A${sym.cdemisharp}`,`${sym.doubleuptick}A${sym.cdemisharp}`,`${sym.dntick}A${sym.csharp}`,`${BIGDOT}A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.doubleuptick}A${sym.csharp}`,`${sym.dntick}B${sym.cdemiflat}`,`${BIGDOT}B${sym.cdemiflat}`,`${sym.uptick}B${sym.cdemiflat}`,`${sym.doubleuptick}B${sym.cdemiflat}`,`${sym.dntick}B-`,`${BIGDOT}B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`,`${sym.dntick}B${sym.cdemisharp}`,`${BIGDOT}B${sym.cdemisharp}`,`${sym.uptick}B${sym.cdemisharp}`,`${sym.doubleuptick}B${sym.cdemisharp}`,`${sym.dntick}C-`]}, sym:[`${BIGDOT}C-`,`${sym.uptick}C-`,`${sym.doubleuptick}C-`,`${sym.dntick}C${sym.cdemisharp}`,`${BIGDOT}C${sym.cdemisharp}`,`${sym.uptick}C${sym.cdemisharp}`,`${sym.doubleuptick}C${sym.cdemisharp}`,`${sym.dntick}C${sym.csharp}`,`${BIGDOT}C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.doubleuptick}C${sym.csharp}`,`${sym.dntick}D${sym.cdemiflat}`,`${BIGDOT}D${sym.cdemiflat}`,`${sym.uptick}D${sym.cdemiflat}`,`${sym.doubleuptick}D${sym.cdemiflat}`,`${sym.dntick}D-`,`${BIGDOT}D-`,`${sym.uptick}D-`,`${sym.doubleuptick}D-`,`${sym.dntick}D${sym.cdemisharp}`,`${BIGDOT}D${sym.cdemisharp}`,`${sym.uptick}D${sym.cdemisharp}`,`${sym.doubleuptick}D${sym.cdemisharp}`,`${sym.dntick}D${sym.csharp}`,`${BIGDOT}D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.doubleuptick}D${sym.csharp}`,`${sym.dntick}E${sym.cdemiflat}`,`${BIGDOT}E${sym.cdemiflat}`,`${sym.uptick}E${sym.cdemiflat}`,`${sym.doubleuptick}E${sym.cdemiflat}`,`${sym.dntick}E-`,`${BIGDOT}E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${sym.dntick}E${sym.cdemisharp}`,`${BIGDOT}E${sym.cdemisharp}`,`${sym.uptick}E${sym.cdemisharp}`,`${sym.doubleuptick}E${sym.cdemisharp}`,`${sym.dntick}F-`,`${BIGDOT}F-`,`${sym.uptick}F-`,`${sym.doubleuptick}F-`,`${sym.dntick}F${sym.cdemisharp}`,`${BIGDOT}F${sym.cdemisharp}`,`${sym.uptick}F${sym.cdemisharp}`,`${sym.doubleuptick}F${sym.cdemisharp}`,`${sym.dntick}F${sym.csharp}`,`${BIGDOT}F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.doubleuptick}F${sym.csharp}`,`${sym.dntick}G${sym.cdemiflat}`,`${BIGDOT}G${sym.cdemiflat}`,`${sym.uptick}G${sym.cdemiflat}`,`${sym.doubleuptick}G${sym.cdemiflat}`,`${sym.dntick}G-`,`${BIGDOT}G-`,`${sym.uptick}G-`,`${sym.doubleuptick}G-`,`${sym.dntick}G${sym.cdemisharp}`,`${BIGDOT}G${sym.cdemisharp}`,`${sym.uptick}G${sym.cdemisharp}`,`${sym.doubleuptick}G${sym.cdemisharp}`,`${sym.dntick}G${sym.csharp}`,`${BIGDOT}G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.doubleuptick}G${sym.csharp}`,`${sym.dntick}A${sym.cdemiflat}`,`${BIGDOT}A${sym.cdemiflat}`,`${sym.uptick}A${sym.cdemiflat}`,`${sym.doubleuptick}A${sym.cdemiflat}`,`${sym.dntick}A-`,`${BIGDOT}A-`,`${sym.uptick}A-`,`${sym.doubleuptick}A-`,`${sym.dntick}A${sym.cdemisharp}`,`${BIGDOT}A${sym.cdemisharp}`,`${sym.uptick}A${sym.cdemisharp}`,`${sym.doubleuptick}A${sym.cdemisharp}`,`${sym.dntick}A${sym.csharp}`,`${BIGDOT}A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.doubleuptick}A${sym.csharp}`,`${sym.dntick}B${sym.cdemiflat}`,`${BIGDOT}B${sym.cdemiflat}`,`${sym.uptick}B${sym.cdemiflat}`,`${sym.doubleuptick}B${sym.cdemiflat}`,`${sym.dntick}B-`,`${BIGDOT}B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`,`${sym.dntick}B${sym.cdemisharp}`,`${BIGDOT}B${sym.cdemisharp}`,`${sym.uptick}B${sym.cdemisharp}`,`${sym.doubleuptick}B${sym.cdemisharp}`,`${sym.dntick}C-`]},
/* 12-TET variations */ /* 12-TET variations */
120:{index:120,name:"12-TET", table:[0x0,0x155,0x2AB,0x400,0x555,0x6AB,0x800,0x955,0xAAB,0xC00,0xD55,0xEAB],interval:0x1000, 120:{index:120,name:"12-TET",table:[0x0,0x155,0x2AB,0x400,0x555,0x6AB,0x800,0x955,0xAAB,0xC00,0xD55,0xEAB],interval:0x1000,t:'d',
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}`]}, 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 dim. 5th", table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x7D8,0x95C,0xA90,0xC14,0xD48,0xECC],interval:0x1000, 10121:{index:10121,name:"Pythagorean dim. 5th",table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x7D8,0x95C,0xA90,0xC14,0xD48,0xECC],interval:0x1000,t:'d',
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}`]}, 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 aug. 4th", table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x828,0x95C,0xA90,0xC14,0xD48,0xECC],interval:0x1000, 10122:{index:10122,name:"Pythagorean aug. 4th",table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x828,0x95C,0xA90,0xC14,0xD48,0xECC],interval:0x1000,t:'d',
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}`]}, 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:"\u00FC\u00FD\u00FE (shi'er lu)", table:[0x0,0x184,0x2B8,0x43C,0x570,0x6F4,0x828,0x95C,0xAE0,0xC14,0xD98,0xECC],interval:0x1000, 10123:{index:10123,name:"\u00FC\u00FD\u00FE (shi'er lu)", table:[0x0,0x184,0x2B8,0x43C,0x570,0x6F4,0x828,0x95C,0xAE0,0xC14,0xD98,0xECC],interval:0x1000,t:'d',
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`]}, 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`]},
/* non-octave */ /* non-octave */
35130:{index:35130,name:"Equal-Tempered Bohlen-Pierce", table:[0x0,0x1F3,0x3E7,0x5DA,0x7CE,0x9C1,0xBB4,0xDA8,0xF9B,0x118E,0x1382,0x1575,0x1769],interval:0x195C, 35130:{index:35130,name:"Equal-Tempered Bohlen-Pierce",table:[0x0,0x1F3,0x3E7,0x5DA,0x7CE,0x9C1,0xBB4,0xDA8,0xF9B,0x118E,0x1382,0x1575,0x1769],interval:0x195C,t:'M',
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`H${sym.accnull}`,`H${sym.sharp}`,`J${sym.accnull}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]}, sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`H${sym.accnull}`,`H${sym.sharp}`,`J${sym.accnull}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
@@ -267,7 +269,8 @@ const colEffOp = 220
const colEffArg = 231 const colEffArg = 231
const colBackPtn = 255 const colBackPtn = 255
let PITCH_PRESET_IDX = 120 // TODO read from the Project Data section of the .taud const PITCH_PRESET_IDX_DEFAULT = 120
let PITCH_PRESET_IDX = PITCH_PRESET_IDX_DEFAULT // TODO read from the Project Data section of the .taud
let beatDivPrimary = 4 // TODO read from the Project Data section of the .taud let beatDivPrimary = 4 // TODO read from the Project Data section of the .taud
let beatDivSecondary = 16 let beatDivSecondary = 16
let hasUnsavedChanges = false let hasUnsavedChanges = false
@@ -463,7 +466,7 @@ function retuneAllPatterns(newIdx, method) {
for (let row = 0; row < ROWS_PER_PAT; row++) { for (let row = 0; row < ROWS_PER_PAT; row++) {
const off = 8 * row const off = 8 * row
const note = ptn[off] | (ptn[off+1] << 8) const note = ptn[off] | (ptn[off+1] << 8)
if (note === 0xFFFF || note === 0xFFFE || note === 0x0000) continue if (note === 0x0000 || note === 0x0001 || note === 0x0002 || (note >= 0x0010 && note <= 0x001F)) continue
// Use the full absolute pitch as tonic; the modular ops // Use the full absolute pitch as tonic; the modular ops
// in _cadTension / _harmonicCost normalise it. // in _cadTension / _harmonicCost normalise it.
tonic = note tonic = note
@@ -473,7 +476,7 @@ function retuneAllPatterns(newIdx, method) {
for (let row = 0; row < ROWS_PER_PAT; row++) { for (let row = 0; row < ROWS_PER_PAT; row++) {
const off = 8 * row const off = 8 * row
const note = ptn[off] | (ptn[off+1] << 8) const note = ptn[off] | (ptn[off+1] << 8)
if (note === 0xFFFF || note === 0xFFFE || note === 0x0000) continue if (note === 0x0000 || note === 0x0001 || note === 0x0002 || (note >= 0x0010 && note <= 0x001F)) continue
const origAbs = note const origAbs = note
let newAbs let newAbs
if ((method === 'delta' || method === 'cadence' || method === 'harmonic') && prevOrigAbs >= 0) { if ((method === 'delta' || method === 'cadence' || method === 'harmonic') && prevOrigAbs >= 0) {
@@ -487,7 +490,7 @@ function retuneAllPatterns(newIdx, method) {
for (let r = row + 1; r < ROWS_PER_PAT; r++) { for (let r = row + 1; r < ROWS_PER_PAT; r++) {
const noff = 8 * r const noff = 8 * r
const n = ptn[noff] | (ptn[noff+1] << 8) const n = ptn[noff] | (ptn[noff+1] << 8)
if (n !== 0x0000) break if (n !== 0x0001) break
duration++ duration++
} }
lambda = 1 - Math.exp(-(duration - 1) / 4) lambda = 1 - Math.exp(-(duration - 1) / 4)
@@ -555,9 +558,10 @@ Number.prototype.decD2 = function() {
function noteToStr(note) { function noteToStr(note) {
if (note === 0xFFFF) return sym.middot.repeat(4) if (note === 0x0000) return sym.middot.repeat(4)
if (note === 0xFFFE) return sym.notecut if (note === 0x0001) return sym.keyoff
if (note === 0x0000) return sym.keyoff if (note === 0x0002) return sym.notecut
if (note >= 0x0010 && note <= 0x001F) return ('Int' + (note & 0xF).toString(16).toUpperCase()).padEnd(4)
const preset = pitchTablePresets[PITCH_PRESET_IDX] const preset = pitchTablePresets[PITCH_PRESET_IDX]
if (preset.table.length === 0) return note.hex04() if (preset.table.length === 0) return note.hex04()
const [period, offset] = decomposeNote(note, preset.interval) const [period, offset] = decomposeNote(note, preset.interval)
@@ -653,7 +657,7 @@ const EMPTY_CELL = {
sPanArg: sym.middot.repeat(2), sPanArg: sym.middot.repeat(2),
sEffOp: sym.middot, sEffOp: sym.middot,
sEffArg: sym.middot.repeat(4), sEffArg: sym.middot.repeat(4),
_note: 0xFFFF, _effop: 0, _effarg: 0, _voleff: 0, _paneff: 0 _note: 0x0000, _effop: 0, _effarg: 0, _voleff: 0, _paneff: 0
} }
function drawCellAt(y, x, cell, back) { function drawCellAt(y, x, cell, back) {
@@ -689,7 +693,7 @@ function drawCellAtStyled(y, x, cell, back, style) {
return return
} }
// Styles 1 and 2: note-or-fx field (5 chars) starts on the border column [+ vol-or-pan (2 chars)] // 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 noteEmpty = (cell._note === 0x0000)
const fxEmpty = (cell._effop === 0 && cell._effarg === 0) const fxEmpty = (cell._effop === 0 && cell._effarg === 0)
const volEmpty = (cell._voleff === 0) const volEmpty = (cell._voleff === 0)
const panEmpty = (cell._paneff === 0) const panEmpty = (cell._paneff === 0)
@@ -864,6 +868,7 @@ function loadTaudSongList(filePath) {
name: '', name: '',
composer: '', composer: '',
copyright: '', copyright: '',
pitchPresetIdx: null,
} }
} }
@@ -908,6 +913,8 @@ function loadTaudSongList(filePath) {
const subStart = q + 5 const subStart = q + 5
if (subStart + subLen > qEnd) break if (subStart + subLen > qEnd) break
// payload: notation(u16) + beat_pri(u8) + beat_sec(u8) + name\0 + composer\0 + copyright\0 // payload: notation(u16) + beat_pri(u8) + beat_sec(u8) + name\0 + composer\0 + copyright\0
const notation = (sys.peek(ptr + subStart) & 0xFF) |
((sys.peek(ptr + subStart + 1) & 0xFF) << 8)
let r = subStart + 4 // skip notation(2) + pri(1) + sec(1) let r = subStart + 4 // skip notation(2) + pri(1) + sec(1)
const strs = [] const strs = []
while (strs.length < 3 && r < subStart + subLen) { while (strs.length < 3 && r < subStart + subLen) {
@@ -920,6 +927,7 @@ function loadTaudSongList(filePath) {
strs.push(s) strs.push(s)
} }
if (idx < numSongs) { if (idx < numSongs) {
songs[idx].pitchPresetIdx = notation
if (strs[0] !== undefined) songs[idx].name = strs[0] if (strs[0] !== undefined) songs[idx].name = strs[0]
if (strs[1] !== undefined) songs[idx].composer = strs[1] if (strs[1] !== undefined) songs[idx].composer = strs[1]
if (strs[2] !== undefined) songs[idx].copyright = strs[2] if (strs[2] !== undefined) songs[idx].copyright = strs[2]
@@ -1372,7 +1380,15 @@ function drawControlHint() {
] ]
const hintElemExternal = [['Tab','Panel'],['sep'],['!','Help']] const hintElemExternal = [['Tab','Panel'],['sep'],['!','Help']]
let hintElems = [hintElemTimeline, hintElemOrders, hintElemPatterns, hintElemExternal, hintElemExternal, hintElemExternal, hintElemExternal] const hintElemProject = [
[`\u008428u\u008429u`,'Nav'],
[`ent`,'Edit/Switch'],
['sep'],
['tab','Panel'],
['sep'],
['!','Help'],
]
let hintElems = [hintElemTimeline, hintElemOrders, hintElemPatterns, hintElemExternal, hintElemExternal, hintElemProject, hintElemExternal]
let hintElemPat = [hintElemEditNoteValue, hintElemEditInstValue, hintElemEditVolEff, hintElemEditPanEff, hintElemEditFxSym, hintElemEditFxVal] let hintElemPat = [hintElemEditNoteValue, hintElemEditInstValue, hintElemEditVolEff, hintElemEditPanEff, hintElemEditFxSym, hintElemEditFxVal]
// erase current line // erase current line
@@ -1530,8 +1546,8 @@ function drawVoiceDetail(isVerticalLayout = false, ptn = null, activeRow = -1, c
if (cumState !== null && lowerH > 0) { if (cumState !== null && lowerH > 0) {
const _apo = Math.abs(cumState.pitchOff) const _apo = Math.abs(cumState.pitchOff)
const _psgn = cumState.pitchOff > 0 ? '+' : cumState.pitchOff < 0 ? '-' : ' ' const _psgn = cumState.pitchOff > 0 ? '+' : cumState.pitchOff < 0 ? '-' : ' '
const _absN = (cumState.lastNote !== 0xFFFF && cumState.pitchOff !== 0) const _absN = (cumState.lastNote !== 0x0000 && cumState.pitchOff !== 0)
? noteToStr(Math.max(0, Math.min(0xFFFE, cumState.lastNote + cumState.pitchOff))) + ' ' ? noteToStr(Math.max(0x20, Math.min(0xFFFF, cumState.lastNote + cumState.pitchOff))) + ' '
: '' : ''
const _clipNm = ['clamp','fold','wrap','wrap'][cumState.clipMode] const _clipNm = ['clamp','fold','wrap','wrap'][cumState.clipMode]
const _bcStr = (cumState.bitcrushDepth === 0 && cumState.bitcrushSkip === 0) const _bcStr = (cumState.bitcrushDepth === 0 && cumState.bitcrushSkip === 0)
@@ -1737,21 +1753,27 @@ if (fullPathObj === undefined) {
return 1 return 1
} }
const logofile = files.open("A:/tvdos/bin/tauthdr.r8") const logofile = files.open("A:"+_TVDOS.variables.DOSDIR+"/bin/tauthdr.r8")
const logoBytes = logofile.bread(); logofile.close() const logoBytes = logofile.bread(); logofile.close()
const logoTexture = new gl.Texture(92, 14, logoBytes) const logoTexture = new gl.Texture(92, 14, logoBytes)
const buttonfile = files.open("A:/tvdos/bin/tautbtn.r8") const buttonfile = files.open("A:"+_TVDOS.variables.DOSDIR+"/bin/tautbtn.r8")
const buttonBytes = buttonfile.bread(); buttonfile.close() const buttonBytes = buttonfile.bread(); buttonfile.close()
const buttonTexture = new gl.Texture(2, 28, buttonBytes) const buttonTexture = new gl.Texture(2, 28, buttonBytes)
//const buttonNullfile = files.open("A:/tvdos/bin/tautbtn0.r8") //const buttonNullfile = files.open("A:"+_TVDOS.variables.DOSDIR+"/bin/tautbtn0.r8")
//const buttonNullBytes = buttonNullfile.bread(); buttonNullfile.close() //const buttonNullBytes = buttonNullfile.bread(); buttonNullfile.close()
//const buttonNullTexture = new gl.Texture(35, 28, buttonNullBytes) //const buttonNullTexture = new gl.Texture(35, 28, buttonNullBytes)
font.setLowRom("A:/tvdos/bin/tautfont_low.chr") font.setLowRom("A:"+_TVDOS.variables.DOSDIR+"/bin/tautfont_low.chr")
font.setHighRom("A:/tvdos/bin/tautfont_high.chr") font.setHighRom("A:"+_TVDOS.variables.DOSDIR+"/bin/tautfont_high.chr")
const songsMeta = loadTaudSongList(fullPathObj.full) const songsMeta = loadTaudSongList(fullPathObj.full)
let currentSongIndex = 0 let currentSongIndex = 0
let projectSongCursor = 0 // Unified cursor: 0..PROJ_META_ROWS_COUNT-1 = editable meta rows (Flags / GVol / MVol);
// >= PROJ_META_ROWS_COUNT = song list, songIdx = projectCursor - PROJ_META_ROWS_COUNT
let projectCursor = 0
const PROJ_META_ROWS_COUNT = 3
const PROJ_META_FLAGS = 0
const PROJ_META_GVOL = 1
const PROJ_META_MVOL = 2
let song = loadTaud(fullPathObj.full, currentSongIndex) let song = loadTaud(fullPathObj.full, currentSongIndex)
const voiceMutes = new Array(NUM_VOICES).fill(false) const voiceMutes = new Array(NUM_VOICES).fill(false)
@@ -1792,6 +1814,12 @@ function switchSong(newIndex) {
currentSongIndex = newIndex currentSongIndex = newIndex
song = loadTaud(fullPathObj.full, newIndex) song = loadTaud(fullPathObj.full, newIndex)
const newPitchIdx = songsMeta.songs[newIndex].pitchPresetIdx
PITCH_PRESET_IDX = (newPitchIdx != null && pitchTablePresets[newPitchIdx])
? newPitchIdx
: PITCH_PRESET_IDX_DEFAULT
rebuildPitchLut()
taud.uploadTaudFile(fullPathObj.full, newIndex, PLAYHEAD) taud.uploadTaudFile(fullPathObj.full, newIndex, PLAYHEAD)
patternsOutOfSync = false patternsOutOfSync = false
audio.setMasterVolume(PLAYHEAD, 255) audio.setMasterVolume(PLAYHEAD, 255)
@@ -2172,42 +2200,31 @@ function visWidth(s) {
return w return w
} }
// Centre-anchored scroll: keep `sel` at the middle row of a `vis`-row viewport,
// clamped at the list's top and bottom. Returns the new scroll offset.
function centerScroll(sel, scroll, vis, total) {
if (sel < scroll) scroll = sel
if (sel < scroll + (vis >>> 1) && scroll > 0) scroll = sel - (vis >>> 1)
if (sel >= scroll + ((vis + 1) >>> 1)) scroll = sel - ((vis + 1) >>> 1) + 1
if (scroll < 0) scroll = 0
if (scroll + vis > total) scroll = Math.max(0, total - vis)
return scroll
}
function clampPatternIdx() { function clampPatternIdx() {
if (song.numPats === 0) { patternIdx = 0; patternListScroll = 0; return } if (song.numPats === 0) { patternIdx = 0; patternListScroll = 0; return }
if (patternIdx < 0) patternIdx = 0 if (patternIdx < 0) patternIdx = 0
if (patternIdx >= song.numPats) patternIdx = song.numPats - 1 if (patternIdx >= song.numPats) patternIdx = song.numPats - 1
if (patternIdx < patternListScroll) patternListScroll = patternIdx patternListScroll = centerScroll(patternIdx, patternListScroll, PTNVIEW_HEIGHT, song.numPats)
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) { function scrollPatternGridTo(row) {
if (row < patternGridScroll) patternGridScroll = row patternGridScroll = centerScroll(row, patternGridScroll, PTNVIEW_HEIGHT, ROWS_PER_PAT)
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 scrollOrdersTo(ci) { function scrollOrdersTo(ci) {
const maxCue = song.lastActiveCue < 0 ? 0 : song.lastActiveCue const maxCue = song.lastActiveCue < 0 ? 0 : song.lastActiveCue
const total = maxCue + 1 ordersScroll = centerScroll(ci, ordersScroll, PTNVIEW_HEIGHT, maxCue + 1)
if (ci < ordersScroll) ordersScroll = ci
if (ci < ordersScroll + (PTNVIEW_HEIGHT >>> 1) && ordersScroll > 0)
ordersScroll = ci - (PTNVIEW_HEIGHT >>> 1)
if (ci >= ordersScroll + ((PTNVIEW_HEIGHT + 1) >>> 1))
ordersScroll = ci - ((PTNVIEW_HEIGHT + 1) >>> 1) + 1
if (ordersScroll < 0) ordersScroll = 0
if (ordersScroll + PTNVIEW_HEIGHT > total)
ordersScroll = Math.max(0, total - PTNVIEW_HEIGHT)
} }
function clampPatternGrid() { function clampPatternGrid() {
@@ -2240,7 +2257,7 @@ function simulateRowState(ptnDat, uptoRow) {
0x0000, 0x0023, 0x0046, 0x0074, 0x0098, 0x00C8, 0x00F9, 0x0110 0x0000, 0x0023, 0x0046, 0x0074, 0x0098, 0x00C8, 0x00F9, 0x0110
] ]
let lastNote = 0xFFFF, lastInst = 0 let lastNote = 0x0000, lastInst = 0
let volAbs = 0x3F // 6-bit per-note volume (engine: noteVolume axis; let volAbs = 0x3F // 6-bit per-note volume (engine: noteVolume axis;
// M / N's per-channel axis is not modelled here) // M / N's per-channel axis is not modelled here)
let panAbs = 0x80 // 8-bit channel pan (engine width); centre = $80 let panAbs = 0x80 // 8-bit channel pan (engine width); centre = $80
@@ -2293,8 +2310,8 @@ function simulateRowState(ptnDat, uptoRow) {
// not tracked by this simulator. The simulator approximates the seed // not tracked by this simulator. The simulator approximates the seed
// as 0x3F (legacy fallback) — see the longer note below. // as 0x3F (legacy fallback) — see the longer note below.
let reloadDefaultVol = false let reloadDefaultVol = false
if (note !== 0xFFFF && note !== 0xFFFE) { if (note !== 0x0000 && note !== 0x0002 && !(note >= 0x0010 && note <= 0x001F)) {
if (note === 0x0000) { if (note === 0x0001) {
// key-off; sample stays referenced // key-off; sample stays referenced
} else if (isGRow) { } else if (isGRow) {
portaTarget = note portaTarget = note
@@ -2417,7 +2434,7 @@ function simulateRowState(ptnDat, uptoRow) {
} }
else if (effop === OP_G) { else if (effop === OP_G) {
if (effarg !== 0) memG = effarg if (effarg !== 0) memG = effarg
if (portaTarget !== -1 && memG !== 0 && lastNote !== 0xFFFF) { if (portaTarget !== -1 && memG !== 0 && lastNote !== 0x0000) {
const curPitch = lastNote + pitchOff const curPitch = lastNote + pitchOff
const diff = portaTarget - curPitch const diff = portaTarget - curPitch
if (diff !== 0) { if (diff !== 0) {
@@ -2703,6 +2720,12 @@ function makeExternalPanelDraw(progName) {
} }
} }
// Row offsets (within the meta block at the top of the Project panel) of the editable rows.
const PROJ_META_ROW_FLAGS = 5
const PROJ_META_ROW_GVOL = 6
const PROJ_META_ROW_MVOL = 7
const PROJ_META_VALUE_X = 12
function drawProjectContents(wo) { function drawProjectContents(wo) {
fillLine(PTNVIEW_OFFSET_Y - 1, colVoiceHdr, 255) fillLine(PTNVIEW_OFFSET_Y - 1, colVoiceHdr, 255)
for (let y = PTNVIEW_OFFSET_Y; y < SCRH; y++) fillLine(y, colBackPtn, 255) for (let y = PTNVIEW_OFFSET_Y; y < SCRH; y++) fillLine(y, colBackPtn, 255)
@@ -2720,15 +2743,29 @@ function drawProjectContents(wo) {
Cues: `${song.lastActiveCue}/1024 ($${song.lastActiveCue.hex03()})`, Cues: `${song.lastActiveCue}/1024 ($${song.lastActiveCue.hex03()})`,
Notation: pitchTablePresets[PITCH_PRESET_IDX].name, Notation: pitchTablePresets[PITCH_PRESET_IDX].name,
Flags: `${flagStrSelected.join(', ')} ($${mixerflag.hex02()})`, Flags: `${flagStrSelected.join(', ')} ($${mixerflag.hex02()})`,
GlobalVol: initialGlobalVolume, GlobalVol: `$${initialGlobalVolume.hex02()}`,
MixingVol: initialMixingVolume MixingVol: `$${initialMixingVolume.hex02()}`
}
const editableMap = {
[PROJ_META_ROW_FLAGS]: PROJ_META_FLAGS,
[PROJ_META_ROW_GVOL] : PROJ_META_GVOL,
[PROJ_META_ROW_MVOL] : PROJ_META_MVOL,
} }
Object.entries(projMeta).forEach(([key, value], index) => { Object.entries(projMeta).forEach(([key, value], index) => {
con.move(PTNVIEW_OFFSET_Y + index, 2) con.move(PTNVIEW_OFFSET_Y + index, 2)
con.color_pair(colStatus, 255); print(key) con.color_pair(colStatus, 255); print(key)
con.move(PTNVIEW_OFFSET_Y + index, 12) con.move(PTNVIEW_OFFSET_Y + index, PROJ_META_VALUE_X)
con.color_pair(colVoiceHdr, colBLACK); print(value) const isEditable = (index in editableMap)
const isSelected = isEditable && projectCursor === editableMap[index]
if (isSelected) {
con.color_pair(colWHITE, colHighlight); print(' ' + value + ' ')
} else if (isEditable) {
con.color_pair(colVoiceHdr, colBackPtn); print(' ' + value + ' ')
} else {
con.color_pair(colVoiceHdr, colBLACK); print(value)
}
}) })
drawProjectSongList() drawProjectSongList()
@@ -2745,14 +2782,18 @@ function projectSongListRowsVisible() {
let projectSongScroll = 0 let projectSongScroll = 0
function clampProjectSongCursor() { function clampProjectCursor() {
const n = songsMeta.numSongs const n = songsMeta.numSongs
if (projectSongCursor < 0) projectSongCursor = 0 const maxCur = PROJ_META_ROWS_COUNT + Math.max(0, n - 1)
if (projectSongCursor > n - 1) projectSongCursor = n - 1 if (projectCursor < 0) projectCursor = 0
if (projectCursor > maxCur) projectCursor = maxCur
const rowsVis = projectSongListRowsVisible() const rowsVis = projectSongListRowsVisible()
if (projectSongCursor < projectSongScroll) projectSongScroll = projectSongCursor if (projectCursor >= PROJ_META_ROWS_COUNT) {
else if (projectSongCursor >= projectSongScroll + rowsVis) const songIdx = projectCursor - PROJ_META_ROWS_COUNT
projectSongScroll = projectSongCursor - rowsVis + 1 if (songIdx < projectSongScroll) projectSongScroll = songIdx
else if (songIdx >= projectSongScroll + rowsVis)
projectSongScroll = songIdx - rowsVis + 1
}
if (projectSongScroll < 0) projectSongScroll = 0 if (projectSongScroll < 0) projectSongScroll = 0
} }
@@ -2775,7 +2816,8 @@ function drawProjectSongList() {
} }
const s = songsMeta.songs[idx] const s = songsMeta.songs[idx]
const isActive = (idx === currentSongIndex) const isActive = (idx === currentSongIndex)
const isSel = (idx === projectSongCursor) const isSel = (projectCursor >= PROJ_META_ROWS_COUNT) &&
(idx === projectCursor - PROJ_META_ROWS_COUNT)
const back = isSel ? colHighlight : colBackPtn const back = isSel ? colHighlight : colBackPtn
const marker = isActive ? sym.playhead : ' ' const marker = isActive ? sym.playhead : ' '
@@ -2818,28 +2860,50 @@ function projectInput(wo, event) {
return return
} }
if (!keyJustHit) return // if (!keyJustHit) return
if (keysym === '<UP>') { if (keysym === '<UP>') {
projectSongCursor -= moveDelta; clampProjectSongCursor(); redrawPanel(); return projectCursor -= moveDelta; clampProjectCursor(); redrawPanel(); return
} }
if (keysym === '<DOWN>') { if (keysym === '<DOWN>') {
projectSongCursor += moveDelta; clampProjectSongCursor(); redrawPanel(); return projectCursor += moveDelta; clampProjectCursor(); redrawPanel(); return
} }
if (keysym === '<PAGE_UP>') { if (keysym === '<PAGE_UP>') {
projectSongCursor -= projectSongListRowsVisible(); clampProjectSongCursor(); redrawPanel(); return projectCursor -= projectSongListRowsVisible(); clampProjectCursor(); redrawPanel(); return
} }
if (keysym === '<PAGE_DOWN>') { if (keysym === '<PAGE_DOWN>') {
projectSongCursor += projectSongListRowsVisible(); clampProjectSongCursor(); redrawPanel(); return projectCursor += projectSongListRowsVisible(); clampProjectCursor(); redrawPanel(); return
} }
if (keysym === '<HOME>') { if (keysym === '<HOME>') {
projectSongCursor = 0; clampProjectSongCursor(); redrawPanel(); return projectCursor = 0; clampProjectCursor(); redrawPanel(); return
} }
if (keysym === '<END>') { if (keysym === '<END>') {
projectSongCursor = songsMeta.numSongs - 1; clampProjectSongCursor(); redrawPanel(); return projectCursor = PROJ_META_ROWS_COUNT + Math.max(0, songsMeta.numSongs - 1)
clampProjectCursor(); redrawPanel(); return
} }
if (keysym === '\n') { if (keysym === '\n') {
if (projectSongCursor !== currentSongIndex) switchSong(projectSongCursor) if (projectCursor === PROJ_META_FLAGS) {
openFlagsPopup()
} else if (projectCursor === PROJ_META_GVOL) {
const v = openInlineHexEdit(PTNVIEW_OFFSET_Y + PROJ_META_ROW_GVOL, PROJ_META_VALUE_X, 2, initialGlobalVolume)
if (v !== null) {
initialGlobalVolume = v & 0xFF
audio.setSongGlobalVolume(PLAYHEAD, initialGlobalVolume)
hasUnsavedChanges = true
}
redrawPanel()
} else if (projectCursor === PROJ_META_MVOL) {
const v = openInlineHexEdit(PTNVIEW_OFFSET_Y + PROJ_META_ROW_MVOL, PROJ_META_VALUE_X, 2, initialMixingVolume)
if (v !== null) {
initialMixingVolume = v & 0xFF
audio.setSongMixingVolume(PLAYHEAD, initialMixingVolume)
hasUnsavedChanges = true
}
redrawPanel()
} else {
const songIdx = projectCursor - PROJ_META_ROWS_COUNT
if (songIdx !== currentSongIndex) switchSong(songIdx)
}
return return
} }
if (keysym === ' ') { if (keysym === ' ') {
@@ -3100,13 +3164,7 @@ function updatePlayback() {
function clampCursor() { function clampCursor() {
if (cursorRow < 0) cursorRow = 0 if (cursorRow < 0) cursorRow = 0
if (cursorRow >= ROWS_PER_PAT) cursorRow = ROWS_PER_PAT - 1 if (cursorRow >= ROWS_PER_PAT) cursorRow = ROWS_PER_PAT - 1
if (cursorRow < scrollRow) scrollRow = cursorRow scrollRow = centerScroll(cursorRow, scrollRow, PTNVIEW_HEIGHT, ROWS_PER_PAT)
// 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() { function clampVoice() {
@@ -3387,6 +3445,10 @@ function openRetunePopup() {
const entries = Object.values(pitchTablePresets).sort((a, b) => a.index - b.index) const entries = Object.values(pitchTablePresets).sort((a, b) => a.index - b.index)
const n = entries.length const n = entries.length
// Foreground colour by tuning type (preset.t):
// 'd' = 12-tone family, 'M' = Macrotonal, 'm' = microtonal, '' = Raw.
const tuningTypeColour = { d: 230, M: colPan, m: colInst, '': colStatus }
const methodLabels = { const methodLabels = {
pitch: 'Nearest-note', pitch: 'Nearest-note',
delta: 'Nearest-delta', delta: 'Nearest-delta',
@@ -3411,7 +3473,7 @@ function openRetunePopup() {
let sel = entries.findIndex(p => p.index === PITCH_PRESET_IDX) let sel = entries.findIndex(p => p.index === PITCH_PRESET_IDX)
if (sel < 0) sel = 0 if (sel < 0) sel = 0
let scroll = (sel >= listH) ? Math.min(Math.max(0, n - listH), sel - (listH >>> 1)) : 0 let scroll = centerScroll(sel, 0, listH, n)
const repaint = () => { const repaint = () => {
con.color_pair(230, colPopupBack) con.color_pair(230, colPopupBack)
@@ -3440,7 +3502,7 @@ function openRetunePopup() {
const isSel = (idx === sel) const isSel = (idx === sel)
const isCur = (e.index === PITCH_PRESET_IDX) const isCur = (e.index === PITCH_PRESET_IDX)
const back = isSel ? colHighlight : colPopupBack const back = isSel ? colHighlight : colPopupBack
const fore = isSel ? colWHITE : (isCur ? colWHITE : 230) const fore = (e.t in tuningTypeColour) ? tuningTypeColour[e.t] : 230
const marker = isCur ? sym.playhead : ' ' const marker = isCur ? sym.playhead : ' '
let label = `${marker} ${e.index.toString().padStart(5, ' ')} ${e.name}` let label = `${marker} ${e.index.toString().padStart(5, ' ')} ${e.name}`
if (label.length > listW) label = label.substring(0, listW) if (label.length > listW) label = label.substring(0, listW)
@@ -3500,30 +3562,17 @@ function openRetunePopup() {
repaint() repaint()
} }
else if (ks === '<UP>') { else if (ks === '<UP>') {
if (sel > 0) { if (sel > 0) { sel--; scroll = centerScroll(sel, scroll, listH, n); repaint() }
sel--
if (sel < scroll) scroll = sel
repaint()
}
} else if (ks === '<DOWN>') { } else if (ks === '<DOWN>') {
if (sel < n - 1) { if (sel < n - 1) { sel++; scroll = centerScroll(sel, scroll, listH, n); repaint() }
sel++
if (sel >= scroll + listH) scroll = sel - listH + 1
repaint()
}
} else if (ks === '<HOME>') { } else if (ks === '<HOME>') {
sel = 0; scroll = 0; repaint() sel = 0; scroll = centerScroll(sel, scroll, listH, n); repaint()
} else if (ks === '<END>') { } else if (ks === '<END>') {
sel = n - 1; scroll = Math.max(0, n - listH); repaint() sel = n - 1; scroll = centerScroll(sel, scroll, listH, n); repaint()
} else if (ks === '<PAGE_UP>') { } else if (ks === '<PAGE_UP>') {
sel = Math.max(0, sel - listH) sel = Math.max(0, sel - listH); scroll = centerScroll(sel, scroll, listH, n); repaint()
scroll = Math.max(0, scroll - listH)
if (sel < scroll) scroll = sel
repaint()
} else if (ks === '<PAGE_DOWN>') { } else if (ks === '<PAGE_DOWN>') {
sel = Math.min(n - 1, sel + listH) sel = Math.min(n - 1, sel + listH); scroll = centerScroll(sel, scroll, listH, n); repaint()
if (sel >= scroll + listH) scroll = Math.min(Math.max(0, n - listH), sel - listH + 1)
repaint()
} }
}) })
} }
@@ -3538,6 +3587,169 @@ function openRetunePopup() {
drawAll() drawAll()
} }
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MIXER FLAGS POPUP
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
function openFlagsPopup() {
const toneNames = ['Linear pitch', 'Amiga pitch', 'Linear freq']
const intpNames = ['Default', 'None', 'A500', 'A1200', 'SNES', 'DPCM']
let toneMode = initialTrackerMixerflags & 3
let intpMode = (initialTrackerMixerflags >>> 2) & 7
if (toneMode >= toneNames.length) toneMode = 0
if (intpMode >= intpNames.length) intpMode = 0
// Build list rows: headers + selectable radio options.
// items[].kind: undefined = header, 'tone' | 'intp' = selectable.
const items = []
items.push({ label: 'Tone Mode:' })
toneNames.forEach((n, i) => items.push({ kind: 'tone', idx: i, label: n }))
items.push({ label: '' })
items.push({ label: 'Interpolation:' })
intpNames.forEach((n, i) => items.push({ kind: 'intp', idx: i, label: n }))
const selectables = []
items.forEach((it, i) => { if (it.kind) selectables.push(i) })
let sel = 0
const pw = 28
const ph = items.length + 4
const px = ((SCRW - pw) / 2 | 0) + 1
const py = ((SCRH - ph) / 2 | 0)
const popup = new win.WindowObject(px, py, pw, ph, ()=>{}, ()=>{}, 'Mixer Flags', popupDrawFrame)
popup.isHighlighted = true
popup.titleBack = colPopupBack
const repaint = () => {
con.color_pair(230, colPopupBack)
popup.drawFrame()
for (let i = 0; i < items.length; i++) {
const it = items[i]
con.move(py + 1 + i, px + 2)
if (!it.kind) {
con.color_pair(colStatus, colPopupBack)
print(it.label.padEnd(pw - 4))
} else {
const isSel = (selectables[sel] === i)
const isChecked = (it.kind === 'tone')
? (toneMode === it.idx)
: (intpMode === it.idx)
const back = isSel ? colHighlight : colPopupBack
const fore = isChecked ? colVoiceHdr : colWHITE
con.color_pair(fore, back)
const line = ' ' + (isChecked ? sym.ticked : sym.unticked) + ' ' + it.label
print(line.padEnd(pw - 4))
}
}
con.move(py + ph - 2, px + 2)
con.color_pair(colVoiceHdr, colPopupBack); print(`\u008418u `)
con.color_pair(colStatus, colPopupBack); print('Sel ')
con.color_pair(colVoiceHdr, colPopupBack); print('sp ')
con.color_pair(colStatus, colPopupBack); print('Tick ')
con.color_pair(colVoiceHdr, colPopupBack); print('ent ')
con.color_pair(colStatus, colPopupBack); print('OK ')
con.color_pair(colVoiceHdr, colPopupBack); print('Q ')
con.color_pair(colStatus, colPopupBack); print('X')
con.color_pair(colStatus, 255)
}
repaint()
let done = false
let confirmed = false
let eventJustReceived = true
while (!done) {
input.withEvent(ev => {
if (ev[0] !== 'key_down') return
if (1 !== ev[2]) return
const ks = ev[1]
if (eventJustReceived) { eventJustReceived = false; return }
if (ks === '<ESC>' || ks === 'q' || ks === 'Q') { done = true; return }
if (ks === '\n') { confirmed = true; done = true; return }
if (ks === '<UP>' && sel > 0) { sel--; repaint(); return }
if (ks === '<DOWN>' && sel < selectables.length-1) { sel++; repaint(); return }
if (ks === ' ') {
const it = items[selectables[sel]]
if (it.kind === 'tone') toneMode = it.idx
else if (it.kind === 'intp') intpMode = it.idx
repaint()
return
}
})
}
if (confirmed) {
const newFlags = (initialTrackerMixerflags & ~0x1F) |
(toneMode & 3) | ((intpMode & 7) << 2)
if (newFlags !== initialTrackerMixerflags) {
initialTrackerMixerflags = newFlags
audio.setTrackerMixerFlags(PLAYHEAD, newFlags)
hasUnsavedChanges = true
}
}
drawAll()
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// INLINE HEX EDITOR
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Overlay an editable hex field at (y, x) with `digits` digits, pre-filled from `initialValue`.
// Returns the new integer on commit, or null on cancel. Reusable for pattern-grid edits.
function openInlineHexEdit(y, x, digits, initialValue) {
let buf = (initialValue >>> 0).toString(16).toUpperCase()
if (buf.length > digits) buf = buf.substring(buf.length - digits)
buf = buf.padStart(digits, '0')
let cur = 0
let cancelled = false
let done = false
const repaint = () => {
con.move(y, x)
con.color_pair(colWHITE, colHighlight)
print(' $' + buf + ' ')
con.move(y, x + 2 + cur)
con.color_pair(colBLACK, colWHITE)
print(buf[cur])
con.color_pair(colStatus, 255)
}
repaint()
let eventJustReceived = true
while (!done) {
input.withEvent(ev => {
if (ev[0] !== 'key_down') return
if (1 !== ev[2]) return
const ks = ev[1]
if (eventJustReceived) { eventJustReceived = false; return }
if (ks === '<ESC>') { cancelled = true; done = true; return }
if (ks === '\n') { done = true; return }
if (ks === '<LEFT>' && cur > 0) { cur--; repaint(); return }
if (ks === '<RIGHT>' && cur < digits - 1) { cur++; repaint(); return }
if (ks === '<HOME>') { cur = 0; repaint(); return }
if (ks === '<END>') { cur = digits - 1; repaint(); return }
if (ks.length === 1 && '0123456789abcdefABCDEF'.includes(ks)) {
buf = buf.substring(0, cur) + ks.toUpperCase() + buf.substring(cur + 1)
if (cur < digits - 1) cur++
else done = true
repaint()
return
}
})
}
return cancelled ? null : parseInt(buf, 16)
}
clampCursor(); clampVoice(); clampCue(); clampOrdersHoriz(); clampPatternIdx(); clampPatternGrid() clampCursor(); clampVoice(); clampCue(); clampOrdersHoriz(); clampPatternIdx(); clampPatternGrid()
drawAll() drawAll()

View File

@@ -116,7 +116,8 @@ Timeline has two distinct modes: view and edit mode. Two modes are toggled using
<b>&nbsp;GLOBAL EDIT</b> <b>&nbsp;GLOBAL EDIT</b>
<b>\u00B7${'\u00B8'.repeat(11)}\u00B9</b> <b>\u00B7${'\u00B8'.repeat(11)}\u00B9</b>
&bul;<b>Q</b> : <O>retune current song into different tuning</O> &bul;<b>Q</b> : <O>retune current song into different tuning and strategy</O>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<O>In general, nearest-note works best for macrotonals, nearest-harmonic and nearest-delta works best for highly microtonals (31+); 17- and 19-TET takes nearest-harmonic pretty well, while 22-TET seem to only benefit from the nearest-note</O>
` `
//////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@@ -66,9 +66,58 @@ const EXEC_FUNS = {
"txt": (f) => _G.shell.execute(`less "${f}"`), "txt": (f) => _G.shell.execute(`less "${f}"`),
"md": (f) => _G.shell.execute(`less "${f}"`), "md": (f) => _G.shell.execute(`less "${f}"`),
"log": (f) => _G.shell.execute(`less "${f}"`), "log": (f) => _G.shell.execute(`less "${f}"`),
"taud": (f) => _G.shell.execute(`taut "${f}"`), "taud": (f) => _G.shell.execute(`microtone "${f}"`),
} }
function makeExecFun(template) {
return (f) => _G.shell.execute(template.replaceAll("{0}", `"${f}"`))
}
function loadZfmrc() {
try {
let zfmrcPath = `A:${_TVDOS.variables.USERCONFIGPATH}\\zfmrc`
let zfmrcFile = files.open(zfmrcPath)
if (!zfmrcFile.exists) return
let content = zfmrcFile.sread()
let lines = content.split(/\r?\n/)
let currentSection = null
for (let i = 0; i < lines.length; i++) {
let line = lines[i].trim()
if (line.length === 0 || line.startsWith("#") || line.startsWith(";")) continue
if (line.startsWith("[") && line.endsWith("]")) {
currentSection = line.substring(1, line.length - 1).toUpperCase()
continue
}
if (currentSection === "EXEC_FUNS") {
let commaIdx = line.indexOf(",")
if (commaIdx < 0) continue
let ext = line.substring(0, commaIdx).trim().toLowerCase()
let template = line.substring(commaIdx + 1).trim()
if (ext.length === 0 || template.length === 0) continue
EXEC_FUNS[ext] = makeExecFun(template)
}
else if (currentSection === "COL_HL_EXT") {
let commaIdx = line.indexOf(",")
if (commaIdx < 0) continue
let ext = line.substring(0, commaIdx).trim().toLowerCase()
let colStr = line.substring(commaIdx + 1).trim()
if (ext.length === 0 || colStr.length === 0) continue
let col = parseInt(colStr, 10)
if (isNaN(col)) continue
COL_HL_EXT[ext] = col
}
}
} catch (e) {
serial.println("zfm: failed to load zfmrc: " + e.message)
}
}
loadZfmrc()
let windowMode = 0 // 0 == left, 1 == right let windowMode = 0 // 0 == left, 1 == right
let windowFocus = [0] // is a stack; 0: files window, 1: palette window, 2: popup window let windowFocus = [0] // is a stack; 0: files window, 1: palette window, 2: popup window
@@ -82,6 +131,7 @@ let cursor = [0, 0] // absolute position!
function bytesToReadable(i) { function bytesToReadable(i) {
return ''+ ( return ''+ (
(i > 999999999999) ? (((i / 10000000000)|0)/100 + "T") :
(i > 999999999) ? (((i / 10000000)|0)/100 + "G") : (i > 999999999) ? (((i / 10000000)|0)/100 + "G") :
(i > 999999) ? (((i / 10000)|0)/100 + "M") : (i > 999999) ? (((i / 10000)|0)/100 + "M") :
(i > 9999) ? (((i / 100)|0)/10 + "K") : (i > 9999) ? (((i / 100)|0)/10 + "K") :
@@ -677,11 +727,11 @@ while (!exit) {
let keysym = event[1] let keysym = event[1]
let keyJustHit = (1 == event[2]) let keyJustHit = (1 == event[2])
if (keyJustHit && event[3] != keys.ENTER) { // release the latch right away if the key is not Return if (keyJustHit && event[3] != keys.ENTER && keysym != "q") { // release the latch right away if the key is neither Return nor 'q'
firstRunLatch = false firstRunLatch = false
} }
if (keyJustHit && firstRunLatch) { // filter out the initial ENTER key as they would cause unwanted behaviours if (keyJustHit && firstRunLatch) { // filter out the initial ENTER/'q' key as they would cause unwanted behaviours
firstRunLatch = false firstRunLatch = false
} }
else { else {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,621 @@
// Terran BASIC runtime helper for compiled programs
// Compiled-by: assets/disk0/tbas/compile.js
// Loaded at runtime by `let bS = require("tbas")`
//
// Contract with compiler:
// - The compiler has lowered every BASIC expression to a JS expression
// that produces the *raw* JS value (number, string, array, ForGen,
// function, BasicMemoMonad, …). Builtins take such raw values, NOT
// SyntaxTreeReturnObj wrappers.
// - Variable reads: bS.__state.vars.X (key always uppercased)
// - Variable writes: bS.__state.vars.X = v
// - Control flow (GOTO/GOSUB/RETURN/FOR/NEXT/IF/ON/END/READ/RESTORE/LABEL/DATA)
// is *not* exposed here — the compiler emits inline JS that updates the
// `pc` and `gosubStack` directly.
//
// Naming: BASIC builtins exposed under their UPPERCASE name (bS.PRINT,
// bS.PLOT, bS.SIN). Compiler-only helpers prefixed with __.
// ---------------------------------------------------------------------------
// Types & helpers
// ---------------------------------------------------------------------------
function isNumable(s) {
if (Array.isArray(s)) return false
if (s === undefined) return false
if (typeof s.trim == "function" && s.trim().length == 0) return false
return !isNaN(s)
}
const tonum = (t) => t * 1.0
function ForGen(s, e, t) {
this.start = s
this.end = e
this.step = t || 1
this.current = this.start
this.stepsgn = (this.step > 0) ? 1 : -1
}
const isGenerator = (o) =>
o !== undefined && o !== null &&
o.start !== undefined && o.end !== undefined &&
o.step !== undefined && o.stepsgn !== undefined
const genToArray = (gen) => {
let a = []
let cur = gen.start
while (cur * gen.stepsgn + gen.step * gen.stepsgn <= (gen.end + gen.step) * gen.stepsgn) {
a.push(cur)
cur += gen.step
}
return a
}
const genHasNext = (o) => o.current * o.stepsgn + o.step * o.stepsgn <= (o.end + o.step) * o.stepsgn
const genGetNext = (gen, mutated) => {
if (mutated !== undefined) gen.current = tonum(mutated)
gen.current += gen.step
return genHasNext(gen) ? gen.current : undefined
}
function BasicMemoMonad(m) { this.mType = "value"; this.mVal = m }
function BasicListMonad(m) { this.mType = "list"; this.mVal = [m] }
function BasicFunSeq(f) { this.mType = "funseq"; this.mVal = f }
const isMonad = (o) => o !== undefined && o !== null && o.mType !== undefined
function arrayToString(a) {
let acc = ""
for (let k = 0; k < a.length; k++) {
if (k > 0) acc += ","
acc += (Array.isArray(a[k])) ? arrayToString(a[k]) : a[k]
}
return "{" + acc + "}"
}
// ---------------------------------------------------------------------------
// State container
// ---------------------------------------------------------------------------
const _initialConsts = () => ({
NIL: [],
PI: Math.PI,
TAU: Math.PI * 2,
EULER: Math.E,
UNDEFINED: undefined,
TRUE: true,
FALSE: false,
// ID is identity-function: emitted as JS arrow when needed
ID: (x) => x,
})
const state = {
vars: _initialConsts(),
indexBase: 0,
dataConsts: [],
dataCursor: 0,
gotoLabels: {}, // labelName -> [lnum, stmt]
lineList: [], // sorted ascending list of existing source lines (for GOTO snap)
rnd: Math.random(),
forVar: {}, // varname -> generator|array (the iterable we still owe to FOR/FOREACH)
forLnums: {}, // varname -> [lnum, stmt of the FOR/FOREACH header]
forStack: [],
trace: false,
debug: false,
}
function __reset() {
state.vars = _initialConsts()
state.indexBase = 0
state.dataConsts = []
state.dataCursor = 0
state.gotoLabels = {}
state.lineList = []
state.rnd = Math.random()
state.forVar = {}
state.forLnums = {}
state.forStack = []
}
function __data(values) { state.dataConsts = values.slice() }
function __labels(map) { state.gotoLabels = Object.assign({}, map) }
function __setLines(arr) { state.lineList = arr.slice() }
// ---------------------------------------------------------------------------
// Compiler-emitted operator helpers (need behaviour not directly expressible
// in raw JS without losing semantics)
// ---------------------------------------------------------------------------
function __add(lh, rh) {
return (!isNaN(lh) && !isNaN(rh)) ? (tonum(lh) + tonum(rh)) : (lh + rh)
}
function __div(lh, rh) { if (rh == 0) throw Error("Division by zero"); return lh / rh }
function __intdiv(lh, rh) { if (rh == 0) throw Error("Division by zero"); return (lh / rh) | 0 }
function __mod(lh, rh) { if (rh == 0) throw Error("Division by zero"); return lh % rh }
function __pow(lh, rh) {
let r = Math.pow(lh, rh)
if (isNaN(r)) throw Error("Illegal function call")
if (!isFinite(r)) throw Error("Division by zero")
return r
}
function __test(v) { return !!v } // matches builtin TEST: string "false" is truthy
function __dim(dims) {
let revdims = dims.slice().reverse()
let inner = new Array(revdims[0]).fill(0)
for (let k = 1; k < revdims.length; k++) {
const sz = revdims[k]
const prev = inner
inner = new Array(sz).fill(0).map(_ => JSON.parse(JSON.stringify(prev)))
}
return inner
}
function __subscriptError(idx, dim) {
return Error("Subscript out of range (index " + idx + ", dim " + dim + ")")
}
function __arrGet(arr, idx) {
let v = arr
for (let i = 0; i < idx.length; i++) {
if (v === undefined || v === null) throw __subscriptError(idx[i], i)
v = v[idx[i] - state.indexBase]
}
return v
}
function __arrSet(arr, idx, value) {
let v = arr
for (let i = 0; i < idx.length - 1; i++) {
if (v === undefined || v === null) throw __subscriptError(idx[i], i)
v = v[idx[i] - state.indexBase]
}
if (v === undefined || v === null) throw __subscriptError(idx[idx.length - 1], idx.length - 1)
v[idx[idx.length - 1] - state.indexBase] = value
}
// FOR / FOREACH setup. Lowered as:
// __forSetup(varname, iterable, bodyLnum, bodyStmt)
// where iterable is a ForGen (FOR…TO…STEP) OR an Array (FOREACH IN…), and
// (bodyLnum, bodyStmt) is the PC of the statement immediately following the
// FOR header — i.e. where NEXT should jump back to. The compiler supplies
// this directly so the state machine doesn't rely on fall-through.
function __forSetup(varname, iterable, bodyLnum, bodyStmt) {
const v = varname.toUpperCase()
if (isGenerator(iterable)) {
state.vars[v] = iterable.start
state.forVar[v] = iterable
} else if (Array.isArray(iterable)) {
state.vars[v] = iterable[0]
state.forVar[v] = iterable.slice(1) // remainder
} else {
throw Error("FOR: not a generator or array")
}
state.forLnums[v] = [bodyLnum, bodyStmt]
state.forStack.push(v)
}
// NEXT [varname]. Without varname, pops the most recent.
// Returns [lnum, stmt] to jump back to (just-after the FOR header) if more
// iterations remain, or undefined if the loop is exhausted (caller falls
// through).
function __forNext(varname) {
let v
if (varname === undefined || varname === null) {
v = state.forStack.pop()
} else {
v = varname.toUpperCase()
// remove this varname from the stack
const idx = state.forStack.lastIndexOf(v)
if (idx >= 0) state.forStack.splice(idx, 1)
}
if (v === undefined) throw Error("NEXT without FOR")
const it = state.forVar[v]
let nextVal
if (isGenerator(it)) {
nextVal = genGetNext(it, state.vars[v])
} else {
nextVal = it.shift()
}
if (nextVal !== undefined) {
state.vars[v] = nextVal
state.forStack.push(v)
return state.forLnums[v] // already the PC of the loop body
} else {
if (isGenerator(it)) state.vars[v] = it.current
return undefined
}
}
function __readData() {
const r = state.dataConsts[state.dataCursor++]
if (r === undefined) throw Error("Out of DATA")
return r
}
// Resolve a GOTO/GOSUB target — accepts numeric line, label string, or
// already-evaluated expression. For numeric targets that don't match an
// existing source line, snap upward to the next one (matches the
// interpreter's behaviour, where the main loop simply increments lnum until
// it finds a populated cmdbuf entry).
function __resolveTarget(t) {
if (typeof t === "string" && state.gotoLabels[t] !== undefined) {
return state.gotoLabels[t]
}
let target
if (typeof t === "number") target = t
else if (isNumable(t)) target = tonum(t)
else throw Error("Invalid jump target: " + t)
const lines = state.lineList
if (lines.length === 0) return [target, 0]
// linear scan is fine for the line counts BASIC programs reach
for (let i = 0; i < lines.length; i++) {
if (lines[i] >= target) return [lines[i], 0]
}
return [Infinity, 0]
}
// Invoke a usrdefun (compiled to a JS function), or — when the parser
// couldn't tell array-indexing apart from function-call (e.g. `A(5)` for an
// unknown identifier) — index into an array. Used by MAP/FOLD/FILTER, monad
// operators, and the compiler's default `function` lowering.
function __runFn(fn, args) {
if (typeof fn === "function") return fn.apply(null, args)
if (Array.isArray(fn)) return __arrGet(fn, args)
if (isMonad(fn) && fn.mType === "funseq") {
let arg = args[0]
for (let i = 0; i < fn.mVal.length; i++) arg = __runFn(fn.mVal[i], [arg])
return arg
}
throw Error("Not a callable: " + fn)
}
// ---------------------------------------------------------------------------
// Operator builtins (where JS doesn't already do the right thing)
// ---------------------------------------------------------------------------
function _AND(a, b) { if (typeof a !== "boolean" || typeof b !== "boolean") throw Error("Type mismatch"); return a && b }
function _OR (a, b) { if (typeof a !== "boolean" || typeof b !== "boolean") throw Error("Type mismatch"); return a || b }
function _NOT(a) { return !a }
function _CONS(lh, rh) { // !
if (Array.isArray(rh)) return [lh].concat(rh)
if (rh && rh.mType === "list") { rh.mVal = [lh].concat(rh.mVal); return rh }
throw Error("Type mismatch")
}
function _PUSH(lh, rh) { // ~
if (Array.isArray(lh)) return lh.concat([rh])
if (lh && lh.mType === "list") { lh.mVal = [lh.mVal].concat([rh]); return lh }
throw Error("Type mismatch")
}
function _CONCAT(lh, rh) { // #
if (Array.isArray(lh) && Array.isArray(rh)) return lh.concat(rh)
if (lh && rh && lh.mType === "list" && rh.mType === "list") return new BasicListMonad(lh.mVal.concat(rh.mVal))
throw Error("Type mismatch")
}
function _TO(from, to) { return new ForGen(from, to, 1) }
function _STEP(gen, step) {
if (!isGenerator(gen)) throw Error("Type mismatch (STEP)")
return new ForGen(gen.start, gen.end, step)
}
// ---------------------------------------------------------------------------
// I/O builtins
// ---------------------------------------------------------------------------
// PRINT(values, seps) — values: array of resolved JS values; seps: array of
// length values.length-1 with "," | ";" between each consecutive pair.
// Trailing semicolon? The compiler signals "no newline" by passing a final
// `null` element in `values` and "noNewline" flag — we use the convention
// that the LAST entry of `values` being a marker `__noNewline` suppresses
// the newline (matches basic.js trailing-null behaviour).
const __PRINT_NONL = Symbol("PRINT_NONL")
function PRINT(values, seps) {
seps = seps || []
if (values.length === 0) {
println()
return
}
let suppressNewline = false
let realLen = values.length
if (values[realLen - 1] === __PRINT_NONL) {
suppressNewline = true
realLen -= 1
}
for (let i = 0; i < realLen; i++) {
if (i >= 1 && seps[i - 1] === ",") print("\t")
const v = values[i]
let s
if (Array.isArray(v)) s = arrayToString(v)
else if (v === undefined || v === "") s = ""
else if (v.toString !== undefined) s = v.toString()
else s = v
print(s)
}
if (!suppressNewline) println()
}
function EMIT(values, seps) {
seps = seps || []
if (values.length === 0) { println(); return }
let suppressNewline = false
let realLen = values.length
if (values[realLen - 1] === __PRINT_NONL) { suppressNewline = true; realLen -= 1 }
for (let i = 0; i < realLen; i++) {
if (i >= 1 && seps[i - 1] === ",") print("\t")
const v = values[i]
if (v === undefined) print("")
else if (isNumable(v)) {
const c = con.getyx()
con.addch(tonum(v))
con.move(c[0], c[1] + 1)
} else if (v.toString !== undefined) print(v.toString())
else print(v)
}
if (!suppressNewline) println()
}
function INPUT(promptOrVarname) {
print("? ")
let r = sys.read().trim()
if (!isNaN(r)) r = tonum(r)
return r
}
function CIN() { return sys.read().trim() }
// ---------------------------------------------------------------------------
// Numeric builtins
// ---------------------------------------------------------------------------
const _num = (f) => (x) => { if (!isNumable(x)) throw Error("Type mismatch"); return f(tonum(x)) }
const _num2 = (f) => (a, b) => {
if (!isNumable(a) || !isNumable(b)) throw Error("Type mismatch")
return f(tonum(a), tonum(b))
}
const ABS = _num(Math.abs)
const SGN = _num(x => x > 0 ? 1 : x < 0 ? -1 : 0)
const INT = _num(Math.floor)
const FLOOR = _num(Math.floor)
const CEIL = _num(Math.ceil)
const FIX = _num(x => x | 0)
const ROUND = _num(Math.round)
const SQR = _num(Math.sqrt)
const CBR = _num(Math.cbrt)
const SIN = _num(Math.sin)
const COS = _num(Math.cos)
const TAN = _num(Math.tan)
const ASN = _num(Math.asin)
const ACO = _num(Math.acos)
const ATN = _num(Math.atan)
const SINH = _num(Math.sinh)
const COSH = _num(Math.cosh)
const TANH = _num(Math.tanh)
const EXP = _num(Math.exp)
const LOG = _num(Math.log)
const MIN = _num2((a,b) => a > b ? b : a)
const MAX = _num2((a,b) => a < b ? b : a)
function RND(x) {
// matches basic.js:1199 — only re-roll when arg !== 0
if (!(x === 0)) state.rnd = Math.random()
return state.rnd
}
// ---------------------------------------------------------------------------
// String builtins
// ---------------------------------------------------------------------------
function SPC(n) { return " ".repeat(n) }
function LEFT(s, n) { return String(s).substring(0, n) }
function RIGHT(s, n) { return String(s).substring(String(s).length - n) }
function MID(s, start, len) { return String(s).substring(start - state.indexBase, start - state.indexBase + len) }
function CHR(n) { return String.fromCharCode(n) }
// ---------------------------------------------------------------------------
// List builtins
// ---------------------------------------------------------------------------
function LEN(x) { if (x === undefined || x.length === undefined) throw Error("Type mismatch"); return x.length }
function HEAD(x) { if (!x || x.length < 1) throw Error("Type mismatch"); return x[0] }
function TAIL(x) { if (!x || x.length < 1) throw Error("Type mismatch"); return x.slice(1) }
function INIT(x) { if (!x || x.length < 1) throw Error("Type mismatch"); return x.slice(0, x.length - 1) }
function LAST(x) { if (!x || x.length < 1) throw Error("Type mismatch"); return x[x.length - 1] }
function MAP(fn, functor) {
if (typeof fn !== "function" && !(isMonad(fn) && fn.mType === "funseq")) throw Error("MAP: not a function")
if (isGenerator(functor)) functor = genToArray(functor)
if (!Array.isArray(functor)) throw Error("MAP: not iterable")
return functor.map(it => __runFn(fn, [it]))
}
function FOLD(fn, init, functor) {
if (typeof fn !== "function" && !(isMonad(fn) && fn.mType === "funseq")) throw Error("FOLD: not a function")
if (isGenerator(functor)) functor = genToArray(functor)
if (!Array.isArray(functor)) throw Error("FOLD: not iterable")
let akku = init
for (let i = 0; i < functor.length; i++) akku = __runFn(fn, [akku, functor[i]])
return akku
}
function FILTER(fn, functor) {
if (typeof fn !== "function" && !(isMonad(fn) && fn.mType === "funseq")) throw Error("FILTER: not a function")
if (isGenerator(functor)) functor = genToArray(functor)
if (!Array.isArray(functor)) throw Error("FILTER: not iterable")
return functor.filter(it => __runFn(fn, [it]))
}
// Array literal constructor — emitted by the compiler for `[a,b,c]` syntax
function ARRAY() { return Array.prototype.slice.call(arguments) }
// ---------------------------------------------------------------------------
// Graphics / system
// ---------------------------------------------------------------------------
function CLS() { con.clear() }
function CLPX() { graphics.clearPixels(255) }
function PLOT(x, y, c) { graphics.plotPixel(x, y, c) }
function GOTOYX(y, x) { con.move(y + (1 - state.indexBase), x + (1 - state.indexBase)) }
function TEXTFORE(c) { print(String.fromCharCode(27, 91) + "38;5;" + (c | 0) + "m") }
function TEXTBACK(c) { print(String.fromCharCode(27, 91) + "48;5;" + (c | 0) + "m") }
function POKE(addr, v) { sys.poke(addr, v) }
function PEEK(addr) { return sys.peek(addr) }
function GETKEYSDOWN() {
const keys = []
sys.poke(-40, 255)
for (let k = -41; k >= -48; k--) keys.push(sys.peek(k))
return keys
}
function CPUT(devnum, msg) { com.sendMessage(devnum, msg); return com.getStatusCode(devnum) }
function CGET(devnum, ptr) {
const msg = com.pullMessage(devnum)
const len = msg.length | 0
for (let i = 0; i < len; i++) sys.poke(ptr + i, msg.charCodeAt(i))
return len
}
function CSTA(devnum) { return com.getStatusCode(devnum) }
// ---------------------------------------------------------------------------
// Type / debug
// ---------------------------------------------------------------------------
function TYPEOF(v) {
if (v === undefined) return "null"
if (typeof v === "boolean") return "bool"
if (Array.isArray(v)) return "array"
if (isGenerator(v)) return "generator"
if (isMonad(v)) return v.mType + "-monad"
if (typeof v === "function") return "usrdefun"
if (isNumable(v)) return "num"
if (typeof v === "string") return "string"
return typeof v
}
function OPTIONBASE(n) {
if (n != 0 && n != 1) throw Error("Syntax error: OPTIONBASE")
state.indexBase = n | 0
}
function OPTIONDEBUG(n) { state.debug = (n | 0) === 1 }
function OPTIONTRACE(n) { state.trace = (n | 0) === 1 }
// ---------------------------------------------------------------------------
// Monad / functional ops (best-effort port)
// ---------------------------------------------------------------------------
function MRET(v) { return new BasicMemoMonad(v) }
function MLIST(v) { return new BasicListMonad(v) }
function MJOIN(m) { if (!isMonad(m)) throw Error("Type mismatch"); return m.mVal }
function _BIND(ma, fn) { // >>=
if (!isMonad(ma)) throw Error(">>=: left is not a monad")
if (typeof fn !== "function") throw Error(">>=: right is not a function")
const mb = __runFn(fn, [ma.mVal])
if (!isMonad(mb)) throw Error(">>=: function did not return a monad")
return mb
}
function _SEQ(ma, mb) { // >>~
if (!isMonad(ma) || !isMonad(mb)) throw Error("Type mismatch")
return mb
}
function _COMPOSE(fa, fb) { // .
const ma = (typeof fa === "function") ? [fa] : fa.mVal
const mb = (typeof fb === "function") ? [fb] : fb.mVal
return new BasicFunSeq(mb.concat(ma))
}
function _APPLY(fn, value) { // $
return __runFn(fn, [value])
}
function _PIPE(value, fn) { // &
return _APPLY(fn, value)
}
function _CURRY(fn, value) { // ~<
if (typeof fn !== "function") throw Error("~<: left is not a function")
return function() {
const rest = Array.prototype.slice.call(arguments)
return fn.apply(null, [value].concat(rest))
}
}
function _SEQAPP(fns, functor) { // <*>
if (!Array.isArray(fns)) throw Error("<*>: first arg must be an array of functions")
if (isGenerator(functor)) functor = genToArray(functor)
if (!Array.isArray(functor)) throw Error("<*>: not iterable")
let ret = []
for (let i = 0; i < fns.length; i++) ret = ret.concat(functor.map(it => __runFn(fns[i], [it])))
return ret
}
function _SEQCURRYMAP(fns, functor) { // <~>
if (typeof fns === "function") fns = [fns]
if (!Array.isArray(fns)) throw Error("<~>: first arg must be a function or array of functions")
if (isGenerator(functor)) functor = genToArray(functor)
if (!Array.isArray(functor)) throw Error("<~>: not iterable")
let ret = []
for (let i = 0; i < fns.length; i++) ret = ret.concat(functor.map(it => _CURRY(fns[i], it)))
return ret
}
// ---------------------------------------------------------------------------
// Exports
// ---------------------------------------------------------------------------
exports = {
// state & introspection
__state: state, __reset, __data, __labels, __setLines,
__PRINT_NONL,
// operator helpers
__add, __div, __intdiv, __mod, __pow, __test,
__dim, __arrGet, __arrSet,
__forSetup, __forNext, __readData, __resolveTarget,
__runFn,
// type ctors
__ForGen: ForGen, __isGenerator: isGenerator, __genToArray: genToArray,
__isMonad: isMonad,
// operators
AND: _AND, OR: _OR, NOT: _NOT,
UNARYLOGICNOT: _NOT,
UNARYBNOT: (a) => ~a,
UNARYMINUS: (a) => -a,
UNARYPLUS: (a) => +a,
BAND: (a,b)=>a&b, BOR: (a,b)=>a|b, BXOR: (a,b)=>a^b,
"<<": (a,b)=>a<<b, ">>": (a,b)=>a>>>b,
"!": _CONS, "~": _PUSH, "#": _CONCAT,
TO: _TO, STEP: _STEP,
// i/o
PRINT, EMIT, INPUT, CIN,
// numeric
ABS, SGN, INT, FLOOR, CEIL, FIX, ROUND, SQR, CBR,
SIN, COS, TAN, ASN, ACO, ATN, SINH, COSH, TANH,
EXP, LOG, MIN, MAX, RND,
// strings
SPC, LEFT, RIGHT, MID, CHR,
// lists
LEN, HEAD, TAIL, INIT, LAST, MAP, FOLD, FILTER,
ARRAY,
// graphics / system
CLS, CLPX, PLOT, GOTOYX, TEXTFORE, TEXTBACK,
POKE, PEEK, GETKEYSDOWN, CPUT, CGET, CSTA,
// type / option
TYPEOF, OPTIONBASE, OPTIONDEBUG, OPTIONTRACE,
// monads / functional
MRET, MLIST, MJOIN,
">>=": _BIND, ">>~": _SEQ,
".": _COMPOSE, "$": _APPLY, "&": _PIPE, "~<": _CURRY,
"<*>": _SEQAPP, "<$>": MAP, "<~>": _SEQCURRYMAP,
// misc
DO: function() { return arguments[arguments.length - 1] },
CLEAR: function() { state.vars = _initialConsts() },
END: function() { /* compiler emits pc=[Infinity,0] */ },
LABEL: function() { /* harvested at compile time */ },
DATA: function() { /* harvested at compile time */ },
// DIM as an expression (e.g. `WS = DIM(H, V)`): allocate and return a
// freshly zero-filled N-D array. The statement form `DIM A(H, V)` is
// compiled inline and never reaches this entry.
DIM: function() { return __dim(Array.prototype.slice.call(arguments)) },
}

View File

@@ -698,7 +698,7 @@ def encode_note_it(it_note: int) -> int:
# IT C-5 anchors to Taud C-4, so offset = it_note - 60. # IT C-5 anchors to Taud C-4, so offset = it_note - 60.
semis = it_note - 60 semis = it_note - 60
val = round(TAUD_C4 + semis * 4096 / 12) val = round(TAUD_C4 + semis * 4096 / 12)
return max(1, min(0xFFFD, val)) return max(0x20, min(0xFFFF, val))
return NOTE_NOP return NOTE_NOP

View File

@@ -250,7 +250,7 @@ def period_to_taud_note(period: int) -> int:
if period <= 0: if period <= 0:
return NOTE_NOP return NOTE_NOP
val = round(TAUD_C4 + 4096.0 * math.log2(PT_REFERENCE_PERIOD / period)) val = round(TAUD_C4 + 4096.0 * math.log2(PT_REFERENCE_PERIOD / period))
return max(1, min(0xFFFD, val)) return max(0x20, min(0xFFFF, val))
# ── PT effect → Taud effect ────────────────────────────────────────────────── # ── PT effect → Taud effect ──────────────────────────────────────────────────

View File

@@ -139,7 +139,7 @@ def mon_note_to_taud(mon_note: int) -> int:
if mon_note == 0x7F: if mon_note == 0x7F:
return NOTE_CUT return NOTE_CUT
val = TAUD_C4 + round((mon_note - MON_NOTE_C4) * 4096.0 / 12.0) val = TAUD_C4 + round((mon_note - MON_NOTE_C4) * 4096.0 / 12.0)
return max(1, min(0xFFFD, val)) return max(0x20, min(0xFFFF, val))
# ── Effect mapping (Monotone 3-bit code + 6-bit data → Taud) ───────────────── # ── Effect mapping (Monotone 3-bit code + 6-bit data → Taud) ─────────────────

View File

@@ -234,7 +234,7 @@ def encode_note(s3m_note: int) -> int:
return NOTE_NOP return NOTE_NOP
semitones = (octave - 4) * 12 + pitch semitones = (octave - 4) * 12 + pitch
val = round(TAUD_C4 + semitones * 4096 / 12) val = round(TAUD_C4 + semitones * 4096 / 12)
return max(1, min(0xFFFD, val)) return max(0x20, min(0xFFFF, val))
def encode_effect(cmd: int, arg: int, ch: int = 0, row: int = 0, def encode_effect(cmd: int, arg: int, ch: int = 0, row: int = 0,

View File

@@ -96,9 +96,9 @@ NUM_VOICES = 20
SAMPLE_LEN_LIMIT = 65535 SAMPLE_LEN_LIMIT = 65535
# Note word sentinels # Note word sentinels
NOTE_NOP = 0xFFFF NOTE_NOP = 0x0000
NOTE_KEYOFF = 0x0000 NOTE_KEYOFF = 0x0001
NOTE_CUT = 0xFFFE NOTE_CUT = 0x0002
TAUD_C4 = 0x5000 # The audio engine's Middle C TAUD_C4 = 0x5000 # The audio engine's Middle C
# Cue sheet instruction byte (cue offset 30; offset 31 = arg byte for 2-byte forms). # Cue sheet instruction byte (cue offset 30; offset 31 = arg byte for 2-byte forms).

View File

@@ -2255,7 +2255,7 @@ from source.
* Semantics (matches IT/Schism player/effects.c:1664-1764 csf_check_nna): * Semantics (matches IT/Schism player/effects.c:1664-1764 csf_check_nna):
- Fires on every fresh foreground note trigger on a channel, BEFORE the - Fires on every fresh foreground note trigger on a channel, BEFORE the
NNA-spawn step that would ghost the existing voice. Does NOT fire on NNA-spawn step that would ghost the existing voice. Does NOT fire on
tone portamento, on note-off (0x0000), on note-cut (0xFFFE), or on tone portamento, on note-off (0x0001), on note-cut (0x0002), or on
empty cells. empty cells.
- The DCT/DCA values consulted belong to the EXISTING voice's instrument - The DCT/DCA values consulted belong to the EXISTING voice's instrument
(i.e. the OLD note's instrument, not the incoming note's). Different (i.e. the OLD note's instrument, not the incoming note's). Different
@@ -2401,6 +2401,8 @@ TODO:
[x] GSLINGER order 0x03 chn 1: L 0100 fades unexpectedly fast? — converter fix [x] GSLINGER order 0x03 chn 1: L 0100 fades unexpectedly fast? — converter fix
[x] do not reset tickspeed on pattern view play / add key to modify tick speed ('[' down/']' up) [x] do not reset tickspeed on pattern view play / add key to modify tick speed ('[' down/']' up)
[x] expose song table on UI (test with `insaniq2.taud`) [x] expose song table on UI (test with `insaniq2.taud`)
[x] 0x0000 - no-op; 0x0001 - key-off; 0x0002 - note-cut 0x0010..0x001F - INT0..INTF
[ ] establish hooks for the interrupts
TODO - list of demo songs that MUST ship with Microtone: TODO - list of demo songs that MUST ship with Microtone:
* 4THSYM (rename to Fourth Symmetriad) — excellent piece for demonstrating NNAs and filter envelopes * 4THSYM (rename to Fourth Symmetriad) — excellent piece for demonstrating NNAs and filter envelopes
@@ -2421,13 +2423,14 @@ Play Data: play data are series of tracker-like instructions, visualised as:
rr||NOTE|Ins|E.Vol|E.Pan|EE.ff| rr||NOTE|Ins|E.Vol|E.Pan|EE.ff|
63||FFFF|255|3 63|3 63|FF FFFF| (8 bytes per line, 512 bytes per pattern, 128 patterns on 64 kB bank, 32 banks available (pattern 0xFFF -- bank 31, pattern 127 is a sentinel value for no-pattern)) 63||FFFF|255|3 63|3 63|FF FFFF| (8 bytes per line, 512 bytes per pattern, 128 patterns on 64 kB bank, 32 banks available (pattern 0xFFF -- bank 31, pattern 127 is a sentinel value for no-pattern))
notes are tuned as 4096 Tone-Equal Temperament. Tuning is set per-sample using their Sampling rate value. notes are tuned as 4096 Tone-Equal Temperament. Tuning is set per-sample using their Sampling rate value. 0x1000: C at zeroth octave; 0xF000: C at 14th octave; 0xFFFF: ~C at 15th octave; 0x0000..0x001F: reserved for sentinels (valid playable note range is 0x0020..0xFFFF)
Special values: Special values:
note 0xFFFF: no-op note 0x0000: no-op
note 0xFFFE: note cut note 0x0001: key-off
note 0x0000: key-off note 0x0002: note cut
note 0x0010..0x001F: Interrupt 0..F (notation: Int0..IntF) — reserved interrupt slots; engine has no default handler.
inst 0: no instrument change inst 0: no instrument change

View File

@@ -23,7 +23,9 @@ import net.torvald.tsvm.peripheral.MP2Env
* 8. Call `setCuePosition(playhead, 0)` then `play(playhead)`. * 8. Call `setCuePosition(playhead, 0)` then `play(playhead)`.
* *
* Note values: 0x4000 = C3 (sample's native pitch), 4096 steps per octave. * Note values: 0x4000 = C3 (sample's native pitch), 4096 steps per octave.
* Empty row: note = 0xFFFF (no trigger). All 256 instrument slots (0-255) are valid. * Empty row: note = 0x0000 (no trigger). Note sentinels (0x0000..0x001F): 0x0000 = no-op,
* 0x0001 = key-off, 0x0002 = note cut, 0x0010..0x001F = Int0..IntF (reserved interrupts).
* Valid playable notes are 0x0020..0xFFFF. All 256 instrument slots (0-255) are valid.
* *
* ## How to upload PCM audio into a playhead * ## How to upload PCM audio into a playhead
* *

View File

@@ -243,7 +243,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
internal val sampleBin = UnsafeHelper.allocate(SAMPLE_BIN_TOTAL, this) internal val sampleBin = UnsafeHelper.allocate(SAMPLE_BIN_TOTAL, this)
@Volatile var sampleBank: Int = 0 // 0..15, controls the 0..524287 window @Volatile var sampleBank: Int = 0 // 0..15, controls the 0..524287 window
internal val instruments = Array(256) { TaudInst(it) } internal val instruments = Array(256) { TaudInst(it) }
internal val playdata = Array(4096) { Array(64) { TaudPlayData(0xFFFF, 0, 0, 0, 32, 0, 0, 0) } } internal val playdata = Array(4096) { Array(64) { TaudPlayData(0x0000, 0, 0, 0, 32, 0, 0, 0) } }
internal val playheads: Array<Playhead> internal val playheads: Array<Playhead>
internal val cueSheet = Array(1024) { PlayCue() } internal val cueSheet = Array(1024) { PlayCue() }
internal val pcmBin = arrayOf( internal val pcmBin = arrayOf(
@@ -2275,7 +2275,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
} }
TaudPlayData( TaudPlayData(
note = if (rawRow.note != 0xFFFF) rawRow.note else src.note, note = if (rawRow.note != 0x0000) rawRow.note else src.note,
instrment = if (rawRow.instrment != 0) rawRow.instrment else src.instrment, instrment = if (rawRow.instrment != 0) rawRow.instrment else src.instrment,
volume = if (volIsSet) rawRow.volume else src.volume, volume = if (volIsSet) rawRow.volume else src.volume,
volumeEff = if (volIsSet) rawRow.volumeEff else src.volumeEff, volumeEff = if (volIsSet) rawRow.volumeEff else src.volumeEff,
@@ -2329,7 +2329,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// physical_presence ord 0x1F ch2: every row carries `... 1E A0F/A09/A02`) // physical_presence ord 0x1F ch2: every row carries `... 1E A0F/A09/A02`)
// silences after the first row because the slide saturates at 0 and there's // silences after the first row because the slide saturates at 0 and there's
// nothing to lift the volume back up before the next slide starts. // nothing to lift the volume back up before the next slide starts.
0xFFFF -> { 0x0000 -> {
if (row.instrment != 0) { if (row.instrment != 0) {
voice.instrumentId = row.instrment voice.instrumentId = row.instrment
val seedVol = rowVolumeFromDefault(instruments[voice.instrumentId]) val seedVol = rowVolumeFromDefault(instruments[voice.instrumentId])
@@ -2345,8 +2345,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
// fadeoutVolume reaches 0, or immediately if FT2-mode fadeStep == 0. Setting // fadeoutVolume reaches 0, or immediately if FT2-mode fadeStep == 0. Setting
// voice.active = false here would defeat both — instruments with sustain points // voice.active = false here would defeat both — instruments with sustain points
// and non-zero fadeout (FT2 sustain-then-fade idiom) would be cut on the spot. // and non-zero fadeout (FT2 sustain-then-fade idiom) would be cut on the spot.
0x0000 -> { voice.keyOff = true } 0x0001 -> { voice.keyOff = true }
0xFFFE -> voice.active = false // note cut (immediate) 0x0002 -> voice.active = false // note cut (immediate)
in 0x0003..0x000F -> { /* reserved sentinel range, no engine handler */ }
in 0x0010..0x001F -> { /* Int0..IntF: reserved interrupt slots, no engine handler yet */ }
else -> { else -> {
if (toneG && voice.active) { if (toneG && voice.active) {
// Tone porta: target the note, do not retrigger sample. // Tone porta: target the note, do not retrigger sample.
@@ -2502,7 +2504,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
1 -> amigaSlideOnce(voice.noteVal, -mag) // Amiga: subtract from pitch ⇒ adds period 1 -> amigaSlideOnce(voice.noteVal, -mag) // Amiga: subtract from pitch ⇒ adds period
2 -> linearFreqSlideOnce(voice.noteVal, -mag) // Hz/tick: pitch down ⇒ -Hz 2 -> linearFreqSlideOnce(voice.noteVal, -mag) // Hz/tick: pitch down ⇒ -Hz
else -> voice.noteVal - mag // linear 4096-TET else -> voice.noteVal - mag // linear 4096-TET
}.coerceIn(1, 0xFFFD) }.coerceIn(0x20, 0xFFFF)
voice.basePitch = voice.noteVal voice.basePitch = voice.noteVal
voice.amigaPeriod = -1.0 // reseed on next per-tick slide voice.amigaPeriod = -1.0 // reseed on next per-tick slide
voice.linearFreq = -1.0 voice.linearFreq = -1.0
@@ -2521,7 +2523,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
1 -> amigaSlideOnce(voice.noteVal, mag) 1 -> amigaSlideOnce(voice.noteVal, mag)
2 -> linearFreqSlideOnce(voice.noteVal, mag) 2 -> linearFreqSlideOnce(voice.noteVal, mag)
else -> voice.noteVal + mag else -> voice.noteVal + mag
}.coerceIn(1, 0xFFFD) }.coerceIn(0x20, 0xFFFF)
voice.basePitch = voice.noteVal voice.basePitch = voice.noteVal
voice.amigaPeriod = -1.0 voice.amigaPeriod = -1.0
voice.linearFreq = -1.0 voice.linearFreq = -1.0
@@ -2730,7 +2732,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
} }
0x1 -> voice.glissandoOn = (x != 0) 0x1 -> voice.glissandoOn = (x != 0)
0x2 -> { 0x2 -> {
voice.noteVal = (voice.noteVal + FINETUNE_OFFSET[x]).coerceIn(1, 0xFFFD) voice.noteVal = (voice.noteVal + FINETUNE_OFFSET[x]).coerceIn(0x20, 0xFFFF)
voice.basePitch = voice.noteVal voice.basePitch = voice.noteVal
voice.amigaPeriod = -1.0 voice.amigaPeriod = -1.0
voice.linearFreq = -1.0 voice.linearFreq = -1.0
@@ -2832,7 +2834,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
1 -> amigaSlideTick(voice, voice.slideArg) 1 -> amigaSlideTick(voice, voice.slideArg)
2 -> linearFreqSlideTick(voice, voice.slideArg) 2 -> linearFreqSlideTick(voice, voice.slideArg)
else -> voice.noteVal + voice.slideArg else -> voice.noteVal + voice.slideArg
}.coerceIn(1, 0xFFFD) }.coerceIn(0x20, 0xFFFF)
voice.basePitch = voice.noteVal voice.basePitch = voice.noteVal
} }
@@ -2854,7 +2856,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
voice.noteVal = target voice.noteVal = target
voice.tonePortaTarget = -1 voice.tonePortaTarget = -1
} else { } else {
voice.noteVal = freqHzToNoteVal(voice.linearFreq).coerceIn(1, 0xFFFD) voice.noteVal = freqHzToNoteVal(voice.linearFreq).coerceIn(0x20, 0xFFFF)
} }
voice.basePitch = voice.noteVal voice.basePitch = voice.noteVal
voice.amigaPeriod = -1.0 voice.amigaPeriod = -1.0
@@ -2912,14 +2914,14 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
if (voice.vibratoActive) { if (voice.vibratoActive) {
val sine = lfoSample(voice.vibratoLfoPos, voice.vibratoWave) val sine = lfoSample(voice.vibratoLfoPos, voice.vibratoWave)
val pitchDelta = (sine * voice.mem.huDepth) shr voice.vibratoFineShift val pitchDelta = (sine * voice.mem.huDepth) shr voice.vibratoFineShift
pitchToMixer = (voice.noteVal + pitchDelta).coerceIn(1, 0xFFFD) pitchToMixer = (voice.noteVal + pitchDelta).coerceIn(0x20, 0xFFFF)
voice.vibratoLfoPos = (voice.vibratoLfoPos + voice.mem.huSpeed * 4) and 0xFF voice.vibratoLfoPos = (voice.vibratoLfoPos + voice.mem.huSpeed * 4) and 0xFF
} }
// Glissando (S$1x) — snap pitchToMixer to nearest semitone but leave noteVal smooth. // Glissando (S$1x) — snap pitchToMixer to nearest semitone but leave noteVal smooth.
if (voice.glissandoOn) { if (voice.glissandoOn) {
val semis = ((pitchToMixer * 12 + 2048) / 4096) val semis = ((pitchToMixer * 12 + 2048) / 4096)
pitchToMixer = (semis * 4096 / 12).coerceIn(1, 0xFFFD) pitchToMixer = (semis * 4096 / 12).coerceIn(0x20, 0xFFFF)
} }
// Tremolo (R) — modulates rowVolume around the per-note volume base. IT's tremolo // Tremolo (R) — modulates rowVolume around the per-note volume base. IT's tremolo
@@ -2946,7 +2948,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
if (voice.arpActive) { if (voice.arpActive) {
val voiceIdx = ts.tickInRow % 3 val voiceIdx = ts.tickInRow % 3
val arpDelta = when (voiceIdx) { 1 -> voice.arpOff1 shl 8; 2 -> voice.arpOff2 shl 8; else -> 0 } val arpDelta = when (voiceIdx) { 1 -> voice.arpOff1 shl 8; 2 -> voice.arpOff2 shl 8; else -> 0 }
pitchToMixer = (voice.basePitch + arpDelta).coerceIn(1, 0xFFFD) pitchToMixer = (voice.basePitch + arpDelta).coerceIn(0x20, 0xFFFF)
voice.lastArpVoice = voiceIdx voice.lastArpVoice = voiceIdx
} }
@@ -2983,7 +2985,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
((voice.envPfValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt() ((voice.envPfValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt()
else 0 else 0
val finalPitch = (pitchToMixer + autoVibDelta + pitchEnvDelta).coerceIn(1, 0xFFFD) val finalPitch = (pitchToMixer + autoVibDelta + pitchEnvDelta).coerceIn(0x20, 0xFFFF)
voice.playbackRate = computePlaybackRate(inst, finalPitch) voice.playbackRate = computePlaybackRate(inst, finalPitch)
// Filter envelope (filter mode): scale baseCut by envValue (0..1, 0.5 = unity). // Filter envelope (filter mode): scale baseCut by envValue (0..1, 0.5 = unity).
@@ -3087,7 +3089,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
val pitchEnvDelta = if (bg.hasPfEnv && bg.pfEnvOn && !bg.envPfIsFilter) val pitchEnvDelta = if (bg.hasPfEnv && bg.pfEnvOn && !bg.envPfIsFilter)
((bg.envPfValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt() ((bg.envPfValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt()
else 0 else 0
val finalPitch = (bg.noteVal + autoVibDelta + pitchEnvDelta).coerceIn(1, 0xFFFD) val finalPitch = (bg.noteVal + autoVibDelta + pitchEnvDelta).coerceIn(0x20, 0xFFFF)
bg.playbackRate = computePlaybackRate(inst, finalPitch) bg.playbackRate = computePlaybackRate(inst, finalPitch)
// Filter-mode pf envelope: same scaling rule as foreground. // Filter-mode pf envelope: same scaling rule as foreground.
if (bg.hasPfEnv && bg.pfEnvOn && bg.envPfIsFilter) { if (bg.hasPfEnv && bg.pfEnvOn && bg.envPfIsFilter) {
@@ -3603,7 +3605,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
var randomPanBias = 0 // signed var randomPanBias = 0 // signed
// Pitch state (4096-TET units, signed when slid). // Pitch state (4096-TET units, signed when slid).
var noteVal = 0xFFFF // The currently sounding base note (no per-row vibrato/arp added) var noteVal = 0x0000 // The currently sounding base note (no per-row vibrato/arp added); 0 = none yet
var basePitch = 0x4000 // Saved pre-effect pitch for vibrato/arp/glissando overlay var basePitch = 0x4000 // Saved pre-effect pitch for vibrato/arp/glissando overlay
// Amiga-mode period state, persisted across ticks so multi-tick E/F slides don't lose // Amiga-mode period state, persisted across ticks so multi-tick E/F slides don't lose
// sub-noteVal precision through repeated round-trip rounding (see amigaSlideTick). // sub-noteVal precision through repeated round-trip rounding (see amigaSlideTick).
@@ -3965,7 +3967,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
it.hasPfEnv = false; it.envPfIsFilter = false it.hasPfEnv = false; it.envPfIsFilter = false
it.fadeoutVolume = 1.0 it.fadeoutVolume = 1.0
it.rampOutSamples = 0; it.rampOutGain = 0.0; it.rampOutStep = 0.0 it.rampOutSamples = 0; it.rampOutGain = 0.0; it.rampOutStep = 0.0
it.noteVal = 0xFFFF; it.basePitch = 0x4000 it.noteVal = 0x0000; it.basePitch = 0x4000
it.amigaPeriod = -1.0; it.linearFreq = -1.0 it.amigaPeriod = -1.0; it.linearFreq = -1.0
it.tonePortaTarget = -1; it.tonePortaSpeed = 0 it.tonePortaTarget = -1; it.tonePortaSpeed = 0
it.filterY1 = 0.0; it.filterY2 = 0.0 it.filterY1 = 0.0; it.filterY2 = 0.0

View File

@@ -561,7 +561,10 @@ class TestDiskDrive(private val vm: VM, private val driveNum: Int, theRootPath:
statusCode.set(STATE_CODE_STANDBY) statusCode.set(STATE_CODE_STANDBY)
} }
else if (inputString.startsWith("USAGE")) { else if (inputString.startsWith("USAGE")) {
recipient?.writeout(composePositiveAns("USED123456/TOTAL654321")) val used = rootPath.walkTopDown().filter { it.isFile }.map { it.length() }.sum()
.coerceIn(0L, Int.MAX_VALUE.toLong())
val total = rootPath.totalSpace.coerceIn(0L, Int.MAX_VALUE.toLong())
recipient?.writeout(composePositiveAns("USED$used/TOTAL$total"))
statusCode.set(STATE_CODE_STANDBY) statusCode.set(STATE_CODE_STANDBY)
} }
else else

View File

@@ -387,7 +387,7 @@ def encode_note_xm(xm_note: int) -> int:
if 1 <= xm_note <= 96: if 1 <= xm_note <= 96:
semis = xm_note - XM_RELNOTE_C4 semis = xm_note - XM_RELNOTE_C4
val = round(TAUD_C4 + semis * 4096 / 12) val = round(TAUD_C4 + semis * 4096 / 12)
return max(1, min(0xFFFD, val)) return max(0x20, min(0xFFFF, val))
return NOTE_NOP return NOTE_NOP