bytebeat-composer/src/bytebeat.ts

842 lines
24 KiB
TypeScript
Raw Normal View History

2023-12-28 06:25:15 +01:00
import { inflateRaw, deflateRaw } from "pako";
2023-12-26 04:11:57 +01:00
import { domLoaded, isPlainObject } from "./common.ts";
2021-11-18 14:36:00 +01:00
const searchParams = new URLSearchParams(location.search);
const timeUnits = [
"t",
"s", // sec
];
2023-12-28 06:25:15 +01:00
const bytebeat = Object.seal({
audioCtx: null,
audioWorklet: null,
audioGain: null,
nextErrType: null,
nextErr: null,
nextErrPriority: undefined,
errorPriority: -Infinity,
canvasCtx: null,
drawSettings: { scale: null },
drawBuffer: [],
drawImageData: null,
byteSample: 0,
isPlaying: false,
songData: { sampleRate: null, mode: null },
playSpeed: 1,
volume: null,
timeUnit: null,
animationFrameId: null,
canvasElem: null,
codeEditor: null,
errorElem: null,
timeCursorElem: null,
contentElem: null,
controlTimeUnit: null,
controlTimeUnitLabel: null,
controlTimeValue: null,
controlScaleUp: null,
controlScaleDown: null,
controlPlaybackMode: null,
controlSampleRate: null,
controlVolume: null,
canvasTogglePlay: null,
async init() {
await this.initAudioContext();
await domLoaded;
this.contentElem = document.getElementById("content");
this.initControls();
await this.initTextarea(document.getElementById("code-editor"));
2023-12-26 04:11:57 +01:00
import("./codemirror.ts").then(o => this.initCodemirror(o.default));
2023-12-25 23:40:54 +01:00
if (globalThis.loadLibrary !== false) {
2023-12-26 04:11:57 +01:00
import("./load-library.ts");
2023-12-25 23:40:54 +01:00
}
this.handleWindowResize(true);
document.defaultView.addEventListener("resize", () => this.handleWindowResize(false));
this.loadSettings();
const songData = this.getUrlData();
this.setSong(songData, false);
this.updateCounterValue();
},
async initAudioContext() {
let audioContextSampleRate = Number(searchParams.get("audioContextSampleRate"));
2023-12-25 23:40:54 +01:00
if (!(audioContextSampleRate > 0)) {
// also true for NaN
audioContextSampleRate = 48000; // TODO this should be set to an lcm > 44100
}
this.audioCtx = new AudioContext({
latencyHint: searchParams.get("audioContextLatencyHint") ?? "balanced",
sampleRate: audioContextSampleRate,
});
this.audioGain = new GainNode(this.audioCtx);
this.audioGain.connect(this.audioCtx.destination);
2023-12-25 18:51:35 +01:00
await this.audioCtx.audioWorklet.addModule("/audio-worklet.js");
2023-12-25 23:40:54 +01:00
this.audioWorklet = new AudioWorkletNode(this.audioCtx, "bytebeatProcessor", {
outputChannelCount: [2],
});
2022-05-13 19:17:17 +02:00
this.audioWorklet.port.addEventListener("message", e => this.handleMessage(e));
this.audioWorklet.port.start();
this.audioWorklet.connect(this.audioGain);
2023-12-25 23:40:54 +01:00
console.info(
`started audio with latency ${this.audioCtx.baseLatency * this.audioCtx.sampleRate} at ${
this.audioCtx.sampleRate
}Hz`,
);
},
handleMessage(e) {
if (isPlainObject(e.data)) {
const data = e.data;
2023-12-25 23:40:54 +01:00
if (data.clearCanvas) {
this.clearCanvas();
2023-12-25 23:40:54 +01:00
} else if (data.clearDrawBuffer) {
this.clearDrawBuffer();
2023-12-25 23:40:54 +01:00
}
2023-12-25 23:40:54 +01:00
if (typeof data.byteSample === "number") {
this.setByteSample(data.byteSample, false);
2023-12-25 23:40:54 +01:00
}
if (Array.isArray(data.drawBuffer)) {
this.drawBuffer = this.drawBuffer.concat(data.drawBuffer);
// prevent buffer accumulation when tab inactive
const maxDrawBufferSize = this.getTimeFromXpos(this.canvasElem.width) - 1;
2023-12-25 23:40:54 +01:00
if (
this.byteSample - this.drawBuffer[this.drawBuffer.length >> 1].t >
maxDrawBufferSize
) {
// reasonable lazy cap
this.drawBuffer = this.drawBuffer.slice(this.drawBuffer.length >> 1);
2023-12-25 23:40:54 +01:00
} else if (this.drawBuffer.length > maxDrawBufferSize) {
// emergency cap
this.drawBuffer = this.drawBuffer.slice(-maxDrawBufferSize);
2023-12-25 23:40:54 +01:00
}
}
2023-12-25 23:40:54 +01:00
if (data.updateUrl) {
this.setUrlData();
2023-12-25 23:40:54 +01:00
}
2021-11-18 14:36:00 +01:00
if (data.errorMessage !== undefined) {
if (isPlainObject(data.errorMessage)) {
if (
typeof data.errorMessage.type === "string" &&
typeof data.errorMessage.err === "string" &&
typeof (data.errorMessage.priority ?? 0) === "number"
) {
if (this.isPlaying) {
this.nextErrType = data.errorMessage.type;
this.nextErr = data.errorMessage.err;
this.nextErrPriority = data.errorMessage.priority;
2023-12-25 23:40:54 +01:00
} else {
this.showErrorMessage(
data.errorMessage.type,
data.errorMessage.err,
data.errorMessage.priority,
);
}
2021-11-18 14:36:00 +01:00
}
2023-12-25 23:40:54 +01:00
} else {
this.hideErrorMessage();
2023-12-25 23:40:54 +01:00
}
2021-11-18 14:36:00 +01:00
}
}
},
saveData: null,
initTextarea(textarea) {
textarea.addEventListener("input", () => this.refreshCode());
{
let keyTrap = true;
textarea.addEventListener("keydown", e => {
if (!e.altKey && !e.ctrlKey) {
if (e.key === "Escape") {
if (keyTrap) {
e.preventDefault();
keyTrap = false;
}
} else if (e.key === "Tab" && keyTrap) {
e.preventDefault();
const el = e.target;
const { selectionStart, selectionEnd } = el;
if (e.shiftKey) {
// remove indentation on all selected lines
let lines = el.value.split("\n");
let getLine = char => {
let line = 0;
for (let c = 0; ; line++) {
c += lines[line].length;
if (c > char) 1;
break;
}
return line;
};
2023-12-25 23:40:54 +01:00
let startLine = getLine(selectionStart);
let endLine = getLine(selectionEnd);
let newSelectionStart = selectionStart;
let newSelectionEnd = selectionEnd;
for (let i = startLine; i <= endLine; i++) {
if (lines[i][0] === "\t") {
lines[i] = lines[i].slice(1);
2023-12-25 23:40:54 +01:00
if (i === startLine) {
newSelectionStart--;
2023-12-25 23:40:54 +01:00
}
newSelectionEnd--;
}
}
el.value = lines.join("\n");
el.setSelectionRange(newSelectionStart, newSelectionEnd);
} else {
// add tab character
2023-12-25 23:40:54 +01:00
el.value = `${el.value.slice(0, selectionStart)}\t${el.value.slice(
selectionEnd,
)}`;
el.setSelectionRange(selectionStart + 1, selectionStart + 1);
}
this.refreshCode();
2023-12-25 23:40:54 +01:00
} else {
keyTrap = false;
2023-12-25 23:40:54 +01:00
}
}
});
}
this.codeEditor = textarea;
},
initCodemirror(createCodemirrorEditor) {
const codemirror = createCodemirrorEditor(() => this.refreshCode());
2023-12-28 06:09:08 +01:00
let selection;
if (this.codeEditor) {
codemirror.dispatch({ changes: { from: 0, insert: this.codeEditor.value } });
2023-12-25 23:40:54 +01:00
if (document.activeElement === this.codeEditor) {
selection = {
anchor: this.codeEditor.selectionStart,
head: this.codeEditor.selectionEnd,
};
}
}
(this.codeEditor ?? document.getElementById("code-editor")).replaceWith(codemirror.dom);
if (selection) {
codemirror.focus();
codemirror.dispatch({ selection });
}
this.codeEditor = codemirror;
},
2023-12-28 06:09:08 +01:00
get codeEditorText(): string {
2023-12-25 23:40:54 +01:00
if (this.codeEditor instanceof Element) {
return this.codeEditor.value;
2023-12-25 23:40:54 +01:00
} else {
return this.codeEditor.state.doc.toString();
2023-12-25 23:40:54 +01:00
}
},
2023-12-28 06:09:08 +01:00
set codeEditorText(value: string) {
2023-12-25 23:40:54 +01:00
if (this.codeEditor instanceof Element) {
this.codeEditor.value = value;
2023-12-25 23:40:54 +01:00
} else {
this.codeEditor.dispatch({
changes: { from: 0, to: this.codeEditor.state.doc.length, insert: value },
});
}
},
getUrlData() {
if (window.location.hash && globalThis.useUrlData !== false) {
const hash = window.location.hash;
2023-12-25 23:40:54 +01:00
const version = hash.startsWith("#v4") ? 4 : hash.startsWith("#v3b64") ? 3 : null;
if (version === 4 || version === 3) {
2023-12-28 06:09:08 +01:00
let dataString: string;
try {
dataString = atob(hash.substring(version === 4 ? 3 : 6));
} catch (err) {
console.error("Couldn't load data from url:", err);
return null;
}
const dataBuffer = new Uint8Array(dataString.length);
2023-12-25 23:40:54 +01:00
for (const i in dataString) {
dataBuffer[i] = dataString.charCodeAt(i);
2023-12-25 23:40:54 +01:00
}
let songData;
try {
const songDataString = inflateRaw(dataBuffer, { to: "string" });
songData = JSON.parse(songDataString);
} catch (err) {
console.error("Couldn't load data from url:", err);
return null;
}
2021-11-18 14:36:00 +01:00
return songData;
2023-12-25 23:40:54 +01:00
} else {
console.error("Unrecognized url data");
2023-12-25 23:40:54 +01:00
}
}
return null;
},
setUrlData() {
if (globalThis.useUrlData !== false) {
const dataBuffer = deflateRaw(JSON.stringify(this.getSong()));
const dataString = String.fromCharCode.apply(undefined, dataBuffer);
window.location.hash = `#v4${btoa(dataString).replaceAll("=", "")}`;
}
},
initControls() {
this.controlTimeUnit = document.getElementById("control-time-unit");
this.controlTimeUnitLabel = document.getElementById("control-time-unit-label");
this.controlTimeValue = document.getElementById("control-time-value");
this.controlScaleDown = document.getElementById("control-scaledown");
this.controlScaleUp = document.getElementById("control-scaleup");
this.controlPlaybackMode = document.getElementById("control-song-mode");
this.controlSampleRate = document.getElementById("control-samplerate");
this.controlVolume = document.getElementById("control-volume");
this.errorElem = document.getElementById("error");
this.canvasTogglePlay = document.getElementById("canvas-toggleplay");
this.timeCursorElem = document.getElementById("canvas-timecursor");
this.canvasElem = document.getElementById("canvas-main");
this.canvasCtx = this.canvasElem.getContext("2d", { alpha: false });
},
refreshCode() {
2023-12-25 23:40:54 +01:00
if (this.audioWorklet) {
this.audioWorklet.port.postMessage({ code: this.codeEditorText.trim() });
2023-12-25 23:40:54 +01:00
}
},
2023-12-28 06:09:08 +01:00
handleWindowResize(force: boolean) {
this.autoSizeCanvas(force);
},
2023-12-28 06:09:08 +01:00
autoSizeCanvas(force: boolean) {
if (!this.canvasElem.dataset.forcedWidth) {
const innerWidth = window.innerWidth;
2023-12-28 06:09:08 +01:00
// 768 is halfway between 512 and 1024, 3 added for outline
if (innerWidth >= 768 + 3) {
let width = 1024;
2023-12-25 23:40:54 +01:00
while (innerWidth - 516 >= width * 2) {
// 516px = 4px (outline) + 512px (library)
width *= 2;
2023-12-25 23:40:54 +01:00
}
2023-12-28 06:09:08 +01:00
this.setCanvasWidth(width, force);
2023-12-25 23:40:54 +01:00
} else {
2023-12-28 06:09:08 +01:00
this.setCanvasWidth(512, force);
2023-12-25 23:40:54 +01:00
}
}
},
2023-12-28 06:09:08 +01:00
setCanvasWidth(width: number, force: boolean = false) {
if (this.canvasElem) {
if (width !== this.canvasElem.width || force) {
this.canvasElem.width = width;
// TODO: see if it's possible to get rid of this
this.contentElem.style.maxWidth = `${width + 4}px`;
}
}
},
animationFrame() {
this.drawGraphics();
2023-12-25 23:40:54 +01:00
if (this.nextErr) {
this.showErrorMessage(this.nextErrType, this.nextErr, this.nextErrPriority);
2023-12-25 23:40:54 +01:00
}
2023-12-25 23:40:54 +01:00
if (this.isPlaying) {
this.animationFrameId = window.requestAnimationFrame(() => this.animationFrame());
2023-12-25 23:40:54 +01:00
} else {
this.animationFrameId = null;
2023-12-25 23:40:54 +01:00
}
},
getSong(includeDefault = false) {
let songData = { code: this.codeEditorText };
2023-12-25 23:40:54 +01:00
if (includeDefault || this.songData.sampleRate !== 8000) {
songData.sampleRate = this.songData.sampleRate;
2023-12-25 23:40:54 +01:00
}
if (includeDefault || this.songData.mode !== "Bytebeat") {
songData.mode = this.songData.mode;
2023-12-25 23:40:54 +01:00
}
return songData;
},
setSong(songData, play = true) {
let code, sampleRate, mode;
if (songData !== null) {
({ code, sampleRate, mode } = songData);
this.codeEditorText = code;
}
this.applySampleRate(sampleRate ?? 8000);
this.applyPlaybackMode(mode ?? "Bytebeat");
this.refreshCode();
if (play) {
this.resetTime();
this.togglePlay(true);
}
},
applySampleRate(rate) {
this.setSampleRate(rate);
this.controlSampleRate.value = rate;
},
applyPlaybackMode(playbackMode) {
this.setPlaybackMode(playbackMode);
this.controlPlaybackMode.value = playbackMode;
},
changeScale(amount) {
if (amount) {
this.drawSettings.scale = Math.max(this.drawSettings.scale + amount, 0);
this.clearCanvas(false);
2023-12-25 23:40:54 +01:00
if (this.drawSettings.scale <= 0) {
this.controlScaleDown.setAttribute("disabled", true);
2023-12-25 23:40:54 +01:00
} else {
this.controlScaleDown.removeAttribute("disabled");
2023-12-25 23:40:54 +01:00
}
2021-11-18 14:36:00 +01:00
this.toggleTimeCursor();
this.moveTimeCursor();
this.saveSettings();
}
},
setVolume(save = true, volume) {
if (volume !== undefined) {
this.volume = volume;
this.controlVolume.value = volume;
2023-12-25 23:40:54 +01:00
} else {
this.volume = this.controlVolume.valueAsNumber;
2023-12-25 23:40:54 +01:00
}
2023-12-25 23:40:54 +01:00
if (this.audioGain !== null) {
this.audioGain.gain.value = this.volume * this.volume;
2023-12-25 23:40:54 +01:00
}
2023-12-25 23:40:54 +01:00
if (save) {
this.saveSettings();
2023-12-25 23:40:54 +01:00
}
},
2021-11-18 14:36:00 +01:00
clearCanvas(clearDrawBuffer = true) {
if (this.canvasCtx) {
this.canvasCtx.fillRect(0, 0, this.canvasElem.width, this.canvasElem.height);
2023-12-25 23:40:54 +01:00
if (clearDrawBuffer) {
this.clearDrawBuffer();
2023-12-25 23:40:54 +01:00
}
}
},
clearDrawBuffer() {
this.drawBuffer = [];
this.drawImageData = null;
},
2023-12-25 23:40:54 +01:00
fmod(a, b) {
return ((a % b) + b) % b;
},
getXpos(t) {
return t / (1 << this.drawSettings.scale);
},
getTimeFromXpos(x) {
return x * (1 << this.drawSettings.scale);
},
drawGraphics() {
const { width, height } = this.canvasElem;
const bufferLen = this.drawBuffer.length;
2023-12-25 23:40:54 +01:00
if (!bufferLen) {
return;
2023-12-25 23:40:54 +01:00
}
const playingForward = this.playSpeed > 0;
2023-12-25 23:40:54 +01:00
let startTime = this.drawBuffer[0].t + (this.drawBuffer.carry ? 1 : 0);
let endTime = this.byteSample;
let lenTime = endTime - startTime;
let startXPos = this.fmod(this.getXpos(startTime), width);
let endXPos = startXPos + this.getXpos(lenTime);
{
2023-12-25 23:40:54 +01:00
let drawStartX = Math.floor(startXPos);
let drawEndX = Math.floor(endXPos);
let drawLenX = Math.abs(drawEndX - drawStartX) + 1;
let drawOverflow = false;
// clip draw area if too large
2023-12-25 23:40:54 +01:00
if (drawLenX > width) {
// TODO: put this into a better section so the variables don't all have to be set again
startTime = this.getTimeFromXpos(this.getXpos(endTime) - width);
let sliceIndex = 0;
2023-12-25 23:40:54 +01:00
for (let i in this.drawBuffer) {
// TODO: replace this with binary search
if ((this.drawBuffer[i + 1]?.t ?? endTime) <= startTime - 1) {
sliceIndex += 1;
2023-12-25 23:40:54 +01:00
} else {
this.drawBuffer[i].t = startTime - 1;
this.drawBuffer[i].carry = true;
this.drawBuffer = this.drawBuffer.slice(sliceIndex);
break;
2021-11-18 14:36:00 +01:00
}
}
lenTime = endTime - startTime;
startXPos = this.fmod(this.getXpos(startTime), width);
endXPos = startXPos + this.getXpos(lenTime);
drawStartX = Math.ceil(startXPos); // this is a bit of a hack, since this doesn't relate to the other variables properly
// i can only get away with this because the other vars like startTime and such aren't used
// the proper solution would be to somehow round up startTime by a pixel
drawEndX = Math.floor(endXPos);
drawLenX = Math.abs(drawEndX - drawStartX) + 1;
drawOverflow = true;
}
2021-11-18 14:36:00 +01:00
const imagePos = Math.min(drawStartX, drawEndX);
// create imageData
let imageData = this.canvasCtx.createImageData(drawLenX, height);
// create / add drawimageData
2023-12-25 23:40:54 +01:00
if (this.drawSettings.scale) {
// full zoom can't have multiple samples on one pixel
if (this.drawImageData) {
if (!drawOverflow) {
// fill in starting area of image data with previously drawn samples
let x = playingForward ? 0 : drawLenX - 1;
for (let y = 0; y < height; y++) {
imageData.data[(drawLenX * y + x) << 2] = this.drawImageData.data[y << 2];
2023-12-25 23:40:54 +01:00
imageData.data[((drawLenX * y + x) << 2) + 1] =
this.drawImageData.data[(y << 2) + 1];
imageData.data[((drawLenX * y + x) << 2) + 2] =
this.drawImageData.data[(y << 2) + 2];
2021-11-18 14:36:00 +01:00
}
}
2023-12-25 23:40:54 +01:00
} else {
this.drawImageData = this.canvasCtx.createImageData(1, height);
2023-12-25 23:40:54 +01:00
}
} else {
this.drawImageData = null;
2023-12-25 23:40:54 +01:00
}
// set alpha
2023-12-25 23:40:54 +01:00
// TODO flatten
for (let x = 0; x < drawLenX; x++) {
for (let y = 0; y < height; y++) {
imageData.data[((drawLenX * y + x) << 2) + 3] = 255;
2023-12-25 23:40:54 +01:00
}
}
// draw
2023-12-25 23:40:54 +01:00
const iterateOverHorizontalLine = (
bufferElem,
nextBufferElemTime,
callback,
initCallback,
) => {
const startX = this.fmod(
Math.floor(this.getXpos(playingForward ? bufferElem.t : nextBufferElemTime + 1)) -
imagePos,
width,
);
const endX = this.fmod(
Math.ceil(this.getXpos(playingForward ? nextBufferElemTime : bufferElem.t + 1)) -
imagePos,
width,
);
if (initCallback) {
initCallback(startX);
}
2023-12-25 23:40:54 +01:00
for (let xPos = startX; xPos !== endX; xPos = this.fmod(xPos + 1, width)) {
callback(xPos, false);
2023-12-25 23:40:54 +01:00
}
};
for (let i = this.drawBuffer[0].t < startTime ? 1 : 0; i < bufferLen; i++) {
let lastBufferElem = this.drawBuffer[i - 1] ?? null;
let bufferElem = this.drawBuffer[i];
let nextBufferElemTime = this.drawBuffer[i + 1]?.t ?? endTime;
2023-12-25 23:40:54 +01:00
if (isNaN(bufferElem.value[0]) || isNaN(bufferElem.value[1])) {
iterateOverHorizontalLine(bufferElem, nextBufferElemTime, xPos => {
for (let h = 0; h < 256; h++) {
const pos = (drawLenX * h + xPos) << 2;
imageData.data[pos] = 96;
}
});
2023-12-25 23:40:54 +01:00
}
for (let c = 0; c < 2; c++) {
if (bufferElem.value[c] >= 0 && bufferElem.value[c] < 256) {
// NaN check is implicit here
iterateOverHorizontalLine(
bufferElem,
nextBufferElemTime,
xPos => {
const pos = (drawLenX * (255 - bufferElem.value[c]) + xPos) << 2;
2023-12-25 23:40:54 +01:00
if (c) {
imageData.data[pos] = imageData.data[pos + 2] = 255;
2023-12-25 23:40:54 +01:00
} else {
imageData.data[pos] = 0; // clear out NaN red
imageData.data[pos + 1] = 255;
}
},
// Waveform draw mode connectors
lastBufferElem && !isNaN(lastBufferElem.value[c])
? xPos => {
const dir = lastBufferElem.value[c] < bufferElem.value[c] ? -1 : 1;
for (
let h = 255 - lastBufferElem.value[c];
h !== 255 - bufferElem.value[c];
h += dir
) {
const pos = (drawLenX * h + xPos) << 2;
if (imageData.data[pos] === 0) {
// don't overwrite filled cells
if (c) {
imageData.data[pos] = imageData.data[pos + 2] = 140;
} else {
imageData.data[pos] = 0; // clear out NaN red
imageData.data[pos + 1] = 140;
}
2023-12-25 23:40:54 +01:00
}
2022-04-29 20:23:12 +02:00
}
}
: () => {},
);
2021-11-18 14:36:00 +01:00
}
2023-12-25 23:40:54 +01:00
}
2021-11-18 14:36:00 +01:00
}
// put imageData
this.canvasCtx.putImageData(imageData, imagePos, 0);
2023-12-25 23:40:54 +01:00
if (endXPos >= width) {
this.canvasCtx.putImageData(imageData, imagePos - width, 0);
2023-12-25 23:40:54 +01:00
} else if (endXPos < 0) {
this.canvasCtx.putImageData(imageData, imagePos + width, 0);
2023-12-25 23:40:54 +01:00
}
// write to drawImageData
2023-12-25 23:40:54 +01:00
if (this.drawSettings.scale) {
// full zoom can't have multiple samples on one pixel
const x = playingForward ? drawLenX - 1 : 0;
for (let y = 0; y < height; y++) {
this.drawImageData.data[y << 2] = imageData.data[(drawLenX * y + x) << 2];
2023-12-25 23:40:54 +01:00
this.drawImageData.data[(y << 2) + 1] =
imageData.data[((drawLenX * y + x) << 2) + 1];
this.drawImageData.data[(y << 2) + 2] =
imageData.data[((drawLenX * y + x) << 2) + 2];
2021-11-18 14:36:00 +01:00
}
}
}
2021-11-18 14:36:00 +01:00
// cursor
this.moveTimeCursor(endTime);
// clear buffer except last sample
this.drawBuffer = [{ t: endTime, value: this.drawBuffer[bufferLen - 1].value, carry: true }];
},
moveTimeCursor(time = this.byteSample) {
if (this.timeCursorElem && this.timeCursorVisible()) {
const width = this.canvasElem.width;
if (this.playSpeed > 0) {
this.timeCursorElem.style.removeProperty("right");
2023-12-25 23:40:54 +01:00
this.timeCursorElem.style.left = `${
(this.fmod(Math.ceil(this.getXpos(time)), width) / width) * 100
}%`;
} else {
this.timeCursorElem.style.removeProperty("left");
2023-12-25 23:40:54 +01:00
this.timeCursorElem.style.right = `${
(1 - (this.fmod(Math.ceil(this.getXpos(time)), width) + 1) / width) * 100
}%`;
}
}
},
hideErrorMessage() {
if (this.errorElem) {
this.errorElem.innerText = "";
this.nextErr = null;
this.nextErrType = null;
this.nextErrPriority = undefined;
this.errorPriority = -Infinity;
}
},
showErrorMessage(errType, err, priority = 0) {
if (this.errorElem && (this.errorPriority < 2 || priority > 0)) {
this.errorElem.dataset.errType = errType;
this.errorElem.innerText = err.toString();
this.nextErr = null;
this.nextErrType = null;
this.nextErrPriority = undefined;
this.errorPriority = priority;
2023-12-25 23:40:54 +01:00
if (this.audioWorklet) {
this.audioWorklet.port.postMessage({ displayedError: true });
2023-12-25 23:40:54 +01:00
}
}
},
resetTime() {
this.setByteSample(0, true, true);
2023-12-25 23:40:54 +01:00
if (!this.isPlaying) {
this.canvasTogglePlay.classList.add("canvas-toggleplay-show");
2023-12-25 23:40:54 +01:00
}
},
setByteSample(value, send = true, clear = false) {
if (this.audioWorklet && isFinite(value)) {
this.byteSample = value;
this.updateCounterValue();
2023-12-25 23:40:54 +01:00
if (send) {
this.audioWorklet.port.postMessage({ setByteSample: [value, clear] });
2023-12-25 23:40:54 +01:00
}
this.moveTimeCursor();
}
},
setPlaybackMode(playbackMode) {
if (this.audioWorklet) {
this.songData.mode = playbackMode;
this.setUrlData();
this.audioWorklet.port.postMessage({ songData: this.songData });
}
},
setSampleRate(sampleRate) {
if (this.audioWorklet) {
this.songData.sampleRate = sampleRate;
this.audioWorklet.port.postMessage({ songData: this.songData, updateSampleRatio: true });
this.toggleTimeCursor();
}
},
2023-12-28 06:09:08 +01:00
setSampleRateDivisor(sampleRateDivisor: number) {
if (this.audioWorklet && sampleRateDivisor > 0) {
this.audioWorklet.port.postMessage({ sampleRateDivisor, updateSampleRatio: true });
2023-12-25 23:40:54 +01:00
}
},
2023-12-28 06:09:08 +01:00
setPlaySpeed(playSpeed: number) {
if (this.audioWorklet && this.playSpeed !== playSpeed) {
this.playSpeed = playSpeed;
this.audioWorklet.port.postMessage({ playSpeed, updateSampleRatio: true });
}
},
toggleTimeCursor() {
2023-12-25 23:40:54 +01:00
if (this.timeCursorElem) {
this.timeCursorElem.classList.toggle("disabled", !this.timeCursorVisible());
2023-12-25 23:40:54 +01:00
}
},
timeCursorVisible() {
return this.songData.sampleRate >> this.drawSettings.scale < 3950;
},
2023-12-28 06:09:08 +01:00
togglePlay(isPlaying: boolean) {
if (this.audioWorklet && isPlaying !== this.isPlaying) {
this.canvasTogglePlay.classList.toggle("canvas-toggleplay-pause", isPlaying);
if (isPlaying) {
// Play
this.canvasTogglePlay.classList.remove("canvas-toggleplay-show");
2023-12-25 23:40:54 +01:00
if (this.audioCtx?.resume) {
this.audioCtx.resume();
2023-12-25 23:40:54 +01:00
}
2022-05-13 19:17:17 +02:00
this.animationFrameId = window.requestAnimationFrame(() => this.animationFrame());
} else {
this.animationFrameId = null;
}
this.isPlaying = isPlaying;
this.audioWorklet.port.postMessage({ isPlaying });
}
},
updateCounterValue() {
this.controlTimeValue.placeholder = this.convertToUnit(this.byteSample);
},
convertFromUnit(value, unit = this.timeUnit) {
switch (unit) {
2023-12-25 23:40:54 +01:00
case "t":
return value;
case "s":
return value * this.songData.sampleRate;
}
},
// IMPORTANT: this function is ONLY used for text formatting, does not work for many conversions, and is inaccurate.
convertToUnit(value, unit = this.timeUnit) {
switch (unit) {
2023-12-25 23:40:54 +01:00
case "t":
return value;
case "s":
return (value / this.songData.sampleRate).toFixed(3);
}
},
setTimeUnit(value, updateCounter = true) {
if (value !== undefined) {
2023-12-25 23:40:54 +01:00
if (typeof value === "number") {
value = timeUnits[value];
2023-12-25 23:40:54 +01:00
}
this.timeUnit = value;
this.controlTimeUnitLabel.innerText = this.timeUnit;
2023-12-25 23:40:54 +01:00
} else {
this.timeUnit = this.controlTimeUnitLabel.innerText;
2023-12-25 23:40:54 +01:00
}
2023-12-25 23:40:54 +01:00
if (updateCounter) {
this.updateCounterValue();
2023-12-25 23:40:54 +01:00
}
this.saveSettings();
},
changeTimeUnit() {
this.timeUnit = timeUnits[(timeUnits.indexOf(this.timeUnit) + 1) % timeUnits.length];
this.controlTimeUnitLabel.innerText = this.timeUnit;
this.updateCounterValue();
this.saveSettings();
},
saveSettings() {
2023-12-25 23:40:54 +01:00
if (globalThis.useLocalStorage !== false) {
localStorage.settings = JSON.stringify({
drawSettings: this.drawSettings,
volume: this.volume,
timeUnit: this.timeUnit,
});
}
},
loadSettings() {
if (localStorage.settings && globalThis.useLocalStorage !== false) {
let settings;
try {
settings = JSON.parse(localStorage.settings);
} catch (err) {
console.error("Couldn't load settings!", localStorage.settings);
localStorage.clear();
this.loadDefaultSettings();
return;
}
if (Object.hasOwnProperty.call(settings, "drawSettings")) {
this.drawSettings = settings.drawSettings;
} else {
this.drawSettings.scale = 5;
}
2023-12-25 23:40:54 +01:00
if (Object.hasOwnProperty.call(settings, "volume")) {
this.setVolume(false, settings.volume);
2023-12-25 23:40:54 +01:00
} else {
this.setVolume(false);
2023-12-25 23:40:54 +01:00
}
2023-12-25 23:40:54 +01:00
if (Object.hasOwnProperty.call(settings, "timeUnit")) {
this.setTimeUnit(settings.timeUnit, false);
2023-12-25 23:40:54 +01:00
} else {
this.setTimeUnit(undefined, false);
2023-12-25 23:40:54 +01:00
}
} else {
this.loadDefaultSettings();
2023-12-25 23:40:54 +01:00
}
},
loadDefaultSettings() {
this.drawSettings.scale = 5;
this.setVolume(false);
this.setTimeUnit(undefined, false);
2023-12-25 23:40:54 +01:00
},
2021-11-18 14:36:00 +01:00
});
2023-12-25 21:56:34 +01:00
// used in html
2022-05-13 19:17:17 +02:00
Object.defineProperty(globalThis, "bytebeat", { value: bytebeat });
2023-12-23 05:31:49 +01:00
2023-04-01 02:45:26 +02:00
bytebeat.init();
export default bytebeat;