Files

685 lines
26 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
console.log("[AudioWaveformSlicer] ===== JS FILE LOADED =====");
const DEBUG = true;
function log(...args) {
if (DEBUG) console.log("[AudioWaveformSlicer]", ...args);
}
const WAVEFORM_TOP_PADDING = 80;
const WAVEFORM_BOTTOM_PADDING = 50;
const WAVEFORM_H_PADDING = 10;
const CUT_LINE_HIT_RADIUS = 6;
const PLAYHEAD_COLOR = "#FFFFFF";
const CUT_LINE_COLOR = "#FF3333";
const SEGMENT_COLORS = [
"rgba(60, 130, 80, 0.12)",
"rgba(60, 80, 160, 0.12)",
"rgba(160, 120, 50, 0.12)",
"rgba(130, 60, 130, 0.12)",
];
function formatTime(seconds) {
if (!seconds || !isFinite(seconds)) return "0:00.0";
const m = Math.floor(seconds / 60);
const s = (seconds % 60).toFixed(1);
return `${m}:${s.padStart(4, "0")}`;
}
app.registerExtension({
name: "AnotherUtils.AudioWaveformSlicer",
async beforeRegisterNodeDef(nodeType, nodeData, appInstance) {
if (nodeData.name === "AudioWaveformSlicer") {
console.log("[AudioWaveformSlicer] ===== FOUND NODE DEF =====", nodeData);
}
if (nodeData.name !== "AudioWaveformSlicer") return;
log("Registering AudioWaveformSlicer extension");
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
log("onNodeCreated called");
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
try {
this._aws = {
peaks: null,
duration: 0,
audioBuffer: null,
audioContext: null,
sourceNode: null,
isPlaying: false,
playStartTime: 0,
playOffset: 0,
playheadPos: 0,
cutLines: [],
draggingCut: -1,
animFrameId: null,
waveformRect: null,
lastAudioFile: null,
};
// Find widgets
this._awsWidgets = {};
if (this.widgets) {
for (const w of this.widgets) {
if (w.name === "audio" || w.name === "cut_positions") {
this._awsWidgets[w.name] = w;
}
}
}
// Hide cut_positions widget
const cpWidget = this._awsWidgets.cut_positions;
if (cpWidget) {
cpWidget.type = "hidden";
cpWidget.computeSize = function () { return [0, -4]; };
}
// Set audio widget callback
const audioWidget = this._awsWidgets.audio;
if (audioWidget) {
const self = this;
audioWidget.callback = function () {
self._aws.lastAudioFile = null;
self._loadAudioFile();
};
}
// Add upload button widget
this._addUploadWidget();
// Set minimum size
if (this.size) {
this.size[0] = Math.max(this.size[0] || 400, 400);
this.size[1] = Math.max(this.size[1] || 300, 300);
}
// Restore cut_positions from widget
this._restoreCutPositions();
// Load initial audio if set
if (audioWidget && audioWidget.value) {
this._loadAudioFile();
}
log("onNodeCreated completed successfully");
} catch (e) {
console.error("[AudioWaveformSlicer] Error in onNodeCreated:", e);
}
return r;
};
// --- Load audio file ---
nodeType.prototype._loadAudioFile = async function () {
var filename = null;
try {
filename = this._awsWidgets && this._awsWidgets.audio ? this._awsWidgets.audio.value : null;
} catch (e) {
return;
}
if (!filename) return;
if (!this._aws) return;
if (filename === this._aws.lastAudioFile) return;
this._stopPlayback();
this._aws.lastAudioFile = filename;
this._aws.peaks = null;
this._aws.audioBuffer = null;
this._aws.duration = 0;
this._aws.playheadPos = 0;
try {
var url = api.apiURL("/view?filename=" + encodeURIComponent(filename) + "&type=input");
log("Loading audio from:", url);
var response = await fetch(url);
var arrayBuffer = await response.arrayBuffer();
if (!this._aws.audioContext) {
this._aws.audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
var audioBuffer = await this._aws.audioContext.decodeAudioData(arrayBuffer);
this._aws.audioBuffer = audioBuffer;
this._aws.duration = audioBuffer.duration;
log("Audio loaded, duration:", audioBuffer.duration);
this._calculatePeaks();
this.setDirtyCanvas(true, true);
} catch (e) {
console.error("[AudioWaveformSlicer] Error loading audio:", e);
}
};
// --- Upload widget ---
nodeType.prototype._addUploadWidget = function () {
var self = this;
var fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = "audio/*,video/*,.mp3,.wav,.flac,.ogg,.mp4,.webm";
fileInput.style.display = "none";
document.body.appendChild(fileInput);
fileInput.addEventListener("change", async function () {
if (!fileInput.files || !fileInput.files.length) return;
var file = fileInput.files[0];
try {
var formData = new FormData();
formData.append("image", file);
formData.append("type", "input");
formData.append("overwrite", "true");
var resp = await api.fetchApi("/upload/image", {
method: "POST",
body: formData,
});
if (resp.ok) {
var data = await resp.json();
var filename = data.name;
log("Uploaded:", filename);
// Refresh combo options and select the new file
var audioWidget = self._awsWidgets.audio;
if (audioWidget) {
// Add to options if not already there
if (audioWidget.options && audioWidget.options.values) {
if (audioWidget.options.values.indexOf(filename) === -1) {
audioWidget.options.values.push(filename);
audioWidget.options.values.sort();
}
}
audioWidget.value = filename;
self._aws.lastAudioFile = null;
self._loadAudioFile();
}
} else {
console.error("[AudioWaveformSlicer] Upload failed:", resp.status);
}
} catch (e) {
console.error("[AudioWaveformSlicer] Upload error:", e);
}
fileInput.value = "";
});
this._aws.fileInput = fileInput;
// Add a button widget
var uploadWidget = this.addWidget("button", "Upload Audio", "upload", function () {
fileInput.click();
});
uploadWidget.serialize = false;
};
// --- Calculate peaks ---
nodeType.prototype._calculatePeaks = function () {
if (!this._aws || !this._aws.audioBuffer) return;
var buf = this._aws.audioBuffer;
var rect = this._getWaveformRect();
var numBars = Math.max(1, Math.floor(rect.w));
var channelData = buf.getChannelData(0);
var samplesPerBar = Math.max(1, Math.floor(channelData.length / numBars));
var peaks = new Float32Array(numBars);
for (var i = 0; i < numBars; i++) {
var max = 0;
var start = i * samplesPerBar;
var end = Math.min(start + samplesPerBar, channelData.length);
for (var j = start; j < end; j++) {
var abs = Math.abs(channelData[j]);
if (abs > max) max = abs;
}
peaks[i] = max;
}
this._aws.peaks = peaks;
};
// --- Get waveform drawing rect ---
nodeType.prototype._getWaveformRect = function () {
var widgetCount = (this.widgets && this.widgets.length) ? this.widgets.length : 2;
var topY = Math.max(WAVEFORM_TOP_PADDING, widgetCount * 28 + 10);
var nodeW = (this.size && this.size[0]) ? this.size[0] : 400;
var nodeH = (this.size && this.size[1]) ? this.size[1] : 300;
var w = nodeW - WAVEFORM_H_PADDING * 2;
var h = nodeH - topY - WAVEFORM_BOTTOM_PADDING;
return {
x: WAVEFORM_H_PADDING,
y: topY,
w: Math.max(w, 10),
h: Math.max(h, 20),
};
};
// --- Sync cut_positions widget ---
nodeType.prototype._syncCutPositions = function () {
if (!this._aws || !this._awsWidgets) return;
var cpWidget = this._awsWidgets.cut_positions;
if (cpWidget) {
var sorted = this._aws.cutLines.slice().sort(function (a, b) { return a - b; });
this._aws.cutLines = sorted;
cpWidget.value = JSON.stringify(sorted);
}
};
// --- Restore cut positions from widget ---
nodeType.prototype._restoreCutPositions = function () {
if (!this._aws) return;
try {
var val = (this._awsWidgets && this._awsWidgets.cut_positions) ? this._awsWidgets.cut_positions.value : "[]";
val = val || "[]";
var arr = JSON.parse(val);
if (Array.isArray(arr)) {
this._aws.cutLines = arr.map(Number).filter(function (n) { return !isNaN(n); });
}
} catch (e) {
this._aws.cutLines = [];
}
};
// --- Playback ---
nodeType.prototype._togglePlayback = function () {
if (!this._aws) return;
if (this._aws.isPlaying) {
this._stopPlayback();
} else {
this._startPlayback();
}
};
nodeType.prototype._startPlayback = function () {
if (!this._aws) return;
var buf = this._aws.audioBuffer;
var ctx = this._aws.audioContext;
if (!buf || !ctx) return;
if (ctx.state === "suspended") ctx.resume();
var source = ctx.createBufferSource();
source.buffer = buf;
source.connect(ctx.destination);
var offset = this._aws.playOffset;
source.start(0, offset);
this._aws.sourceNode = source;
this._aws.playStartTime = ctx.currentTime - offset;
this._aws.isPlaying = true;
var self = this;
source.onended = function () {
if (self._aws && self._aws.isPlaying) {
self._aws.isPlaying = false;
self._aws.playOffset = 0;
self._aws.playheadPos = 0;
if (self._aws.animFrameId) {
cancelAnimationFrame(self._aws.animFrameId);
self._aws.animFrameId = null;
}
self.setDirtyCanvas(true, true);
}
};
this._animatePlayhead();
};
nodeType.prototype._stopPlayback = function () {
if (!this._aws) return;
if (this._aws.sourceNode) {
try {
this._aws.sourceNode.onended = null;
this._aws.sourceNode.stop();
} catch (e) { /* ignore */ }
this._aws.sourceNode = null;
}
if (this._aws.isPlaying && this._aws.audioContext) {
this._aws.playOffset = this._aws.audioContext.currentTime - this._aws.playStartTime;
}
this._aws.isPlaying = false;
if (this._aws.animFrameId) {
cancelAnimationFrame(this._aws.animFrameId);
this._aws.animFrameId = null;
}
this.setDirtyCanvas(true, true);
};
nodeType.prototype._animatePlayhead = function () {
if (!this._aws || !this._aws.isPlaying) return;
var ctx = this._aws.audioContext;
if (!ctx) return;
var elapsed = ctx.currentTime - this._aws.playStartTime;
var duration = this._aws.duration || 1;
this._aws.playheadPos = Math.min(elapsed / duration, 1);
if (this._aws.playheadPos >= 1) {
this._aws.isPlaying = false;
this._aws.playOffset = 0;
this._aws.playheadPos = 0;
this._aws.sourceNode = null;
this.setDirtyCanvas(true, true);
return;
}
this.setDirtyCanvas(true, true);
var self = this;
this._aws.animFrameId = requestAnimationFrame(function () {
self._animatePlayhead();
});
};
// --- Time <-> X coordinate conversion ---
nodeType.prototype._timeToX = function (seconds) {
var rect = this._getWaveformRect();
var duration = (this._aws && this._aws.duration) ? this._aws.duration : 1;
return rect.x + (seconds / duration) * rect.w;
};
nodeType.prototype._xToTime = function (x) {
var rect = this._getWaveformRect();
var duration = (this._aws && this._aws.duration) ? this._aws.duration : 1;
var ratio = (x - rect.x) / rect.w;
return Math.max(0, Math.min(ratio * duration, duration));
};
// --- Draw ---
var origOnDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function (ctx) {
if (origOnDrawForeground) origOnDrawForeground.apply(this, arguments);
if (!this._aws) return;
try {
var rect = this._getWaveformRect();
this._aws.waveformRect = rect;
// Background
ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
if (!this._aws.peaks || this._aws.peaks.length === 0) {
ctx.fillStyle = "#888";
ctx.font = "12px sans-serif";
ctx.textAlign = "center";
ctx.fillText("Select an audio file", rect.x + rect.w / 2, rect.y + rect.h / 2);
ctx.textAlign = "left";
// Still draw transport bar
this._drawTransportBar(ctx, rect);
return;
}
// Recalculate peaks if width changed
if (this._aws.peaks.length !== Math.floor(rect.w)) {
this._calculatePeaks();
}
var peaks = this._aws.peaks;
if (!peaks) return;
// Draw segment backgrounds
var cuts = this._aws.cutLines.slice().sort(function (a, b) { return a - b; });
var segments = [0].concat(cuts, [this._aws.duration]);
for (var i = 0; i < segments.length - 1; i++) {
var sx = this._timeToX(segments[i]);
var ex = this._timeToX(segments[i + 1]);
ctx.fillStyle = SEGMENT_COLORS[i % SEGMENT_COLORS.length];
ctx.fillRect(sx, rect.y, ex - sx, rect.h);
}
// Draw waveform bars
var centerY = rect.y + rect.h / 2;
var maxBarH = rect.h / 2 - 2;
for (var i = 0; i < peaks.length; i++) {
var amp = peaks[i];
var barH = amp * maxBarH;
var x = rect.x + i;
var r = Math.floor(60 + amp * 195);
var g = Math.floor(180 - amp * 100);
var b = 60;
ctx.fillStyle = "rgb(" + r + "," + g + "," + b + ")";
ctx.fillRect(x, centerY - barH, 1, barH * 2);
}
// Draw cut lines
for (var i = 0; i < this._aws.cutLines.length; i++) {
var t = this._aws.cutLines[i];
var cx = this._timeToX(t);
ctx.strokeStyle = CUT_LINE_COLOR;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(cx, rect.y);
ctx.lineTo(cx, rect.y + rect.h);
ctx.stroke();
// Handle triangle
ctx.fillStyle = CUT_LINE_COLOR;
ctx.beginPath();
ctx.moveTo(cx - 5, rect.y);
ctx.lineTo(cx + 5, rect.y);
ctx.lineTo(cx, rect.y + 10);
ctx.closePath();
ctx.fill();
// Timestamp label
ctx.fillStyle = "#FFF";
ctx.font = "10px sans-serif";
ctx.textAlign = "center";
ctx.fillText(formatTime(t), cx, rect.y - 3);
}
// Draw playhead
if (this._aws.duration > 0) {
var phX = rect.x + this._aws.playheadPos * rect.w;
ctx.strokeStyle = PLAYHEAD_COLOR;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(phX, rect.y);
ctx.lineTo(phX, rect.y + rect.h);
ctx.stroke();
}
// Transport bar
this._drawTransportBar(ctx, rect);
} catch (e) {
console.error("[AudioWaveformSlicer] Draw error:", e);
}
// Reset text alignment
ctx.textAlign = "left";
};
// --- Draw transport bar ---
nodeType.prototype._drawTransportBar = function (ctx, rect) {
if (!this._aws) return;
var barY = rect.y + rect.h + 5;
// Play/pause button
ctx.fillStyle = "#CCC";
ctx.font = "16px sans-serif";
ctx.textAlign = "left";
var btnText = this._aws.isPlaying ? "\u23F8" : "\u25B6";
ctx.fillText(btnText, rect.x + 5, barY + 16);
// Time display
var currentTime = this._aws.playheadPos * (this._aws.duration || 0);
ctx.fillStyle = "#AAA";
ctx.font = "12px sans-serif";
ctx.fillText(
formatTime(currentTime) + " / " + formatTime(this._aws.duration),
rect.x + 28,
barY + 14
);
// Slice count
var numCuts = this._aws.cutLines ? this._aws.cutLines.length : 0;
ctx.textAlign = "right";
ctx.fillText(
numCuts + " cuts \u2192 " + (numCuts + 1) + " slices",
rect.x + rect.w,
barY + 14
);
ctx.textAlign = "left";
};
// --- Mouse events ---
var origOnMouseDown = nodeType.prototype.onMouseDown;
nodeType.prototype.onMouseDown = function (event, pos, graphCanvas) {
if (!this._aws || !this._aws.waveformRect) {
return origOnMouseDown ? origOnMouseDown.apply(this, arguments) : false;
}
var rect = this._aws.waveformRect;
var x = pos[0];
var y = pos[1];
// Check play button click
var barY = rect.y + rect.h + 5;
if (x >= rect.x && x <= rect.x + 25 && y >= barY && y <= barY + 22) {
this._togglePlayback();
return true;
}
// Check if inside waveform area
if (x < rect.x || x > rect.x + rect.w || y < rect.y || y > rect.y + rect.h) {
return origOnMouseDown ? origOnMouseDown.apply(this, arguments) : false;
}
// Check if near a cut line (for dragging)
if (this._aws.cutLines) {
for (var i = 0; i < this._aws.cutLines.length; i++) {
var cx = this._timeToX(this._aws.cutLines[i]);
if (Math.abs(x - cx) <= CUT_LINE_HIT_RADIUS) {
this._aws.draggingCut = i;
return true;
}
}
}
// Single click: seek playhead
if (this._aws.duration > 0) {
var time = this._xToTime(x);
this._aws.playheadPos = time / this._aws.duration;
this._aws.playOffset = time;
if (this._aws.isPlaying) {
this._stopPlayback();
this._startPlayback();
}
this.setDirtyCanvas(true, true);
}
return true;
};
var origOnMouseMove = nodeType.prototype.onMouseMove;
nodeType.prototype.onMouseMove = function (event, pos, graphCanvas) {
if (this._aws && this._aws.draggingCut >= 0) {
var x = pos[0];
var time = this._xToTime(x);
this._aws.cutLines[this._aws.draggingCut] = time;
this._syncCutPositions();
this.setDirtyCanvas(true, true);
return true;
}
return origOnMouseMove ? origOnMouseMove.apply(this, arguments) : false;
};
var origOnMouseUp = nodeType.prototype.onMouseUp;
nodeType.prototype.onMouseUp = function (event, pos, graphCanvas) {
if (this._aws && this._aws.draggingCut >= 0) {
this._aws.draggingCut = -1;
this._syncCutPositions();
this.setDirtyCanvas(true, true);
return true;
}
return origOnMouseUp ? origOnMouseUp.apply(this, arguments) : false;
};
var origOnDblClick = nodeType.prototype.onDblClick;
nodeType.prototype.onDblClick = function (event, pos, graphCanvas) {
if (!this._aws || !this._aws.waveformRect || !this._aws.peaks) {
return origOnDblClick ? origOnDblClick.apply(this, arguments) : undefined;
}
var rect = this._aws.waveformRect;
var x = pos[0];
var y = pos[1];
// Check if inside waveform
if (x >= rect.x && x <= rect.x + rect.w && y >= rect.y && y <= rect.y + rect.h) {
// Check if near existing cut line -> remove it
for (var i = 0; i < this._aws.cutLines.length; i++) {
var cx = this._timeToX(this._aws.cutLines[i]);
if (Math.abs(x - cx) <= CUT_LINE_HIT_RADIUS) {
this._aws.cutLines.splice(i, 1);
this._syncCutPositions();
this.setDirtyCanvas(true, true);
return true;
}
}
// Add new cut line
var time = this._xToTime(x);
this._aws.cutLines.push(time);
this._syncCutPositions();
this.setDirtyCanvas(true, true);
return true;
}
return origOnDblClick ? origOnDblClick.apply(this, arguments) : undefined;
};
// --- Configure (workflow load) ---
var origOnConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (info) {
if (origOnConfigure) origOnConfigure.apply(this, arguments);
try {
// Re-find widgets after configure
this._awsWidgets = {};
if (this.widgets) {
for (var i = 0; i < this.widgets.length; i++) {
var w = this.widgets[i];
if (w.name === "audio" || w.name === "cut_positions") {
this._awsWidgets[w.name] = w;
}
}
}
// Hide cut_positions
var cpWidget = this._awsWidgets.cut_positions;
if (cpWidget) {
cpWidget.type = "hidden";
cpWidget.computeSize = function () { return [0, -4]; };
}
if (this._aws) {
this._restoreCutPositions();
}
if (this._awsWidgets.audio && this._awsWidgets.audio.value) {
this._loadAudioFile();
}
} catch (e) {
console.error("[AudioWaveformSlicer] Error in onConfigure:", e);
}
};
// --- Compute size ---
var origComputeSize = nodeType.prototype.computeSize;
nodeType.prototype.computeSize = function () {
var size = origComputeSize ? origComputeSize.apply(this, arguments) : [400, 300];
if (!size) size = [400, 300];
return [Math.max(size[0], 400), Math.max(size[1], 300)];
};
log("Extension registered successfully");
},
});