ltgcgo/octavia

View on GitHub
src/cambiare/index.mjs

Summary

Maintainability
Test Coverage
// 2022-2025 (C) Lightingale Community
// Licensed under GNU LGPL v3.0 license.
 
"use strict";
 
import {OctaviaDevice, allocated, ccToPos, getDebugState} from "../state/index.mjs";
import {RootDisplay} from "../basic/index.mjs";
import {MxFont40} from "../basic/mxReader.js";
 
const targetRatio = 16 / 9;
const pixelBlurSpeed = 64;
const piMulti = new Float64Array(49); // 0~360, 90 deg -> 15 deg
const chTypes = "Vx,Dr,D1,D2,D3,D4,D5,D6,D7,D8".split(",");
const blackKeys = [1, 3, 6, 8, 10],
keyXs = [0, 0.5, 1, 1.5, 2, 3, 3.5, 4, 4.5, 5, 5.5, 6];
const fullRotation = 2 * Math.PI;
const modeNames = {
"?": "Unset",
"gm": "General MIDI",
"g2": "General MIDI 2",
"xg": "Yamaha XG",
"gs": "Roland GS",
"sc": "Roland GS",
"mt32": "Roland MT-32",
"doc": "Yamaha DOC",
"qy10": "Yamaha QY10",
"qy20": "Yamaha QY20",
"sd": "Roland SD",
"x5d": "Korg X5D",
"05rw": "Korg 05R/W",
"ns5r": "Korg NS5R",
"k11": "Kawai K11",
"sg": "Akai SG",
"trin": "Korg Trinity",
"krs": "Korg KROSS 2",
"s90es": "Yamaha S90 ES",
"cs6x": "Yamaha CS6x",
"cs1x": "Yamaha CS1x",
"motif": "Yamaha Motif ES",
"pa": "Korg PA"
};
const metaNames = {
"Copyrite": "Copyright",
"Cmn.Text": "Text",
"C.Lyrics": "Lyrics",
"C.Marker": "Marker",
"CuePoint": "Cue Point",
"Instrmnt": "Instrument",
"Kar.Info": "Kar. Info",
"Kar.Mode": "Karaoke",
"Kar.Lang": "Language",
"KarTitle": "Kar. Title",
"KarLyric": "Kar. Lyrics",
"Kar.Ver.": "Kar. Version",
"SGLyrics": "SG Lyrics",
"TrkTitle": "Title",
"EORTitle": "Real Title", // Extension-overriden real title
"OSysMeta": "Octavia Sys.",
"XfSngDte": "XF Date",
"XfSngRgn": "XF Region",
"XfSngCat": "XF Category",
"XfSongBt": "XF Beat",
"XfSngIns": "XF Instr.",
"XfSngVoc": "XF Vocalist",
"XfSngCmp": "XF Compose",
"XfSngLrc": "XF Lyricist",
"XfSngArr": "XF Arranger",
"XfSngPer": "XF Perform.",
"XfSngPrg": "XF Program.",
"XfSngTag": "XF Tags",
"XfKarLng": "XF Lang.",
"XfKarNme": "XF Name",
"XfKarCmp": "XK Compo.",
"XfKarLrc": "XK Lyricist",
"XfKarArr": "XK Arranger",
"XfKarPer": "XK Perform.",
"XfKarPrg": "XK Progr.",
"XfScneNo": "XF Scene #",
"XfMeloCh": "XF Melody",
"XfLyrOff": "XL Offset",
"XfLyrEnc": "XL Lang.",
"XfSngPrt": "XL Part",
"YMCSSect": "Section Ctrl.",
"YStyleId": "Active Style"
}, metaBlocklist = [
"XfSongBt",
"XfSngIns",
"XfSngVoc",
"XfLyrOff",
"XfSngRgn",
"ChordCtl",
"RhrslMrk"
];
const lineDash = [[], [4, 2], [3, 2]];
const portPos = [{l: 0, t: 0}, {l: 0, t: 416}, {l: 960, t: 0}, {l: 960, t: 416}];
 
const pixelProfiles = {
"none": {
"font4": [0, 0], // y, x
"cfont4": [0, 0],
"font7": [0, 0]
},
"macos": {
"font4": [2, 0],
"cfont4": [1, 0],
"font7": [0, 0]
},
"chromium": {
"font4": [1, 0],
"cfont4": [0, 0],
"font7": [1, 0]
}
};
 
const modeColourPool = {
"xg": ["9efaa0", "006415"],
"doc": ["9efaa0", "006415"],
"qy10": ["9efaa0", "006415"],
"qy20": ["9efaa0", "006415"],
"ns5r": ["9efaa0", "006415"],
"x5d": ["9efaa0", "006415"],
"05rw": ["9efaa0", "006415"],
"trin": ["9efaa0", "006415"],
"k11": ["9efaa0", "006415"],
"s90es": ["9efaa0", "006415"],
"motif": ["9efaa0", "006415"],
"cs6x": ["9efaa0", "006415"],
"an1x": ["9efaa0", "006415"],
"cs1x": ["9efaa0", "006415"],
"gm": ["a1f3ff", "005e88"],
"g2": ["a1f3ff", "005e88"],
"pa": ["a1f3ff", "005e88"],
"krs": ["a1f3ff", "005e88"],
"gs": ["ffe1a5", "804e00"],
"sc": ["ffe1a5", "804e00"],
"mt32": ["ffe1a5", "804e00"],
"sd": ["ffe1a5", "804e00"],
"sg": ["ffdddd", "990022"]
};
/*const modeGlobalClasses = [];
for (let mode in modeNames) {
modeGlobalClasses.push(`cambiare-mode-${mode}`);
};*/
 
piMulti.forEach((e, i, a) => {
a[i] = Math.PI * i / 12;
});
 
let createElement = function (tag, classes, details = {}) {
let target = document.createElement(tag);
classes?.forEach((e) => {
target.classList.add(e);
});
let {t, l, w, h, i, a} = details;
t?.constructor && (target.style.top = t?.length ? t : `${t}px`);
l?.constructor && (target.style.left = l?.length ? l :`${l}px`);
w?.constructor && (target.style.width = w?.length ? w :`${w}px`);
h?.constructor && (target.style.height = h?.length ? h :`${h}px`);
i?.constructor && (target.appendChild(document.createTextNode(i)));
a?.constructor && (target.style.textAlign = a);
return target;
};
let createSVG = function (tag, details) {
let target = document.createElementNS("http://www.w3.org/2000/svg", tag);
for (let key in details) {
target.setAttribute(key, details[key]);
};
return target;
};
let mountElement = function (root, children) {
children?.forEach((e) => {
root.appendChild(e);
});
};
let classOff = function (target, classes) {
classes.forEach((e) => {
if (target.classList.contains(e)) {
target.classList.remove(e);
//console.debug(`Removed class ${e}.`);
};
});
};
let classOn = function (target, classes) {
classes.forEach((e) => {
if (!target.classList.contains(e)) {
target.classList.add(e);
//console.debug(`Added class ${e}.`);
}/* else {
console.debug(`Failed to add class as it already exists: ${target.classList}`);
}*/;
});
};
 
// Cache for CC register display heights
let heightCache = new Array(128).fill(0);
heightCache.forEach((e, i, a) => {
a[i] = Math.floor(24 * i / 12.7) / 10;
});
// Cache for panpot display widths
let widthCache = new Array(128).fill(0);
widthCache.forEach((e, i, a) => {
a[i] = Math.abs(Math.round(48 * (i - 64) / 12.7) / 10);
});
// Cache for the scale segments
let leftCache = new Array(11).fill(null);
leftCache.forEach((e, i, a) => {
a[i] = `${Math.round(i * 12 / 0.0128) / 100}%`;
});
let centCache = new Array(128).fill(null);
centCache.forEach((e, i, a) => {
a[i] = `${Math.round(i / 1.27) / 100}`;
});
let setCcSvg = function (svg, value) {
let hV = heightCache[value];
svg.setAttribute("height", hV);
svg.setAttribute("y", 24 - hV);
};
 
let setCanvasText = function (context, text) {
context.innerText = text;
context.rNew = true;
//context.rOffset = 0;
let measured = context.measureText(text);
context.rWidth = measured.width;
};
 
HTMLElement.prototype.setTextRaw = function (text) {
let textNode;
if (this.childNodes[0]?.nodeType === 3) {
textNode = this.childNodes[0];
textNode.data = text;
} else {
textNode = document.createTextNode(text);
this.prepend(textNode);
};
};
 
let Cambiare = class extends RootDisplay {
#metaGcLine = 16;
#metaGcStart = 32;
#metaMaxLine = 128;
#metaAmend = false;
#metaType = "";
#metaLastLine;
#metaLastWheel = 0;
#metaMoveX = 0;
#metaMoveY = 0;
#maxPoly = 0;
#maxPolyEC = 0;
#metaGcThread;
#metaGcAt = 0;
#metaGcScheduled = false;
#underlinedCh = allocated.invalidCh;
#renderRange = 1;
#renderPort = 0;
#lastFrame = 0;
#scheme = 0;
#bufLo = new Uint8Array(1280);
#bufLm = new Uint8Array(1280);
#bufLn = new Uint8Array(1280);
#bufBo = new Uint8Array(512);
#bufBm = new Uint8Array(512);
#bufBn = new Uint8Array(512);
#hideCh = new Uint8Array(allocated.ch);
#clockSource;
#visualizer;
#container;
#canvas;
#pixelProfile;
#accent = "fcdaff";
#chAccent = new Array(allocated.ch);
#chMode = new Array(allocated.ch);
#foreground = "ffffff";
#mode = "?";
#sectInfo = {};
#sectMark = {};
#sectPart = [];
#sectMeta = {};
#sectPix = {};
eventViewMode = 0; // 0 for event count, 1 for FPS
useElementCount = true;
//#noteEvents = [];
#pitchEvents = [];
#style = "comb";
glyphs = new MxFont40();
panStyle = 11; // Block, Pin, Arc, Dash
pixelMin = 12;
pixelMax = 255;
#drawNote(context, note, velo, state = 0, pitch = 0, part) {
// Param calculation
let upThis = this;
let {width, height} = context.canvas;
let sx, ex, dx, border;
let range = upThis.#renderRange;
let internalNoteStyle = 0,
isBlackKey = blackKeys.indexOf(note % 12) > -1;
switch (state) {
case upThis.device.NOTE_HELD:
case upThis.device.NOTE_SOSTENUTO_SUSTAIN:
case upThis.device.NOTE_SOSTENUTO_HELD: {
internalNoteStyle = 1;
break;
};
case upThis.device.NOTE_MUTED_RECOVERABLE: {
internalNoteStyle = 2;
break;
};
};
switch (upThis.#style) {
case "block":
case "comb": {
sx = Math.round(note * width / 128);
ex = Math.round((note + 1) * width / 128);
dx = ex - sx;
border = range === 1 ? 2 : 1;
break;
};
case "piano": {
sx = Math.round((Math.floor(note / 12) * 7 + keyXs[note % 12]) * width / 75 * 1.0044642857142856);
ex = Math.round((Math.floor(note / 12) * 7 + keyXs[note % 12] + 1) * width / 75 * 1.0044642857142856) - 1;
dx = ex - sx;
border = range === 1 ? 3 : 1;
break;
};
case "line": {
let originPitch = note - pitch;
if (Math.abs(pitch) > 2) {
originPitch = note - Math.sign(pitch) * 2;
};
ex = Math.round((note + 0.5) * width / 128);
sx = Math.round((originPitch + 0.5) * width / 128);
};
default: {
// Nothing yet
};
};
// Colours
context.fillStyle = `#${isBlackKey ? (upThis.getChAccent(part)) : (upThis.#foreground)}${((velo << 1) | (velo >> 6)).toString(16).padStart(2, "0")}`;
context.strokeStyle = context.fillStyle;
context.lineWidth = range === 1 ? 4 : 2;
context.lineDashOffset = 0;
// Draw calls
switch (upThis.#style) {
case "block": {
let h = context.canvas.height - 1;
context.fillRect(sx, 0, dx, h);
if (internalNoteStyle > 0) {
context.clearRect(sx + border, border, dx - (border << 1), h - (border << 1));
};
if (internalNoteStyle === 2) {
context.clearRect(sx, border << 1, dx, h - (border << 2));
};
break;
};
case "comb": {
let h = (isBlackKey ? Math.round((context.canvas.height << 1) / 3) : context.canvas.height) - 1;
context.fillRect(sx, 0, dx, h);
if (internalNoteStyle > 0) {
context.clearRect(sx + border, border, dx - (border << 1), h - (border << 1));
};
if (internalNoteStyle === 2) {
context.clearRect(sx, border << 1, dx, h - (border << 2));
};
break;
};
case "piano": {
let sh = (isBlackKey ? 0 : context.canvas.height >> 1),
dh = (context.canvas.height >> 1) - 1;
context.fillRect(sx, sh, dx, dh);
if (internalNoteStyle > 0) {
context.clearRect(sx + border, sh + border, dx - (border << 1), dh - (border << 1));
};
if (internalNoteStyle === 2) {
context.clearRect(sx, sh + (border << 1), dx, dh - (border << 2));
};
break;
};
case "line": {
if (internalNoteStyle === 1) {
switch (range) {
case 4: {
context.setLineDash(lineDash[2]);
//context.lineDashOffset = -1;
break;
};
default: {
context.setLineDash(lineDash[1]);
//context.lineDashOffset = 0;
};
};
} else {
context.setLineDash(lineDash[0]);
if (range !== 4 && self?.document?.mozFullScreen) {
sx += 0.5;
ex += 0.5;
};
};
context.beginPath();
context.moveTo(sx, (range === 4 || !internalNoteStyle) && self?.document?.mozFullScreen ? 1 : 0);
context.lineTo(ex, (height >> 1) + 1);
context.lineTo(sx, height);
context.stroke();
break;
};
default: {
// Nothing yet
};
};
};
#redrawNotesInternal(sum, overrideActiveCh) {
let upThis = this;
(sum?.chInUse || overrideActiveCh).forEach((e, part) => {
if (e) {
let context = upThis.#sectPart[part >> 4][part & 15].cxt;
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
sum.chKeyPr[part].forEach(({v, s}, note) => {
upThis.#drawNote(context, note, v, s, upThis.device.getPitchShift(part), part);
});
};
});
};
#scrollMeta(resetTime) {
let upThis = this;
if (Date.now() - upThis.#metaLastWheel > 4000) {
upThis.#metaMoveX = 0;
upThis.#metaMoveY = 142 - upThis.#sectMeta.view.clientHeight;
if ((upThis.#metaLastLine?.clientWidth || 0) > 840) {
upThis.#metaMoveX = 840 - upThis.#metaLastLine.clientWidth;
};
upThis.#sectMeta.view.style.transform = `translateX(${upThis.#metaMoveX}px) translateY(${upThis.#metaMoveY}px)`;
if (resetTime) {
upThis.#metaLastWheel = 0;
};
};
};
#resizerSrc() {
let aspectRatio = self.innerWidth / self.innerHeight;
let targetZoom = 1;
let targetWidth = self.innerWidth,
targetHeight = self.innerHeight;
if (aspectRatio >= targetRatio) {
targetZoom = Math.min(Math.round(self.innerHeight / 1080 * 10000) / 10000, 100);
targetWidth = Math.ceil(self.innerHeight * targetRatio);
} else if (aspectRatio < targetRatio) {
targetZoom = Math.min(Math.round(self.innerWidth / 1920 * 10000) / 10000, 100);
targetHeight = Math.ceil(self.innerWidth / targetRatio);
};
//console.debug(targetZoom);
this.#container.style.width = `${targetWidth}px`;
this.#container.style.height = `${targetHeight}px`;
this.#canvas.style.transform = `scale(${targetZoom})`;
};
#resizer;
#rendererSrc() {
let upThis = this,
clock = upThis.#clockSource?.currentTime || 0,
sum = upThis.render(clock),
timeNow = Date.now();
let curPoly = sum.curPoly + sum.extraPoly;
let curPolyEC = sum.curPolyEC + sum.extraPolyEC;
if (upThis.#maxPoly < curPoly) {
upThis.#maxPoly = curPoly;
};
if (upThis.#maxPolyEC < curPolyEC) {
upThis.#maxPolyEC = curPolyEC;
};
upThis.#sectInfo.curPoly.setTextRaw(`${upThis.useElementCount ? curPolyEC : curPoly}`.padStart(3, "0"));
upThis.#sectInfo.maxPoly.setTextRaw(`${upThis.useElementCount ? upThis.#maxPolyEC : upThis.#maxPoly}`.padStart(3, "0"));
if (upThis.#clockSource?.realtime) {
upThis.#sectInfo.barCount.setTextRaw("LINE");
upThis.#sectInfo.barDelim.style.display = "none";
upThis.#sectInfo.barNote.setTextRaw("IN");
} else {
upThis.#sectInfo.barCount.setTextRaw(sum.noteBar + 1);
upThis.#sectInfo.barDelim.style.display = "";
upThis.#sectInfo.barNote.setTextRaw(Math.floor(sum.noteBeat) + 1);
};
//upThis.#scrollMeta(true);
let ccCandidates = [7, 11, 1, 91, 93, 94, 74, 5, 256, 256];
let renderPortMax = upThis.#renderPort + upThis.#renderRange;
for (let part = 0; part < allocated.ch; part ++) {
let port = part >> 4,
chOff = part * allocated.cc,
aceOff = part * allocated.ace,
e = upThis.#sectPart[port][part & 15];
if (visualizer.device.getActive()[part] && port >= upThis.#renderPort && port < renderPortMax) {
// Render CC draw calls
ccCandidates[8] = sum.ace[aceOff + 0] || 256;
ccCandidates[9] = sum.ace[aceOff + 1] || 256;
e.ccVis.clearRect(0, 0, 109, 25);
e.ccVis.fillStyle = `#${upThis.getChAccent(part)}`;
e.ccVis.strokeStyle = `#${upThis.getChAccent(part)}`;
e.ccVis.lineWidth = 2;
for (let cci = 0; cci < ccCandidates.length; cci ++) {
let cce = ccCandidates[cci];
if (cce < 256) {
let ccValue = sum.chContr[chOff + ccToPos[cce]],
ccHeight = ccValue * 24 / 127;
if (ccHeight > 0) {
let ccTop = 24 - ccHeight;
e.ccVis.fillRect(cci * 6, ccTop, 4, ccHeight);
};
};
};
let pan = sum.chContr[chOff + ccToPos[10]] || 1;
switch (upThis.panStyle) {
case 7:
case 6:
case 5:
case 3:
case 2:
case 1: {
// Calculate pan
let panDegreeCache = (pan + 125) * 0.024933275, // - 64 + 3 * 63
panRotateCache = (pan - 64) * 0.024933275;
e.ccVis.beginPath();
// Render pan needle
if (pan > 127) {
if ((upThis.panStyle >> 1) & 1) {
e.ccVis.arc(84.5, 22.5, 21.5, 0, piMulti[12], true);
e.ccVis.stroke();
};
} else {
if ((upThis.panStyle >> 1) & 1) {
if (pan < 64) {
e.ccVis.arc(84.5, 22.5, 21.5, piMulti[18], panDegreeCache, true);
e.ccVis.stroke();
} else if (pan > 64) {
e.ccVis.arc(84.5, 22.5, 21.5, piMulti[18], panDegreeCache);
e.ccVis.stroke();
};
};
if (upThis.panStyle & 1) {
e.ccVis.strokeStyle = `#${upThis.#foreground}`;
e.ccVis.lineWidth = upThis.panStyle >> 2 ? 1 : 3;
e.ccVis.beginPath();
e.ccVis.moveTo(84.5, 22.5);
if (pan === 64) {
e.ccVis.lineTo(84.5, 4);
} else {
e.ccVis.lineTo(84.5 + 18 * Math.sin(panRotateCache), 22.5 - 18.5 * Math.cos(panRotateCache));
};
e.ccVis.stroke();
};
};
if ((upThis.panStyle >> 2) & 1) {
e.ccVis.fillStyle = `#${upThis.#foreground}`;
e.ccVis.beginPath();
e.ccVis.arc(84.5, 22.5, 2.5, 0, piMulti[4]);
e.ccVis.fill();
};
break;
};
case 11:
case 10:
case 9: {
// Calculate pan
let panDegreeCache = (pan + 77) * 0.033244367, // - 64 + 3 * 63
panRotateCache = (pan - 64) * 0.033244367;
e.ccVis.beginPath();
// Render pan needle
if (pan > 127) {
if ((upThis.panStyle >> 1) & 1) {
e.ccVis.arc(84.5, 16.5, 15.5, piMulti[26], piMulti[34], true);
e.ccVis.stroke();
};
} else {
if ((upThis.panStyle >> 1) & 1) {
if (pan < 64) {
e.ccVis.arc(84.5, 16.5, 15.5, piMulti[18], panDegreeCache, true);
e.ccVis.stroke();
} else if (pan > 64) {
e.ccVis.arc(84.5, 16.5, 15.5, piMulti[18], panDegreeCache);
e.ccVis.stroke();
};
};
if (upThis.panStyle & 1) {
e.ccVis.strokeStyle = `#${upThis.#foreground}`;
e.ccVis.lineWidth = 3;
e.ccVis.beginPath();
e.ccVis.moveTo(84.5, 16.5);
if (pan === 64) {
e.ccVis.lineTo(84.5, 4);
} else {
e.ccVis.lineTo(84.5 + 11.5 * Math.sin(panRotateCache), 16.5 - 11.5 * Math.cos(panRotateCache));
};
e.ccVis.stroke();
};
};
if ((upThis.panStyle & 1) === 0 || pan >> 7) {
e.ccVis.fillStyle = `#${upThis.#foreground}`;
e.ccVis.beginPath();
e.ccVis.arc(84.5, 16.5, 2.5, 0, piMulti[24]);
e.ccVis.fill();
};
break;
};
default: {
// Calculate pan
let panWidthCache = Math.abs(pan - 64) / 2.625;
if (pan < 64) {
e.ccVis.fillRect(84 - panWidthCache, 0, panWidthCache, 24);
} else if (pan > 127) {
e.ccVis.fillRect(60, 0, 49, 24);
e.ccVis.clearRect(61, 1, 47, 22);
} else {
e.ccVis.fillRect(85, 0, panWidthCache, 24);
};
// Render pan divider
e.ccVis.fillStyle = `#${upThis.#foreground}`;
e.ccVis.fillRect(84, 0, 1, 24);
};
};
// Render strength metre
e.metre.clearRect(0, 0, 121, 25);
e.metre.fillStyle = `#${upThis.#foreground}`;
e.metre.globalCompositeOperation = "source-over";
if (e.metre.rWidth > e.metre.canvas.width) {
if (e.metre.rNew) {
e.metre.rNew = false;
e.metre.rOffset = clock;
};
let runCourse = clock - (e.metre.rOffset || 0),
runPadding = 32,
runBoundary = e.metre.rWidth - e.metre.canvas.width + runPadding,
offsetX = (runCourse * -25) % (e.metre.rWidth + runPadding + 48) + 48;
if (offsetX > 0) {
offsetX = 0;
};
e.metre.fillText(e.metre.innerText, offsetX, 3 + upThis.#pixelProfile.cfont4[0]);
if (Math.abs(offsetX) > runBoundary) {
e.metre.fillText(e.metre.innerText, offsetX + e.metre.rWidth + runPadding, 3 + upThis.#pixelProfile.cfont4[0]);
};
} else {
e.metre.fillText(e.metre.innerText, 0, 3 + upThis.#pixelProfile.cfont4[0]);
};
e.metre.globalCompositeOperation = "xor";
e.metre.fillRect(0, 0, sum.strength[part] * 121 / 255, 25);
// Extensible visualizer
e.extVis.clearRect(0, 0, 47, 25);
e.extVis.fillStyle = `#${upThis.#foreground}`;
switch (sum.chExt[part][0]) {
case upThis.device.EXT_VL: {
let mouth = (sum.chContr[chOff + ccToPos[136]] - 64) / 64 || sum.rawPitch[part] / 8192;
mouth = mouth * -4 + 4;
// Enabling global mod wheel locking will cause ghost activations. Must improve later.
let velocity = +(sum.rawVelo[part] > 0 || sum.chContr[chOff + ccToPos[1]] > 0) * (sum.chContr[chOff + ccToPos[129]] * sum.chContr[chOff + ccToPos[11]] / 16129);
if (!velocity && sum.rawStrength[part]) {
velocity = sum.rawStrength[part] * sum.chContr[chOff + ccToPos[11]] / 16129;
};
velocity *= 32;
let breathNoise = sum.chContr[chOff + ccToPos[1]] / 127 * 8;
e.extVis.beginPath();
e.extVis.moveTo(0, 12 - mouth - 3);
e.extVis.lineTo(7 + velocity, 12);
e.extVis.lineTo(0, 12 + mouth + 3);
e.extVis.fill();
e.extVis.fillStyle = `#${upThis.getChAccent(part)}`;
e.extVis.beginPath();
e.extVis.ellipse(43, 12, 4, 4 + breathNoise, 0, 0, fullRotation);
e.extVis.fill();
//console.debug(`Painted!`);
break;
};
case upThis.device.EXT_DX: {
let dxView = sum.chContr.subarray(chOff + ccToPos[142], chOff + ccToPos[157] + 1);
dxView.forEach((v, i) => {
if (i >= 8) {
e.extVis.fillStyle = `#${upThis.getChAccent(part)}`;
};
let x = i * 3;
let size = (v - 64) / 5.82;
if (size >= 0) {
e.extVis.fillRect(x, 12 - size, 2, size + 1);
} else {
e.extVis.fillRect(x, 12, 2, 1 - size);
};
});
break;
};
};
};
};
// Note visualization
let channels = new Array(allocated.ch);
upThis.#sectPart.forEach((e, port) => {
e.forEach((e0, part) => {
if (e0.refresh) {
e0.refresh = false;
channels[port << 7 | part] = true;
};
});
});
if (['line'].indexOf(upThis.#style) > -1) {
// Sift through pitch events
while (upThis.#pitchEvents.length > 0) {
let e = upThis.#pitchEvents.shift();
channels[e.part] = true;
};
};
// Draw every note that has channels updated
upThis.#redrawNotesInternal(sum, channels);
// Draw every note inside extraStates
sum.extraNotes.forEach((ev) => {
let {part, note, velo, state} = ev;
let context = upThis.#sectPart[part >> 4][part & 15].cxt;
upThis.#drawNote(context, note, velo, state, upThis.device.getPitchShift(part), part);
//console.debug(part, note);
});
// Write to the new pixel display buffers
let ccxt = upThis.#sectPix.cxt;
if (timeNow > sum.bitmap.expire) {
upThis.#bufBn.fill(0);
} else if (sum.bitmap.bitmap.length > 256) {
sum.bitmap.bitmap.forEach((e, i) => {
upThis.#bufBn[i] = e ? upThis.pixelMax : upThis.pixelMin;
});
} else {
sum.bitmap.bitmap.forEach((e, i) => {
upThis.#bufBn[i << 1] = e ? upThis.pixelMax : upThis.pixelMin;
upThis.#bufBn[(i << 1) | 1] = e ? upThis.pixelMax : upThis.pixelMin;
});
};
upThis.#bufLn.fill(0);
if (timeNow <= sum.letter.expire) {
upThis.glyphs.getStr(sum.letter.text.padEnd(32, " ")).forEach((e0, i0) => {
// Per character
let baseX = (i0 & 15) * 5, baseY = (i0 >> 4) << 3;
e0.forEach((e, i) => {
// Per pixel in character
let x = baseX + i % 5, y = baseY + Math.floor(i / 5);
upThis.#bufLn[y * 80 + x] = e ? upThis.pixelMax : upThis.pixelMin;
});
});
};
// Apply pixel blurs
upThis.#bufBo.forEach((e, i, a) => {
let e0 = upThis.#bufBn[i];
if (e0 > e) {
a[i] += Math.min(e0 - e, pixelBlurSpeed);
} else if (e0 < e) {
a[i] -= Math.min(e - e0, pixelBlurSpeed);
};
});
upThis.#bufLo.forEach((e, i, a) => {
let e0 = upThis.#bufLn[i];
if (e0 > e) {
a[i] += Math.min(e0 - e, pixelBlurSpeed);
} else if (e0 < e) {
a[i] -= Math.min(e - e0, pixelBlurSpeed);
};
});
// Render the old pixel display buffers
upThis.#bufBo.forEach((e, i) => {
let y = i >> 5, x = i & 31;
if (upThis.#bufBm[i] !== e) {
ccxt.clearRect(252 + (x << 2), y << 2, 3, 3);
if (e) {
ccxt.fillStyle = `#${upThis.#foreground}${e.toString(16).padStart(2, "0")}`;
ccxt.fillRect(252 + (x << 2), y << 2, 3, 3);
};
} else if (getDebugState()) {
ccxt.clearRect(252 + (x << 2), y << 2, 3, 3);
if (e) {
ccxt.fillStyle = `#ff0000${e.toString(16).padStart(2, "0")}`;
ccxt.fillRect(252 + (x << 2), y << 2, 3, 3);
};
};
});
upThis.#bufLo.forEach((e, i) => {
let y = Math.floor(i / 80), x = i % 80;
x += Math.floor(x / 5);
if (upThis.#bufLm[i] !== e) {
ccxt.clearRect(x << 2, (y | 16) << 2, 3, 3);
if (e) {
ccxt.fillStyle = `#${upThis.#foreground}${e.toString(16).padStart(2, "0")}`;
ccxt.fillRect(x << 2, (y | 16) << 2, 3, 3);
};
} else if (getDebugState()) {
ccxt.clearRect(x << 2, (y | 16) << 2, 3, 3);
if (e) {
ccxt.fillStyle = `#ff0000${e.toString(16).padStart(2, "0")}`;
ccxt.fillRect(x << 2, (y | 16) << 2, 3, 3);
};
};
});
// Update the intermediary cache
upThis.#bufBm.forEach((e, i, a) => {
a[i] = upThis.#bufBo[i];
});
upThis.#bufLm.forEach((e, i, a) => {
a[i] = upThis.#bufLo[i];
});
// If under debug mode, also visualize the polyphonic state
if (getDebugState()) {
ccxt.clearRect(0, 0, 251, 63);
for (let slot = 0; slot <= upThis.device.polyIndexLast; slot ++) {
let pX = slot & 31, pY = slot >> 5;
if (slot + 1 === upThis.device.polyIndexLatest) {
ccxt.fillStyle = "#ff0"; // Yellow for the most recently accessed register
} else {
switch(upThis.device.getPolyState(slot)) {
case upThis.device.NOTE_SUSTAIN: {
// Active
ccxt.fillStyle = "#0f0";
break;
};
case upThis.device.NOTE_HELD:
case upThis.device.NOTE_SOSTENUTO_HELD: {
// Held
ccxt.fillStyle = "#0f7";
break;
};
case upThis.device.NOTE_SOSTENUTO_SUSTAIN: {
// Sostenuto
ccxt.fillStyle = "#70f";
break;
};
case upThis.device.NOTE_MUTED_RECOVERABLE: {
// Recoverable
ccxt.fillStyle = "#f70";
break;
};
default: {
ccxt.fillStyle = "#f00";
};
};
};
ccxt.fillRect(pX << 2, pY << 2, 3, 3);
};
};
let finishNow = (self.performance || self.Date).now();
switch (upThis.eventViewMode) {
case 0: {
upThis.#sectInfo.events.setTextRaw(`${sum.eventCount}`.padStart(3, "0"));
break;
};
case 1: {
let fpsCount = 1000 / (finishNow - upThis.#lastFrame);
if (fpsCount > 99) {
fpsCount = Math.round(fpsCount);
upThis.#sectInfo.events.setTextRaw(`${fpsCount}`);
} else if (fpsCount > 9) {
fpsCount = Math.round(fpsCount * 10) / 10;
upThis.#sectInfo.events.setTextRaw(`${Math.floor(fpsCount)}.${Math.floor(fpsCount * 10 % 10)}`);
} else {
fpsCount = Math.round(fpsCount * 100) / 100;
upThis.#sectInfo.events.setTextRaw(`${Math.floor(fpsCount)}.${Math.floor(fpsCount * 100 % 100)}`);
};
break;
};
case 2: {
upThis.#sectInfo.events.setTextRaw(`${upThis.device.polyIndexLatest}`.padStart(3, "0"));
break;
};
default: {
upThis.#sectInfo.events.setTextRaw(`???`);
};
};
upThis.#lastFrame = finishNow;
};
#renderer;
#renderThread;
get style() {
return this.#style;
};
set style(value) {
let upThis = this;
upThis.#style = value;
upThis.#redrawNotesInternal(upThis.render(upThis.#clockSource?.currentTime || 0));
classOff(upThis.#canvas, [`cambiare-style-block`, `cambiare-style-comb`, `cambiare-style-piano`, `cambiare-style-line`]);
classOn(upThis.#canvas, [`cambiare-style-${value}`]);
};
getChMode(part = 0, disableFallback) {
if (part >= allocated.ch) {
throw(new RangeError("Invalid part number"));
};
let upThis = this;
if (disableFallback) {
return upThis.#chMode[part];
} else {
return upThis.#chMode[part] ?? upThis.#mode;
};
};
getChAccent(part = 0) {
if (part >= allocated.ch) {
throw(new RangeError("Invalid part number"));
};
let upThis = this;
return upThis.#chAccent[part] ?? upThis.#accent;
};
setClockSource(clockSource) {
this.#clockSource = clockSource;
};
setPixelProfile(profileName) {
let upThis = this;
if (pixelProfiles[profileName]) {
let profileDetails = pixelProfiles[profileName]
upThis.#pixelProfile = profileDetails;
if (upThis.#canvas) {
upThis.#canvas.style.setProperty("--pcp-font4", `translate(${profileDetails.font4[1]}px, ${profileDetails.font4[0]}px)`);
upThis.#canvas.style.setProperty("--pcp-font7", `translate(${profileDetails.font7[1]}px, ${profileDetails.font7[0]}px)`);
/*for (let profile in pixelProfiles) {
classOff(upThis.#canvas, [`cambiare-pixel-${profile}`]);
};
classOn(upThis.#canvas, [`cambiare-pixel-${profileName}`]);*/
};
} else {
throw(new Error(`"${profileName}" is not a valid pixel correction profile`));
};
};
setMode(mode) {
let upThis = this;
upThis.#mode = mode;
upThis.#accent = (modeColourPool[mode] || ["fcdaff", "742b81"])[upThis.#scheme];
//classOff(upThis.#canvas, modeGlobalClasses);
for (let className of upThis.#canvas.classList) {
if (className.substring(0, 14) === "cambiare-mode-") {
upThis.#canvas.classList.remove(className);
};
};
if (mode !== "?") {
upThis.#canvas.classList.add(`cambiare-mode-${mode}`);
};
};
setChMode(part, mode) {
let upThis = this;
upThis.#chMode[part] = mode;
let partViewer = upThis.#sectPart[part >> 4][part & 15];
for (let className of partViewer.root.classList) {
if (className.substring(0, 10) === "part-mode-") {
partViewer.root.classList.remove(className);
};
};
if (mode !== "?") {
partViewer.root.classList.add(`part-mode-${mode}`);
};
};
setScheme(scheme = 0) {
let upThis = this;
upThis.#scheme = scheme ? 1 : 0;
upThis.#foreground = ["ffffff", "000000"][upThis.#scheme];
[classOff, classOn][upThis.#scheme](upThis.#canvas, [`cambiare-scheme-light`]);
upThis.#accent = (modeColourPool[upThis.#mode] || ["fcdaff", "742b81"])[upThis.#scheme];
for (let part = 0; part < allocated.ch; part ++) {
if (upThis.device.getChActive(part) === 0) {
continue;
};
if (upThis.#chAccent[part]) {
let targetColour = modeColourPool[upThis.#chMode[part]];
if (targetColour) {
upThis.#chAccent[part] = targetColour[upThis.#scheme];
};
};
};
};
#setPortView(canvasUpdate) {
let upThis = this;
let range = upThis.#renderRange, port = upThis.#renderPort;
upThis.#sectPart.forEach((e, i) => {
if (i >= port && i < (port + range)) {
classOn(e.root, [`port-active`]);
let index = i - port;
let {l, t} = portPos[index * (4 / range)];
e.root.style.top = `${t}px`;
e.root.style.left = `${l}px`;
e.forEach((e, i) => {
e.root.style.top = `${i * (range > 2 ? 26 : 52)}px`;
});
} else {
classOff(e.root, [`port-active`]);
e.root.style.top = "";
e.root.style.left = "";
e.forEach((e, i) => {
e.root.style.top = "";
});
};
if (canvasUpdate) {
e.forEach((e0, i0) => {
//console.debug(e0, i, i0);
e0.cxt.canvas.width = upThis.#renderRange === 1 ? 1193 : 495;
e0.cxt.canvas.height = upThis.#renderRange === 4 ? 26 : 52;
});
};
});
};
setPort(port) {
let upThis = this;
classOff(upThis.#canvas, [`cambiare-start0`, `cambiare-start1`, `cambiare-start2`, `cambiare-start3`, `cambiare-start4`, `cambiare-start5`, `cambiare-start6`, `cambiare-start7`]);
classOn(upThis.#canvas, [`cambiare-start${port}`]);
upThis.#renderPort = port;
upThis.#setPortView(false);
};
setRange(mode) {
let upThis = this;
classOff(upThis.#canvas, [`cambiare-port1`, `cambiare-port2`, `cambiare-port4`, `cambiare-compact`]);
classOn(upThis.#canvas, [`cambiare-${mode}`]);
upThis.#renderRange = parseInt(mode.slice(4)) || 1;
upThis.#setPortView(true);
};
setFrameTime(frameTime = 20) {
let upThis = this;
if (upThis.#renderThread?.constructor) {
clearInterval(upThis.#renderThread);
};
if (frameTime < 5) {
frameTime = 5;
} else if (frameTime > 500) {
frameTime = 500;
};
upThis.#renderThread = setInterval(upThis.#renderer, frameTime);
upThis.smoothingAtk = Math.pow(0.1, frameTime / 20);
upThis.smoothingDcy = Math.pow(0.75, frameTime / 20);
};
attach(attachElement) {
let upThis = this;
upThis.#visualizer = attachElement;
// Insert a container
let containerElement = createElement("div", ["cambiare-container"]);
attachElement.appendChild(containerElement);
upThis.#container = containerElement;
// Insert the canvas
let canvasElement = createElement("div", [/*"debug",*/ "cambiare-canvas", "cambiare-port1", "cambiare-start0", "cambiare-style-comb"]);
containerElement.appendChild(canvasElement);
upThis.#canvas = canvasElement;
// Start the resizer
self.addEventListener("resize", upThis.#resizer);
upThis.#resizer();
upThis.setFrameTime(20);
// Begin inserting the info section
upThis.#sectInfo.root = createElement("div", ["sect-info"]);
upThis.#sectInfo.events = createElement("span", ["field", "pcp-font4"], {t: 1, l: 0, w: 35, h: 33});
upThis.#sectInfo.curPoly = createElement("span", ["field", "pcp-font4"], {t: 1, l: 52, w: 35, h: 33});
upThis.#sectInfo.maxPoly = createElement("span", ["field", "pcp-font4"], {t: 1, l: 98, w: 35, h: 33});
upThis.#sectInfo.sigN = createElement("span", ["field", "pcp-font4"], {t: 1, l: 194, w: 23, h: 33, a: "right"});
upThis.#sectInfo.sigD = createElement("span", ["field", "pcp-font4"], {t: 1, l: 232, w: 23, h: 33});
upThis.#sectInfo.barCount = createElement("span", ["field", "pcp-font4"], {t: 1, l: 304, w: 35, h: 33, a: "right"});
upThis.#sectInfo.barDelim = createElement("span", ["field", "field-label", "pcp-font4"], {t: 0, l: 343, w: 8, h: 33, i: "/"});
upThis.#sectInfo.barNote = createElement("span", ["field", "pcp-font4"], {t: 1, l: 354, w: 23, h: 33});
upThis.#sectInfo.tempo = createElement("span", ["field", "pcp-font4"], {t: 1, l: 454, w: 64, h: 33, a: "right"});
upThis.#sectInfo.volume = createElement("span", ["field", "pcp-font4"], {t: 1, l: 562, w: 63, h: 33, a: "right"});
upThis.#sectInfo.mode = createElement("span", ["field", "pcp-font4"], {t: 1, l: 708, w: 152, h: 33});
upThis.#sectInfo.reverb = createElement("span", ["field", "pcp-font4"], {t: 1, l: 1000, w: 190, h: 33});
upThis.#sectInfo.chorus = createElement("span", ["field", "pcp-font4"], {t: 1, l: 1235, w: 190, h: 33});
upThis.#sectInfo.delay = createElement("span", ["field", "pcp-font4"], {t: 1, l: 1471, w: 190, h: 33});
upThis.#sectInfo.insert = createElement("span", ["field", "pcp-font4"], {t: 1, l: 1706, w: 190, h: 33});
upThis.#sectInfo.title = createElement("span", ["field", "pcp-font4"], {t: 35, l: 50, w: 810, h: 33});
upThis.#sectInfo.insert1 = createElement("span", ["field", "pcp-font4"], {t: 0, l: 40, w: 190, h: 33})
upThis.#sectInfo.insert2 = createElement("span", ["field", "pcp-font4"], {t: 0, l: 40, w: 190, h: 33})
upThis.#sectInfo.insert3 = createElement("span", ["field", "pcp-font4"], {t: 0, l: 40, w: 190, h: 33})
upThis.#sectInfo.insert4 = createElement("span", ["field", "pcp-font4"], {t: 0, l: 40, w: 190, h: 33})
upThis.#sectInfo.inscon1 = createElement("span", ["field", "field-collapsive"], {t: 35, l: 960, w: 230, h: 33});
upThis.#sectInfo.inscon2 = createElement("span", ["field", "field-collapsive"], {t: 35, l: 1195, w: 230, h: 33});
upThis.#sectInfo.inscon3 = createElement("span", ["field", "field-collapsive"], {t: 35, l: 1431, w: 230, h: 33});
upThis.#sectInfo.inscon4 = createElement("span", ["field", "field-collapsive"], {t: 35, l: 1666, w: 230, h: 33});
mountElement(upThis.#sectInfo.inscon1, [
createElement("span", ["field", "field-key", "pcp-font7"], {t: 0, l: 0, w: 36, h: 33, i: "In2", a: "right"}),
upThis.#sectInfo.insert1
]);
mountElement(upThis.#sectInfo.inscon2, [
createElement("span", ["field", "field-key", "pcp-font7"], {t: 0, l: 0, w: 36, h: 33, i: "In3", a: "right"}),
upThis.#sectInfo.insert2
]);
mountElement(upThis.#sectInfo.inscon3, [
createElement("span", ["field", "field-key", "pcp-font7"], {t: 0, l: 0, w: 36, h: 33, i: "In4", a: "right"}),
upThis.#sectInfo.insert3
]);
mountElement(upThis.#sectInfo.inscon4, [
createElement("span", ["field", "field-key", "pcp-font7"], {t: 0, l: 0, w: 36, h: 33, i: "In5", a: "right"}),
upThis.#sectInfo.insert4
]);
/*upThis.#sectInfo.reverb = createElement("span", ["field", "pcp-font4"], {t: 35, l: 40, w: 190, h: 33});
upThis.#sectInfo.chorus = createElement("span", ["field", "pcp-font4"], {t: 35, l: 280, w: 190, h: 33});
upThis.#sectInfo.delay = createElement("span", ["field", "pcp-font4"], {t: 35, l: 515, w: 190, h: 33});
upThis.#sectInfo.insert = createElement("span", ["field", "pcp-font4"], {t: 35, l: 746, w: 190, h: 33});
upThis.#sectInfo.title = createElement("span", ["field", "pcp-font4"], {t: 1, l: 1010, w: 810, h: 33});*/
canvasElement.appendChild(upThis.#sectInfo.root);
mountElement(upThis.#sectInfo.root, [
upThis.#sectInfo.events,
upThis.#sectInfo.curPoly,
createElement("span", ["field", "field-label", "pcp-font4"], {t: 1, l: 89, w: 5, h: 33, i: ":"}),
upThis.#sectInfo.maxPoly,
createElement("span", ["field", "field-key", "pcp-font7"], {t: 1, l: 148, w: 41, h: 33, i: "TSig"}),
upThis.#sectInfo.sigN,
createElement("span", ["field", "field-label", "pcp-font4"], {t: 0, l: 221, w: 8, h: 33, i: "/"}),
upThis.#sectInfo.sigD,
createElement("span", ["field", "field-key", "pcp-font7"], {t: 1, l: 268, w: 30, h: 33, i: "Bar"}),
upThis.#sectInfo.barCount,
upThis.#sectInfo.barDelim,
upThis.#sectInfo.barNote,
createElement("span", ["field", "field-key", "pcp-font7"], {t: 1, l: 390, w: 61, h: 33, i: "Tempo", a: "right"}),
upThis.#sectInfo.tempo,
createElement("span", ["field", "field-key", "pcp-font7"], {t: 1, l: 528, w: 29, h: 33, i: "Vol"}),
upThis.#sectInfo.volume,
createElement("span", ["field", "field-label", "pcp-font4"], {t: 1, l: 626, w: 17, h: 33, i: "%"}),
createElement("span", ["field", "field-key", "pcp-font7"], {t: 1, l: 652, w: 52, h: 33, i: "Mode"}),
upThis.#sectInfo.mode,
createElement("span", ["field", "field-key", "pcp-font7"], {t: 1, l: 960, w: 36, h: 33, i: "Rev", a: "right"}),
//createElement("span", ["field", "field-key", "pcp-font7"], {t: 35, l: 0, w: 34, h: 33, i: "Rev"}),
upThis.#sectInfo.reverb,
createElement("span", ["field", "field-key", "pcp-font7"], {t: 1, l: 1195, w: 36, h: 33, i: "Cho", a: "right"}),
//createElement("span", ["field", "field-key", "pcp-font7"], {t: 35, l: 238, w: 36, h: 33, i: "Cho"}),
upThis.#sectInfo.chorus,
createElement("span", ["field", "field-key", "pcp-font7"], {t: 1, l: 1431, w: 36, h: 33, i: "Var", a: "right"}),
//createElement("span", ["field", "field-key", "pcp-font7"], {t: 35, l: 478, w: 31, h: 33, i: "Var"}),
upThis.#sectInfo.delay,
createElement("span", ["field", "field-key", "pcp-font7"], {t: 1, l: 1666, w: 36, h: 33, i: "In1", a: "right"}),
//createElement("span", ["field", "field-key", "pcp-font7"], {t: 35, l: 713, w: 27, h: 33, i: "Ins"}),
upThis.#sectInfo.insert,
upThis.#sectInfo.inscon1,
upThis.#sectInfo.inscon2,
upThis.#sectInfo.inscon3,
upThis.#sectInfo.inscon4,
createElement("span", ["field", "field-key", "pcp-font7"], {t: 35, l: 0, w: 44, h: 33, i: "Title"}),
//createElement("span", ["field", "field-key", "pcp-font7"], {t: 1, l: 960, w: 44, h: 33, i: "Title"}),
upThis.#sectInfo.title
]);
// Begin inserting the marker section
upThis.#sectMark.root = createElement("div", ["sect-mark"]);
upThis.#sectMark.left = createElement("div", ["sect-mark-left", "boundary"], {t: 0, l: 0});
upThis.#sectMark.right = createElement("div", ["sect-mark-right", "boundary"], {t: 0, l: 960});
canvasElement.appendChild(upThis.#sectMark.root);
mountElement(upThis.#sectMark.root, [
upThis.#sectMark.left,
upThis.#sectMark.right
]);
mountElement(upThis.#sectMark.left, [
createElement("span", ["field", "field-key"], {t: 0, l: 0, w: 26, h: 33, i: "CH"}),
createElement("span", ["field", "field-key"], {t: 0, l: 30, w: 49, h: 33, i: "Voice"}),
createElement("span", ["field", "field-key", "mark-send-title"], {t: 2, l: 164, w: 25, h: 18, i: "Send"}),
createElement("span", ["field", "field-label", "mark-send-param"], {t: 16, l: 146, w: 58, h: 16, i: "VEMRCDBP12", a: "center"}),
createElement("span", ["field", "field-key"], {t: 0, l: 212, w: 35, h: 33, i: "Pan"}),
createElement("span", ["field", "field-key"], {t: 0, l: 256, w: 45, h: 33, i: "Note"})
]);
mountElement(upThis.#sectMark.right, [
createElement("span", ["field", "field-key"], {t: 0, l: 0, w: 26, h: 33, i: "CH"}),
createElement("span", ["field", "field-key"], {t: 0, l: 30, w: 49, h: 33, i: "Voice"}),
createElement("span", ["field", "field-key", "mark-send-title"], {t: 2, l: 164, w: 25, h: 18, i: "Send"}),
createElement("span", ["field", "field-label", "mark-send-param"], {t: 16, l: 146, w: 58, h: 16, i: "VEMRCDBP12", a: "center"}),
createElement("span", ["field", "field-key"], {t: 0, l: 212, w: 35, h: 33, i: "Pan"}),
createElement("span", ["field", "field-key"], {t: 0, l: 256, w: 45, h: 33, i: "Note"})
]);
// Begin inserting the channel section
upThis.#sectPart.root = createElement("div", ["sect-part"]);
for (let port = 0; port < (allocated.ch >> 4); port ++) {
let startCh = port << 4;
upThis.#sectPart[port] = [];
upThis.#sectPart[port].root = createElement("div", [`boundary`, `part-port-${port}`]);
for (let part = 0; part < 16; part ++) {
let dispPart = (startCh | part) + 1;
if (dispPart >= 100) {
dispPart = `${Math.floor(dispPart / 10).toString(16)}${dispPart % 10}`;
} else {
dispPart = `${dispPart}`.padStart(2, "0");
};
upThis.#sectPart[port][part] = {
"root": createElement("div", [`boundary`, `part-channel`]),
"major": createElement("div", [`boundary`, `part-info-major`]),
"minor": createElement("div", [`boundary`, `part-info-minor`], {t: 26}),
"keys": createElement("div", [`boundary`, `part-keys`]),
"notes": createElement("div", [`boundary`, `part-keyboard`]),
"cxt": createElement("canvas", [`field`]).getContext("2d"),
"number": createElement("span", [`field`, `field-label`, `pcp-font4`], {t: 1, w: 18, h: 25, i: dispPart}),
"voice": createElement("span", [`field`], {l: 22, t: 1, w: 121, h: 25}),
"metre": createElement("canvas", [`field`]).getContext("2d"),
"type": createElement("span", [`field`, `field-label`, `pcp-font4`], {t: 1, w: 18, h: 25}),
"std": createElement("span", [`field`, `pcp-font4`], {l: 22, t: 1, w: 20, h: 25, a: "center"}),
"msb": createElement("span", [`field`, `pcp-font4`], {l: 48, t: 1, w: 27, h: 25}),
"prg": createElement("span", [`field`, `pcp-font4`], {l: 81, t: 1, w: 27, h: 25}),
"lsb": createElement("span", [`field`, `pcp-font4`], {l: 114, t: 1, w: 27, h: 25}),
"ccVis": createElement("canvas", [`field`], {l: 145, t: 1}).getContext("2d"),
"extVis": createElement("canvas", [`field`], {l: 207, t: 1}).getContext("2d"),
ccUpdate: false
};
let e = upThis.#sectPart[port][part];
leftCache.forEach((e0) => {
e.notes.appendChild(createElement("span", [`field`, `part-csplit`], {l: e0}));
});
e.notes.appendChild(createElement("span", [`field`, `part-csplit`, `part-cdive`], {l: 0, w: `100%`, h: 1}));
e.metre.canvas.width = 121;
e.metre.canvas.height = 25;
e.metre.textBaseline = "top";
e.metre.font = "20px 'PT Sans Narrow'";
e.ccVis.canvas.width = 109;
e.ccVis.canvas.height = 25;
e.ccVis.fillStyle = `#${upThis.#foreground}`;
e.extVis.canvas.width = 47;
e.extVis.canvas.height = 25;
e.extVis.fillStyle = `#${upThis.#foreground}`;
mountElement(e.notes, [
e.cxt.canvas
]);
mountElement(e.keys, [
e.notes
]);
mountElement(e.voice, [
e.metre.canvas
]);
mountElement(e.major, [
e.number,
e.voice,
e.ccVis.canvas
]);
mountElement(e.minor, [
e.type,
e.std,
e.msb,
e.prg,
e.lsb,
e.extVis.canvas
]);
mountElement(e.root, [
e.major,
e.minor,
e.keys
]);
mountElement(upThis.#sectPart[port].root, [
e.root
]);
e.number.addEventListener("contextmenu", (ev) => {
ev.preventDefault();
ev.stopImmediatePropagation();
let ch = (port << 4) | part;
upThis.#hideCh[ch] = +!upThis.#hideCh[ch];
//console.debug(upThis.#hideCh[ch]);
[classOff, classOn][upThis.#hideCh[ch]](e.root, ['part-hidden']);
});
};
upThis.#sectPart.root.appendChild(upThis.#sectPart[port].root);
};
canvasElement.appendChild(upThis.#sectPart.root);
// Begin inserting the meta section
upThis.#sectMeta.root = createElement("div", ["sect-meta"]);
upThis.#sectMeta.view = createElement("div", ["boundary"]);
canvasElement.appendChild(upThis.#sectMeta.root);
mountElement(upThis.#sectMeta.root, [
upThis.#sectMeta.view
]);
// Begin inserting the pixel render section
upThis.#sectPix.root = createElement("div", ["sect-pix", "boundary"], {l: 1529, t: 950, w: 379, h: 127});
upThis.#sectPix.cxt = createElement("canvas", [`field`]).getContext("2d");
upThis.#sectPix.fps = createElement("span", [`field`, `field-key`], {l: 0, w: `100%`, h: 1});
upThis.#sectPix.cxt.canvas.width = 379;
upThis.#sectPix.cxt.canvas.height = 127;
mountElement(upThis.#sectPix.root, [
upThis.#sectPix.cxt.canvas
]);
canvasElement.appendChild(upThis.#sectPix.root);
// Opportunistic value refreshing
upThis.addEventListener("mode", (ev) => {
upThis.#sectInfo.mode.setTextRaw(`${modeNames[ev.data]}`);
upThis.setMode(ev.data);
});
upThis.addEventListener("mastervolume", (ev) => {
let cramVolume = Math.round(ev.data * 100) / 100;
upThis.#sectInfo.volume.setTextRaw(`${Math.floor(cramVolume)}.${`${Math.floor((cramVolume % 1) * 100)}`.padStart(2, "0")}`);
});
upThis.addEventListener("tempo", (ev) => {
let cramTempo = Math.round(ev.data * 100);
upThis.#sectInfo.tempo.setTextRaw(`${Math.floor(cramTempo / 100)}.${`${Math.floor(cramTempo % 100)}`.padStart(2, "0")}`);
});
upThis.addEventListener("tsig", (ev) => {
[upThis.#sectInfo.sigN.innerText, upThis.#sectInfo.sigD.innerText] = ev.data;
});
upThis.addEventListener("title", (ev) => {
upThis.#sectInfo.title.setTextRaw(ev.data || `No Title`);
/*if (self?.navigator?.mediaSession) {
if (!navigator.mediaSession.metadata) {
navigator.mediaSession.metadata = new MediaMetadata({});
};
navigator.mediaSession.metadata.title = ev.data || null;
};*/
});
upThis.addEventListener("voice", ({data}) => {
let voice = upThis.getChVoice(data.part),
target = upThis.#sectPart[data.part >> 4][data.part & 15];
setCanvasText(target.metre, upThis.getMapped(voice.name));
target.type.setTextRaw(chTypes[upThis.device.getChType()[data.part]]);
target.std.setTextRaw(voice.standard);
target.msb.setTextRaw(`${voice.sid[0]}`.padStart(3, "0"));
target.prg.setTextRaw(`${voice.sid[1]}`.padStart(3, "0"));
target.lsb.setTextRaw(`${voice.sid[2]}`.padStart(3, "0"));
//console.debug(data);
});
upThis.addEventListener("pitch", (ev) => {
let {part, pitch} = ev.data;
upThis.#sectPart[part >> 4][part & 15].notes.style.transform = `translateX(${pitch / 1.28}%)`;
});
upThis.addEventListener("efxreverb", (ev) => {
if (!ev.data?.id) {
debugger;
};
upThis.#sectInfo.reverb.setTextRaw(upThis.getEfx(ev.data.id));
});
upThis.addEventListener("efxchorus", (ev) => {
if (!ev.data?.id) {
debugger;
};
upThis.#sectInfo.chorus.setTextRaw(upThis.getEfx(ev.data.id));
});
upThis.addEventListener("efxdelay", (ev) => {
if (!ev.data?.id) {
debugger;
};
upThis.#sectInfo.delay.setTextRaw(upThis.getEfx(ev.data.id));
});
upThis.addEventListener("efxinsert0", (ev) => {
if (!ev.data?.id) {
debugger;
};
upThis.#sectInfo.insert.setTextRaw(upThis.getEfx(ev.data.id));
});
upThis.addEventListener("efxinsert1", (ev) => {
if (!ev.data?.id) {
debugger;
};
upThis.#sectInfo.insert1.setTextRaw(upThis.getEfx(ev.data.id));
if (ev.data?.hidden) {
classOff(upThis.#sectInfo.inscon1, ["field-active"]);
} else {
classOn(upThis.#sectInfo.inscon1, ["field-active"]);
};
});
upThis.addEventListener("efxinsert2", (ev) => {
if (!ev.data?.id) {
debugger;
};
upThis.#sectInfo.insert2.setTextRaw(upThis.getEfx(ev.data.id));
if (ev.data?.hidden) {
classOff(upThis.#sectInfo.inscon2, ["field-active"]);
} else {
classOn(upThis.#sectInfo.inscon2, ["field-active"]);
};
});
upThis.addEventListener("efxinsert3", (ev) => {
if (!ev.data?.id) {
debugger;
};
upThis.#sectInfo.insert3.setTextRaw(upThis.getEfx(ev.data.id));
if (ev.data?.hidden) {
classOff(upThis.#sectInfo.inscon3, ["field-active"]);
} else {
classOn(upThis.#sectInfo.inscon3, ["field-active"]);
};
});
upThis.addEventListener("efxinsert4", (ev) => {
if (!ev.data?.id) {
debugger;
};
upThis.#sectInfo.insert4.setTextRaw(upThis.getEfx(ev.data.id));
if (ev.data?.hidden) {
classOff(upThis.#sectInfo.inscon4, ["field-active"]);
} else {
classOn(upThis.#sectInfo.inscon4, ["field-active"]);
};
});
upThis.addEventListener("partefxtoggle", (ev) => {
let {part, active} = ev.data;
([classOff, classOn][active ? 1 : 0])(upThis.#sectPart[part >> 4][part & 15].number, [
`part-efx`
]);
});
upThis.addEventListener("channeltoggle", (ev) => {
let {part, active} = ev.data;
([classOff, classOn][active ? 1 : 0])(upThis.#sectPart[part >> 4][part & 15].root, [
`part-active`
]);
});
upThis.addEventListener("channelactive", (ev) => {
if (upThis.#underlinedCh < allocated.ch) {
let lastCh = upThis.#sectPart[upThis.#underlinedCh >> 4][upThis.#underlinedCh & 15].number;
classOff(lastCh, ["part-focus"]);
};
let part = ev.data;
if (part < allocated.ch) {
let newCh = upThis.#sectPart[part >> 4][part & 15].number;
classOn(newCh, ["part-focus"]);
upThis.#underlinedCh = part;
};
});
upThis.addEventListener("metacommit", (ev) => {
let meta = ev.data, isHandled = false;
//console.debug(meta);
if (upThis.#metaAmend && meta.type === upThis.#metaType && upThis.#metaLastLine) {
// Amend the last line
switch (meta.type) {
case "C.Lyrics":
case "KarLyric":
case "SGLyrics": {
mountElement(upThis.#metaLastLine, [
createElement("span", ["meta-slice"], {i: meta.data})
]);
break;
};
default: {
upThis.#metaLastLine.childNodes[0].data += meta.data;
};
};
isHandled = true;
} else if (meta.data?.length && metaBlocklist.indexOf(meta.type) === -1) {
// Commit a new line
let metaLineRoot = createElement("div", ["meta-line"]),
metaLineType = createElement("span", ["field", "field-key", "meta-type"], {i: metaNames[meta.type] || meta.type});
if (meta.mask) {
metaLineType.style.display = "none";
};
switch (meta.type) {
case "C.Lyrics":
case "KarLyric":
case "SGLyrics": {
upThis.#metaLastLine = createElement("span", ["field", "meta-data"]);
mountElement(upThis.#metaLastLine, [
createElement("span", ["meta-slice"], {i: meta.data})
]);
break;
};
default: {
upThis.#metaLastLine = createElement("span", ["field", "meta-data"], {i: meta.data});
};
};
upThis.#sectMeta.view.appendChild(metaLineRoot);
mountElement(metaLineRoot, [
metaLineType,
upThis.#metaLastLine
]);
if (upThis.#sectMeta.view.children.length > upThis.#metaGcStart) {
upThis.#metaGcAt = Date.now() + 500;
if (upThis.#metaGcScheduled === 0) {
console.debug(`Meta event garbage collector triggered.`);
};
upThis.#metaGcScheduled = 1;
};
while (upThis.#sectMeta.view.children.length > upThis.#metaMaxLine) {
upThis.#sectMeta.view.children[0].remove();
};
isHandled = true;
};
if (isHandled) {
upThis.#metaAmend = meta.amend ?? false;
upThis.#metaType = meta.type ?? "";
};
upThis.#scrollMeta();
});
upThis.#sectMeta.view.style.transform = `translateX(0px) translateY(140px)`;
upThis.#metaGcThread = setInterval(async () => {
let timeNow = Date.now();
if (upThis.#metaGcScheduled === 0) {
return;
};
switch (upThis.#metaGcScheduled) {
case 1: {
if (timeNow < upThis.#metaGcAt) {
break;
};
upThis.#sectMeta.view.style.transition = "none";
let gcCount = 0;
while (upThis.#sectMeta.view.children.length > upThis.#metaGcLine) {
upThis.#sectMeta.view.children[0].remove();
gcCount ++;
};
upThis.#scrollMeta();
console.debug(`Meta event garbage collector removed ${gcCount} events.`);
upThis.#metaGcScheduled = 2;
break;
};
case 2: {
if (timeNow < upThis.#metaGcAt + 100) {
break;
};
upThis.#sectMeta.view.style.transition = "";
getDebugState() && console.debug(`Meta event garbage collector restored meta animation.`);
upThis.#metaGcScheduled = 0;
break;
};
};
}, 40);
upThis.dispatchEvent("mode", "?");
upThis.dispatchEvent("mastervolume", 100);
upThis.dispatchEvent("tempo", 120);
upThis.dispatchEvent("tsig", [4, 4]);
upThis.dispatchEvent("title", "");
upThis.dispatchEvent(`efxreverb`, {"id": upThis.device.getEffectType(0)});
upThis.dispatchEvent(`efxchorus`, {"id": upThis.device.getEffectType(1)});
upThis.dispatchEvent(`efxdelay`, {"id": upThis.device.getEffectType(2)});
upThis.dispatchEvent(`efxinsert0`, {"id": upThis.device.getEffectType(3)});
upThis.#setPortView(true);
};
detach(attachElement) {
let upThis = this;
self.removeEventListener("resize", upThis.#resizer);
upThis.#canvas.remove();
upThis.#canvas = undefined;
upThis.#container.remove();
upThis.#container = undefined;
upThis.#visualizer = undefined;
clearInterval(upThis.#renderThread);
clearInterval(upThis.#metaGcThread);
};
constructor(attachElement, clockSource) {
super(new OctaviaDevice, 0.1, 0.75);
let upThis = this;
upThis.#resizer = upThis.#resizerSrc.bind(this);
upThis.#renderer = upThis.#rendererSrc.bind(this);
upThis.#chAccent.fill(null);
upThis.#chMode.fill(null);
if (attachElement) {
upThis.attach(attachElement);
};
if (clockSource) {
upThis.setClockSource(clockSource);
};
upThis.setPixelProfile("none");
upThis.addEventListener("reset", () => {
upThis.#maxPoly = 0;
upThis.#maxPolyEC = 0;
upThis.#metaAmend = false;
upThis.#metaType = "";
upThis.#metaLastLine = null;
upThis.#underlinedCh = allocated.invalidCh;
upThis.#chAccent.fill(null);
upThis.#chMode.fill(null);
classOff(upThis.#sectInfo.inscon1, ["field-active"]);
classOff(upThis.#sectInfo.inscon2, ["field-active"]);
classOff(upThis.#sectInfo.inscon3, ["field-active"]);
classOff(upThis.#sectInfo.inscon4, ["field-active"]);
try {
// Remove all meta
let list = upThis.#sectMeta.view.children;
for (let pointer = list.length - 1; pointer >= 0; pointer --) {
list[pointer].remove();
};
upThis.#sectMeta.view.style.transform = `translateX(0px) translateY(140px)`;
// Reset channels
for (let part = 0; part < allocated.ch; part ++) {
let e = upThis.#sectPart[part >> 4][part & 15];
classOff(e.root, [
`part-active`
]);
classOff(e.number, [
`part-efx`, `part-focus`
]);
for (let className of e.root.classList) {
if (className.substring(0, 10) === "part-mode-") {
e.root.classList.remove(className);
};
};
setCanvasText(e.metre, "");
e.type.setTextRaw("");
e.std.setTextRaw("");
e.msb.setTextRaw("");
e.prg.setTextRaw("");
e.lsb.setTextRaw("");
e.notes.style.transform = "";
e.ccUpdate = true;
};
} catch (err) {};
});
/*upThis.addEventListener("note", ({data}) => {
upThis.#noteEvents.push(data);
//console.debug(data);
});*/
upThis.addEventListener("chmode", ({data}) => {
let {part, mode} = data;
/* classOn(upThis.#sectPart[part >> 4][part & 15]?.root, [
`part-mode-${mode}`
]); */
upThis.setChMode(part, mode);
let resultColour = modeColourPool[mode];
if (resultColour) {
upThis.#chAccent[part] = resultColour[upThis.#scheme];
};
//console.debug(part, mode);
});
upThis.addEventListener("pitch", ({data}) => {
upThis.#pitchEvents.push(data);
});
};
};
 
export {
Cambiare
};