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)
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
# 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..."
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
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
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.

View File

@@ -3,3 +3,4 @@
* 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
}
const THEVERSION = "1.2.1"
const THEVERSION = "1.2.2"
const PROD = true
let INDEX_BASE = 0
@@ -4197,7 +4197,7 @@ bF.load = function(args) { // LOAD function
if (args[1] === undefined) throw lang.missingOperand
var fileOpened = fs.open(args[1], "R")
serial.printerr('load '+args[1])
if (replUsrConfirmed || cmdbuf.length == 0) {
if (!fileOpened) {
fileOpened = fs.open(args[1]+".BAS", "R")
@@ -4241,7 +4241,7 @@ bF.yes = 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')
if (!pathOpened) {
throw lang.noSuchFile
@@ -4251,6 +4251,57 @@ bF.catalog = function(args) { // CATALOG function
com.sendMessage(port, "LIST")
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)
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);
}
}
t.OFF = 0x0000; // key-off
t.CUT = 0xFFFE; // note cut (immediate)
t.NOP = 0xFFFF; // no-op (empty row)
t.NOP = 0x0000; // no-op (empty row)
t.OFF = 0x0001; // key-off
t.CUT = 0x0002; // note cut (immediate)
return t;
}());

View File

@@ -147,10 +147,12 @@ _TVDOS.variables = {
LANG: "EN",
KEYBOARD: "us_qwerty",
PATH: "\\tvdos\\bin;\\home",
INCLPATH: "\\tvdos\\include;\\home",
PATHEXT: ".com;.bat;.app;.js;.alias",
HELPPATH: "\\tvdos\\help",
OS_NAME: "TSVM Disk Operating System",
OS_VERSION: _TVDOS.VERSION
OS_VERSION: _TVDOS.VERSION,
USERCONFIGPATH: "\\home\\config",
};
Object.freeze(_TVDOS);
@@ -1405,9 +1407,6 @@ let requireFromMemory = (ptr) => {
}*/
var GL = require("A:/tvdos/include/gl.mjs")
// @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.
// 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};` +
`${appname}`); // making 'exec_args' a app-level global
execAppPrg(args);
return execAppPrg(args);
}

View File

@@ -577,6 +577,59 @@ shell.coreutils = {
ver: function(args) {
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) {
throw Error("Panicking command.js")
}
@@ -590,6 +643,7 @@ shell.coreutils.ls = shell.coreutils.dir
shell.coreutils.time = shell.coreutils.date
shell.coreutils.md = shell.coreutils.mkdir
shell.coreutils.move = shell.coreutils.mv
shell.coreutils.where = shell.coreutils.which
// end of command aliases
Object.freeze(shell.coreutils)
shell.stdio = {
@@ -614,13 +668,25 @@ require = function(path) {
if (path[1] == ":") return shell.require(path)
else {
// 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")
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
let parsedTokens = shell.parse(line) // echo, "hai", |, less
let statements = [] // [[echo, "hai"], [less]]
@@ -757,19 +823,28 @@ shell.execute = function(line) {
// parse alias
// $0: all 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!
lines.forEach(function(line) {
var newLine = line
// replace $1..$9
for (let j = 1; j < 9; j++) {
newLine = newLine.replaceAll('$'+j, tokens[j])
for (let j = 1; j <= 9; j++) {
newLine = newLine.replaceAll('$'+j, quoteAliasArg(tokens[j]))
}
// 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) {
@@ -786,6 +861,10 @@ shell.execute = function(line) {
errorlevel = 0 // reset the number
if (_G.shellProgramTitles === undefined) _G.shellProgramTitles = []
if (nameOverride !== undefined) {
tokens[0] = (''+nameOverride)
cmd = tokens[0]
}
_G.shellProgramTitles.push(cmd.toUpperCase())
sendLcdMsg(_G.shellProgramTitles[_G.shellProgramTitles.length - 1])
//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) {
// only meaningful switches would be either -c or -k anyway
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
exec_args[1] = targetPath
if (playerFile) {
let playerPath = `A:\\tvdos\\bin\\${playerFile}.js`
let playerPath = `A:${_TVDOS.variables.DOSDIR}/bin/${playerFile}.js`
if (files.open(playerPath).exists) {
eval(files.readText(playerPath))
} else {
@@ -334,7 +334,7 @@ for (let i = 0; i < cueElements.length; i++) {
}
// 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) {
serial.println(`Warning: Player script not found: ${playerPath}`)
continue

View File

@@ -169,61 +169,63 @@ const volFxNames = {
const pitchTablePresets = {
// 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 */
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}`]},
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}`]},
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}`]},
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}`]},
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}`]},
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}`]},
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}`]},
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}`]},
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}`]},
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}`]},
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}`]},
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}`]},
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}`]},
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}`]},
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}`]},
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}`]},
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}`]},
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-`]},
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-`]},
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}`]},
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-`]},
/* 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}`]},
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}`]},
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}`]},
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`]},
/* 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}`]},
@@ -267,7 +269,8 @@ const colEffOp = 220
const colEffArg = 231
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 beatDivSecondary = 16
let hasUnsavedChanges = false
@@ -463,7 +466,7 @@ function retuneAllPatterns(newIdx, method) {
for (let row = 0; row < ROWS_PER_PAT; row++) {
const off = 8 * row
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
// in _cadTension / _harmonicCost normalise it.
tonic = note
@@ -473,7 +476,7 @@ function retuneAllPatterns(newIdx, method) {
for (let row = 0; row < ROWS_PER_PAT; row++) {
const off = 8 * row
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
let newAbs
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++) {
const noff = 8 * r
const n = ptn[noff] | (ptn[noff+1] << 8)
if (n !== 0x0000) break
if (n !== 0x0001) break
duration++
}
lambda = 1 - Math.exp(-(duration - 1) / 4)
@@ -555,9 +558,10 @@ Number.prototype.decD2 = function() {
function noteToStr(note) {
if (note === 0xFFFF) return sym.middot.repeat(4)
if (note === 0xFFFE) return sym.notecut
if (note === 0x0000) return sym.keyoff
if (note === 0x0000) return sym.middot.repeat(4)
if (note === 0x0001) 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]
if (preset.table.length === 0) return note.hex04()
const [period, offset] = decomposeNote(note, preset.interval)
@@ -653,7 +657,7 @@ const EMPTY_CELL = {
sPanArg: sym.middot.repeat(2),
sEffOp: sym.middot,
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) {
@@ -689,7 +693,7 @@ function drawCellAtStyled(y, x, cell, back, style) {
return
}
// Styles 1 and 2: note-or-fx field (5 chars) starts on the border column [+ vol-or-pan (2 chars)]
const noteEmpty = (cell._note === 0xFFFF)
const noteEmpty = (cell._note === 0x0000)
const fxEmpty = (cell._effop === 0 && cell._effarg === 0)
const volEmpty = (cell._voleff === 0)
const panEmpty = (cell._paneff === 0)
@@ -864,6 +868,7 @@ function loadTaudSongList(filePath) {
name: '',
composer: '',
copyright: '',
pitchPresetIdx: null,
}
}
@@ -908,6 +913,8 @@ function loadTaudSongList(filePath) {
const subStart = q + 5
if (subStart + subLen > qEnd) break
// 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)
const strs = []
while (strs.length < 3 && r < subStart + subLen) {
@@ -920,6 +927,7 @@ function loadTaudSongList(filePath) {
strs.push(s)
}
if (idx < numSongs) {
songs[idx].pitchPresetIdx = notation
if (strs[0] !== undefined) songs[idx].name = strs[0]
if (strs[1] !== undefined) songs[idx].composer = strs[1]
if (strs[2] !== undefined) songs[idx].copyright = strs[2]
@@ -1372,7 +1380,15 @@ function drawControlHint() {
]
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]
// erase current line
@@ -1530,8 +1546,8 @@ function drawVoiceDetail(isVerticalLayout = false, ptn = null, activeRow = -1, c
if (cumState !== null && lowerH > 0) {
const _apo = Math.abs(cumState.pitchOff)
const _psgn = cumState.pitchOff > 0 ? '+' : cumState.pitchOff < 0 ? '-' : ' '
const _absN = (cumState.lastNote !== 0xFFFF && cumState.pitchOff !== 0)
? noteToStr(Math.max(0, Math.min(0xFFFE, cumState.lastNote + cumState.pitchOff))) + ' '
const _absN = (cumState.lastNote !== 0x0000 && cumState.pitchOff !== 0)
? noteToStr(Math.max(0x20, Math.min(0xFFFF, cumState.lastNote + cumState.pitchOff))) + ' '
: ''
const _clipNm = ['clamp','fold','wrap','wrap'][cumState.clipMode]
const _bcStr = (cumState.bitcrushDepth === 0 && cumState.bitcrushSkip === 0)
@@ -1737,21 +1753,27 @@ if (fullPathObj === undefined) {
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 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 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 buttonNullTexture = new gl.Texture(35, 28, buttonNullBytes)
font.setLowRom("A:/tvdos/bin/tautfont_low.chr")
font.setHighRom("A:/tvdos/bin/tautfont_high.chr")
font.setLowRom("A:"+_TVDOS.variables.DOSDIR+"/bin/tautfont_low.chr")
font.setHighRom("A:"+_TVDOS.variables.DOSDIR+"/bin/tautfont_high.chr")
const songsMeta = loadTaudSongList(fullPathObj.full)
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)
const voiceMutes = new Array(NUM_VOICES).fill(false)
@@ -1792,6 +1814,12 @@ function switchSong(newIndex) {
currentSongIndex = 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)
patternsOutOfSync = false
audio.setMasterVolume(PLAYHEAD, 255)
@@ -2172,42 +2200,31 @@ function visWidth(s) {
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() {
if (song.numPats === 0) { patternIdx = 0; patternListScroll = 0; return }
if (patternIdx < 0) patternIdx = 0
if (patternIdx >= song.numPats) patternIdx = song.numPats - 1
if (patternIdx < patternListScroll) patternListScroll = patternIdx
if (patternIdx < patternListScroll + (PTNVIEW_HEIGHT >>> 1) && patternListScroll > 0)
patternListScroll = patternIdx - (PTNVIEW_HEIGHT >>> 1)
if (patternIdx >= patternListScroll + ((PTNVIEW_HEIGHT + 1) >>> 1))
patternListScroll = patternIdx - ((PTNVIEW_HEIGHT + 1) >>> 1) + 1
if (patternListScroll < 0) patternListScroll = 0
if (patternListScroll + PTNVIEW_HEIGHT > song.numPats)
patternListScroll = Math.max(0, song.numPats - PTNVIEW_HEIGHT)
patternListScroll = centerScroll(patternIdx, patternListScroll, PTNVIEW_HEIGHT, song.numPats)
}
function scrollPatternGridTo(row) {
if (row < patternGridScroll) patternGridScroll = row
if (row < patternGridScroll + (PTNVIEW_HEIGHT >>> 1) && patternGridScroll > 0)
patternGridScroll = row - (PTNVIEW_HEIGHT >>> 1)
if (row >= patternGridScroll + ((PTNVIEW_HEIGHT + 1) >>> 1))
patternGridScroll = row - ((PTNVIEW_HEIGHT + 1) >>> 1) + 1
if (patternGridScroll < 0) patternGridScroll = 0
if (patternGridScroll + PTNVIEW_HEIGHT > ROWS_PER_PAT)
patternGridScroll = Math.max(0, ROWS_PER_PAT - PTNVIEW_HEIGHT)
patternGridScroll = centerScroll(row, patternGridScroll, PTNVIEW_HEIGHT, ROWS_PER_PAT)
}
function scrollOrdersTo(ci) {
const maxCue = song.lastActiveCue < 0 ? 0 : song.lastActiveCue
const total = 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)
ordersScroll = centerScroll(ci, ordersScroll, PTNVIEW_HEIGHT, maxCue + 1)
}
function clampPatternGrid() {
@@ -2240,7 +2257,7 @@ function simulateRowState(ptnDat, uptoRow) {
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;
// M / N's per-channel axis is not modelled here)
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
// as 0x3F (legacy fallback) — see the longer note below.
let reloadDefaultVol = false
if (note !== 0xFFFF && note !== 0xFFFE) {
if (note === 0x0000) {
if (note !== 0x0000 && note !== 0x0002 && !(note >= 0x0010 && note <= 0x001F)) {
if (note === 0x0001) {
// key-off; sample stays referenced
} else if (isGRow) {
portaTarget = note
@@ -2417,7 +2434,7 @@ function simulateRowState(ptnDat, uptoRow) {
}
else if (effop === OP_G) {
if (effarg !== 0) memG = effarg
if (portaTarget !== -1 && memG !== 0 && lastNote !== 0xFFFF) {
if (portaTarget !== -1 && memG !== 0 && lastNote !== 0x0000) {
const curPitch = lastNote + pitchOff
const diff = portaTarget - curPitch
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) {
fillLine(PTNVIEW_OFFSET_Y - 1, colVoiceHdr, 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()})`,
Notation: pitchTablePresets[PITCH_PRESET_IDX].name,
Flags: `${flagStrSelected.join(', ')} ($${mixerflag.hex02()})`,
GlobalVol: initialGlobalVolume,
MixingVol: initialMixingVolume
GlobalVol: `$${initialGlobalVolume.hex02()}`,
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) => {
con.move(PTNVIEW_OFFSET_Y + index, 2)
con.color_pair(colStatus, 255); print(key)
con.move(PTNVIEW_OFFSET_Y + index, 12)
con.color_pair(colVoiceHdr, colBLACK); print(value)
con.move(PTNVIEW_OFFSET_Y + index, PROJ_META_VALUE_X)
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()
@@ -2745,14 +2782,18 @@ function projectSongListRowsVisible() {
let projectSongScroll = 0
function clampProjectSongCursor() {
function clampProjectCursor() {
const n = songsMeta.numSongs
if (projectSongCursor < 0) projectSongCursor = 0
if (projectSongCursor > n - 1) projectSongCursor = n - 1
const maxCur = PROJ_META_ROWS_COUNT + Math.max(0, n - 1)
if (projectCursor < 0) projectCursor = 0
if (projectCursor > maxCur) projectCursor = maxCur
const rowsVis = projectSongListRowsVisible()
if (projectSongCursor < projectSongScroll) projectSongScroll = projectSongCursor
else if (projectSongCursor >= projectSongScroll + rowsVis)
projectSongScroll = projectSongCursor - rowsVis + 1
if (projectCursor >= PROJ_META_ROWS_COUNT) {
const songIdx = projectCursor - PROJ_META_ROWS_COUNT
if (songIdx < projectSongScroll) projectSongScroll = songIdx
else if (songIdx >= projectSongScroll + rowsVis)
projectSongScroll = songIdx - rowsVis + 1
}
if (projectSongScroll < 0) projectSongScroll = 0
}
@@ -2775,7 +2816,8 @@ function drawProjectSongList() {
}
const s = songsMeta.songs[idx]
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 marker = isActive ? sym.playhead : ' '
@@ -2818,28 +2860,50 @@ function projectInput(wo, event) {
return
}
if (!keyJustHit) return
// if (!keyJustHit) return
if (keysym === '<UP>') {
projectSongCursor -= moveDelta; clampProjectSongCursor(); redrawPanel(); return
projectCursor -= moveDelta; clampProjectCursor(); redrawPanel(); return
}
if (keysym === '<DOWN>') {
projectSongCursor += moveDelta; clampProjectSongCursor(); redrawPanel(); return
projectCursor += moveDelta; clampProjectCursor(); redrawPanel(); return
}
if (keysym === '<PAGE_UP>') {
projectSongCursor -= projectSongListRowsVisible(); clampProjectSongCursor(); redrawPanel(); return
projectCursor -= projectSongListRowsVisible(); clampProjectCursor(); redrawPanel(); return
}
if (keysym === '<PAGE_DOWN>') {
projectSongCursor += projectSongListRowsVisible(); clampProjectSongCursor(); redrawPanel(); return
projectCursor += projectSongListRowsVisible(); clampProjectCursor(); redrawPanel(); return
}
if (keysym === '<HOME>') {
projectSongCursor = 0; clampProjectSongCursor(); redrawPanel(); return
projectCursor = 0; clampProjectCursor(); redrawPanel(); return
}
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 (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
}
if (keysym === ' ') {
@@ -3100,13 +3164,7 @@ function updatePlayback() {
function clampCursor() {
if (cursorRow < 0) cursorRow = 0
if (cursorRow >= ROWS_PER_PAT) cursorRow = ROWS_PER_PAT - 1
if (cursorRow < scrollRow) scrollRow = cursorRow
// these two IF statements will keep the cursor at the centre until viewpoint scroll edge has reached
if (cursorRow < scrollRow + (PTNVIEW_HEIGHT>>>1) && scrollRow > 0) scrollRow = cursorRow - (PTNVIEW_HEIGHT>>>1)
if (cursorRow >= scrollRow + ((PTNVIEW_HEIGHT+1)>>>1)) scrollRow = cursorRow - ((PTNVIEW_HEIGHT+1)>>>1) + 1
if (scrollRow < 0) scrollRow = 0
if (scrollRow + PTNVIEW_HEIGHT > ROWS_PER_PAT)
scrollRow = Math.max(0, ROWS_PER_PAT - PTNVIEW_HEIGHT)
scrollRow = centerScroll(cursorRow, scrollRow, PTNVIEW_HEIGHT, ROWS_PER_PAT)
}
function clampVoice() {
@@ -3387,6 +3445,10 @@ function openRetunePopup() {
const entries = Object.values(pitchTablePresets).sort((a, b) => a.index - b.index)
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 = {
pitch: 'Nearest-note',
delta: 'Nearest-delta',
@@ -3411,7 +3473,7 @@ function openRetunePopup() {
let sel = entries.findIndex(p => p.index === PITCH_PRESET_IDX)
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 = () => {
con.color_pair(230, colPopupBack)
@@ -3440,7 +3502,7 @@ function openRetunePopup() {
const isSel = (idx === sel)
const isCur = (e.index === PITCH_PRESET_IDX)
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 : ' '
let label = `${marker} ${e.index.toString().padStart(5, ' ')} ${e.name}`
if (label.length > listW) label = label.substring(0, listW)
@@ -3500,30 +3562,17 @@ function openRetunePopup() {
repaint()
}
else if (ks === '<UP>') {
if (sel > 0) {
sel--
if (sel < scroll) scroll = sel
repaint()
}
if (sel > 0) { sel--; scroll = centerScroll(sel, scroll, listH, n); repaint() }
} else if (ks === '<DOWN>') {
if (sel < n - 1) {
sel++
if (sel >= scroll + listH) scroll = sel - listH + 1
repaint()
}
if (sel < n - 1) { sel++; scroll = centerScroll(sel, scroll, listH, n); repaint() }
} else if (ks === '<HOME>') {
sel = 0; scroll = 0; repaint()
sel = 0; scroll = centerScroll(sel, scroll, listH, n); repaint()
} 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>') {
sel = Math.max(0, sel - listH)
scroll = Math.max(0, scroll - listH)
if (sel < scroll) scroll = sel
repaint()
sel = Math.max(0, sel - listH); scroll = centerScroll(sel, scroll, listH, n); repaint()
} else if (ks === '<PAGE_DOWN>') {
sel = Math.min(n - 1, sel + listH)
if (sel >= scroll + listH) scroll = Math.min(Math.max(0, n - listH), sel - listH + 1)
repaint()
sel = Math.min(n - 1, sel + listH); scroll = centerScroll(sel, scroll, listH, n); repaint()
}
})
}
@@ -3538,6 +3587,169 @@ function openRetunePopup() {
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()
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>\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}"`),
"md": (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 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) {
return ''+ (
(i > 999999999999) ? (((i / 10000000000)|0)/100 + "T") :
(i > 999999999) ? (((i / 10000000)|0)/100 + "G") :
(i > 999999) ? (((i / 10000)|0)/100 + "M") :
(i > 9999) ? (((i / 100)|0)/10 + "K") :
@@ -677,11 +727,11 @@ while (!exit) {
let keysym = event[1]
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
}
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
}
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.
semis = it_note - 60
val = round(TAUD_C4 + semis * 4096 / 12)
return max(1, min(0xFFFD, val))
return max(0x20, min(0xFFFF, val))
return NOTE_NOP

View File

@@ -250,7 +250,7 @@ def period_to_taud_note(period: int) -> int:
if period <= 0:
return NOTE_NOP
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 ──────────────────────────────────────────────────

View File

@@ -139,7 +139,7 @@ def mon_note_to_taud(mon_note: int) -> int:
if mon_note == 0x7F:
return NOTE_CUT
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) ─────────────────

View File

@@ -234,7 +234,7 @@ def encode_note(s3m_note: int) -> int:
return NOTE_NOP
semitones = (octave - 4) * 12 + pitch
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,

View File

@@ -96,9 +96,9 @@ NUM_VOICES = 20
SAMPLE_LEN_LIMIT = 65535
# Note word sentinels
NOTE_NOP = 0xFFFF
NOTE_KEYOFF = 0x0000
NOTE_CUT = 0xFFFE
NOTE_NOP = 0x0000
NOTE_KEYOFF = 0x0001
NOTE_CUT = 0x0002
TAUD_C4 = 0x5000 # The audio engine's Middle C
# 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):
- 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
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.
- 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
@@ -2401,6 +2401,8 @@ TODO:
[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] 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:
* 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|
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:
note 0xFFFF: no-op
note 0xFFFE: note cut
note 0x0000: key-off
note 0x0000: no-op
note 0x0001: 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

View File

@@ -23,7 +23,9 @@ import net.torvald.tsvm.peripheral.MP2Env
* 8. Call `setCuePosition(playhead, 0)` then `play(playhead)`.
*
* 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
*

View File

@@ -243,7 +243,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
internal val sampleBin = UnsafeHelper.allocate(SAMPLE_BIN_TOTAL, this)
@Volatile var sampleBank: Int = 0 // 0..15, controls the 0..524287 window
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 cueSheet = Array(1024) { PlayCue() }
internal val pcmBin = arrayOf(
@@ -2275,7 +2275,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
}
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,
volume = if (volIsSet) rawRow.volume else src.volume,
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`)
// 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.
0xFFFF -> {
0x0000 -> {
if (row.instrment != 0) {
voice.instrumentId = row.instrment
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
// 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.
0x0000 -> { voice.keyOff = true }
0xFFFE -> voice.active = false // note cut (immediate)
0x0001 -> { voice.keyOff = true }
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 -> {
if (toneG && voice.active) {
// 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
2 -> linearFreqSlideOnce(voice.noteVal, -mag) // Hz/tick: pitch down ⇒ -Hz
else -> voice.noteVal - mag // linear 4096-TET
}.coerceIn(1, 0xFFFD)
}.coerceIn(0x20, 0xFFFF)
voice.basePitch = voice.noteVal
voice.amigaPeriod = -1.0 // reseed on next per-tick slide
voice.linearFreq = -1.0
@@ -2521,7 +2523,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
1 -> amigaSlideOnce(voice.noteVal, mag)
2 -> linearFreqSlideOnce(voice.noteVal, mag)
else -> voice.noteVal + mag
}.coerceIn(1, 0xFFFD)
}.coerceIn(0x20, 0xFFFF)
voice.basePitch = voice.noteVal
voice.amigaPeriod = -1.0
voice.linearFreq = -1.0
@@ -2730,7 +2732,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
}
0x1 -> voice.glissandoOn = (x != 0)
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.amigaPeriod = -1.0
voice.linearFreq = -1.0
@@ -2832,7 +2834,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
1 -> amigaSlideTick(voice, voice.slideArg)
2 -> linearFreqSlideTick(voice, voice.slideArg)
else -> voice.noteVal + voice.slideArg
}.coerceIn(1, 0xFFFD)
}.coerceIn(0x20, 0xFFFF)
voice.basePitch = voice.noteVal
}
@@ -2854,7 +2856,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
voice.noteVal = target
voice.tonePortaTarget = -1
} else {
voice.noteVal = freqHzToNoteVal(voice.linearFreq).coerceIn(1, 0xFFFD)
voice.noteVal = freqHzToNoteVal(voice.linearFreq).coerceIn(0x20, 0xFFFF)
}
voice.basePitch = voice.noteVal
voice.amigaPeriod = -1.0
@@ -2912,14 +2914,14 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
if (voice.vibratoActive) {
val sine = lfoSample(voice.vibratoLfoPos, voice.vibratoWave)
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
}
// Glissando (S$1x) — snap pitchToMixer to nearest semitone but leave noteVal smooth.
if (voice.glissandoOn) {
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
@@ -2946,7 +2948,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
if (voice.arpActive) {
val voiceIdx = ts.tickInRow % 3
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
}
@@ -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()
else 0
val finalPitch = (pitchToMixer + autoVibDelta + pitchEnvDelta).coerceIn(1, 0xFFFD)
val finalPitch = (pitchToMixer + autoVibDelta + pitchEnvDelta).coerceIn(0x20, 0xFFFF)
voice.playbackRate = computePlaybackRate(inst, finalPitch)
// 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)
((bg.envPfValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt()
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)
// Filter-mode pf envelope: same scaling rule as foreground.
if (bg.hasPfEnv && bg.pfEnvOn && bg.envPfIsFilter) {
@@ -3603,7 +3605,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
var randomPanBias = 0 // signed
// 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
// 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).
@@ -3965,7 +3967,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
it.hasPfEnv = false; it.envPfIsFilter = false
it.fadeoutVolume = 1.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.tonePortaTarget = -1; it.tonePortaSpeed = 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)
}
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)
}
else

View File

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