mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
Compare commits
9 Commits
e3bd4a1b59
...
1d28c89937
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d28c89937 | ||
|
|
61524b3685 | ||
|
|
e6f77c4789 | ||
|
|
00c0e18c1a | ||
|
|
135c7b9c4e | ||
|
|
295c1f7fe2 | ||
|
|
e74a373605 | ||
|
|
b1a0a9f801 | ||
|
|
bdc2578072 |
@@ -779,7 +779,7 @@ if V.dittoActive and armRow <= N <= V.dittoEndRow:
|
|||||||
srcRow = V.dittoSourceStart + ((N - V.dittoSourceStart) mod V.dittoLength)
|
srcRow = V.dittoSourceStart + ((N - V.dittoSourceStart) mod V.dittoLength)
|
||||||
src = patternRows[V.pattern][srcRow]
|
src = patternRows[V.pattern][srcRow]
|
||||||
|
|
||||||
cell.note = (raw.note != 0xFFFF) ? raw.note : src.note
|
cell.note = (raw.note != 0x0000) ? raw.note : src.note
|
||||||
cell.instrument = (raw.instrument != 0) ? raw.instrument : src.instrument
|
cell.instrument = (raw.instrument != 0) ? raw.instrument : src.instrument
|
||||||
|
|
||||||
# SEL_FINE / 0 is the canonical no-op encoding for the vol- and pan-columns;
|
# SEL_FINE / 0 is the canonical no-op encoding for the vol- and pan-columns;
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
echo "Starting TVDOS..."
|
echo "Starting TVDOS..."
|
||||||
|
|
||||||
rem put set-xxx commands here:
|
rem put set-xxx commands here:
|
||||||
set PATH=\tvdos\installer;\tvdos\tuidev;$PATH
|
set PATH=\tvdos\installer;\tvdos\tuidev;\tbas;\hopper\bin;$PATH
|
||||||
|
set INCLPATH=\hopper\include;$INCLPATH
|
||||||
|
set HELPPATH=\hopper\help;$HELPPATH
|
||||||
set KEYBOARD=us_colemak
|
set KEYBOARD=us_colemak
|
||||||
|
|
||||||
rem this line specifies which shell to be presented after the boot precess:
|
rem this line specifies which shell to be presented after the boot precess:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2020-2024 CuriousTorvald
|
Copyright (c) 2020-2026 CuriousTorvald
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
TVDOS (c) 2020-2024 CuriousTorvald
|
TVDOS (c) 2020-2026 CuriousTorvald
|
||||||
|
|
||||||
TVDOS is provided "as is", without warranty of any kind; in no event shall the authors or copyright holders be liable for any claim, damages or other liabilities. Run 'less COPYING' for more information.
|
TVDOS is provided "as is", without warranty of any kind; in no event shall the authors or copyright holders be liable for any claim, damages or other liabilities. Run 'less COPYING' for more information.
|
||||||
@@ -3,3 +3,4 @@
|
|||||||
* Created by CuriousTorvald on 2026-04-16
|
* Created by CuriousTorvald on 2026-04-16
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
println("Hopper - Package manager for TSVM")
|
||||||
@@ -32,7 +32,7 @@ if (exec_args !== undefined && exec_args[1] !== undefined && exec_args[1].starts
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const THEVERSION = "1.2.1"
|
const THEVERSION = "1.2.2"
|
||||||
|
|
||||||
const PROD = true
|
const PROD = true
|
||||||
let INDEX_BASE = 0
|
let INDEX_BASE = 0
|
||||||
@@ -4197,7 +4197,7 @@ bF.load = function(args) { // LOAD function
|
|||||||
if (args[1] === undefined) throw lang.missingOperand
|
if (args[1] === undefined) throw lang.missingOperand
|
||||||
var fileOpened = fs.open(args[1], "R")
|
var fileOpened = fs.open(args[1], "R")
|
||||||
|
|
||||||
|
serial.printerr('load '+args[1])
|
||||||
if (replUsrConfirmed || cmdbuf.length == 0) {
|
if (replUsrConfirmed || cmdbuf.length == 0) {
|
||||||
if (!fileOpened) {
|
if (!fileOpened) {
|
||||||
fileOpened = fs.open(args[1]+".BAS", "R")
|
fileOpened = fs.open(args[1]+".BAS", "R")
|
||||||
@@ -4241,7 +4241,7 @@ bF.yes = function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
bF.catalog = function(args) { // CATALOG function
|
bF.catalog = function(args) { // CATALOG function
|
||||||
if (args[1] === undefined) args[1] = "\\"
|
if (args[1] === undefined) args[1] = BASIC_HOME_PATH
|
||||||
var pathOpened = fs.open(args[1], 'R')
|
var pathOpened = fs.open(args[1], 'R')
|
||||||
if (!pathOpened) {
|
if (!pathOpened) {
|
||||||
throw lang.noSuchFile
|
throw lang.noSuchFile
|
||||||
@@ -4251,6 +4251,57 @@ bF.catalog = function(args) { // CATALOG function
|
|||||||
com.sendMessage(port, "LIST")
|
com.sendMessage(port, "LIST")
|
||||||
println(com.pullMessage(port))
|
println(com.pullMessage(port))
|
||||||
}
|
}
|
||||||
|
// Load a file by absolute disk path (bypasses BASIC_HOME_PATH).
|
||||||
|
// Used by COMPILE to fetch /tbas/compile.js.
|
||||||
|
bF._slurpAbsolute = function(path) {
|
||||||
|
var port = _BIOS.FIRST_BOOTABLE_PORT
|
||||||
|
com.sendMessage(port[0], "FLUSH")
|
||||||
|
com.sendMessage(port[0], "CLOSE")
|
||||||
|
com.sendMessage(port[0], 'OPENR"' + path + '",' + port[1])
|
||||||
|
if (com.getStatusCode(port[0]) != 0) return undefined
|
||||||
|
com.sendMessage(port[0], "READ")
|
||||||
|
if (com.getStatusCode(port[0]) >= 128) return undefined
|
||||||
|
var s = com.pullMessage(port[0])
|
||||||
|
com.sendMessage(port[0], "FLUSH"); com.sendMessage(port[0], "CLOSE")
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
bF.compile = function(args) { // COMPILE "OUT.JS" -- transpile cmdbuf to JS
|
||||||
|
if (args[1] === undefined) {
|
||||||
|
println("Usage: COMPILE \"out.js\""); return
|
||||||
|
}
|
||||||
|
if (cmdbuf.length === 0) {
|
||||||
|
println("No program loaded"); return
|
||||||
|
}
|
||||||
|
if (bS._compileImpl === undefined) {
|
||||||
|
// Lazy-load compile.js from /tbas/compile.js
|
||||||
|
var src = bF._slurpAbsolute("/tbas/compile.js")
|
||||||
|
if (src === undefined) {
|
||||||
|
println("Cannot load /tbas/compile.js")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try { eval(src) } catch (e) {
|
||||||
|
println("Failed to load compiler: " + e); return
|
||||||
|
}
|
||||||
|
if (bS._compileImpl === undefined) {
|
||||||
|
println("compile.js loaded but did not define bS._compileImpl"); return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var outpath = args[1]
|
||||||
|
// Strip surrounding quotes if any
|
||||||
|
if ((outpath.charAt(0) === '"' || outpath.charAt(0) === "'") &&
|
||||||
|
outpath.charAt(outpath.length - 1) === outpath.charAt(0)) {
|
||||||
|
outpath = outpath.substring(1, outpath.length - 1)
|
||||||
|
}
|
||||||
|
// Default to .js extension if missing
|
||||||
|
if (!/\.[A-Za-z0-9]+$/.test(outpath)) outpath += ".js"
|
||||||
|
try {
|
||||||
|
var n = bS._compileImpl(outpath)
|
||||||
|
println("Wrote " + n + " bytes to " + outpath)
|
||||||
|
} catch (e) {
|
||||||
|
serial.printerr(e + "\n" + (e.stack || ""))
|
||||||
|
println("Compile error: " + e)
|
||||||
|
}
|
||||||
|
}
|
||||||
Object.freeze(bF)
|
Object.freeze(bF)
|
||||||
|
|
||||||
if (exec_args !== undefined && exec_args[1] !== undefined) {
|
if (exec_args !== undefined && exec_args[1] !== undefined) {
|
||||||
|
|||||||
564
assets/disk0/tbas/compile.js
Normal file
564
assets/disk0/tbas/compile.js
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
@@ -19,9 +19,9 @@ var Note = (function() {
|
|||||||
if (flats[s] !== names[s]) t[flats[s] + oct] = n(oct, s);
|
if (flats[s] !== names[s]) t[flats[s] + oct] = n(oct, s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.OFF = 0x0000; // key-off
|
t.NOP = 0x0000; // no-op (empty row)
|
||||||
t.CUT = 0xFFFE; // note cut (immediate)
|
t.OFF = 0x0001; // key-off
|
||||||
t.NOP = 0xFFFF; // no-op (empty row)
|
t.CUT = 0x0002; // note cut (immediate)
|
||||||
return t;
|
return t;
|
||||||
}());
|
}());
|
||||||
|
|
||||||
|
|||||||
@@ -147,10 +147,12 @@ _TVDOS.variables = {
|
|||||||
LANG: "EN",
|
LANG: "EN",
|
||||||
KEYBOARD: "us_qwerty",
|
KEYBOARD: "us_qwerty",
|
||||||
PATH: "\\tvdos\\bin;\\home",
|
PATH: "\\tvdos\\bin;\\home",
|
||||||
|
INCLPATH: "\\tvdos\\include;\\home",
|
||||||
PATHEXT: ".com;.bat;.app;.js;.alias",
|
PATHEXT: ".com;.bat;.app;.js;.alias",
|
||||||
HELPPATH: "\\tvdos\\help",
|
HELPPATH: "\\tvdos\\help",
|
||||||
OS_NAME: "TSVM Disk Operating System",
|
OS_NAME: "TSVM Disk Operating System",
|
||||||
OS_VERSION: _TVDOS.VERSION
|
OS_VERSION: _TVDOS.VERSION,
|
||||||
|
USERCONFIGPATH: "\\home\\config",
|
||||||
};
|
};
|
||||||
Object.freeze(_TVDOS);
|
Object.freeze(_TVDOS);
|
||||||
|
|
||||||
@@ -1405,9 +1407,6 @@ let requireFromMemory = (ptr) => {
|
|||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
|
||||||
var GL = require("A:/tvdos/include/gl.mjs")
|
|
||||||
|
|
||||||
|
|
||||||
// @param cmdsrc JS source code
|
// @param cmdsrc JS source code
|
||||||
// @param args arguments for the program, must be Array, and args[0] is always the name of the program, e.g.
|
// @param args arguments for the program, must be Array, and args[0] is always the name of the program, e.g.
|
||||||
// for command line 'echo foo bar', args[0] must be 'echo'
|
// for command line 'echo foo bar', args[0] must be 'echo'
|
||||||
@@ -1420,7 +1419,7 @@ var execApp = (cmdsrc, args, appname) => {
|
|||||||
`var ${appname}=function(exec_args){${injectIntChk(cmdsrc, intchkFunName)}\n};` +
|
`var ${appname}=function(exec_args){${injectIntChk(cmdsrc, intchkFunName)}\n};` +
|
||||||
`${appname}`); // making 'exec_args' a app-level global
|
`${appname}`); // making 'exec_args' a app-level global
|
||||||
|
|
||||||
execAppPrg(args);
|
return execAppPrg(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -577,6 +577,59 @@ shell.coreutils = {
|
|||||||
ver: function(args) {
|
ver: function(args) {
|
||||||
println(welcome_text)
|
println(welcome_text)
|
||||||
},
|
},
|
||||||
|
which: function(args) {
|
||||||
|
if (args[1] === undefined) {
|
||||||
|
printerrln(`Usage: ${args[0].toUpperCase()} program_name`)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
let cmd = args[1]
|
||||||
|
|
||||||
|
if (shell.coreutils[cmd.toLowerCase()] !== undefined) {
|
||||||
|
println(`${cmd}: shell built-in command`)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileExists = false
|
||||||
|
var searchFile
|
||||||
|
var searchPath = ""
|
||||||
|
|
||||||
|
if (shell.isValidDriveLetter(cmd[0]) && cmd[1] == ':') {
|
||||||
|
searchFile = files.open(cmd)
|
||||||
|
searchPath = trimStartRevSlash(searchFile.path)
|
||||||
|
fileExists = searchFile.exists
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var searchDir = (cmd.startsWith("/")) ? [""] : ["/"+shell_pwd.join("/")].concat(_TVDOS.getPath())
|
||||||
|
|
||||||
|
var pathExt = []
|
||||||
|
if (cmd.split(".")[1] === undefined)
|
||||||
|
_TVDOS.variables.PATHEXT.split(';').forEach(function(it) { pathExt.push(it); pathExt.push(it.toUpperCase()); })
|
||||||
|
else
|
||||||
|
pathExt.push("")
|
||||||
|
|
||||||
|
searchLoop:
|
||||||
|
for (var i = 0; i < searchDir.length; i++) {
|
||||||
|
for (var j = 0; j < pathExt.length; j++) {
|
||||||
|
let search = searchDir[i]; if (!search.endsWith('\\')) search += '\\'
|
||||||
|
searchPath = trimStartRevSlash(search + cmd + pathExt[j])
|
||||||
|
|
||||||
|
searchFile = files.open(`${CURRENT_DRIVE}:\\${searchPath}`)
|
||||||
|
if (searchFile.exists) {
|
||||||
|
fileExists = true
|
||||||
|
break searchLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fileExists) {
|
||||||
|
printerrln(`${cmd}: not found`)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
println(searchFile.fullPath)
|
||||||
|
return 0
|
||||||
|
},
|
||||||
panic: function(args) {
|
panic: function(args) {
|
||||||
throw Error("Panicking command.js")
|
throw Error("Panicking command.js")
|
||||||
}
|
}
|
||||||
@@ -590,6 +643,7 @@ shell.coreutils.ls = shell.coreutils.dir
|
|||||||
shell.coreutils.time = shell.coreutils.date
|
shell.coreutils.time = shell.coreutils.date
|
||||||
shell.coreutils.md = shell.coreutils.mkdir
|
shell.coreutils.md = shell.coreutils.mkdir
|
||||||
shell.coreutils.move = shell.coreutils.mv
|
shell.coreutils.move = shell.coreutils.mv
|
||||||
|
shell.coreutils.where = shell.coreutils.which
|
||||||
// end of command aliases
|
// end of command aliases
|
||||||
Object.freeze(shell.coreutils)
|
Object.freeze(shell.coreutils)
|
||||||
shell.stdio = {
|
shell.stdio = {
|
||||||
@@ -614,13 +668,25 @@ require = function(path) {
|
|||||||
if (path[1] == ":") return shell.require(path)
|
if (path[1] == ":") return shell.require(path)
|
||||||
else {
|
else {
|
||||||
// if the path starts with ".", look for the current directory
|
// if the path starts with ".", look for the current directory
|
||||||
// if the path starts with [A-Za-z0-9], look for the DOSDIR/includes
|
// if the path starts with [A-Za-z0-9], search through INCLPATH
|
||||||
if (path[0] == '.') return shell.require(shell.resolvePathInput(path).full + ".mjs")
|
if (path[0] == '.') return shell.require(shell.resolvePathInput(path).full + ".mjs")
|
||||||
else return shell.require(`A:${_TVDOS.variables.DOSDIR}/include/${path}.mjs`)
|
else {
|
||||||
|
let inclDirs = (_TVDOS.variables.INCLPATH || "").split(';').filter(function(it) { return it.length > 0 })
|
||||||
|
for (let i = 0; i < inclDirs.length; i++) {
|
||||||
|
let dir = inclDirs[i]
|
||||||
|
if (!dir.endsWith('\\') && !dir.endsWith('/')) dir += '\\'
|
||||||
|
let candidate = `${CURRENT_DRIVE}:${dir}${path}.mjs`
|
||||||
|
if (files.open(candidate).exists) return shell.require(candidate)
|
||||||
|
}
|
||||||
|
// no match found; defer to shell.require with the first entry so the error mentions a sensible path
|
||||||
|
let firstDir = inclDirs[0] || `${_TVDOS.variables.DOSDIR}\\include`
|
||||||
|
if (!firstDir.endsWith('\\') && !firstDir.endsWith('/')) firstDir += '\\'
|
||||||
|
return shell.require(`${CURRENT_DRIVE}:${firstDir}${path}.mjs`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shell.execute = function(line) {
|
shell.execute = function(line, nameOverride) {
|
||||||
if (0 == line.size) return
|
if (0 == line.size) return
|
||||||
let parsedTokens = shell.parse(line) // echo, "hai", |, less
|
let parsedTokens = shell.parse(line) // echo, "hai", |, less
|
||||||
let statements = [] // [[echo, "hai"], [less]]
|
let statements = [] // [[echo, "hai"], [less]]
|
||||||
@@ -757,19 +823,28 @@ shell.execute = function(line) {
|
|||||||
// parse alias
|
// parse alias
|
||||||
// $0: all arguments
|
// $0: all arguments
|
||||||
// $1..9: specific arguments
|
// $1..9: specific arguments
|
||||||
|
// Tokens that contain whitespace or shell metacharacters must be re-quoted
|
||||||
|
// before re-execution, otherwise the re-parse splits them on spaces.
|
||||||
|
var quoteAliasArg = function(s) {
|
||||||
|
if (s === undefined || s === null) return ""
|
||||||
|
s = ''+s
|
||||||
|
if (s.length === 0) return ""
|
||||||
|
if (/[\s"|><&]/.test(s)) return '"' + s.replaceAll('"', '^"') + '"'
|
||||||
|
return s
|
||||||
|
}
|
||||||
var lines = programCode.split('\n').filter(function(it) { return it.length > 0 }) // this return is not shell's return!
|
var lines = programCode.split('\n').filter(function(it) { return it.length > 0 }) // this return is not shell's return!
|
||||||
lines.forEach(function(line) {
|
lines.forEach(function(line) {
|
||||||
var newLine = line
|
var newLine = line
|
||||||
|
|
||||||
// replace $1..$9
|
// replace $1..$9
|
||||||
for (let j = 1; j < 9; j++) {
|
for (let j = 1; j <= 9; j++) {
|
||||||
newLine = newLine.replaceAll('$'+j, tokens[j])
|
newLine = newLine.replaceAll('$'+j, quoteAliasArg(tokens[j]))
|
||||||
}
|
}
|
||||||
|
|
||||||
// replace $0
|
// replace $0
|
||||||
newLine = newLine.replaceAll('$0', tokens.slice(1).join(' '))
|
newLine = newLine.replaceAll('$0', tokens.slice(1).map(quoteAliasArg).join(' '))
|
||||||
|
|
||||||
shell.execute(newLine)
|
shell.execute(newLine, cmd)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else if ("APP" == extension) {
|
else if ("APP" == extension) {
|
||||||
@@ -786,6 +861,10 @@ shell.execute = function(line) {
|
|||||||
errorlevel = 0 // reset the number
|
errorlevel = 0 // reset the number
|
||||||
|
|
||||||
if (_G.shellProgramTitles === undefined) _G.shellProgramTitles = []
|
if (_G.shellProgramTitles === undefined) _G.shellProgramTitles = []
|
||||||
|
if (nameOverride !== undefined) {
|
||||||
|
tokens[0] = (''+nameOverride)
|
||||||
|
cmd = tokens[0]
|
||||||
|
}
|
||||||
_G.shellProgramTitles.push(cmd.toUpperCase())
|
_G.shellProgramTitles.push(cmd.toUpperCase())
|
||||||
sendLcdMsg(_G.shellProgramTitles[_G.shellProgramTitles.length - 1])
|
sendLcdMsg(_G.shellProgramTitles[_G.shellProgramTitles.length - 1])
|
||||||
//serial.println(_G.shellProgramTitles)
|
//serial.println(_G.shellProgramTitles)
|
||||||
@@ -885,6 +964,18 @@ _G.shell = shell
|
|||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// ensure USERCONFIGPATH directory exists
|
||||||
|
try {
|
||||||
|
let userConfigPath = `${CURRENT_DRIVE}:${_TVDOS.variables.USERCONFIGPATH}`
|
||||||
|
let userConfigDir = files.open(userConfigPath)
|
||||||
|
if (!userConfigDir.exists) {
|
||||||
|
debugprintln(`command.js > creating USERCONFIGPATH at ${userConfigPath}`)
|
||||||
|
userConfigDir.mkDir()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugprintln("command.js > USERCONFIGPATH creation failed: " + e.message)
|
||||||
|
}
|
||||||
|
|
||||||
if (exec_args[1] !== undefined) {
|
if (exec_args[1] !== undefined) {
|
||||||
// only meaningful switches would be either -c or -k anyway
|
// only meaningful switches would be either -c or -k anyway
|
||||||
var firstSwitch = exec_args[1].toLowerCase()
|
var firstSwitch = exec_args[1].toLowerCase()
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ for (let i = 0; i < cueElements.length; i++) {
|
|||||||
// Execute the player with modified environment
|
// Execute the player with modified environment
|
||||||
exec_args[1] = targetPath
|
exec_args[1] = targetPath
|
||||||
if (playerFile) {
|
if (playerFile) {
|
||||||
let playerPath = `A:\\tvdos\\bin\\${playerFile}.js`
|
let playerPath = `A:${_TVDOS.variables.DOSDIR}/bin/${playerFile}.js`
|
||||||
if (files.open(playerPath).exists) {
|
if (files.open(playerPath).exists) {
|
||||||
eval(files.readText(playerPath))
|
eval(files.readText(playerPath))
|
||||||
} else {
|
} else {
|
||||||
@@ -334,7 +334,7 @@ for (let i = 0; i < cueElements.length; i++) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute the appropriate player
|
// Execute the appropriate player
|
||||||
let playerPath = `A:\\tvdos\\bin\\${playerFile}.js`
|
let playerPath = `A:${_TVDOS.variables.DOSDIR}/bin/${playerFile}.js`
|
||||||
if (!files.open(playerPath).exists) {
|
if (!files.open(playerPath).exists) {
|
||||||
serial.println(`Warning: Player script not found: ${playerPath}`)
|
serial.println(`Warning: Player script not found: ${playerPath}`)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -169,61 +169,63 @@ const volFxNames = {
|
|||||||
|
|
||||||
const pitchTablePresets = {
|
const pitchTablePresets = {
|
||||||
// index: pitch table number to be recorded on .taudproj file
|
// index: pitch table number to be recorded on .taudproj file
|
||||||
0:{index:0,name:"Raw format",table:[],interval:0x1000,sym:[]}, // when null is specified, hex numbers will be displayed instead
|
// t: type of the tuning. M - Macrotonal, m - microtonal, d - 12-tone
|
||||||
|
|
||||||
|
0:{index:0,name:"Raw format",table:[],interval:0x1000,t:'',sym:[]}, // when null is specified, hex numbers will be displayed instead
|
||||||
/* Xenharmonic, equal temperament */
|
/* Xenharmonic, equal temperament */
|
||||||
10:{index:10,name:"Octave only", table:[0x0],interval:0x1000,
|
10:{index:10,name:"Octave only",table:[0x0],interval:0x1000,t:'M',
|
||||||
sym:[`C${sym.accnull}`]},
|
sym:[`C${sym.accnull}`]},
|
||||||
20:{index:20,name:"2-TET", table:[0x0,0x800],interval:0x1000,
|
20:{index:20,name:"2-TET",table:[0x0,0x800],interval:0x1000,t:'M',
|
||||||
sym:[`C${sym.accnull}`,`F${sym.sharp}`]},
|
sym:[`C${sym.accnull}`,`F${sym.sharp}`]},
|
||||||
30:{index:30,name:"3-TET", table:[0x0,0x555,0xAAB],interval:0x1000,
|
30:{index:30,name:"3-TET",table:[0x0,0x555,0xAAB],interval:0x1000,t:'M',
|
||||||
sym:[`C${sym.accnull}`,`E${sym.accnull}`,`G${sym.sharp}`]},
|
sym:[`C${sym.accnull}`,`E${sym.accnull}`,`G${sym.sharp}`]},
|
||||||
40:{index:40,name:"4-TET", table:[0x0,0x400,0x800,0xC00],interval:0x1000,
|
40:{index:40,name:"4-TET",table:[0x0,0x400,0x800,0xC00],interval:0x1000,t:'M',
|
||||||
sym:[`C${sym.accnull}`,`D${sym.sharp}`,`F${sym.sharp}`,`A${sym.accnull}`]},
|
sym:[`C${sym.accnull}`,`D${sym.sharp}`,`F${sym.sharp}`,`A${sym.accnull}`]},
|
||||||
50:{index:50,name:"5-TET", table:[0x0,0x333,0x666,0x99A,0xCCD],interval:0x1000,
|
50:{index:50,name:"5-TET",table:[0x0,0x333,0x666,0x99A,0xCCD],interval:0x1000,t:'M',
|
||||||
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`]},
|
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`]},
|
||||||
60:{index:60,name:"6-TET", table:[0x0,0x2AB,0x555,0x800,0xAAB,0xD55],interval:0x1000,
|
60:{index:60,name:"6-TET",table:[0x0,0x2AB,0x555,0x800,0xAAB,0xD55],interval:0x1000,t:'M',
|
||||||
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.sharp}`,`G${sym.sharp}`,`A${sym.sharp}`]},
|
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.sharp}`,`G${sym.sharp}`,`A${sym.sharp}`]},
|
||||||
70:{index:70,name:"7-TET", table:[0x0,0x249,0x492,0x6DB,0x925,0xB6E,0xDB7],interval:0x1000,
|
70:{index:70,name:"7-TET",table:[0x0,0x249,0x492,0x6DB,0x925,0xB6E,0xDB7],interval:0x1000,t:'M',
|
||||||
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`,`B${sym.accnull}`]},
|
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`,`B${sym.accnull}`]},
|
||||||
80:{index:80,name:"8-TET", table:[0x0,0x200,0x400,0x600,0x800,0xA00,0xC00,0xE00],interval:0x1000,
|
80:{index:80,name:"8-TET",table:[0x0,0x200,0x400,0x600,0x800,0xA00,0xC00,0xE00],interval:0x1000,t:'M',
|
||||||
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.sharp}`,`A${sym.accnull}`,`B${sym.accnull}`]},
|
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.sharp}`,`A${sym.accnull}`,`B${sym.accnull}`]},
|
||||||
90:{index:90,name:"9-TET", table:[0x0,0x1C7,0x38E,0x555,0x71C,0x8E4,0xAAB,0xC72,0xE39],interval:0x1000,
|
90:{index:90,name:"9-TET",table:[0x0,0x1C7,0x38E,0x555,0x71C,0x8E4,0xAAB,0xC72,0xE39],interval:0x1000,t:'M',
|
||||||
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`,`B${sym.accnull}`,`B${sym.sharp}`]},
|
sym:[`C${sym.accnull}`,`D${sym.accnull}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.accnull}`,`G${sym.accnull}`,`A${sym.accnull}`,`B${sym.accnull}`,`B${sym.sharp}`]},
|
||||||
100:{index:100,name:"10-TET", table:[0x0,0x19A,0x333,0x4CD,0x666,0x800,0x99A,0xB33,0xCCD,0xE66],interval:0x1000,
|
100:{index:100,name:"10-TET",table:[0x0,0x19A,0x333,0x4CD,0x666,0x800,0x99A,0xB33,0xCCD,0xE66],interval:0x1000,t:'M',
|
||||||
sym:[`C${sym.accnull}`,`D${sym.flat}`,`D${sym.accnull}`,`E${sym.flat}`,`E${sym.accnull}`,`E${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`]},
|
sym:[`C${sym.accnull}`,`D${sym.flat}`,`D${sym.accnull}`,`E${sym.flat}`,`E${sym.accnull}`,`E${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`]},
|
||||||
150:{index:150,name:"15-TET", table:[0x0,0x111,0x222,0x333,0x444,0x555,0x666,0x777,0x889,0x99A,0xAAB,0xBBC,0xCCD,0xDDE,0xEEF],interval:0x1000,
|
150:{index:150,name:"15-TET",table:[0x0,0x111,0x222,0x333,0x444,0x555,0x666,0x777,0x889,0x99A,0xAAB,0xBBC,0xCCD,0xDDE,0xEEF],interval:0x1000,t:'m',
|
||||||
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.flat}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.flat}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.flat}`,`B${sym.accnull}`]},
|
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.flat}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.flat}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.flat}`,`B${sym.accnull}`]},
|
||||||
160:{index:160,name:"16-TET", table:[0x0,0x100,0x200,0x300,0x400,0x500,0x600,0x700,0x800,0x900,0xA00,0xB00,0xC00,0xD00,0xE00,0xF00],interval:0x1000,
|
160:{index:160,name:"16-TET",table:[0x0,0x100,0x200,0x300,0x400,0x500,0x600,0x700,0x800,0x900,0xA00,0xB00,0xC00,0xD00,0xE00,0xF00],interval:0x1000,t:'m',
|
||||||
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.flat}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`,`B${sym.sharp}`,`C${sym.flat}`]},
|
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.flat}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`,`B${sym.sharp}`,`C${sym.flat}`]},
|
||||||
170:{index:170,name:"17-TET", table:[0x0,0xF1,0x1E2,0x2D3,0x3C4,0x4B5,0x5A6,0x697,0x788,0x878,0x969,0xA5A,0xB4B,0xC3C,0xD2D,0xE1E,0xF0F],interval:0x1000,
|
170:{index:170,name:"17-TET",table:[0x0,0xF1,0x1E2,0x2D3,0x3C4,0x4B5,0x5A6,0x697,0x788,0x878,0x969,0xA5A,0xB4B,0xC3C,0xD2D,0xE1E,0xF0F],interval:0x1000,t:'m',
|
||||||
sym:[`C${sym.accnull}`,`D${sym.flat}`,`C${sym.sharp}`,`D${sym.accnull}`,`E${sym.flat}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`G${sym.flat}`,`F${sym.sharp}`,`G${sym.accnull}`,`A${sym.flat}`,`G${sym.sharp}`,`A${sym.accnull}`,`B${sym.flat}`,`A${sym.sharp}`,`B${sym.accnull}`]},
|
sym:[`C${sym.accnull}`,`D${sym.flat}`,`C${sym.sharp}`,`D${sym.accnull}`,`E${sym.flat}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`G${sym.flat}`,`F${sym.sharp}`,`G${sym.accnull}`,`A${sym.flat}`,`G${sym.sharp}`,`A${sym.accnull}`,`B${sym.flat}`,`A${sym.sharp}`,`B${sym.accnull}`]},
|
||||||
190:{index:190,name:"19-TET", table:[0x0,0xD8,0x1AF,0x287,0x35E,0x436,0x50D,0x5E5,0x6BD,0x794,0x86C,0x943,0xA1B,0xAF3,0xBCA,0xCA2,0xD79,0xE51,0xF28],interval:0x1000,
|
190:{index:190,name:"19-TET",table:[0x0,0xD8,0x1AF,0x287,0x35E,0x436,0x50D,0x5E5,0x6BD,0x794,0x86C,0x943,0xA1B,0xAF3,0xBCA,0xCA2,0xD79,0xE51,0xF28],interval:0x1000,t:'m',
|
||||||
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.flat}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.flat}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.flat}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.flat}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.flat}`,`B${sym.accnull}`,`B${sym.sharp}`]},
|
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.flat}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.flat}`,`E${sym.accnull}`,`E${sym.sharp}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.flat}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.flat}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.flat}`,`B${sym.accnull}`,`B${sym.sharp}`]},
|
||||||
220:{index:220,name:"22-TET", table:[0x0,0xBA,0x174,0x22F,0x2E9,0x3A3,0x45D,0x517,0x5D1,0x68C,0x746,0x800,0x8BA,0x974,0xA2F,0xAE9,0xBA3,0xC5D,0xD17,0xDD1,0xE8C,0xF46],interval:0x1000,
|
220:{index:220,name:"22-TET",table:[0x0,0xBA,0x174,0x22F,0x2E9,0x3A3,0x45D,0x517,0x5D1,0x68C,0x746,0x800,0x8BA,0x974,0xA2F,0xAE9,0xBA3,0xC5D,0xD17,0xDD1,0xE8C,0xF46],interval:0x1000,t:'m',
|
||||||
sym:[`C${sym.accnull}`,`C${sym.demisharp}`,`C${sym.sharp}`,`D${sym.demiflat}`,`D${sym.accnull}`,`D${sym.demisharp}`,`D${sym.sharp}`,`E${sym.demiflat}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.demisharp}`,`F${sym.sharp}`,`G${sym.demiflat}`,`G${sym.accnull}`,`G${sym.demisharp}`,`G${sym.sharp}`,`A${sym.demiflat}`,`A${sym.accnull}`,`A${sym.demisharp}`,`A${sym.sharp}`,`B${sym.demiflat}`,`B${sym.accnull}`]},
|
sym:[`C${sym.accnull}`,`C${sym.demisharp}`,`C${sym.sharp}`,`D${sym.demiflat}`,`D${sym.accnull}`,`D${sym.demisharp}`,`D${sym.sharp}`,`E${sym.demiflat}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.demisharp}`,`F${sym.sharp}`,`G${sym.demiflat}`,`G${sym.accnull}`,`G${sym.demisharp}`,`G${sym.sharp}`,`A${sym.demiflat}`,`A${sym.accnull}`,`A${sym.demisharp}`,`A${sym.sharp}`,`B${sym.demiflat}`,`B${sym.accnull}`]},
|
||||||
240:{index:240,name:"24-TET", table:[0x0,0xAB,0x155,0x200,0x2AB,0x355,0x400,0x4AB,0x555,0x600,0x6AB,0x755,0x800,0x8AB,0x955,0xA00,0xAAB,0xB55,0xC00,0xCAB,0xD55,0xE00,0xEAB,0xF55],interval:0x1000,
|
240:{index:240,name:"24-TET",table:[0x0,0xAB,0x155,0x200,0x2AB,0x355,0x400,0x4AB,0x555,0x600,0x6AB,0x755,0x800,0x8AB,0x955,0xA00,0xAAB,0xB55,0xC00,0xCAB,0xD55,0xE00,0xEAB,0xF55],interval:0x1000,t:'m',
|
||||||
sym:[`C${sym.accnull}`,`C${sym.demisharp}`,`C${sym.sharp}`,`D${sym.demiflat}`,`D${sym.accnull}`,`D${sym.demisharp}`,`D${sym.sharp}`,`E${sym.demiflat}`,`E${sym.accnull}`,`E${sym.demisharp}`,`F${sym.accnull}`,`F${sym.demisharp}`,`F${sym.sharp}`,`G${sym.demiflat}`,`G${sym.accnull}`,`G${sym.demisharp}`,`G${sym.sharp}`,`A${sym.demiflat}`,`A${sym.accnull}`,`A${sym.demisharp}`,`A${sym.sharp}`,`B${sym.demiflat}`,`B${sym.accnull}`,`B${sym.demisharp}`]},
|
sym:[`C${sym.accnull}`,`C${sym.demisharp}`,`C${sym.sharp}`,`D${sym.demiflat}`,`D${sym.accnull}`,`D${sym.demisharp}`,`D${sym.sharp}`,`E${sym.demiflat}`,`E${sym.accnull}`,`E${sym.demisharp}`,`F${sym.accnull}`,`F${sym.demisharp}`,`F${sym.sharp}`,`G${sym.demiflat}`,`G${sym.accnull}`,`G${sym.demisharp}`,`G${sym.sharp}`,`A${sym.demiflat}`,`A${sym.accnull}`,`A${sym.demisharp}`,`A${sym.sharp}`,`B${sym.demiflat}`,`B${sym.accnull}`,`B${sym.demisharp}`]},
|
||||||
310:{index:310,name:"31-TET", table:[0x0,0x84,0x108,0x18C,0x211,0x295,0x319,0x39D,0x421,0x4A5,0x529,0x5AD,0x632,0x6B6,0x73A,0x7BE,0x842,0x8C6,0x94A,0x9CE,0xA53,0xAD7,0xB5B,0xBDF,0xC63,0xCE7,0xD6B,0xDEF,0xE74,0xEF8,0xF7C],interval:0x1000,
|
310:{index:310,name:"31-TET",table:[0x0,0x84,0x108,0x18C,0x211,0x295,0x319,0x39D,0x421,0x4A5,0x529,0x5AD,0x632,0x6B6,0x73A,0x7BE,0x842,0x8C6,0x94A,0x9CE,0xA53,0xAD7,0xB5B,0xBDF,0xC63,0xCE7,0xD6B,0xDEF,0xE74,0xEF8,0xF7C],interval:0x1000,t:'m',
|
||||||
sym:[`C${sym.accnull}`,`C${sym.demisharp}`,`C${sym.sharp}`,`D${sym.flat}`,`D${sym.demiflat}`,`D${sym.accnull}`,`D${sym.demisharp}`,`D${sym.sharp}`,`E${sym.flat}`,`E${sym.demiflat}`,`E${sym.accnull}`,`E${sym.demisharp}`,`F${sym.demiflat}`,`F${sym.accnull}`,`F${sym.demisharp}`,`F${sym.sharp}`,`G${sym.flat}`,`G${sym.demiflat}`,`G${sym.accnull}`,`G${sym.demisharp}`,`G${sym.sharp}`,`A${sym.flat}`,`A${sym.demiflat}`,`A${sym.accnull}`,`A${sym.demisharp}`,`A${sym.sharp}`,`B${sym.flat}`,`B${sym.demiflat}`,`B${sym.accnull}`,`B${sym.demisharp}`,`C${sym.demiflat}`]},
|
sym:[`C${sym.accnull}`,`C${sym.demisharp}`,`C${sym.sharp}`,`D${sym.flat}`,`D${sym.demiflat}`,`D${sym.accnull}`,`D${sym.demisharp}`,`D${sym.sharp}`,`E${sym.flat}`,`E${sym.demiflat}`,`E${sym.accnull}`,`E${sym.demisharp}`,`F${sym.demiflat}`,`F${sym.accnull}`,`F${sym.demisharp}`,`F${sym.sharp}`,`G${sym.flat}`,`G${sym.demiflat}`,`G${sym.accnull}`,`G${sym.demisharp}`,`G${sym.sharp}`,`A${sym.flat}`,`A${sym.demiflat}`,`A${sym.accnull}`,`A${sym.demisharp}`,`A${sym.sharp}`,`B${sym.flat}`,`B${sym.demiflat}`,`B${sym.accnull}`,`B${sym.demisharp}`,`C${sym.demiflat}`]},
|
||||||
410:{index:410,name:"41-TET (Kite)", table:[0x0,0x64,0xC8,0x12C,0x190,0x1F4,0x257,0x2BB,0x31F,0x383,0x3E7,0x44B,0x4AF,0x513,0x577,0x5DB,0x63E,0x6A2,0x706,0x76A,0x7CE,0x832,0x896,0x8FA,0x95E,0x9C2,0xA25,0xA89,0xAED,0xB51,0xBB5,0xC19,0xC7D,0xCE1,0xD45,0xDA9,0xE0C,0xE70,0xED4,0xF38,0xF9C],interval:0x1000,
|
410:{index:410,name:"41-TET (Kite)",table:[0x0,0x64,0xC8,0x12C,0x190,0x1F4,0x257,0x2BB,0x31F,0x383,0x3E7,0x44B,0x4AF,0x513,0x577,0x5DB,0x63E,0x6A2,0x706,0x76A,0x7CE,0x832,0x896,0x8FA,0x95E,0x9C2,0xA25,0xA89,0xAED,0xB51,0xBB5,0xC19,0xC7D,0xCE1,0xD45,0xDA9,0xE0C,0xE70,0xED4,0xF38,0xF9C],interval:0x1000,t:'m',
|
||||||
sym:[`${BIGDOT}C-`,`${sym.uptick}C-`,`${sym.doubledntick}C${sym.csharp}`,`${sym.dntick}C${sym.csharp}`,`${BIGDOT}C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.dntick}D-`,`${BIGDOT}D-`,`${sym.uptick}D-`,`${sym.doubledntick}D${sym.csharp}`,`${sym.dntick}D${sym.csharp}`,`${BIGDOT}D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.dntick}E-`,`${BIGDOT}E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${BIGDOT}F-`,`${sym.uptick}F-`,`${sym.doubledntick}F${sym.csharp}`,`${sym.dntick}F${sym.csharp}`,`${BIGDOT}F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.dntick}G-`,`${BIGDOT}G-`,`${sym.uptick}G-`,`${sym.doubledntick}G${sym.csharp}`,`${sym.dntick}G${sym.csharp}`,`${BIGDOT}G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.dntick}A-`,`${BIGDOT}A-`,`${sym.uptick}A-`,`${sym.doubledntick}A${sym.csharp}`,`${sym.dntick}A${sym.csharp}`,`${BIGDOT}A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.dntick}B-`,`${BIGDOT}B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`]},
|
sym:[`${BIGDOT}C-`,`${sym.uptick}C-`,`${sym.doubledntick}C${sym.csharp}`,`${sym.dntick}C${sym.csharp}`,`${BIGDOT}C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.dntick}D-`,`${BIGDOT}D-`,`${sym.uptick}D-`,`${sym.doubledntick}D${sym.csharp}`,`${sym.dntick}D${sym.csharp}`,`${BIGDOT}D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.dntick}E-`,`${BIGDOT}E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${BIGDOT}F-`,`${sym.uptick}F-`,`${sym.doubledntick}F${sym.csharp}`,`${sym.dntick}F${sym.csharp}`,`${BIGDOT}F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.dntick}G-`,`${BIGDOT}G-`,`${sym.uptick}G-`,`${sym.doubledntick}G${sym.csharp}`,`${sym.dntick}G${sym.csharp}`,`${BIGDOT}G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.dntick}A-`,`${BIGDOT}A-`,`${sym.uptick}A-`,`${sym.doubledntick}A${sym.csharp}`,`${sym.dntick}A${sym.csharp}`,`${BIGDOT}A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.dntick}B-`,`${BIGDOT}B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`]},
|
||||||
530:{index:530,name:"53-TET (Kite)", table:[0x0,0x4D,0x9B,0xE8,0x135,0x182,0x1D0,0x21D,0x26A,0x2B8,0x305,0x352,0x39F,0x3ED,0x43A,0x487,0x4D5,0x522,0x56F,0x5BC,0x60A,0x657,0x6A4,0x6F2,0x73F,0x78C,0x7D9,0x827,0x874,0x8C1,0x90E,0x95C,0x9A9,0x9F6,0xA44,0xA91,0xADE,0xB2B,0xB79,0xBC6,0xC13,0xC61,0xCAE,0xCFB,0xD48,0xD96,0xDE3,0xE30,0xE7E,0xECB,0xF18,0xF65,0xFB3],interval:0x1000,
|
530:{index:530,name:"53-TET (Kite)",table:[0x0,0x4D,0x9B,0xE8,0x135,0x182,0x1D0,0x21D,0x26A,0x2B8,0x305,0x352,0x39F,0x3ED,0x43A,0x487,0x4D5,0x522,0x56F,0x5BC,0x60A,0x657,0x6A4,0x6F2,0x73F,0x78C,0x7D9,0x827,0x874,0x8C1,0x90E,0x95C,0x9A9,0x9F6,0xA44,0xA91,0xADE,0xB2B,0xB79,0xBC6,0xC13,0xC61,0xCAE,0xCFB,0xD48,0xD96,0xDE3,0xE30,0xE7E,0xECB,0xF18,0xF65,0xFB3],interval:0x1000,t:'m',
|
||||||
sym:[`${BIGDOT}C-`,`${sym.uptick}C-`,`${sym.doubleuptick}C-`,`${sym.doubledntick}C${sym.csharp}`,`${sym.dntick}C${sym.csharp}`,`${BIGDOT}C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.doubledntick}D-`,`${sym.dntick}D-`,`${BIGDOT}D-`,`${sym.uptick}D-`,`${sym.doubleuptick}D-`,`${sym.doubledntick}D${sym.csharp}`,`${sym.dntick}D${sym.csharp}`,`${BIGDOT}D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.doubledntick}E-`,`${sym.dntick}E-`,`${BIGDOT}E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${sym.dntick}F-`,`${BIGDOT}F-`,`${sym.uptick}F-`,`${sym.doubleuptick}F-`,`${sym.doubledntick}F${sym.csharp}`,`${sym.dntick}F${sym.csharp}`,`${BIGDOT}F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.doubledntick}G-`,`${sym.dntick}G-`,`${BIGDOT}G-`,`${sym.uptick}G-`,`${sym.doubleuptick}G-`,`${sym.doubledntick}G${sym.csharp}`,`${sym.dntick}G${sym.csharp}`,`${BIGDOT}G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.doubledntick}A-`,`${sym.dntick}A-`,`${BIGDOT}A-`,`${sym.uptick}A-`,`${sym.doubleuptick}A-`,`${sym.doubledntick}A${sym.csharp}`,`${sym.dntick}A${sym.csharp}`,`${BIGDOT}A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.doubledntick}B-`,`${sym.dntick}B-`,`${BIGDOT}B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`,`${sym.dntick}C-`]},
|
sym:[`${BIGDOT}C-`,`${sym.uptick}C-`,`${sym.doubleuptick}C-`,`${sym.doubledntick}C${sym.csharp}`,`${sym.dntick}C${sym.csharp}`,`${BIGDOT}C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.doubledntick}D-`,`${sym.dntick}D-`,`${BIGDOT}D-`,`${sym.uptick}D-`,`${sym.doubleuptick}D-`,`${sym.doubledntick}D${sym.csharp}`,`${sym.dntick}D${sym.csharp}`,`${BIGDOT}D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.doubledntick}E-`,`${sym.dntick}E-`,`${BIGDOT}E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${sym.dntick}F-`,`${BIGDOT}F-`,`${sym.uptick}F-`,`${sym.doubleuptick}F-`,`${sym.doubledntick}F${sym.csharp}`,`${sym.dntick}F${sym.csharp}`,`${BIGDOT}F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.doubledntick}G-`,`${sym.dntick}G-`,`${BIGDOT}G-`,`${sym.uptick}G-`,`${sym.doubleuptick}G-`,`${sym.doubledntick}G${sym.csharp}`,`${sym.dntick}G${sym.csharp}`,`${BIGDOT}G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.doubledntick}A-`,`${sym.dntick}A-`,`${BIGDOT}A-`,`${sym.uptick}A-`,`${sym.doubleuptick}A-`,`${sym.doubledntick}A${sym.csharp}`,`${sym.dntick}A${sym.csharp}`,`${BIGDOT}A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.doubledntick}B-`,`${sym.dntick}B-`,`${BIGDOT}B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`,`${sym.dntick}C-`]},
|
||||||
531:{index:531,name:"53-TET (Pythagorean)", table:[0x0,0x4D,0x9B,0xE8,0x135,0x182,0x1D0,0x21D,0x26A,0x2B8,0x305,0x352,0x39F,0x3ED,0x43A,0x487,0x4D5,0x522,0x56F,0x5BC,0x60A,0x657,0x6A4,0x6F2,0x73F,0x78C,0x7D9,0x827,0x874,0x8C1,0x90E,0x95C,0x9A9,0x9F6,0xA44,0xA91,0xADE,0xB2B,0xB79,0xBC6,0xC13,0xC61,0xCAE,0xCFB,0xD48,0xD96,0xDE3,0xE30,0xE7E,0xECB,0xF18,0xF65,0xFB3],interval:0x1000,
|
531:{index:531,name:"53-TET (Pythagorean)",table:[0x0,0x4D,0x9B,0xE8,0x135,0x182,0x1D0,0x21D,0x26A,0x2B8,0x305,0x352,0x39F,0x3ED,0x43A,0x487,0x4D5,0x522,0x56F,0x5BC,0x60A,0x657,0x6A4,0x6F2,0x73F,0x78C,0x7D9,0x827,0x874,0x8C1,0x90E,0x95C,0x9A9,0x9F6,0xA44,0xA91,0xADE,0xB2B,0xB79,0xBC6,0xC13,0xC61,0xCAE,0xCFB,0xD48,0xD96,0xDE3,0xE30,0xE7E,0xECB,0xF18,0xF65,0xFB3],interval:0x1000,t:'m',
|
||||||
sym:[`C${sym.accnull}`,`B${sym.sharp}`,`A${sym.triplesharp}`,`E${sym.tripleflat}`,`D${sym.flat}`,`C${sym.sharp}`,`B${sym.doublesharp}`,`F${sym.tripleflat}`,`E${sym.doubleflat}`,`D${sym.accnull}`,`C${sym.doublesharp}`,`B${sym.triplesharp}`,`F${sym.doubleflat}`,`E${sym.flat}`,`D${sym.sharp}`,`C${sym.triplesharp}`,`G${sym.tripleflat}`,`F${sym.flat}`,`E${sym.accnull}`,`D${sym.doublesharp}`,`C${sym.quadsharp}`,`G${sym.doubleflat}`,`F${sym.accnull}`,`E${sym.sharp}`,`D${sym.triplesharp}`,`A${sym.tripleflat}`,`G${sym.flat}`,`F${sym.sharp}`,`E${sym.doublesharp}`,`D${sym.quadsharp}`,`A${sym.doubleflat}`,`G${sym.accnull}`,`F${sym.doublesharp}`,`E${sym.triplesharp}`,`B${sym.tripleflat}`,`A${sym.flat}`,`G${sym.sharp}`,`F${sym.triplesharp}`,`C${sym.tripleflat}`,`B${sym.doubleflat}`,`A${sym.accnull}`,`G${sym.doublesharp}`,`F${sym.quadsharp}`,`C${sym.doubleflat}`,`B${sym.flat}`,`A${sym.sharp}`,`G${sym.triplesharp}`,`D${sym.tripleflat}`,`C${sym.flat}`,`B${sym.accnull}`,`A${sym.doublesharp}`,`G${sym.quadsharp}`,`D${sym.doubleflat}`]},
|
sym:[`C${sym.accnull}`,`B${sym.sharp}`,`A${sym.triplesharp}`,`E${sym.tripleflat}`,`D${sym.flat}`,`C${sym.sharp}`,`B${sym.doublesharp}`,`F${sym.tripleflat}`,`E${sym.doubleflat}`,`D${sym.accnull}`,`C${sym.doublesharp}`,`B${sym.triplesharp}`,`F${sym.doubleflat}`,`E${sym.flat}`,`D${sym.sharp}`,`C${sym.triplesharp}`,`G${sym.tripleflat}`,`F${sym.flat}`,`E${sym.accnull}`,`D${sym.doublesharp}`,`C${sym.quadsharp}`,`G${sym.doubleflat}`,`F${sym.accnull}`,`E${sym.sharp}`,`D${sym.triplesharp}`,`A${sym.tripleflat}`,`G${sym.flat}`,`F${sym.sharp}`,`E${sym.doublesharp}`,`D${sym.quadsharp}`,`A${sym.doubleflat}`,`G${sym.accnull}`,`F${sym.doublesharp}`,`E${sym.triplesharp}`,`B${sym.tripleflat}`,`A${sym.flat}`,`G${sym.sharp}`,`F${sym.triplesharp}`,`C${sym.tripleflat}`,`B${sym.doubleflat}`,`A${sym.accnull}`,`G${sym.doublesharp}`,`F${sym.quadsharp}`,`C${sym.doubleflat}`,`B${sym.flat}`,`A${sym.sharp}`,`G${sym.triplesharp}`,`D${sym.tripleflat}`,`C${sym.flat}`,`B${sym.accnull}`,`A${sym.doublesharp}`,`G${sym.quadsharp}`,`D${sym.doubleflat}`]},
|
||||||
960:{index:960,name:"96-TET (Kite)", table:[0x0,0x2B,0x55,0x80,0xAB,0xD5,0x100,0x12B,0x155,0x180,0x1AB,0x1D5,0x200,0x22B,0x255,0x280,0x2AB,0x2D5,0x300,0x32B,0x355,0x380,0x3AB,0x3D5,0x400,0x42B,0x455,0x480,0x4AB,0x4D5,0x500,0x52B,0x555,0x580,0x5AB,0x5D5,0x600,0x62B,0x655,0x680,0x6AB,0x6D5,0x700,0x72B,0x755,0x780,0x7AB,0x7D5,0x800,0x82B,0x855,0x880,0x8AB,0x8D5,0x900,0x92B,0x955,0x980,0x9AB,0x9D5,0xA00,0xA2B,0xA55,0xA80,0xAAB,0xAD5,0xB00,0xB2B,0xB55,0xB80,0xBAB,0xBD5,0xC00,0xC2B,0xC55,0xC80,0xCAB,0xCD5,0xD00,0xD2B,0xD55,0xD80,0xDAB,0xDD5,0xE00,0xE2B,0xE55,0xE80,0xEAB,0xED5,0xF00,0xF2B,0xF55,0xF80,0xFAB,0xFD5],interval:0x1000,
|
960:{index:960,name:"96-TET (Kite)",table:[0x0,0x2B,0x55,0x80,0xAB,0xD5,0x100,0x12B,0x155,0x180,0x1AB,0x1D5,0x200,0x22B,0x255,0x280,0x2AB,0x2D5,0x300,0x32B,0x355,0x380,0x3AB,0x3D5,0x400,0x42B,0x455,0x480,0x4AB,0x4D5,0x500,0x52B,0x555,0x580,0x5AB,0x5D5,0x600,0x62B,0x655,0x680,0x6AB,0x6D5,0x700,0x72B,0x755,0x780,0x7AB,0x7D5,0x800,0x82B,0x855,0x880,0x8AB,0x8D5,0x900,0x92B,0x955,0x980,0x9AB,0x9D5,0xA00,0xA2B,0xA55,0xA80,0xAAB,0xAD5,0xB00,0xB2B,0xB55,0xB80,0xBAB,0xBD5,0xC00,0xC2B,0xC55,0xC80,0xCAB,0xCD5,0xD00,0xD2B,0xD55,0xD80,0xDAB,0xDD5,0xE00,0xE2B,0xE55,0xE80,0xEAB,0xED5,0xF00,0xF2B,0xF55,0xF80,0xFAB,0xFD5],interval:0x1000,t:'m',
|
||||||
sym:[`${BIGDOT}C-`,`${sym.uptick}C-`,`${sym.doubleuptick}C-`,`${sym.dntick}C${sym.cdemisharp}`,`${BIGDOT}C${sym.cdemisharp}`,`${sym.uptick}C${sym.cdemisharp}`,`${sym.doubleuptick}C${sym.cdemisharp}`,`${sym.dntick}C${sym.csharp}`,`${BIGDOT}C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.doubleuptick}C${sym.csharp}`,`${sym.dntick}D${sym.cdemiflat}`,`${BIGDOT}D${sym.cdemiflat}`,`${sym.uptick}D${sym.cdemiflat}`,`${sym.doubleuptick}D${sym.cdemiflat}`,`${sym.dntick}D-`,`${BIGDOT}D-`,`${sym.uptick}D-`,`${sym.doubleuptick}D-`,`${sym.dntick}D${sym.cdemisharp}`,`${BIGDOT}D${sym.cdemisharp}`,`${sym.uptick}D${sym.cdemisharp}`,`${sym.doubleuptick}D${sym.cdemisharp}`,`${sym.dntick}D${sym.csharp}`,`${BIGDOT}D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.doubleuptick}D${sym.csharp}`,`${sym.dntick}E${sym.cdemiflat}`,`${BIGDOT}E${sym.cdemiflat}`,`${sym.uptick}E${sym.cdemiflat}`,`${sym.doubleuptick}E${sym.cdemiflat}`,`${sym.dntick}E-`,`${BIGDOT}E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${sym.dntick}E${sym.cdemisharp}`,`${BIGDOT}E${sym.cdemisharp}`,`${sym.uptick}E${sym.cdemisharp}`,`${sym.doubleuptick}E${sym.cdemisharp}`,`${sym.dntick}F-`,`${BIGDOT}F-`,`${sym.uptick}F-`,`${sym.doubleuptick}F-`,`${sym.dntick}F${sym.cdemisharp}`,`${BIGDOT}F${sym.cdemisharp}`,`${sym.uptick}F${sym.cdemisharp}`,`${sym.doubleuptick}F${sym.cdemisharp}`,`${sym.dntick}F${sym.csharp}`,`${BIGDOT}F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.doubleuptick}F${sym.csharp}`,`${sym.dntick}G${sym.cdemiflat}`,`${BIGDOT}G${sym.cdemiflat}`,`${sym.uptick}G${sym.cdemiflat}`,`${sym.doubleuptick}G${sym.cdemiflat}`,`${sym.dntick}G-`,`${BIGDOT}G-`,`${sym.uptick}G-`,`${sym.doubleuptick}G-`,`${sym.dntick}G${sym.cdemisharp}`,`${BIGDOT}G${sym.cdemisharp}`,`${sym.uptick}G${sym.cdemisharp}`,`${sym.doubleuptick}G${sym.cdemisharp}`,`${sym.dntick}G${sym.csharp}`,`${BIGDOT}G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.doubleuptick}G${sym.csharp}`,`${sym.dntick}A${sym.cdemiflat}`,`${BIGDOT}A${sym.cdemiflat}`,`${sym.uptick}A${sym.cdemiflat}`,`${sym.doubleuptick}A${sym.cdemiflat}`,`${sym.dntick}A-`,`${BIGDOT}A-`,`${sym.uptick}A-`,`${sym.doubleuptick}A-`,`${sym.dntick}A${sym.cdemisharp}`,`${BIGDOT}A${sym.cdemisharp}`,`${sym.uptick}A${sym.cdemisharp}`,`${sym.doubleuptick}A${sym.cdemisharp}`,`${sym.dntick}A${sym.csharp}`,`${BIGDOT}A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.doubleuptick}A${sym.csharp}`,`${sym.dntick}B${sym.cdemiflat}`,`${BIGDOT}B${sym.cdemiflat}`,`${sym.uptick}B${sym.cdemiflat}`,`${sym.doubleuptick}B${sym.cdemiflat}`,`${sym.dntick}B-`,`${BIGDOT}B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`,`${sym.dntick}B${sym.cdemisharp}`,`${BIGDOT}B${sym.cdemisharp}`,`${sym.uptick}B${sym.cdemisharp}`,`${sym.doubleuptick}B${sym.cdemisharp}`,`${sym.dntick}C-`]},
|
sym:[`${BIGDOT}C-`,`${sym.uptick}C-`,`${sym.doubleuptick}C-`,`${sym.dntick}C${sym.cdemisharp}`,`${BIGDOT}C${sym.cdemisharp}`,`${sym.uptick}C${sym.cdemisharp}`,`${sym.doubleuptick}C${sym.cdemisharp}`,`${sym.dntick}C${sym.csharp}`,`${BIGDOT}C${sym.csharp}`,`${sym.uptick}C${sym.csharp}`,`${sym.doubleuptick}C${sym.csharp}`,`${sym.dntick}D${sym.cdemiflat}`,`${BIGDOT}D${sym.cdemiflat}`,`${sym.uptick}D${sym.cdemiflat}`,`${sym.doubleuptick}D${sym.cdemiflat}`,`${sym.dntick}D-`,`${BIGDOT}D-`,`${sym.uptick}D-`,`${sym.doubleuptick}D-`,`${sym.dntick}D${sym.cdemisharp}`,`${BIGDOT}D${sym.cdemisharp}`,`${sym.uptick}D${sym.cdemisharp}`,`${sym.doubleuptick}D${sym.cdemisharp}`,`${sym.dntick}D${sym.csharp}`,`${BIGDOT}D${sym.csharp}`,`${sym.uptick}D${sym.csharp}`,`${sym.doubleuptick}D${sym.csharp}`,`${sym.dntick}E${sym.cdemiflat}`,`${BIGDOT}E${sym.cdemiflat}`,`${sym.uptick}E${sym.cdemiflat}`,`${sym.doubleuptick}E${sym.cdemiflat}`,`${sym.dntick}E-`,`${BIGDOT}E-`,`${sym.uptick}E-`,`${sym.doubleuptick}E-`,`${sym.dntick}E${sym.cdemisharp}`,`${BIGDOT}E${sym.cdemisharp}`,`${sym.uptick}E${sym.cdemisharp}`,`${sym.doubleuptick}E${sym.cdemisharp}`,`${sym.dntick}F-`,`${BIGDOT}F-`,`${sym.uptick}F-`,`${sym.doubleuptick}F-`,`${sym.dntick}F${sym.cdemisharp}`,`${BIGDOT}F${sym.cdemisharp}`,`${sym.uptick}F${sym.cdemisharp}`,`${sym.doubleuptick}F${sym.cdemisharp}`,`${sym.dntick}F${sym.csharp}`,`${BIGDOT}F${sym.csharp}`,`${sym.uptick}F${sym.csharp}`,`${sym.doubleuptick}F${sym.csharp}`,`${sym.dntick}G${sym.cdemiflat}`,`${BIGDOT}G${sym.cdemiflat}`,`${sym.uptick}G${sym.cdemiflat}`,`${sym.doubleuptick}G${sym.cdemiflat}`,`${sym.dntick}G-`,`${BIGDOT}G-`,`${sym.uptick}G-`,`${sym.doubleuptick}G-`,`${sym.dntick}G${sym.cdemisharp}`,`${BIGDOT}G${sym.cdemisharp}`,`${sym.uptick}G${sym.cdemisharp}`,`${sym.doubleuptick}G${sym.cdemisharp}`,`${sym.dntick}G${sym.csharp}`,`${BIGDOT}G${sym.csharp}`,`${sym.uptick}G${sym.csharp}`,`${sym.doubleuptick}G${sym.csharp}`,`${sym.dntick}A${sym.cdemiflat}`,`${BIGDOT}A${sym.cdemiflat}`,`${sym.uptick}A${sym.cdemiflat}`,`${sym.doubleuptick}A${sym.cdemiflat}`,`${sym.dntick}A-`,`${BIGDOT}A-`,`${sym.uptick}A-`,`${sym.doubleuptick}A-`,`${sym.dntick}A${sym.cdemisharp}`,`${BIGDOT}A${sym.cdemisharp}`,`${sym.uptick}A${sym.cdemisharp}`,`${sym.doubleuptick}A${sym.cdemisharp}`,`${sym.dntick}A${sym.csharp}`,`${BIGDOT}A${sym.csharp}`,`${sym.uptick}A${sym.csharp}`,`${sym.doubleuptick}A${sym.csharp}`,`${sym.dntick}B${sym.cdemiflat}`,`${BIGDOT}B${sym.cdemiflat}`,`${sym.uptick}B${sym.cdemiflat}`,`${sym.doubleuptick}B${sym.cdemiflat}`,`${sym.dntick}B-`,`${BIGDOT}B-`,`${sym.uptick}B-`,`${sym.doubleuptick}B-`,`${sym.dntick}B${sym.cdemisharp}`,`${BIGDOT}B${sym.cdemisharp}`,`${sym.uptick}B${sym.cdemisharp}`,`${sym.doubleuptick}B${sym.cdemisharp}`,`${sym.dntick}C-`]},
|
||||||
/* 12-TET variations */
|
/* 12-TET variations */
|
||||||
120:{index:120,name:"12-TET", table:[0x0,0x155,0x2AB,0x400,0x555,0x6AB,0x800,0x955,0xAAB,0xC00,0xD55,0xEAB],interval:0x1000,
|
120:{index:120,name:"12-TET",table:[0x0,0x155,0x2AB,0x400,0x555,0x6AB,0x800,0x955,0xAAB,0xC00,0xD55,0xEAB],interval:0x1000,t:'d',
|
||||||
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
|
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
|
||||||
10121:{index:10121,name:"Pythagorean dim. 5th", table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x7D8,0x95C,0xA90,0xC14,0xD48,0xECC],interval:0x1000,
|
10121:{index:10121,name:"Pythagorean dim. 5th",table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x7D8,0x95C,0xA90,0xC14,0xD48,0xECC],interval:0x1000,t:'d',
|
||||||
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
|
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
|
||||||
10122:{index:10122,name:"Pythagorean aug. 4th", table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x828,0x95C,0xA90,0xC14,0xD48,0xECC],interval:0x1000,
|
10122:{index:10122,name:"Pythagorean aug. 4th",table:[0x0,0x134,0x2B8,0x3EC,0x570,0x6A4,0x828,0x95C,0xA90,0xC14,0xD48,0xECC],interval:0x1000,t:'d',
|
||||||
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
|
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`D${sym.sharp}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`G${sym.sharp}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
|
||||||
10123:{index:10123,name:"\u00FC\u00FD\u00FE (shi'er lu)", table:[0x0,0x184,0x2B8,0x43C,0x570,0x6F4,0x828,0x95C,0xAE0,0xC14,0xD98,0xECC],interval:0x1000,
|
10123:{index:10123,name:"\u00FC\u00FD\u00FE (shi'er lu)", table:[0x0,0x184,0x2B8,0x43C,0x570,0x6F4,0x828,0x95C,0xAE0,0xC14,0xD98,0xECC],interval:0x1000,t:'d',
|
||||||
sym:[` \u00E0\u00E1`,` \u00E2\u00E3`,` \u00E4\u00E5`,` \u00E6\u00E7`,` \u00E8\u00E9`,` \u00EA\u00EB`,` \u00EC\u00ED`,` \u00EE\u00EF`,` \u00F0\u00F1`,` \u00F2\u00F3`,` \u00F4\u00F5`,` \u00F6\u00F7`]},
|
sym:[` \u00E0\u00E1`,` \u00E2\u00E3`,` \u00E4\u00E5`,` \u00E6\u00E7`,` \u00E8\u00E9`,` \u00EA\u00EB`,` \u00EC\u00ED`,` \u00EE\u00EF`,` \u00F0\u00F1`,` \u00F2\u00F3`,` \u00F4\u00F5`,` \u00F6\u00F7`]},
|
||||||
/* non-octave */
|
/* non-octave */
|
||||||
35130:{index:35130,name:"Equal-Tempered Bohlen-Pierce", table:[0x0,0x1F3,0x3E7,0x5DA,0x7CE,0x9C1,0xBB4,0xDA8,0xF9B,0x118E,0x1382,0x1575,0x1769],interval:0x195C,
|
35130:{index:35130,name:"Equal-Tempered Bohlen-Pierce",table:[0x0,0x1F3,0x3E7,0x5DA,0x7CE,0x9C1,0xBB4,0xDA8,0xF9B,0x118E,0x1382,0x1575,0x1769],interval:0x195C,t:'M',
|
||||||
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`H${sym.accnull}`,`H${sym.sharp}`,`J${sym.accnull}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
|
sym:[`C${sym.accnull}`,`C${sym.sharp}`,`D${sym.accnull}`,`E${sym.accnull}`,`F${sym.accnull}`,`F${sym.sharp}`,`G${sym.accnull}`,`H${sym.accnull}`,`H${sym.sharp}`,`J${sym.accnull}`,`A${sym.accnull}`,`A${sym.sharp}`,`B${sym.accnull}`]},
|
||||||
|
|
||||||
|
|
||||||
@@ -267,7 +269,8 @@ const colEffOp = 220
|
|||||||
const colEffArg = 231
|
const colEffArg = 231
|
||||||
const colBackPtn = 255
|
const colBackPtn = 255
|
||||||
|
|
||||||
let PITCH_PRESET_IDX = 120 // TODO read from the Project Data section of the .taud
|
const PITCH_PRESET_IDX_DEFAULT = 120
|
||||||
|
let PITCH_PRESET_IDX = PITCH_PRESET_IDX_DEFAULT // TODO read from the Project Data section of the .taud
|
||||||
let beatDivPrimary = 4 // TODO read from the Project Data section of the .taud
|
let beatDivPrimary = 4 // TODO read from the Project Data section of the .taud
|
||||||
let beatDivSecondary = 16
|
let beatDivSecondary = 16
|
||||||
let hasUnsavedChanges = false
|
let hasUnsavedChanges = false
|
||||||
@@ -463,7 +466,7 @@ function retuneAllPatterns(newIdx, method) {
|
|||||||
for (let row = 0; row < ROWS_PER_PAT; row++) {
|
for (let row = 0; row < ROWS_PER_PAT; row++) {
|
||||||
const off = 8 * row
|
const off = 8 * row
|
||||||
const note = ptn[off] | (ptn[off+1] << 8)
|
const note = ptn[off] | (ptn[off+1] << 8)
|
||||||
if (note === 0xFFFF || note === 0xFFFE || note === 0x0000) continue
|
if (note === 0x0000 || note === 0x0001 || note === 0x0002 || (note >= 0x0010 && note <= 0x001F)) continue
|
||||||
// Use the full absolute pitch as tonic; the modular ops
|
// Use the full absolute pitch as tonic; the modular ops
|
||||||
// in _cadTension / _harmonicCost normalise it.
|
// in _cadTension / _harmonicCost normalise it.
|
||||||
tonic = note
|
tonic = note
|
||||||
@@ -473,7 +476,7 @@ function retuneAllPatterns(newIdx, method) {
|
|||||||
for (let row = 0; row < ROWS_PER_PAT; row++) {
|
for (let row = 0; row < ROWS_PER_PAT; row++) {
|
||||||
const off = 8 * row
|
const off = 8 * row
|
||||||
const note = ptn[off] | (ptn[off+1] << 8)
|
const note = ptn[off] | (ptn[off+1] << 8)
|
||||||
if (note === 0xFFFF || note === 0xFFFE || note === 0x0000) continue
|
if (note === 0x0000 || note === 0x0001 || note === 0x0002 || (note >= 0x0010 && note <= 0x001F)) continue
|
||||||
const origAbs = note
|
const origAbs = note
|
||||||
let newAbs
|
let newAbs
|
||||||
if ((method === 'delta' || method === 'cadence' || method === 'harmonic') && prevOrigAbs >= 0) {
|
if ((method === 'delta' || method === 'cadence' || method === 'harmonic') && prevOrigAbs >= 0) {
|
||||||
@@ -487,7 +490,7 @@ function retuneAllPatterns(newIdx, method) {
|
|||||||
for (let r = row + 1; r < ROWS_PER_PAT; r++) {
|
for (let r = row + 1; r < ROWS_PER_PAT; r++) {
|
||||||
const noff = 8 * r
|
const noff = 8 * r
|
||||||
const n = ptn[noff] | (ptn[noff+1] << 8)
|
const n = ptn[noff] | (ptn[noff+1] << 8)
|
||||||
if (n !== 0x0000) break
|
if (n !== 0x0001) break
|
||||||
duration++
|
duration++
|
||||||
}
|
}
|
||||||
lambda = 1 - Math.exp(-(duration - 1) / 4)
|
lambda = 1 - Math.exp(-(duration - 1) / 4)
|
||||||
@@ -555,9 +558,10 @@ Number.prototype.decD2 = function() {
|
|||||||
|
|
||||||
|
|
||||||
function noteToStr(note) {
|
function noteToStr(note) {
|
||||||
if (note === 0xFFFF) return sym.middot.repeat(4)
|
if (note === 0x0000) return sym.middot.repeat(4)
|
||||||
if (note === 0xFFFE) return sym.notecut
|
if (note === 0x0001) return sym.keyoff
|
||||||
if (note === 0x0000) return sym.keyoff
|
if (note === 0x0002) return sym.notecut
|
||||||
|
if (note >= 0x0010 && note <= 0x001F) return ('Int' + (note & 0xF).toString(16).toUpperCase()).padEnd(4)
|
||||||
const preset = pitchTablePresets[PITCH_PRESET_IDX]
|
const preset = pitchTablePresets[PITCH_PRESET_IDX]
|
||||||
if (preset.table.length === 0) return note.hex04()
|
if (preset.table.length === 0) return note.hex04()
|
||||||
const [period, offset] = decomposeNote(note, preset.interval)
|
const [period, offset] = decomposeNote(note, preset.interval)
|
||||||
@@ -653,7 +657,7 @@ const EMPTY_CELL = {
|
|||||||
sPanArg: sym.middot.repeat(2),
|
sPanArg: sym.middot.repeat(2),
|
||||||
sEffOp: sym.middot,
|
sEffOp: sym.middot,
|
||||||
sEffArg: sym.middot.repeat(4),
|
sEffArg: sym.middot.repeat(4),
|
||||||
_note: 0xFFFF, _effop: 0, _effarg: 0, _voleff: 0, _paneff: 0
|
_note: 0x0000, _effop: 0, _effarg: 0, _voleff: 0, _paneff: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawCellAt(y, x, cell, back) {
|
function drawCellAt(y, x, cell, back) {
|
||||||
@@ -689,7 +693,7 @@ function drawCellAtStyled(y, x, cell, back, style) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Styles 1 and 2: note-or-fx field (5 chars) starts on the border column [+ vol-or-pan (2 chars)]
|
// Styles 1 and 2: note-or-fx field (5 chars) starts on the border column [+ vol-or-pan (2 chars)]
|
||||||
const noteEmpty = (cell._note === 0xFFFF)
|
const noteEmpty = (cell._note === 0x0000)
|
||||||
const fxEmpty = (cell._effop === 0 && cell._effarg === 0)
|
const fxEmpty = (cell._effop === 0 && cell._effarg === 0)
|
||||||
const volEmpty = (cell._voleff === 0)
|
const volEmpty = (cell._voleff === 0)
|
||||||
const panEmpty = (cell._paneff === 0)
|
const panEmpty = (cell._paneff === 0)
|
||||||
@@ -864,6 +868,7 @@ function loadTaudSongList(filePath) {
|
|||||||
name: '',
|
name: '',
|
||||||
composer: '',
|
composer: '',
|
||||||
copyright: '',
|
copyright: '',
|
||||||
|
pitchPresetIdx: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -908,6 +913,8 @@ function loadTaudSongList(filePath) {
|
|||||||
const subStart = q + 5
|
const subStart = q + 5
|
||||||
if (subStart + subLen > qEnd) break
|
if (subStart + subLen > qEnd) break
|
||||||
// payload: notation(u16) + beat_pri(u8) + beat_sec(u8) + name\0 + composer\0 + copyright\0
|
// payload: notation(u16) + beat_pri(u8) + beat_sec(u8) + name\0 + composer\0 + copyright\0
|
||||||
|
const notation = (sys.peek(ptr + subStart) & 0xFF) |
|
||||||
|
((sys.peek(ptr + subStart + 1) & 0xFF) << 8)
|
||||||
let r = subStart + 4 // skip notation(2) + pri(1) + sec(1)
|
let r = subStart + 4 // skip notation(2) + pri(1) + sec(1)
|
||||||
const strs = []
|
const strs = []
|
||||||
while (strs.length < 3 && r < subStart + subLen) {
|
while (strs.length < 3 && r < subStart + subLen) {
|
||||||
@@ -920,6 +927,7 @@ function loadTaudSongList(filePath) {
|
|||||||
strs.push(s)
|
strs.push(s)
|
||||||
}
|
}
|
||||||
if (idx < numSongs) {
|
if (idx < numSongs) {
|
||||||
|
songs[idx].pitchPresetIdx = notation
|
||||||
if (strs[0] !== undefined) songs[idx].name = strs[0]
|
if (strs[0] !== undefined) songs[idx].name = strs[0]
|
||||||
if (strs[1] !== undefined) songs[idx].composer = strs[1]
|
if (strs[1] !== undefined) songs[idx].composer = strs[1]
|
||||||
if (strs[2] !== undefined) songs[idx].copyright = strs[2]
|
if (strs[2] !== undefined) songs[idx].copyright = strs[2]
|
||||||
@@ -1372,7 +1380,15 @@ function drawControlHint() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
const hintElemExternal = [['Tab','Panel'],['sep'],['!','Help']]
|
const hintElemExternal = [['Tab','Panel'],['sep'],['!','Help']]
|
||||||
let hintElems = [hintElemTimeline, hintElemOrders, hintElemPatterns, hintElemExternal, hintElemExternal, hintElemExternal, hintElemExternal]
|
const hintElemProject = [
|
||||||
|
[`\u008428u\u008429u`,'Nav'],
|
||||||
|
[`ent`,'Edit/Switch'],
|
||||||
|
['sep'],
|
||||||
|
['tab','Panel'],
|
||||||
|
['sep'],
|
||||||
|
['!','Help'],
|
||||||
|
]
|
||||||
|
let hintElems = [hintElemTimeline, hintElemOrders, hintElemPatterns, hintElemExternal, hintElemExternal, hintElemProject, hintElemExternal]
|
||||||
let hintElemPat = [hintElemEditNoteValue, hintElemEditInstValue, hintElemEditVolEff, hintElemEditPanEff, hintElemEditFxSym, hintElemEditFxVal]
|
let hintElemPat = [hintElemEditNoteValue, hintElemEditInstValue, hintElemEditVolEff, hintElemEditPanEff, hintElemEditFxSym, hintElemEditFxVal]
|
||||||
|
|
||||||
// erase current line
|
// erase current line
|
||||||
@@ -1530,8 +1546,8 @@ function drawVoiceDetail(isVerticalLayout = false, ptn = null, activeRow = -1, c
|
|||||||
if (cumState !== null && lowerH > 0) {
|
if (cumState !== null && lowerH > 0) {
|
||||||
const _apo = Math.abs(cumState.pitchOff)
|
const _apo = Math.abs(cumState.pitchOff)
|
||||||
const _psgn = cumState.pitchOff > 0 ? '+' : cumState.pitchOff < 0 ? '-' : ' '
|
const _psgn = cumState.pitchOff > 0 ? '+' : cumState.pitchOff < 0 ? '-' : ' '
|
||||||
const _absN = (cumState.lastNote !== 0xFFFF && cumState.pitchOff !== 0)
|
const _absN = (cumState.lastNote !== 0x0000 && cumState.pitchOff !== 0)
|
||||||
? noteToStr(Math.max(0, Math.min(0xFFFE, cumState.lastNote + cumState.pitchOff))) + ' '
|
? noteToStr(Math.max(0x20, Math.min(0xFFFF, cumState.lastNote + cumState.pitchOff))) + ' '
|
||||||
: ''
|
: ''
|
||||||
const _clipNm = ['clamp','fold','wrap','wrap'][cumState.clipMode]
|
const _clipNm = ['clamp','fold','wrap','wrap'][cumState.clipMode]
|
||||||
const _bcStr = (cumState.bitcrushDepth === 0 && cumState.bitcrushSkip === 0)
|
const _bcStr = (cumState.bitcrushDepth === 0 && cumState.bitcrushSkip === 0)
|
||||||
@@ -1737,21 +1753,27 @@ if (fullPathObj === undefined) {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
const logofile = files.open("A:/tvdos/bin/tauthdr.r8")
|
const logofile = files.open("A:"+_TVDOS.variables.DOSDIR+"/bin/tauthdr.r8")
|
||||||
const logoBytes = logofile.bread(); logofile.close()
|
const logoBytes = logofile.bread(); logofile.close()
|
||||||
const logoTexture = new gl.Texture(92, 14, logoBytes)
|
const logoTexture = new gl.Texture(92, 14, logoBytes)
|
||||||
const buttonfile = files.open("A:/tvdos/bin/tautbtn.r8")
|
const buttonfile = files.open("A:"+_TVDOS.variables.DOSDIR+"/bin/tautbtn.r8")
|
||||||
const buttonBytes = buttonfile.bread(); buttonfile.close()
|
const buttonBytes = buttonfile.bread(); buttonfile.close()
|
||||||
const buttonTexture = new gl.Texture(2, 28, buttonBytes)
|
const buttonTexture = new gl.Texture(2, 28, buttonBytes)
|
||||||
//const buttonNullfile = files.open("A:/tvdos/bin/tautbtn0.r8")
|
//const buttonNullfile = files.open("A:"+_TVDOS.variables.DOSDIR+"/bin/tautbtn0.r8")
|
||||||
//const buttonNullBytes = buttonNullfile.bread(); buttonNullfile.close()
|
//const buttonNullBytes = buttonNullfile.bread(); buttonNullfile.close()
|
||||||
//const buttonNullTexture = new gl.Texture(35, 28, buttonNullBytes)
|
//const buttonNullTexture = new gl.Texture(35, 28, buttonNullBytes)
|
||||||
|
|
||||||
font.setLowRom("A:/tvdos/bin/tautfont_low.chr")
|
font.setLowRom("A:"+_TVDOS.variables.DOSDIR+"/bin/tautfont_low.chr")
|
||||||
font.setHighRom("A:/tvdos/bin/tautfont_high.chr")
|
font.setHighRom("A:"+_TVDOS.variables.DOSDIR+"/bin/tautfont_high.chr")
|
||||||
const songsMeta = loadTaudSongList(fullPathObj.full)
|
const songsMeta = loadTaudSongList(fullPathObj.full)
|
||||||
let currentSongIndex = 0
|
let currentSongIndex = 0
|
||||||
let projectSongCursor = 0
|
// Unified cursor: 0..PROJ_META_ROWS_COUNT-1 = editable meta rows (Flags / GVol / MVol);
|
||||||
|
// >= PROJ_META_ROWS_COUNT = song list, songIdx = projectCursor - PROJ_META_ROWS_COUNT
|
||||||
|
let projectCursor = 0
|
||||||
|
const PROJ_META_ROWS_COUNT = 3
|
||||||
|
const PROJ_META_FLAGS = 0
|
||||||
|
const PROJ_META_GVOL = 1
|
||||||
|
const PROJ_META_MVOL = 2
|
||||||
let song = loadTaud(fullPathObj.full, currentSongIndex)
|
let song = loadTaud(fullPathObj.full, currentSongIndex)
|
||||||
|
|
||||||
const voiceMutes = new Array(NUM_VOICES).fill(false)
|
const voiceMutes = new Array(NUM_VOICES).fill(false)
|
||||||
@@ -1792,6 +1814,12 @@ function switchSong(newIndex) {
|
|||||||
currentSongIndex = newIndex
|
currentSongIndex = newIndex
|
||||||
song = loadTaud(fullPathObj.full, newIndex)
|
song = loadTaud(fullPathObj.full, newIndex)
|
||||||
|
|
||||||
|
const newPitchIdx = songsMeta.songs[newIndex].pitchPresetIdx
|
||||||
|
PITCH_PRESET_IDX = (newPitchIdx != null && pitchTablePresets[newPitchIdx])
|
||||||
|
? newPitchIdx
|
||||||
|
: PITCH_PRESET_IDX_DEFAULT
|
||||||
|
rebuildPitchLut()
|
||||||
|
|
||||||
taud.uploadTaudFile(fullPathObj.full, newIndex, PLAYHEAD)
|
taud.uploadTaudFile(fullPathObj.full, newIndex, PLAYHEAD)
|
||||||
patternsOutOfSync = false
|
patternsOutOfSync = false
|
||||||
audio.setMasterVolume(PLAYHEAD, 255)
|
audio.setMasterVolume(PLAYHEAD, 255)
|
||||||
@@ -2172,42 +2200,31 @@ function visWidth(s) {
|
|||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Centre-anchored scroll: keep `sel` at the middle row of a `vis`-row viewport,
|
||||||
|
// clamped at the list's top and bottom. Returns the new scroll offset.
|
||||||
|
function centerScroll(sel, scroll, vis, total) {
|
||||||
|
if (sel < scroll) scroll = sel
|
||||||
|
if (sel < scroll + (vis >>> 1) && scroll > 0) scroll = sel - (vis >>> 1)
|
||||||
|
if (sel >= scroll + ((vis + 1) >>> 1)) scroll = sel - ((vis + 1) >>> 1) + 1
|
||||||
|
if (scroll < 0) scroll = 0
|
||||||
|
if (scroll + vis > total) scroll = Math.max(0, total - vis)
|
||||||
|
return scroll
|
||||||
|
}
|
||||||
|
|
||||||
function clampPatternIdx() {
|
function clampPatternIdx() {
|
||||||
if (song.numPats === 0) { patternIdx = 0; patternListScroll = 0; return }
|
if (song.numPats === 0) { patternIdx = 0; patternListScroll = 0; return }
|
||||||
if (patternIdx < 0) patternIdx = 0
|
if (patternIdx < 0) patternIdx = 0
|
||||||
if (patternIdx >= song.numPats) patternIdx = song.numPats - 1
|
if (patternIdx >= song.numPats) patternIdx = song.numPats - 1
|
||||||
if (patternIdx < patternListScroll) patternListScroll = patternIdx
|
patternListScroll = centerScroll(patternIdx, patternListScroll, PTNVIEW_HEIGHT, song.numPats)
|
||||||
if (patternIdx < patternListScroll + (PTNVIEW_HEIGHT >>> 1) && patternListScroll > 0)
|
|
||||||
patternListScroll = patternIdx - (PTNVIEW_HEIGHT >>> 1)
|
|
||||||
if (patternIdx >= patternListScroll + ((PTNVIEW_HEIGHT + 1) >>> 1))
|
|
||||||
patternListScroll = patternIdx - ((PTNVIEW_HEIGHT + 1) >>> 1) + 1
|
|
||||||
if (patternListScroll < 0) patternListScroll = 0
|
|
||||||
if (patternListScroll + PTNVIEW_HEIGHT > song.numPats)
|
|
||||||
patternListScroll = Math.max(0, song.numPats - PTNVIEW_HEIGHT)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollPatternGridTo(row) {
|
function scrollPatternGridTo(row) {
|
||||||
if (row < patternGridScroll) patternGridScroll = row
|
patternGridScroll = centerScroll(row, patternGridScroll, PTNVIEW_HEIGHT, ROWS_PER_PAT)
|
||||||
if (row < patternGridScroll + (PTNVIEW_HEIGHT >>> 1) && patternGridScroll > 0)
|
|
||||||
patternGridScroll = row - (PTNVIEW_HEIGHT >>> 1)
|
|
||||||
if (row >= patternGridScroll + ((PTNVIEW_HEIGHT + 1) >>> 1))
|
|
||||||
patternGridScroll = row - ((PTNVIEW_HEIGHT + 1) >>> 1) + 1
|
|
||||||
if (patternGridScroll < 0) patternGridScroll = 0
|
|
||||||
if (patternGridScroll + PTNVIEW_HEIGHT > ROWS_PER_PAT)
|
|
||||||
patternGridScroll = Math.max(0, ROWS_PER_PAT - PTNVIEW_HEIGHT)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollOrdersTo(ci) {
|
function scrollOrdersTo(ci) {
|
||||||
const maxCue = song.lastActiveCue < 0 ? 0 : song.lastActiveCue
|
const maxCue = song.lastActiveCue < 0 ? 0 : song.lastActiveCue
|
||||||
const total = maxCue + 1
|
ordersScroll = centerScroll(ci, ordersScroll, PTNVIEW_HEIGHT, maxCue + 1)
|
||||||
if (ci < ordersScroll) ordersScroll = ci
|
|
||||||
if (ci < ordersScroll + (PTNVIEW_HEIGHT >>> 1) && ordersScroll > 0)
|
|
||||||
ordersScroll = ci - (PTNVIEW_HEIGHT >>> 1)
|
|
||||||
if (ci >= ordersScroll + ((PTNVIEW_HEIGHT + 1) >>> 1))
|
|
||||||
ordersScroll = ci - ((PTNVIEW_HEIGHT + 1) >>> 1) + 1
|
|
||||||
if (ordersScroll < 0) ordersScroll = 0
|
|
||||||
if (ordersScroll + PTNVIEW_HEIGHT > total)
|
|
||||||
ordersScroll = Math.max(0, total - PTNVIEW_HEIGHT)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampPatternGrid() {
|
function clampPatternGrid() {
|
||||||
@@ -2240,7 +2257,7 @@ function simulateRowState(ptnDat, uptoRow) {
|
|||||||
0x0000, 0x0023, 0x0046, 0x0074, 0x0098, 0x00C8, 0x00F9, 0x0110
|
0x0000, 0x0023, 0x0046, 0x0074, 0x0098, 0x00C8, 0x00F9, 0x0110
|
||||||
]
|
]
|
||||||
|
|
||||||
let lastNote = 0xFFFF, lastInst = 0
|
let lastNote = 0x0000, lastInst = 0
|
||||||
let volAbs = 0x3F // 6-bit per-note volume (engine: noteVolume axis;
|
let volAbs = 0x3F // 6-bit per-note volume (engine: noteVolume axis;
|
||||||
// M / N's per-channel axis is not modelled here)
|
// M / N's per-channel axis is not modelled here)
|
||||||
let panAbs = 0x80 // 8-bit channel pan (engine width); centre = $80
|
let panAbs = 0x80 // 8-bit channel pan (engine width); centre = $80
|
||||||
@@ -2293,8 +2310,8 @@ function simulateRowState(ptnDat, uptoRow) {
|
|||||||
// not tracked by this simulator. The simulator approximates the seed
|
// not tracked by this simulator. The simulator approximates the seed
|
||||||
// as 0x3F (legacy fallback) — see the longer note below.
|
// as 0x3F (legacy fallback) — see the longer note below.
|
||||||
let reloadDefaultVol = false
|
let reloadDefaultVol = false
|
||||||
if (note !== 0xFFFF && note !== 0xFFFE) {
|
if (note !== 0x0000 && note !== 0x0002 && !(note >= 0x0010 && note <= 0x001F)) {
|
||||||
if (note === 0x0000) {
|
if (note === 0x0001) {
|
||||||
// key-off; sample stays referenced
|
// key-off; sample stays referenced
|
||||||
} else if (isGRow) {
|
} else if (isGRow) {
|
||||||
portaTarget = note
|
portaTarget = note
|
||||||
@@ -2417,7 +2434,7 @@ function simulateRowState(ptnDat, uptoRow) {
|
|||||||
}
|
}
|
||||||
else if (effop === OP_G) {
|
else if (effop === OP_G) {
|
||||||
if (effarg !== 0) memG = effarg
|
if (effarg !== 0) memG = effarg
|
||||||
if (portaTarget !== -1 && memG !== 0 && lastNote !== 0xFFFF) {
|
if (portaTarget !== -1 && memG !== 0 && lastNote !== 0x0000) {
|
||||||
const curPitch = lastNote + pitchOff
|
const curPitch = lastNote + pitchOff
|
||||||
const diff = portaTarget - curPitch
|
const diff = portaTarget - curPitch
|
||||||
if (diff !== 0) {
|
if (diff !== 0) {
|
||||||
@@ -2703,6 +2720,12 @@ function makeExternalPanelDraw(progName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Row offsets (within the meta block at the top of the Project panel) of the editable rows.
|
||||||
|
const PROJ_META_ROW_FLAGS = 5
|
||||||
|
const PROJ_META_ROW_GVOL = 6
|
||||||
|
const PROJ_META_ROW_MVOL = 7
|
||||||
|
const PROJ_META_VALUE_X = 12
|
||||||
|
|
||||||
function drawProjectContents(wo) {
|
function drawProjectContents(wo) {
|
||||||
fillLine(PTNVIEW_OFFSET_Y - 1, colVoiceHdr, 255)
|
fillLine(PTNVIEW_OFFSET_Y - 1, colVoiceHdr, 255)
|
||||||
for (let y = PTNVIEW_OFFSET_Y; y < SCRH; y++) fillLine(y, colBackPtn, 255)
|
for (let y = PTNVIEW_OFFSET_Y; y < SCRH; y++) fillLine(y, colBackPtn, 255)
|
||||||
@@ -2720,15 +2743,29 @@ function drawProjectContents(wo) {
|
|||||||
Cues: `${song.lastActiveCue}/1024 ($${song.lastActiveCue.hex03()})`,
|
Cues: `${song.lastActiveCue}/1024 ($${song.lastActiveCue.hex03()})`,
|
||||||
Notation: pitchTablePresets[PITCH_PRESET_IDX].name,
|
Notation: pitchTablePresets[PITCH_PRESET_IDX].name,
|
||||||
Flags: `${flagStrSelected.join(', ')} ($${mixerflag.hex02()})`,
|
Flags: `${flagStrSelected.join(', ')} ($${mixerflag.hex02()})`,
|
||||||
GlobalVol: initialGlobalVolume,
|
GlobalVol: `$${initialGlobalVolume.hex02()}`,
|
||||||
MixingVol: initialMixingVolume
|
MixingVol: `$${initialMixingVolume.hex02()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const editableMap = {
|
||||||
|
[PROJ_META_ROW_FLAGS]: PROJ_META_FLAGS,
|
||||||
|
[PROJ_META_ROW_GVOL] : PROJ_META_GVOL,
|
||||||
|
[PROJ_META_ROW_MVOL] : PROJ_META_MVOL,
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.entries(projMeta).forEach(([key, value], index) => {
|
Object.entries(projMeta).forEach(([key, value], index) => {
|
||||||
con.move(PTNVIEW_OFFSET_Y + index, 2)
|
con.move(PTNVIEW_OFFSET_Y + index, 2)
|
||||||
con.color_pair(colStatus, 255); print(key)
|
con.color_pair(colStatus, 255); print(key)
|
||||||
con.move(PTNVIEW_OFFSET_Y + index, 12)
|
con.move(PTNVIEW_OFFSET_Y + index, PROJ_META_VALUE_X)
|
||||||
con.color_pair(colVoiceHdr, colBLACK); print(value)
|
const isEditable = (index in editableMap)
|
||||||
|
const isSelected = isEditable && projectCursor === editableMap[index]
|
||||||
|
if (isSelected) {
|
||||||
|
con.color_pair(colWHITE, colHighlight); print(' ' + value + ' ')
|
||||||
|
} else if (isEditable) {
|
||||||
|
con.color_pair(colVoiceHdr, colBackPtn); print(' ' + value + ' ')
|
||||||
|
} else {
|
||||||
|
con.color_pair(colVoiceHdr, colBLACK); print(value)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
drawProjectSongList()
|
drawProjectSongList()
|
||||||
@@ -2745,14 +2782,18 @@ function projectSongListRowsVisible() {
|
|||||||
|
|
||||||
let projectSongScroll = 0
|
let projectSongScroll = 0
|
||||||
|
|
||||||
function clampProjectSongCursor() {
|
function clampProjectCursor() {
|
||||||
const n = songsMeta.numSongs
|
const n = songsMeta.numSongs
|
||||||
if (projectSongCursor < 0) projectSongCursor = 0
|
const maxCur = PROJ_META_ROWS_COUNT + Math.max(0, n - 1)
|
||||||
if (projectSongCursor > n - 1) projectSongCursor = n - 1
|
if (projectCursor < 0) projectCursor = 0
|
||||||
|
if (projectCursor > maxCur) projectCursor = maxCur
|
||||||
const rowsVis = projectSongListRowsVisible()
|
const rowsVis = projectSongListRowsVisible()
|
||||||
if (projectSongCursor < projectSongScroll) projectSongScroll = projectSongCursor
|
if (projectCursor >= PROJ_META_ROWS_COUNT) {
|
||||||
else if (projectSongCursor >= projectSongScroll + rowsVis)
|
const songIdx = projectCursor - PROJ_META_ROWS_COUNT
|
||||||
projectSongScroll = projectSongCursor - rowsVis + 1
|
if (songIdx < projectSongScroll) projectSongScroll = songIdx
|
||||||
|
else if (songIdx >= projectSongScroll + rowsVis)
|
||||||
|
projectSongScroll = songIdx - rowsVis + 1
|
||||||
|
}
|
||||||
if (projectSongScroll < 0) projectSongScroll = 0
|
if (projectSongScroll < 0) projectSongScroll = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2775,7 +2816,8 @@ function drawProjectSongList() {
|
|||||||
}
|
}
|
||||||
const s = songsMeta.songs[idx]
|
const s = songsMeta.songs[idx]
|
||||||
const isActive = (idx === currentSongIndex)
|
const isActive = (idx === currentSongIndex)
|
||||||
const isSel = (idx === projectSongCursor)
|
const isSel = (projectCursor >= PROJ_META_ROWS_COUNT) &&
|
||||||
|
(idx === projectCursor - PROJ_META_ROWS_COUNT)
|
||||||
const back = isSel ? colHighlight : colBackPtn
|
const back = isSel ? colHighlight : colBackPtn
|
||||||
|
|
||||||
const marker = isActive ? sym.playhead : ' '
|
const marker = isActive ? sym.playhead : ' '
|
||||||
@@ -2818,28 +2860,50 @@ function projectInput(wo, event) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!keyJustHit) return
|
// if (!keyJustHit) return
|
||||||
|
|
||||||
if (keysym === '<UP>') {
|
if (keysym === '<UP>') {
|
||||||
projectSongCursor -= moveDelta; clampProjectSongCursor(); redrawPanel(); return
|
projectCursor -= moveDelta; clampProjectCursor(); redrawPanel(); return
|
||||||
}
|
}
|
||||||
if (keysym === '<DOWN>') {
|
if (keysym === '<DOWN>') {
|
||||||
projectSongCursor += moveDelta; clampProjectSongCursor(); redrawPanel(); return
|
projectCursor += moveDelta; clampProjectCursor(); redrawPanel(); return
|
||||||
}
|
}
|
||||||
if (keysym === '<PAGE_UP>') {
|
if (keysym === '<PAGE_UP>') {
|
||||||
projectSongCursor -= projectSongListRowsVisible(); clampProjectSongCursor(); redrawPanel(); return
|
projectCursor -= projectSongListRowsVisible(); clampProjectCursor(); redrawPanel(); return
|
||||||
}
|
}
|
||||||
if (keysym === '<PAGE_DOWN>') {
|
if (keysym === '<PAGE_DOWN>') {
|
||||||
projectSongCursor += projectSongListRowsVisible(); clampProjectSongCursor(); redrawPanel(); return
|
projectCursor += projectSongListRowsVisible(); clampProjectCursor(); redrawPanel(); return
|
||||||
}
|
}
|
||||||
if (keysym === '<HOME>') {
|
if (keysym === '<HOME>') {
|
||||||
projectSongCursor = 0; clampProjectSongCursor(); redrawPanel(); return
|
projectCursor = 0; clampProjectCursor(); redrawPanel(); return
|
||||||
}
|
}
|
||||||
if (keysym === '<END>') {
|
if (keysym === '<END>') {
|
||||||
projectSongCursor = songsMeta.numSongs - 1; clampProjectSongCursor(); redrawPanel(); return
|
projectCursor = PROJ_META_ROWS_COUNT + Math.max(0, songsMeta.numSongs - 1)
|
||||||
|
clampProjectCursor(); redrawPanel(); return
|
||||||
}
|
}
|
||||||
if (keysym === '\n') {
|
if (keysym === '\n') {
|
||||||
if (projectSongCursor !== currentSongIndex) switchSong(projectSongCursor)
|
if (projectCursor === PROJ_META_FLAGS) {
|
||||||
|
openFlagsPopup()
|
||||||
|
} else if (projectCursor === PROJ_META_GVOL) {
|
||||||
|
const v = openInlineHexEdit(PTNVIEW_OFFSET_Y + PROJ_META_ROW_GVOL, PROJ_META_VALUE_X, 2, initialGlobalVolume)
|
||||||
|
if (v !== null) {
|
||||||
|
initialGlobalVolume = v & 0xFF
|
||||||
|
audio.setSongGlobalVolume(PLAYHEAD, initialGlobalVolume)
|
||||||
|
hasUnsavedChanges = true
|
||||||
|
}
|
||||||
|
redrawPanel()
|
||||||
|
} else if (projectCursor === PROJ_META_MVOL) {
|
||||||
|
const v = openInlineHexEdit(PTNVIEW_OFFSET_Y + PROJ_META_ROW_MVOL, PROJ_META_VALUE_X, 2, initialMixingVolume)
|
||||||
|
if (v !== null) {
|
||||||
|
initialMixingVolume = v & 0xFF
|
||||||
|
audio.setSongMixingVolume(PLAYHEAD, initialMixingVolume)
|
||||||
|
hasUnsavedChanges = true
|
||||||
|
}
|
||||||
|
redrawPanel()
|
||||||
|
} else {
|
||||||
|
const songIdx = projectCursor - PROJ_META_ROWS_COUNT
|
||||||
|
if (songIdx !== currentSongIndex) switchSong(songIdx)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (keysym === ' ') {
|
if (keysym === ' ') {
|
||||||
@@ -3100,13 +3164,7 @@ function updatePlayback() {
|
|||||||
function clampCursor() {
|
function clampCursor() {
|
||||||
if (cursorRow < 0) cursorRow = 0
|
if (cursorRow < 0) cursorRow = 0
|
||||||
if (cursorRow >= ROWS_PER_PAT) cursorRow = ROWS_PER_PAT - 1
|
if (cursorRow >= ROWS_PER_PAT) cursorRow = ROWS_PER_PAT - 1
|
||||||
if (cursorRow < scrollRow) scrollRow = cursorRow
|
scrollRow = centerScroll(cursorRow, scrollRow, PTNVIEW_HEIGHT, ROWS_PER_PAT)
|
||||||
// these two IF statements will keep the cursor at the centre until viewpoint scroll edge has reached
|
|
||||||
if (cursorRow < scrollRow + (PTNVIEW_HEIGHT>>>1) && scrollRow > 0) scrollRow = cursorRow - (PTNVIEW_HEIGHT>>>1)
|
|
||||||
if (cursorRow >= scrollRow + ((PTNVIEW_HEIGHT+1)>>>1)) scrollRow = cursorRow - ((PTNVIEW_HEIGHT+1)>>>1) + 1
|
|
||||||
if (scrollRow < 0) scrollRow = 0
|
|
||||||
if (scrollRow + PTNVIEW_HEIGHT > ROWS_PER_PAT)
|
|
||||||
scrollRow = Math.max(0, ROWS_PER_PAT - PTNVIEW_HEIGHT)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampVoice() {
|
function clampVoice() {
|
||||||
@@ -3387,6 +3445,10 @@ function openRetunePopup() {
|
|||||||
const entries = Object.values(pitchTablePresets).sort((a, b) => a.index - b.index)
|
const entries = Object.values(pitchTablePresets).sort((a, b) => a.index - b.index)
|
||||||
const n = entries.length
|
const n = entries.length
|
||||||
|
|
||||||
|
// Foreground colour by tuning type (preset.t):
|
||||||
|
// 'd' = 12-tone family, 'M' = Macrotonal, 'm' = microtonal, '' = Raw.
|
||||||
|
const tuningTypeColour = { d: 230, M: colPan, m: colInst, '': colStatus }
|
||||||
|
|
||||||
const methodLabels = {
|
const methodLabels = {
|
||||||
pitch: 'Nearest-note',
|
pitch: 'Nearest-note',
|
||||||
delta: 'Nearest-delta',
|
delta: 'Nearest-delta',
|
||||||
@@ -3411,7 +3473,7 @@ function openRetunePopup() {
|
|||||||
|
|
||||||
let sel = entries.findIndex(p => p.index === PITCH_PRESET_IDX)
|
let sel = entries.findIndex(p => p.index === PITCH_PRESET_IDX)
|
||||||
if (sel < 0) sel = 0
|
if (sel < 0) sel = 0
|
||||||
let scroll = (sel >= listH) ? Math.min(Math.max(0, n - listH), sel - (listH >>> 1)) : 0
|
let scroll = centerScroll(sel, 0, listH, n)
|
||||||
|
|
||||||
const repaint = () => {
|
const repaint = () => {
|
||||||
con.color_pair(230, colPopupBack)
|
con.color_pair(230, colPopupBack)
|
||||||
@@ -3440,7 +3502,7 @@ function openRetunePopup() {
|
|||||||
const isSel = (idx === sel)
|
const isSel = (idx === sel)
|
||||||
const isCur = (e.index === PITCH_PRESET_IDX)
|
const isCur = (e.index === PITCH_PRESET_IDX)
|
||||||
const back = isSel ? colHighlight : colPopupBack
|
const back = isSel ? colHighlight : colPopupBack
|
||||||
const fore = isSel ? colWHITE : (isCur ? colWHITE : 230)
|
const fore = (e.t in tuningTypeColour) ? tuningTypeColour[e.t] : 230
|
||||||
const marker = isCur ? sym.playhead : ' '
|
const marker = isCur ? sym.playhead : ' '
|
||||||
let label = `${marker} ${e.index.toString().padStart(5, ' ')} ${e.name}`
|
let label = `${marker} ${e.index.toString().padStart(5, ' ')} ${e.name}`
|
||||||
if (label.length > listW) label = label.substring(0, listW)
|
if (label.length > listW) label = label.substring(0, listW)
|
||||||
@@ -3500,30 +3562,17 @@ function openRetunePopup() {
|
|||||||
repaint()
|
repaint()
|
||||||
}
|
}
|
||||||
else if (ks === '<UP>') {
|
else if (ks === '<UP>') {
|
||||||
if (sel > 0) {
|
if (sel > 0) { sel--; scroll = centerScroll(sel, scroll, listH, n); repaint() }
|
||||||
sel--
|
|
||||||
if (sel < scroll) scroll = sel
|
|
||||||
repaint()
|
|
||||||
}
|
|
||||||
} else if (ks === '<DOWN>') {
|
} else if (ks === '<DOWN>') {
|
||||||
if (sel < n - 1) {
|
if (sel < n - 1) { sel++; scroll = centerScroll(sel, scroll, listH, n); repaint() }
|
||||||
sel++
|
|
||||||
if (sel >= scroll + listH) scroll = sel - listH + 1
|
|
||||||
repaint()
|
|
||||||
}
|
|
||||||
} else if (ks === '<HOME>') {
|
} else if (ks === '<HOME>') {
|
||||||
sel = 0; scroll = 0; repaint()
|
sel = 0; scroll = centerScroll(sel, scroll, listH, n); repaint()
|
||||||
} else if (ks === '<END>') {
|
} else if (ks === '<END>') {
|
||||||
sel = n - 1; scroll = Math.max(0, n - listH); repaint()
|
sel = n - 1; scroll = centerScroll(sel, scroll, listH, n); repaint()
|
||||||
} else if (ks === '<PAGE_UP>') {
|
} else if (ks === '<PAGE_UP>') {
|
||||||
sel = Math.max(0, sel - listH)
|
sel = Math.max(0, sel - listH); scroll = centerScroll(sel, scroll, listH, n); repaint()
|
||||||
scroll = Math.max(0, scroll - listH)
|
|
||||||
if (sel < scroll) scroll = sel
|
|
||||||
repaint()
|
|
||||||
} else if (ks === '<PAGE_DOWN>') {
|
} else if (ks === '<PAGE_DOWN>') {
|
||||||
sel = Math.min(n - 1, sel + listH)
|
sel = Math.min(n - 1, sel + listH); scroll = centerScroll(sel, scroll, listH, n); repaint()
|
||||||
if (sel >= scroll + listH) scroll = Math.min(Math.max(0, n - listH), sel - listH + 1)
|
|
||||||
repaint()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -3538,6 +3587,169 @@ function openRetunePopup() {
|
|||||||
drawAll()
|
drawAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// MIXER FLAGS POPUP
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
function openFlagsPopup() {
|
||||||
|
const toneNames = ['Linear pitch', 'Amiga pitch', 'Linear freq']
|
||||||
|
const intpNames = ['Default', 'None', 'A500', 'A1200', 'SNES', 'DPCM']
|
||||||
|
|
||||||
|
let toneMode = initialTrackerMixerflags & 3
|
||||||
|
let intpMode = (initialTrackerMixerflags >>> 2) & 7
|
||||||
|
if (toneMode >= toneNames.length) toneMode = 0
|
||||||
|
if (intpMode >= intpNames.length) intpMode = 0
|
||||||
|
|
||||||
|
// Build list rows: headers + selectable radio options.
|
||||||
|
// items[].kind: undefined = header, 'tone' | 'intp' = selectable.
|
||||||
|
const items = []
|
||||||
|
items.push({ label: 'Tone Mode:' })
|
||||||
|
toneNames.forEach((n, i) => items.push({ kind: 'tone', idx: i, label: n }))
|
||||||
|
items.push({ label: '' })
|
||||||
|
items.push({ label: 'Interpolation:' })
|
||||||
|
intpNames.forEach((n, i) => items.push({ kind: 'intp', idx: i, label: n }))
|
||||||
|
|
||||||
|
const selectables = []
|
||||||
|
items.forEach((it, i) => { if (it.kind) selectables.push(i) })
|
||||||
|
let sel = 0
|
||||||
|
|
||||||
|
const pw = 28
|
||||||
|
const ph = items.length + 4
|
||||||
|
const px = ((SCRW - pw) / 2 | 0) + 1
|
||||||
|
const py = ((SCRH - ph) / 2 | 0)
|
||||||
|
|
||||||
|
const popup = new win.WindowObject(px, py, pw, ph, ()=>{}, ()=>{}, 'Mixer Flags', popupDrawFrame)
|
||||||
|
popup.isHighlighted = true
|
||||||
|
popup.titleBack = colPopupBack
|
||||||
|
|
||||||
|
const repaint = () => {
|
||||||
|
con.color_pair(230, colPopupBack)
|
||||||
|
popup.drawFrame()
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const it = items[i]
|
||||||
|
con.move(py + 1 + i, px + 2)
|
||||||
|
if (!it.kind) {
|
||||||
|
con.color_pair(colStatus, colPopupBack)
|
||||||
|
print(it.label.padEnd(pw - 4))
|
||||||
|
} else {
|
||||||
|
const isSel = (selectables[sel] === i)
|
||||||
|
const isChecked = (it.kind === 'tone')
|
||||||
|
? (toneMode === it.idx)
|
||||||
|
: (intpMode === it.idx)
|
||||||
|
const back = isSel ? colHighlight : colPopupBack
|
||||||
|
const fore = isChecked ? colVoiceHdr : colWHITE
|
||||||
|
con.color_pair(fore, back)
|
||||||
|
const line = ' ' + (isChecked ? sym.ticked : sym.unticked) + ' ' + it.label
|
||||||
|
print(line.padEnd(pw - 4))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
con.move(py + ph - 2, px + 2)
|
||||||
|
con.color_pair(colVoiceHdr, colPopupBack); print(`\u008418u `)
|
||||||
|
con.color_pair(colStatus, colPopupBack); print('Sel ')
|
||||||
|
con.color_pair(colVoiceHdr, colPopupBack); print('sp ')
|
||||||
|
con.color_pair(colStatus, colPopupBack); print('Tick ')
|
||||||
|
con.color_pair(colVoiceHdr, colPopupBack); print('ent ')
|
||||||
|
con.color_pair(colStatus, colPopupBack); print('OK ')
|
||||||
|
con.color_pair(colVoiceHdr, colPopupBack); print('Q ')
|
||||||
|
con.color_pair(colStatus, colPopupBack); print('X')
|
||||||
|
|
||||||
|
con.color_pair(colStatus, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
repaint()
|
||||||
|
|
||||||
|
let done = false
|
||||||
|
let confirmed = false
|
||||||
|
let eventJustReceived = true
|
||||||
|
while (!done) {
|
||||||
|
input.withEvent(ev => {
|
||||||
|
if (ev[0] !== 'key_down') return
|
||||||
|
if (1 !== ev[2]) return
|
||||||
|
const ks = ev[1]
|
||||||
|
if (eventJustReceived) { eventJustReceived = false; return }
|
||||||
|
|
||||||
|
if (ks === '<ESC>' || ks === 'q' || ks === 'Q') { done = true; return }
|
||||||
|
if (ks === '\n') { confirmed = true; done = true; return }
|
||||||
|
if (ks === '<UP>' && sel > 0) { sel--; repaint(); return }
|
||||||
|
if (ks === '<DOWN>' && sel < selectables.length-1) { sel++; repaint(); return }
|
||||||
|
if (ks === ' ') {
|
||||||
|
const it = items[selectables[sel]]
|
||||||
|
if (it.kind === 'tone') toneMode = it.idx
|
||||||
|
else if (it.kind === 'intp') intpMode = it.idx
|
||||||
|
repaint()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (confirmed) {
|
||||||
|
const newFlags = (initialTrackerMixerflags & ~0x1F) |
|
||||||
|
(toneMode & 3) | ((intpMode & 7) << 2)
|
||||||
|
if (newFlags !== initialTrackerMixerflags) {
|
||||||
|
initialTrackerMixerflags = newFlags
|
||||||
|
audio.setTrackerMixerFlags(PLAYHEAD, newFlags)
|
||||||
|
hasUnsavedChanges = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// INLINE HEX EDITOR
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Overlay an editable hex field at (y, x) with `digits` digits, pre-filled from `initialValue`.
|
||||||
|
// Returns the new integer on commit, or null on cancel. Reusable for pattern-grid edits.
|
||||||
|
function openInlineHexEdit(y, x, digits, initialValue) {
|
||||||
|
let buf = (initialValue >>> 0).toString(16).toUpperCase()
|
||||||
|
if (buf.length > digits) buf = buf.substring(buf.length - digits)
|
||||||
|
buf = buf.padStart(digits, '0')
|
||||||
|
|
||||||
|
let cur = 0
|
||||||
|
let cancelled = false
|
||||||
|
let done = false
|
||||||
|
|
||||||
|
const repaint = () => {
|
||||||
|
con.move(y, x)
|
||||||
|
con.color_pair(colWHITE, colHighlight)
|
||||||
|
print(' $' + buf + ' ')
|
||||||
|
con.move(y, x + 2 + cur)
|
||||||
|
con.color_pair(colBLACK, colWHITE)
|
||||||
|
print(buf[cur])
|
||||||
|
con.color_pair(colStatus, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
repaint()
|
||||||
|
let eventJustReceived = true
|
||||||
|
while (!done) {
|
||||||
|
input.withEvent(ev => {
|
||||||
|
if (ev[0] !== 'key_down') return
|
||||||
|
if (1 !== ev[2]) return
|
||||||
|
const ks = ev[1]
|
||||||
|
if (eventJustReceived) { eventJustReceived = false; return }
|
||||||
|
|
||||||
|
if (ks === '<ESC>') { cancelled = true; done = true; return }
|
||||||
|
if (ks === '\n') { done = true; return }
|
||||||
|
if (ks === '<LEFT>' && cur > 0) { cur--; repaint(); return }
|
||||||
|
if (ks === '<RIGHT>' && cur < digits - 1) { cur++; repaint(); return }
|
||||||
|
if (ks === '<HOME>') { cur = 0; repaint(); return }
|
||||||
|
if (ks === '<END>') { cur = digits - 1; repaint(); return }
|
||||||
|
if (ks.length === 1 && '0123456789abcdefABCDEF'.includes(ks)) {
|
||||||
|
buf = buf.substring(0, cur) + ks.toUpperCase() + buf.substring(cur + 1)
|
||||||
|
if (cur < digits - 1) cur++
|
||||||
|
else done = true
|
||||||
|
repaint()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return cancelled ? null : parseInt(buf, 16)
|
||||||
|
}
|
||||||
|
|
||||||
clampCursor(); clampVoice(); clampCue(); clampOrdersHoriz(); clampPatternIdx(); clampPatternGrid()
|
clampCursor(); clampVoice(); clampCue(); clampOrdersHoriz(); clampPatternIdx(); clampPatternGrid()
|
||||||
drawAll()
|
drawAll()
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,8 @@ Timeline has two distinct modes: view and edit mode. Two modes are toggled using
|
|||||||
|
|
||||||
<b> GLOBAL EDIT</b>
|
<b> GLOBAL EDIT</b>
|
||||||
<b>\u00B7${'\u00B8'.repeat(11)}\u00B9</b>
|
<b>\u00B7${'\u00B8'.repeat(11)}\u00B9</b>
|
||||||
&bul;<b>Q</b> : <O>retune current song into different tuning</O>
|
&bul;<b>Q</b> : <O>retune current song into different tuning and strategy</O>
|
||||||
|
<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>
|
||||||
`
|
`
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
@@ -66,9 +66,58 @@ const EXEC_FUNS = {
|
|||||||
"txt": (f) => _G.shell.execute(`less "${f}"`),
|
"txt": (f) => _G.shell.execute(`less "${f}"`),
|
||||||
"md": (f) => _G.shell.execute(`less "${f}"`),
|
"md": (f) => _G.shell.execute(`less "${f}"`),
|
||||||
"log": (f) => _G.shell.execute(`less "${f}"`),
|
"log": (f) => _G.shell.execute(`less "${f}"`),
|
||||||
"taud": (f) => _G.shell.execute(`taut "${f}"`),
|
"taud": (f) => _G.shell.execute(`microtone "${f}"`),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeExecFun(template) {
|
||||||
|
return (f) => _G.shell.execute(template.replaceAll("{0}", `"${f}"`))
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadZfmrc() {
|
||||||
|
try {
|
||||||
|
let zfmrcPath = `A:${_TVDOS.variables.USERCONFIGPATH}\\zfmrc`
|
||||||
|
let zfmrcFile = files.open(zfmrcPath)
|
||||||
|
if (!zfmrcFile.exists) return
|
||||||
|
|
||||||
|
let content = zfmrcFile.sread()
|
||||||
|
let lines = content.split(/\r?\n/)
|
||||||
|
let currentSection = null
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
let line = lines[i].trim()
|
||||||
|
if (line.length === 0 || line.startsWith("#") || line.startsWith(";")) continue
|
||||||
|
|
||||||
|
if (line.startsWith("[") && line.endsWith("]")) {
|
||||||
|
currentSection = line.substring(1, line.length - 1).toUpperCase()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSection === "EXEC_FUNS") {
|
||||||
|
let commaIdx = line.indexOf(",")
|
||||||
|
if (commaIdx < 0) continue
|
||||||
|
let ext = line.substring(0, commaIdx).trim().toLowerCase()
|
||||||
|
let template = line.substring(commaIdx + 1).trim()
|
||||||
|
if (ext.length === 0 || template.length === 0) continue
|
||||||
|
EXEC_FUNS[ext] = makeExecFun(template)
|
||||||
|
}
|
||||||
|
else if (currentSection === "COL_HL_EXT") {
|
||||||
|
let commaIdx = line.indexOf(",")
|
||||||
|
if (commaIdx < 0) continue
|
||||||
|
let ext = line.substring(0, commaIdx).trim().toLowerCase()
|
||||||
|
let colStr = line.substring(commaIdx + 1).trim()
|
||||||
|
if (ext.length === 0 || colStr.length === 0) continue
|
||||||
|
let col = parseInt(colStr, 10)
|
||||||
|
if (isNaN(col)) continue
|
||||||
|
COL_HL_EXT[ext] = col
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
serial.println("zfm: failed to load zfmrc: " + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadZfmrc()
|
||||||
|
|
||||||
let windowMode = 0 // 0 == left, 1 == right
|
let windowMode = 0 // 0 == left, 1 == right
|
||||||
let windowFocus = [0] // is a stack; 0: files window, 1: palette window, 2: popup window
|
let windowFocus = [0] // is a stack; 0: files window, 1: palette window, 2: popup window
|
||||||
|
|
||||||
@@ -82,6 +131,7 @@ let cursor = [0, 0] // absolute position!
|
|||||||
|
|
||||||
function bytesToReadable(i) {
|
function bytesToReadable(i) {
|
||||||
return ''+ (
|
return ''+ (
|
||||||
|
(i > 999999999999) ? (((i / 10000000000)|0)/100 + "T") :
|
||||||
(i > 999999999) ? (((i / 10000000)|0)/100 + "G") :
|
(i > 999999999) ? (((i / 10000000)|0)/100 + "G") :
|
||||||
(i > 999999) ? (((i / 10000)|0)/100 + "M") :
|
(i > 999999) ? (((i / 10000)|0)/100 + "M") :
|
||||||
(i > 9999) ? (((i / 100)|0)/10 + "K") :
|
(i > 9999) ? (((i / 100)|0)/10 + "K") :
|
||||||
@@ -677,11 +727,11 @@ while (!exit) {
|
|||||||
let keysym = event[1]
|
let keysym = event[1]
|
||||||
let keyJustHit = (1 == event[2])
|
let keyJustHit = (1 == event[2])
|
||||||
|
|
||||||
if (keyJustHit && event[3] != keys.ENTER) { // release the latch right away if the key is not Return
|
if (keyJustHit && event[3] != keys.ENTER && keysym != "q") { // release the latch right away if the key is neither Return nor 'q'
|
||||||
firstRunLatch = false
|
firstRunLatch = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyJustHit && firstRunLatch) { // filter out the initial ENTER key as they would cause unwanted behaviours
|
if (keyJustHit && firstRunLatch) { // filter out the initial ENTER/'q' key as they would cause unwanted behaviours
|
||||||
firstRunLatch = false
|
firstRunLatch = false
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
1129
assets/disk0/tvdos/include/fs.mjs
Normal file
1129
assets/disk0/tvdos/include/fs.mjs
Normal file
File diff suppressed because it is too large
Load Diff
621
assets/disk0/tvdos/include/tbas.mjs
Normal file
621
assets/disk0/tvdos/include/tbas.mjs
Normal 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)) },
|
||||||
|
}
|
||||||
@@ -698,7 +698,7 @@ def encode_note_it(it_note: int) -> int:
|
|||||||
# IT C-5 anchors to Taud C-4, so offset = it_note - 60.
|
# IT C-5 anchors to Taud C-4, so offset = it_note - 60.
|
||||||
semis = it_note - 60
|
semis = it_note - 60
|
||||||
val = round(TAUD_C4 + semis * 4096 / 12)
|
val = round(TAUD_C4 + semis * 4096 / 12)
|
||||||
return max(1, min(0xFFFD, val))
|
return max(0x20, min(0xFFFF, val))
|
||||||
return NOTE_NOP
|
return NOTE_NOP
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ def period_to_taud_note(period: int) -> int:
|
|||||||
if period <= 0:
|
if period <= 0:
|
||||||
return NOTE_NOP
|
return NOTE_NOP
|
||||||
val = round(TAUD_C4 + 4096.0 * math.log2(PT_REFERENCE_PERIOD / period))
|
val = round(TAUD_C4 + 4096.0 * math.log2(PT_REFERENCE_PERIOD / period))
|
||||||
return max(1, min(0xFFFD, val))
|
return max(0x20, min(0xFFFF, val))
|
||||||
|
|
||||||
|
|
||||||
# ── PT effect → Taud effect ──────────────────────────────────────────────────
|
# ── PT effect → Taud effect ──────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ def mon_note_to_taud(mon_note: int) -> int:
|
|||||||
if mon_note == 0x7F:
|
if mon_note == 0x7F:
|
||||||
return NOTE_CUT
|
return NOTE_CUT
|
||||||
val = TAUD_C4 + round((mon_note - MON_NOTE_C4) * 4096.0 / 12.0)
|
val = TAUD_C4 + round((mon_note - MON_NOTE_C4) * 4096.0 / 12.0)
|
||||||
return max(1, min(0xFFFD, val))
|
return max(0x20, min(0xFFFF, val))
|
||||||
|
|
||||||
|
|
||||||
# ── Effect mapping (Monotone 3-bit code + 6-bit data → Taud) ─────────────────
|
# ── Effect mapping (Monotone 3-bit code + 6-bit data → Taud) ─────────────────
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ def encode_note(s3m_note: int) -> int:
|
|||||||
return NOTE_NOP
|
return NOTE_NOP
|
||||||
semitones = (octave - 4) * 12 + pitch
|
semitones = (octave - 4) * 12 + pitch
|
||||||
val = round(TAUD_C4 + semitones * 4096 / 12)
|
val = round(TAUD_C4 + semitones * 4096 / 12)
|
||||||
return max(1, min(0xFFFD, val))
|
return max(0x20, min(0xFFFF, val))
|
||||||
|
|
||||||
|
|
||||||
def encode_effect(cmd: int, arg: int, ch: int = 0, row: int = 0,
|
def encode_effect(cmd: int, arg: int, ch: int = 0, row: int = 0,
|
||||||
|
|||||||
@@ -96,9 +96,9 @@ NUM_VOICES = 20
|
|||||||
SAMPLE_LEN_LIMIT = 65535
|
SAMPLE_LEN_LIMIT = 65535
|
||||||
|
|
||||||
# Note word sentinels
|
# Note word sentinels
|
||||||
NOTE_NOP = 0xFFFF
|
NOTE_NOP = 0x0000
|
||||||
NOTE_KEYOFF = 0x0000
|
NOTE_KEYOFF = 0x0001
|
||||||
NOTE_CUT = 0xFFFE
|
NOTE_CUT = 0x0002
|
||||||
TAUD_C4 = 0x5000 # The audio engine's Middle C
|
TAUD_C4 = 0x5000 # The audio engine's Middle C
|
||||||
|
|
||||||
# Cue sheet instruction byte (cue offset 30; offset 31 = arg byte for 2-byte forms).
|
# Cue sheet instruction byte (cue offset 30; offset 31 = arg byte for 2-byte forms).
|
||||||
|
|||||||
@@ -2255,7 +2255,7 @@ from source.
|
|||||||
* Semantics (matches IT/Schism player/effects.c:1664-1764 csf_check_nna):
|
* Semantics (matches IT/Schism player/effects.c:1664-1764 csf_check_nna):
|
||||||
- Fires on every fresh foreground note trigger on a channel, BEFORE the
|
- Fires on every fresh foreground note trigger on a channel, BEFORE the
|
||||||
NNA-spawn step that would ghost the existing voice. Does NOT fire on
|
NNA-spawn step that would ghost the existing voice. Does NOT fire on
|
||||||
tone portamento, on note-off (0x0000), on note-cut (0xFFFE), or on
|
tone portamento, on note-off (0x0001), on note-cut (0x0002), or on
|
||||||
empty cells.
|
empty cells.
|
||||||
- The DCT/DCA values consulted belong to the EXISTING voice's instrument
|
- The DCT/DCA values consulted belong to the EXISTING voice's instrument
|
||||||
(i.e. the OLD note's instrument, not the incoming note's). Different
|
(i.e. the OLD note's instrument, not the incoming note's). Different
|
||||||
@@ -2401,6 +2401,8 @@ TODO:
|
|||||||
[x] GSLINGER order 0x03 chn 1: L 0100 fades unexpectedly fast? — converter fix
|
[x] GSLINGER order 0x03 chn 1: L 0100 fades unexpectedly fast? — converter fix
|
||||||
[x] do not reset tickspeed on pattern view play / add key to modify tick speed ('[' down/']' up)
|
[x] do not reset tickspeed on pattern view play / add key to modify tick speed ('[' down/']' up)
|
||||||
[x] expose song table on UI (test with `insaniq2.taud`)
|
[x] expose song table on UI (test with `insaniq2.taud`)
|
||||||
|
[x] 0x0000 - no-op; 0x0001 - key-off; 0x0002 - note-cut 0x0010..0x001F - INT0..INTF
|
||||||
|
[ ] establish hooks for the interrupts
|
||||||
|
|
||||||
TODO - list of demo songs that MUST ship with Microtone:
|
TODO - list of demo songs that MUST ship with Microtone:
|
||||||
* 4THSYM (rename to Fourth Symmetriad) — excellent piece for demonstrating NNAs and filter envelopes
|
* 4THSYM (rename to Fourth Symmetriad) — excellent piece for demonstrating NNAs and filter envelopes
|
||||||
@@ -2421,13 +2423,14 @@ Play Data: play data are series of tracker-like instructions, visualised as:
|
|||||||
rr||NOTE|Ins|E.Vol|E.Pan|EE.ff|
|
rr||NOTE|Ins|E.Vol|E.Pan|EE.ff|
|
||||||
63||FFFF|255|3 63|3 63|FF FFFF| (8 bytes per line, 512 bytes per pattern, 128 patterns on 64 kB bank, 32 banks available (pattern 0xFFF -- bank 31, pattern 127 is a sentinel value for no-pattern))
|
63||FFFF|255|3 63|3 63|FF FFFF| (8 bytes per line, 512 bytes per pattern, 128 patterns on 64 kB bank, 32 banks available (pattern 0xFFF -- bank 31, pattern 127 is a sentinel value for no-pattern))
|
||||||
|
|
||||||
notes are tuned as 4096 Tone-Equal Temperament. Tuning is set per-sample using their Sampling rate value.
|
notes are tuned as 4096 Tone-Equal Temperament. Tuning is set per-sample using their Sampling rate value. 0x1000: C at zeroth octave; 0xF000: C at 14th octave; 0xFFFF: ~C at 15th octave; 0x0000..0x001F: reserved for sentinels (valid playable note range is 0x0020..0xFFFF)
|
||||||
|
|
||||||
Special values:
|
Special values:
|
||||||
|
|
||||||
note 0xFFFF: no-op
|
note 0x0000: no-op
|
||||||
note 0xFFFE: note cut
|
note 0x0001: key-off
|
||||||
note 0x0000: key-off
|
note 0x0002: note cut
|
||||||
|
note 0x0010..0x001F: Interrupt 0..F (notation: Int0..IntF) — reserved interrupt slots; engine has no default handler.
|
||||||
|
|
||||||
inst 0: no instrument change
|
inst 0: no instrument change
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ import net.torvald.tsvm.peripheral.MP2Env
|
|||||||
* 8. Call `setCuePosition(playhead, 0)` then `play(playhead)`.
|
* 8. Call `setCuePosition(playhead, 0)` then `play(playhead)`.
|
||||||
*
|
*
|
||||||
* Note values: 0x4000 = C3 (sample's native pitch), 4096 steps per octave.
|
* Note values: 0x4000 = C3 (sample's native pitch), 4096 steps per octave.
|
||||||
* Empty row: note = 0xFFFF (no trigger). All 256 instrument slots (0-255) are valid.
|
* Empty row: note = 0x0000 (no trigger). Note sentinels (0x0000..0x001F): 0x0000 = no-op,
|
||||||
|
* 0x0001 = key-off, 0x0002 = note cut, 0x0010..0x001F = Int0..IntF (reserved interrupts).
|
||||||
|
* Valid playable notes are 0x0020..0xFFFF. All 256 instrument slots (0-255) are valid.
|
||||||
*
|
*
|
||||||
* ## How to upload PCM audio into a playhead
|
* ## How to upload PCM audio into a playhead
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
internal val sampleBin = UnsafeHelper.allocate(SAMPLE_BIN_TOTAL, this)
|
internal val sampleBin = UnsafeHelper.allocate(SAMPLE_BIN_TOTAL, this)
|
||||||
@Volatile var sampleBank: Int = 0 // 0..15, controls the 0..524287 window
|
@Volatile var sampleBank: Int = 0 // 0..15, controls the 0..524287 window
|
||||||
internal val instruments = Array(256) { TaudInst(it) }
|
internal val instruments = Array(256) { TaudInst(it) }
|
||||||
internal val playdata = Array(4096) { Array(64) { TaudPlayData(0xFFFF, 0, 0, 0, 32, 0, 0, 0) } }
|
internal val playdata = Array(4096) { Array(64) { TaudPlayData(0x0000, 0, 0, 0, 32, 0, 0, 0) } }
|
||||||
internal val playheads: Array<Playhead>
|
internal val playheads: Array<Playhead>
|
||||||
internal val cueSheet = Array(1024) { PlayCue() }
|
internal val cueSheet = Array(1024) { PlayCue() }
|
||||||
internal val pcmBin = arrayOf(
|
internal val pcmBin = arrayOf(
|
||||||
@@ -2275,7 +2275,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TaudPlayData(
|
TaudPlayData(
|
||||||
note = if (rawRow.note != 0xFFFF) rawRow.note else src.note,
|
note = if (rawRow.note != 0x0000) rawRow.note else src.note,
|
||||||
instrment = if (rawRow.instrment != 0) rawRow.instrment else src.instrment,
|
instrment = if (rawRow.instrment != 0) rawRow.instrment else src.instrment,
|
||||||
volume = if (volIsSet) rawRow.volume else src.volume,
|
volume = if (volIsSet) rawRow.volume else src.volume,
|
||||||
volumeEff = if (volIsSet) rawRow.volumeEff else src.volumeEff,
|
volumeEff = if (volIsSet) rawRow.volumeEff else src.volumeEff,
|
||||||
@@ -2329,7 +2329,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
// physical_presence ord 0x1F ch2: every row carries `... 1E A0F/A09/A02`)
|
// physical_presence ord 0x1F ch2: every row carries `... 1E A0F/A09/A02`)
|
||||||
// silences after the first row because the slide saturates at 0 and there's
|
// silences after the first row because the slide saturates at 0 and there's
|
||||||
// nothing to lift the volume back up before the next slide starts.
|
// nothing to lift the volume back up before the next slide starts.
|
||||||
0xFFFF -> {
|
0x0000 -> {
|
||||||
if (row.instrment != 0) {
|
if (row.instrment != 0) {
|
||||||
voice.instrumentId = row.instrment
|
voice.instrumentId = row.instrment
|
||||||
val seedVol = rowVolumeFromDefault(instruments[voice.instrumentId])
|
val seedVol = rowVolumeFromDefault(instruments[voice.instrumentId])
|
||||||
@@ -2345,8 +2345,10 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
// fadeoutVolume reaches 0, or immediately if FT2-mode fadeStep == 0. Setting
|
// fadeoutVolume reaches 0, or immediately if FT2-mode fadeStep == 0. Setting
|
||||||
// voice.active = false here would defeat both — instruments with sustain points
|
// voice.active = false here would defeat both — instruments with sustain points
|
||||||
// and non-zero fadeout (FT2 sustain-then-fade idiom) would be cut on the spot.
|
// and non-zero fadeout (FT2 sustain-then-fade idiom) would be cut on the spot.
|
||||||
0x0000 -> { voice.keyOff = true }
|
0x0001 -> { voice.keyOff = true }
|
||||||
0xFFFE -> voice.active = false // note cut (immediate)
|
0x0002 -> voice.active = false // note cut (immediate)
|
||||||
|
in 0x0003..0x000F -> { /* reserved sentinel range, no engine handler */ }
|
||||||
|
in 0x0010..0x001F -> { /* Int0..IntF: reserved interrupt slots, no engine handler yet */ }
|
||||||
else -> {
|
else -> {
|
||||||
if (toneG && voice.active) {
|
if (toneG && voice.active) {
|
||||||
// Tone porta: target the note, do not retrigger sample.
|
// Tone porta: target the note, do not retrigger sample.
|
||||||
@@ -2502,7 +2504,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
1 -> amigaSlideOnce(voice.noteVal, -mag) // Amiga: subtract from pitch ⇒ adds period
|
1 -> amigaSlideOnce(voice.noteVal, -mag) // Amiga: subtract from pitch ⇒ adds period
|
||||||
2 -> linearFreqSlideOnce(voice.noteVal, -mag) // Hz/tick: pitch down ⇒ -Hz
|
2 -> linearFreqSlideOnce(voice.noteVal, -mag) // Hz/tick: pitch down ⇒ -Hz
|
||||||
else -> voice.noteVal - mag // linear 4096-TET
|
else -> voice.noteVal - mag // linear 4096-TET
|
||||||
}.coerceIn(1, 0xFFFD)
|
}.coerceIn(0x20, 0xFFFF)
|
||||||
voice.basePitch = voice.noteVal
|
voice.basePitch = voice.noteVal
|
||||||
voice.amigaPeriod = -1.0 // reseed on next per-tick slide
|
voice.amigaPeriod = -1.0 // reseed on next per-tick slide
|
||||||
voice.linearFreq = -1.0
|
voice.linearFreq = -1.0
|
||||||
@@ -2521,7 +2523,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
1 -> amigaSlideOnce(voice.noteVal, mag)
|
1 -> amigaSlideOnce(voice.noteVal, mag)
|
||||||
2 -> linearFreqSlideOnce(voice.noteVal, mag)
|
2 -> linearFreqSlideOnce(voice.noteVal, mag)
|
||||||
else -> voice.noteVal + mag
|
else -> voice.noteVal + mag
|
||||||
}.coerceIn(1, 0xFFFD)
|
}.coerceIn(0x20, 0xFFFF)
|
||||||
voice.basePitch = voice.noteVal
|
voice.basePitch = voice.noteVal
|
||||||
voice.amigaPeriod = -1.0
|
voice.amigaPeriod = -1.0
|
||||||
voice.linearFreq = -1.0
|
voice.linearFreq = -1.0
|
||||||
@@ -2730,7 +2732,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
}
|
}
|
||||||
0x1 -> voice.glissandoOn = (x != 0)
|
0x1 -> voice.glissandoOn = (x != 0)
|
||||||
0x2 -> {
|
0x2 -> {
|
||||||
voice.noteVal = (voice.noteVal + FINETUNE_OFFSET[x]).coerceIn(1, 0xFFFD)
|
voice.noteVal = (voice.noteVal + FINETUNE_OFFSET[x]).coerceIn(0x20, 0xFFFF)
|
||||||
voice.basePitch = voice.noteVal
|
voice.basePitch = voice.noteVal
|
||||||
voice.amigaPeriod = -1.0
|
voice.amigaPeriod = -1.0
|
||||||
voice.linearFreq = -1.0
|
voice.linearFreq = -1.0
|
||||||
@@ -2832,7 +2834,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
1 -> amigaSlideTick(voice, voice.slideArg)
|
1 -> amigaSlideTick(voice, voice.slideArg)
|
||||||
2 -> linearFreqSlideTick(voice, voice.slideArg)
|
2 -> linearFreqSlideTick(voice, voice.slideArg)
|
||||||
else -> voice.noteVal + voice.slideArg
|
else -> voice.noteVal + voice.slideArg
|
||||||
}.coerceIn(1, 0xFFFD)
|
}.coerceIn(0x20, 0xFFFF)
|
||||||
voice.basePitch = voice.noteVal
|
voice.basePitch = voice.noteVal
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2854,7 +2856,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
voice.noteVal = target
|
voice.noteVal = target
|
||||||
voice.tonePortaTarget = -1
|
voice.tonePortaTarget = -1
|
||||||
} else {
|
} else {
|
||||||
voice.noteVal = freqHzToNoteVal(voice.linearFreq).coerceIn(1, 0xFFFD)
|
voice.noteVal = freqHzToNoteVal(voice.linearFreq).coerceIn(0x20, 0xFFFF)
|
||||||
}
|
}
|
||||||
voice.basePitch = voice.noteVal
|
voice.basePitch = voice.noteVal
|
||||||
voice.amigaPeriod = -1.0
|
voice.amigaPeriod = -1.0
|
||||||
@@ -2912,14 +2914,14 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
if (voice.vibratoActive) {
|
if (voice.vibratoActive) {
|
||||||
val sine = lfoSample(voice.vibratoLfoPos, voice.vibratoWave)
|
val sine = lfoSample(voice.vibratoLfoPos, voice.vibratoWave)
|
||||||
val pitchDelta = (sine * voice.mem.huDepth) shr voice.vibratoFineShift
|
val pitchDelta = (sine * voice.mem.huDepth) shr voice.vibratoFineShift
|
||||||
pitchToMixer = (voice.noteVal + pitchDelta).coerceIn(1, 0xFFFD)
|
pitchToMixer = (voice.noteVal + pitchDelta).coerceIn(0x20, 0xFFFF)
|
||||||
voice.vibratoLfoPos = (voice.vibratoLfoPos + voice.mem.huSpeed * 4) and 0xFF
|
voice.vibratoLfoPos = (voice.vibratoLfoPos + voice.mem.huSpeed * 4) and 0xFF
|
||||||
}
|
}
|
||||||
|
|
||||||
// Glissando (S$1x) — snap pitchToMixer to nearest semitone but leave noteVal smooth.
|
// Glissando (S$1x) — snap pitchToMixer to nearest semitone but leave noteVal smooth.
|
||||||
if (voice.glissandoOn) {
|
if (voice.glissandoOn) {
|
||||||
val semis = ((pitchToMixer * 12 + 2048) / 4096)
|
val semis = ((pitchToMixer * 12 + 2048) / 4096)
|
||||||
pitchToMixer = (semis * 4096 / 12).coerceIn(1, 0xFFFD)
|
pitchToMixer = (semis * 4096 / 12).coerceIn(0x20, 0xFFFF)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tremolo (R) — modulates rowVolume around the per-note volume base. IT's tremolo
|
// Tremolo (R) — modulates rowVolume around the per-note volume base. IT's tremolo
|
||||||
@@ -2946,7 +2948,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
if (voice.arpActive) {
|
if (voice.arpActive) {
|
||||||
val voiceIdx = ts.tickInRow % 3
|
val voiceIdx = ts.tickInRow % 3
|
||||||
val arpDelta = when (voiceIdx) { 1 -> voice.arpOff1 shl 8; 2 -> voice.arpOff2 shl 8; else -> 0 }
|
val arpDelta = when (voiceIdx) { 1 -> voice.arpOff1 shl 8; 2 -> voice.arpOff2 shl 8; else -> 0 }
|
||||||
pitchToMixer = (voice.basePitch + arpDelta).coerceIn(1, 0xFFFD)
|
pitchToMixer = (voice.basePitch + arpDelta).coerceIn(0x20, 0xFFFF)
|
||||||
voice.lastArpVoice = voiceIdx
|
voice.lastArpVoice = voiceIdx
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2983,7 +2985,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
((voice.envPfValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt()
|
((voice.envPfValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt()
|
||||||
else 0
|
else 0
|
||||||
|
|
||||||
val finalPitch = (pitchToMixer + autoVibDelta + pitchEnvDelta).coerceIn(1, 0xFFFD)
|
val finalPitch = (pitchToMixer + autoVibDelta + pitchEnvDelta).coerceIn(0x20, 0xFFFF)
|
||||||
voice.playbackRate = computePlaybackRate(inst, finalPitch)
|
voice.playbackRate = computePlaybackRate(inst, finalPitch)
|
||||||
|
|
||||||
// Filter envelope (filter mode): scale baseCut by envValue (0..1, 0.5 = unity).
|
// Filter envelope (filter mode): scale baseCut by envValue (0..1, 0.5 = unity).
|
||||||
@@ -3087,7 +3089,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
val pitchEnvDelta = if (bg.hasPfEnv && bg.pfEnvOn && !bg.envPfIsFilter)
|
val pitchEnvDelta = if (bg.hasPfEnv && bg.pfEnvOn && !bg.envPfIsFilter)
|
||||||
((bg.envPfValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt()
|
((bg.envPfValue - 0.5) * 2.0 * 16.0 * 4096.0 / 12.0).toInt()
|
||||||
else 0
|
else 0
|
||||||
val finalPitch = (bg.noteVal + autoVibDelta + pitchEnvDelta).coerceIn(1, 0xFFFD)
|
val finalPitch = (bg.noteVal + autoVibDelta + pitchEnvDelta).coerceIn(0x20, 0xFFFF)
|
||||||
bg.playbackRate = computePlaybackRate(inst, finalPitch)
|
bg.playbackRate = computePlaybackRate(inst, finalPitch)
|
||||||
// Filter-mode pf envelope: same scaling rule as foreground.
|
// Filter-mode pf envelope: same scaling rule as foreground.
|
||||||
if (bg.hasPfEnv && bg.pfEnvOn && bg.envPfIsFilter) {
|
if (bg.hasPfEnv && bg.pfEnvOn && bg.envPfIsFilter) {
|
||||||
@@ -3603,7 +3605,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
var randomPanBias = 0 // signed
|
var randomPanBias = 0 // signed
|
||||||
|
|
||||||
// Pitch state (4096-TET units, signed when slid).
|
// Pitch state (4096-TET units, signed when slid).
|
||||||
var noteVal = 0xFFFF // The currently sounding base note (no per-row vibrato/arp added)
|
var noteVal = 0x0000 // The currently sounding base note (no per-row vibrato/arp added); 0 = none yet
|
||||||
var basePitch = 0x4000 // Saved pre-effect pitch for vibrato/arp/glissando overlay
|
var basePitch = 0x4000 // Saved pre-effect pitch for vibrato/arp/glissando overlay
|
||||||
// Amiga-mode period state, persisted across ticks so multi-tick E/F slides don't lose
|
// Amiga-mode period state, persisted across ticks so multi-tick E/F slides don't lose
|
||||||
// sub-noteVal precision through repeated round-trip rounding (see amigaSlideTick).
|
// sub-noteVal precision through repeated round-trip rounding (see amigaSlideTick).
|
||||||
@@ -3965,7 +3967,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
|
|||||||
it.hasPfEnv = false; it.envPfIsFilter = false
|
it.hasPfEnv = false; it.envPfIsFilter = false
|
||||||
it.fadeoutVolume = 1.0
|
it.fadeoutVolume = 1.0
|
||||||
it.rampOutSamples = 0; it.rampOutGain = 0.0; it.rampOutStep = 0.0
|
it.rampOutSamples = 0; it.rampOutGain = 0.0; it.rampOutStep = 0.0
|
||||||
it.noteVal = 0xFFFF; it.basePitch = 0x4000
|
it.noteVal = 0x0000; it.basePitch = 0x4000
|
||||||
it.amigaPeriod = -1.0; it.linearFreq = -1.0
|
it.amigaPeriod = -1.0; it.linearFreq = -1.0
|
||||||
it.tonePortaTarget = -1; it.tonePortaSpeed = 0
|
it.tonePortaTarget = -1; it.tonePortaSpeed = 0
|
||||||
it.filterY1 = 0.0; it.filterY2 = 0.0
|
it.filterY1 = 0.0; it.filterY2 = 0.0
|
||||||
|
|||||||
@@ -561,7 +561,10 @@ class TestDiskDrive(private val vm: VM, private val driveNum: Int, theRootPath:
|
|||||||
statusCode.set(STATE_CODE_STANDBY)
|
statusCode.set(STATE_CODE_STANDBY)
|
||||||
}
|
}
|
||||||
else if (inputString.startsWith("USAGE")) {
|
else if (inputString.startsWith("USAGE")) {
|
||||||
recipient?.writeout(composePositiveAns("USED123456/TOTAL654321"))
|
val used = rootPath.walkTopDown().filter { it.isFile }.map { it.length() }.sum()
|
||||||
|
.coerceIn(0L, Int.MAX_VALUE.toLong())
|
||||||
|
val total = rootPath.totalSpace.coerceIn(0L, Int.MAX_VALUE.toLong())
|
||||||
|
recipient?.writeout(composePositiveAns("USED$used/TOTAL$total"))
|
||||||
statusCode.set(STATE_CODE_STANDBY)
|
statusCode.set(STATE_CODE_STANDBY)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -387,7 +387,7 @@ def encode_note_xm(xm_note: int) -> int:
|
|||||||
if 1 <= xm_note <= 96:
|
if 1 <= xm_note <= 96:
|
||||||
semis = xm_note - XM_RELNOTE_C4
|
semis = xm_note - XM_RELNOTE_C4
|
||||||
val = round(TAUD_C4 + semis * 4096 / 12)
|
val = round(TAUD_C4 + semis * 4096 / 12)
|
||||||
return max(1, min(0xFFFD, val))
|
return max(0x20, min(0xFFFF, val))
|
||||||
return NOTE_NOP
|
return NOTE_NOP
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user