Files
MajoorWaldi-ComfyUI-Majoor-…/js/features/viewer/audioVisualizer.js
T
Ewald f5917eef6e feat(execution): implement idle grace period and enhance asset handling
- Add EXECUTION_IDLE_GRACE_MS config to control post-execution delay
- Implement post-execution work scheduling to optimize resource handling
- Enhance asset upsert logic for live assets during stack finalization
- Add runtime status synchronization to backend
- Improve execution start/end handling with new job ID tracking
- Refactor live asset event processing to reduce complexity
- Optimize imports and event handling in various modules
2026-04-09 01:55:32 +02:00

524 lines
17 KiB
JavaScript

import { APP_CONFIG } from "../../app/config.js";
import { EVENTS } from "../../app/events.js";
function clamp(n, min, max) {
const v = Number(n);
if (!Number.isFinite(v)) return min;
return Math.max(min, Math.min(max, v));
}
function resolveMode(modeOverride) {
try {
const direct = String(modeOverride || "").toLowerCase();
if (direct === "simple" || direct === "artistic") return direct;
// Legacy fallback if old state/config contains removed modes.
if (direct === "webgl" || direct === "webgl3d") return "simple";
} catch (e) {
console.debug?.(e);
}
try {
const raw = String(APP_CONFIG?.VIEWER_AUDIO_VISUALIZER_MODE || "simple").toLowerCase();
if (raw === "artistic") return "artistic";
if (raw === "webgl3d" || raw === "webgl") return "simple";
} catch (e) {
console.debug?.(e);
}
return "simple";
}
function createMinimal2DDrawer(canvas) {
const ctx = canvas.getContext("2d");
if (!ctx) return null;
const sample = (freqData, tt) => {
try {
const idx = Math.max(
0,
Math.min(freqData.length - 1, Math.floor(tt * (freqData.length - 1))),
);
return (Number(freqData[idx]) || 0) / 255;
} catch {
return 0;
}
};
const vocalBand = (freqData, tt) => {
// Approximate vocal presence in low-mid / presence region.
// Weighted blend around ~0.18..0.46 normalized bins.
const a = sample(freqData, 0.18 + tt * 0.18);
const b = sample(freqData, 0.28 + tt * 0.16);
const c = sample(freqData, 0.4 + tt * 0.06);
const d = sample(freqData, 0.46 + tt * 0.03);
return a * 0.28 + b * 0.36 + c * 0.24 + d * 0.12;
};
const bassBand = (freqData, tt) => {
const a = sample(freqData, 0.01 + tt * 0.06);
const b = sample(freqData, 0.04 + tt * 0.07);
const c = sample(freqData, 0.09 + tt * 0.04);
return a * 0.45 + b * 0.35 + c * 0.2;
};
const trebleBand = (freqData, tt) => {
const a = sample(freqData, 0.64 + tt * 0.18);
const b = sample(freqData, 0.78 + tt * 0.12);
const c = sample(freqData, 0.92 + tt * 0.06);
return a * 0.34 + b * 0.38 + c * 0.28;
};
return {
draw(freqData, _timeData, tsMs = 0) {
try {
const w = canvas.width || 0;
const h = canvas.height || 0;
if (!(w > 1 && h > 1)) return;
const tSec = Number(tsMs) * 0.001;
const scroll = (tSec * 0.12) % 1;
// Transparent clear (no gray veil).
ctx.clearRect(0, 0, w, h);
const cx = w * 0.5;
const midY = h * 0.52;
const count = Math.max(36, Math.min(140, Math.floor(w / 12)));
const barSpan = Math.min(w * 0.56, count * 8);
const step = barSpan / Math.max(1, count - 1);
const startX = cx - barSpan * 0.5;
const topAnchorY = midY - h * 0.08;
// Top dotted line = vocals only; anchored and moving upward only.
ctx.fillStyle = "rgba(255,255,255,0.95)";
for (let i = 0; i < count; i++) {
const x = startX + i * step;
const t = i / Math.max(1, count - 1);
const tt = (t + scroll) % 1;
const vocal = vocalBand(freqData, tt);
const y = topAnchorY - vocal * h * 0.11;
const r = 1.2 + vocal * 1.2;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
}
// Mid dotted anchor line
ctx.fillStyle = "rgba(255,255,255,0.9)";
for (let i = 0; i < count; i++) {
const x = startX + i * step;
const r = 1.6;
ctx.beginPath();
ctx.arc(x, midY, r, 0, Math.PI * 2);
ctx.fill();
}
// Bars = bass+treble, anchored at center and moving downward only.
const barW = Math.max(1.5, step * 0.45);
for (let i = 0; i < count; i++) {
const x = startX + i * step;
const t = i / Math.max(1, count - 1);
const tt = (t + scroll) % 1;
const bass = bassBand(freqData, tt);
const treble = trebleBand(freqData, tt);
const centerBoost = 1 - Math.abs(t * 2 - 1);
const shaped = Math.pow(
(bass * 0.62 + treble * 0.38) * 0.84 + centerBoost * 0.16,
1.1,
);
const lenDown = shaped * h * 0.32;
ctx.fillStyle = "rgba(255,255,255,0.96)";
ctx.fillRect(x - barW * 0.5, midY + 1, barW, lenDown);
}
} catch (e) {
console.debug?.(e);
}
},
destroy() {},
};
}
function _createMinimalWebGLDrawer(canvas, { pseudo3d = false } = {}) {
let gl = null;
try {
gl = canvas.getContext("webgl", {
antialias: true,
alpha: true,
preserveDrawingBuffer: true,
});
} catch {
gl = null;
}
if (!gl) return null;
const vs = `
attribute vec2 aPos;
void main() {
gl_Position = vec4(aPos, 0.0, 1.0);
}
`;
const fs = `
precision mediump float;
uniform vec4 uColor;
void main() {
gl_FragColor = uColor;
}
`;
const compile = (type, src) => {
const sh = gl.createShader(type);
if (!sh) return null;
gl.shaderSource(sh, src);
gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
gl.deleteShader(sh);
return null;
}
return sh;
};
const vsh = compile(gl.VERTEX_SHADER, vs);
const fsh = compile(gl.FRAGMENT_SHADER, fs);
if (!vsh || !fsh) return null;
const program = gl.createProgram();
if (!program) return null;
gl.attachShader(program, vsh);
gl.attachShader(program, fsh);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return null;
gl.useProgram(program);
const aPos = gl.getAttribLocation(program, "aPos");
const uColor = gl.getUniformLocation(program, "uColor");
const buffer = gl.createBuffer();
if (!buffer || aPos < 0 || !uColor) return null;
const drawStrip = (verts, color) => {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, verts, gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
gl.uniform4f(uColor, color[0], color[1], color[2], color[3]);
gl.drawArrays(gl.LINE_STRIP, 0, Math.floor(verts.length / 2));
};
const sample = (freqData, tt) => {
try {
const idx = Math.max(
0,
Math.min(freqData.length - 1, Math.floor(tt * (freqData.length - 1))),
);
return (Number(freqData[idx]) || 0) / 255;
} catch {
return 0;
}
};
const vocalBand = (freqData, tt) => {
const a = sample(freqData, 0.18 + tt * 0.18);
const b = sample(freqData, 0.28 + tt * 0.16);
const c = sample(freqData, 0.4 + tt * 0.06);
const d = sample(freqData, 0.46 + tt * 0.03);
return a * 0.28 + b * 0.36 + c * 0.24 + d * 0.12;
};
const bassBand = (freqData, tt) => {
const a = sample(freqData, 0.01 + tt * 0.06);
const b = sample(freqData, 0.04 + tt * 0.07);
const c = sample(freqData, 0.09 + tt * 0.04);
return a * 0.45 + b * 0.35 + c * 0.2;
};
const trebleBand = (freqData, tt) => {
const a = sample(freqData, 0.64 + tt * 0.18);
const b = sample(freqData, 0.78 + tt * 0.12);
const c = sample(freqData, 0.92 + tt * 0.06);
return a * 0.34 + b * 0.38 + c * 0.28;
};
return {
draw(freqData, _timeData, tsMs = 0) {
try {
gl.viewport(0, 0, canvas.width || 1, canvas.height || 1);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
const n = Math.max(48, Math.min(180, Math.floor((canvas.width || 640) / 7)));
const t = Number(tsMs) * 0.001;
// Top line (vocals): only upward from an anchor.
const top = new Float32Array(n * 2);
for (let i = 0; i < n; i++) {
const nx = i / Math.max(1, n - 1);
const x = nx * 2 - 1;
const vocal = vocalBand(freqData, nx);
const zLike = pseudo3d ? Math.sin(nx * Math.PI * 4 + t * 1.1) * 0.18 : 0;
const persp = pseudo3d ? 1 / (1 + Math.max(-0.7, zLike) * 0.8) : 1;
const anchor = 0.18;
const y = clamp((anchor + vocal * 0.32) * persp, -0.95, 0.95);
top[i * 2] = x;
top[i * 2 + 1] = y;
}
drawStrip(top, [1, 1, 1, 0.95]);
// Lower bar profile (bass+treble): only downward from center.
const down = new Float32Array(n * 2);
for (let i = 0; i < n; i++) {
const nx = i / Math.max(1, n - 1);
const x = nx * 2 - 1;
const bass = bassBand(freqData, nx);
const treble = trebleBand(freqData, nx);
const mix = bass * 0.62 + treble * 0.38;
const zLike = pseudo3d ? Math.sin(nx * Math.PI * 3 + t * 1.0) * 0.14 : 0;
const persp = pseudo3d ? 1 / (1 + Math.max(-0.7, zLike) * 0.8) : 1;
const y = clamp(-mix * 0.62 * persp, -0.95, 0.0);
down[i * 2] = x;
down[i * 2 + 1] = y;
}
drawStrip(down, [1, 1, 1, 0.9]);
} catch (e) {
console.debug?.(e);
}
},
destroy() {
try {
gl.deleteBuffer(buffer);
} catch (e) {
console.debug?.(e);
}
try {
gl.deleteProgram(program);
} catch (e) {
console.debug?.(e);
}
try {
gl.deleteShader(vsh);
} catch (e) {
console.debug?.(e);
}
try {
gl.deleteShader(fsh);
} catch (e) {
console.debug?.(e);
}
},
};
}
export function createAudioVisualizer({
canvas,
audioEl,
mode: modeOverride,
pauseDuringExecution = null,
} = {}) {
if (!canvas || !audioEl) return { destroy() {} };
const shouldPauseDuringExecution =
pauseDuringExecution == null
? !!APP_CONFIG?.VIEWER_PAUSE_DURING_EXECUTION
: !!pauseDuringExecution;
let raf = null;
let disposed = false;
let audioCtx = null;
let source = null;
let analyser = null;
let freqData = null;
let timeData = null;
let drawer = null;
let lastAt = 0;
let runtimePaused = false;
const targetFps = clamp(APP_CONFIG?.VIEWER_AUDIO_VIS_FPS ?? 24, 8, 60);
const minDt = 1000 / targetFps;
let currentMode = resolveMode(modeOverride);
const resize = () => {
try {
const dpr = clamp(window.devicePixelRatio || 1, 1, 2);
const w = Math.max(32, Math.floor((canvas.clientWidth || 640) * dpr));
const h = Math.max(24, Math.floor((canvas.clientHeight || 140) * dpr));
if (canvas.width !== w) canvas.width = w;
if (canvas.height !== h) canvas.height = h;
} catch (e) {
console.debug?.(e);
}
};
const rebuildDrawer = (nextMode = currentMode) => {
currentMode = resolveMode(nextMode);
try {
drawer?.destroy?.();
} catch (e) {
console.debug?.(e);
}
drawer = null;
try {
resize();
} catch (e) {
console.debug?.(e);
}
try {
const preferWebGL =
currentMode === "artistic" && !Boolean(APP_CONFIG?.VIEWER_DISABLE_WEBGL_AUDIO);
if (preferWebGL) drawer = _createMinimalWebGLDrawer(canvas, { pseudo3d: true });
} catch (e) {
console.debug?.(e);
}
if (!drawer) {
try {
drawer = createMinimal2DDrawer(canvas);
} catch (e) {
console.debug?.(e);
drawer = null;
}
}
lastAt = 0;
return currentMode;
};
const init = () => {
if (disposed || analyser) return;
try {
const Ctx = window.AudioContext || window.webkitAudioContext;
if (!Ctx) return;
audioCtx = new Ctx();
source = audioCtx.createMediaElementSource(audioEl);
analyser = audioCtx.createAnalyser();
analyser.fftSize = 1024;
analyser.smoothingTimeConstant = 0.8;
source.connect(analyser);
analyser.connect(audioCtx.destination);
freqData = new Uint8Array(analyser.frequencyBinCount);
timeData = new Uint8Array(analyser.fftSize);
if (!drawer) rebuildDrawer(currentMode);
} catch {
analyser = null;
}
};
const tick = (ts) => {
if (disposed) return;
try {
raf = requestAnimationFrame(tick);
} catch {
raf = null;
return;
}
if (runtimePaused) return;
if (!analyser || !drawer) return;
if (ts - lastAt < minDt) return;
lastAt = ts;
try {
resize();
analyser.getByteFrequencyData(freqData);
analyser.getByteTimeDomainData(timeData);
drawer.draw(freqData, timeData, ts);
} catch (e) {
console.debug?.(e);
}
};
const start = async () => {
try {
if (runtimePaused) return;
init();
if (!audioCtx) return;
if (audioCtx.state === "suspended") {
try {
await audioCtx.resume();
} catch (e) {
console.debug?.(e);
}
}
if (raf == null) raf = requestAnimationFrame(tick);
} catch (e) {
console.debug?.(e);
}
};
const stop = () => {
try {
if (raf != null) cancelAnimationFrame(raf);
} catch (e) {
console.debug?.(e);
}
raf = null;
};
const onPlay = () => {
void start();
};
const onPause = () => stop();
const onEnded = () => stop();
const onResize = () => resize();
const onRuntimeStatus = (event) => {
if (!shouldPauseDuringExecution) return;
const activePromptId = String(event?.detail?.active_prompt_id || "").trim();
runtimePaused = !!activePromptId;
if (runtimePaused) {
stop();
return;
}
if (!audioEl?.paused) {
void start();
}
};
try {
resize();
rebuildDrawer(currentMode);
} catch (e) {
console.debug?.(e);
}
try {
audioEl.addEventListener("play", onPlay, { passive: true });
audioEl.addEventListener("pause", onPause, { passive: true });
audioEl.addEventListener("ended", onEnded, { passive: true });
window.addEventListener("resize", onResize, { passive: true });
if (shouldPauseDuringExecution) {
window.addEventListener(EVENTS.RUNTIME_STATUS, onRuntimeStatus);
if (String(window?.__MJR_EXECUTION_RUNTIME__?.active_prompt_id || "").trim()) {
runtimePaused = true;
}
}
} catch (e) {
console.debug?.(e);
}
return {
setMode(nextMode) {
if (disposed) return currentMode;
return rebuildDrawer(nextMode);
},
destroy() {
if (disposed) return;
disposed = true;
stop();
try {
audioEl.removeEventListener("play", onPlay);
audioEl.removeEventListener("pause", onPause);
audioEl.removeEventListener("ended", onEnded);
window.removeEventListener("resize", onResize);
window.removeEventListener(EVENTS.RUNTIME_STATUS, onRuntimeStatus);
} catch (e) {
console.debug?.(e);
}
try {
drawer?.destroy?.();
} catch (e) {
console.debug?.(e);
}
try {
source?.disconnect?.();
} catch (e) {
console.debug?.(e);
}
try {
analyser?.disconnect?.();
} catch (e) {
console.debug?.(e);
}
try {
audioCtx?.close?.();
} catch (e) {
console.debug?.(e);
}
source = null;
analyser = null;
audioCtx = null;
drawer = null;
},
};
}