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