Add AudioWaveformSlicer and AudioSliceSelector nodes

Interactive audio waveform node with cut lines, playback, and
slice selector for filtering individual audio segments.
This commit is contained in:
Marco
2026-02-15 13:43:20 -03:00
parent 83834d1344
commit 6ca03b227e
4 changed files with 802 additions and 4 deletions
+34 -4
View File
@@ -31,7 +31,18 @@ from .comparison_swipe import ComparisonSwipeNode
from .folder_video_concatenator import FolderVideoConcatenator
from .interactive_crop import InteractiveCropNode
from .caption_image_loader import CaptionImageLoader
from . import server_routes # Register Custom API Routes
from .video_audio_combiner import (
VideoAudioCombiner,
VideoAudioCombinerSimple,
HAS_NEW_VIDEO_API,
)
if HAS_NEW_VIDEO_API:
from .video_audio_combiner import VideoAudioCombinerV3
from .acestep_loader import AceStepLoKrLoader
from .audio_waveform_slicer import AudioWaveformSlicer
from .audio_slice_selector import AudioSliceSelector
from . import server_routes # Register Custom API Routes
NODE_CLASS_MAPPINGS = {
"CustomCrop": CustomCropNode,
@@ -57,9 +68,18 @@ NODE_CLASS_MAPPINGS = {
"ComparisonSwipe": ComparisonSwipeNode,
"FolderVideoConcatenator": FolderVideoConcatenator,
"InteractiveCrop": InteractiveCropNode,
"CaptionImageLoader": CaptionImageLoader
"CaptionImageLoader": CaptionImageLoader,
"VideoAudioCombiner": VideoAudioCombiner,
"VideoAudioCombinerSimple": VideoAudioCombinerSimple,
"AceStepLoKrLoader": AceStepLoKrLoader,
"AudioWaveformSlicer": AudioWaveformSlicer,
"AudioSliceSelector": AudioSliceSelector,
}
# Add V3 nodes if the new API is available
if HAS_NEW_VIDEO_API:
NODE_CLASS_MAPPINGS["VideoAudioCombinerV3"] = VideoAudioCombinerV3
NODE_DISPLAY_NAME_MAPPINGS = {
"CustomCrop": "Custom Crop",
"SmartResize": "Smart Resize with Border Fill",
@@ -83,8 +103,18 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"ComparisonSwipe": "Comparison Swipe Video",
"FolderVideoConcatenator": "Folder Video Concatenator (OpenCV)",
"InteractiveCrop": "Interactive Crop",
"CaptionImageLoader": "Caption Image Loader"
"CaptionImageLoader": "Caption Image Loader",
"VideoAudioCombiner": "Video + Audio Combiner",
"VideoAudioCombinerSimple": "Video + Audio Combiner (Simple)",
"AceStepLoKrLoader": "AceStep LoKr Loader",
"AudioWaveformSlicer": "Audio Waveform Slicer",
"AudioSliceSelector": "Audio Slice Selector",
}
# Add V3 display names if available
if HAS_NEW_VIDEO_API:
NODE_DISPLAY_NAME_MAPPINGS["VideoAudioCombinerV3"] = "Video + Audio Combiner (V3)"
WEB_DIRECTORY = "./js"
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+26
View File
@@ -0,0 +1,26 @@
class AudioSliceSelector:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"audio_list": ("AUDIO",),
"index": ("INT", {"default": 0, "min": 0, "max": 999}),
},
}
RETURN_TYPES = ("AUDIO",)
RETURN_NAMES = ("audio",)
INPUT_IS_LIST = True
FUNCTION = "select"
CATEGORY = "audio"
def select(self, audio_list, index):
# When INPUT_IS_LIST is True, all inputs arrive as lists
idx = index[0] if isinstance(index, list) else index
if not audio_list:
raise ValueError("No audio slices received.")
# Clamp index
idx = max(0, min(idx, len(audio_list) - 1))
return (audio_list[idx],)
+126
View File
@@ -0,0 +1,126 @@
import torch
import av
import os
import json
import hashlib
import folder_paths
def f32_pcm(wav: torch.Tensor) -> torch.Tensor:
if wav.dtype.is_floating_point:
return wav
elif wav.dtype == torch.int16:
return wav.float() / (2 ** 15)
elif wav.dtype == torch.int32:
return wav.float() / (2 ** 31)
raise ValueError(f"Unsupported wav dtype: {wav.dtype}")
def load_audio(filepath: str) -> tuple:
with av.open(filepath) as af:
if not af.streams.audio:
raise ValueError("No audio stream found in the file.")
stream = af.streams.audio[0]
sr = stream.codec_context.sample_rate
n_channels = stream.channels
frames = []
for frame in af.decode(streams=stream.index):
buf = torch.from_numpy(frame.to_ndarray())
if buf.shape[0] != n_channels:
buf = buf.view(-1, n_channels).t()
frames.append(buf)
if not frames:
raise ValueError("No audio frames decoded.")
wav = torch.cat(frames, dim=1)
wav = f32_pcm(wav)
return wav, sr
class AudioWaveformSlicer:
@classmethod
def INPUT_TYPES(cls):
input_dir = folder_paths.get_input_directory()
files = folder_paths.filter_files_content_types(
os.listdir(input_dir), ["audio", "video"]
)
return {
"required": {
"audio": (sorted(files), {"audio_upload": True}),
"cut_positions": ("STRING", {"default": "[]", "multiline": False}),
},
}
RETURN_TYPES = ("AUDIO",)
RETURN_NAMES = ("audio_slices",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "slice_audio"
CATEGORY = "audio"
def slice_audio(self, audio, cut_positions):
audio_path = folder_paths.get_annotated_filepath(audio)
waveform, sample_rate = load_audio(audio_path)
# waveform shape: [C, T]
total_samples = waveform.shape[1]
# Parse cut positions (in seconds), sort them
try:
cuts = json.loads(cut_positions)
except (json.JSONDecodeError, TypeError):
cuts = []
if not isinstance(cuts, list):
cuts = []
# Convert seconds to sample indices, filter valid ones
cut_samples = []
for c in cuts:
try:
s = int(float(c) * sample_rate)
if 0 < s < total_samples:
cut_samples.append(s)
except (ValueError, TypeError):
continue
cut_samples = sorted(set(cut_samples))
# Build slice boundaries
boundaries = [0] + cut_samples + [total_samples]
slices = []
for i in range(len(boundaries) - 1):
start = boundaries[i]
end = boundaries[i + 1]
if end > start:
slice_waveform = waveform[:, start:end]
slices.append({
"waveform": slice_waveform.unsqueeze(0), # [1, C, T]
"sample_rate": sample_rate,
})
if not slices:
slices.append({
"waveform": waveform.unsqueeze(0),
"sample_rate": sample_rate,
})
return (slices,)
@classmethod
def IS_CHANGED(cls, audio, cut_positions):
audio_path = folder_paths.get_annotated_filepath(audio)
m = hashlib.sha256()
with open(audio_path, "rb") as f:
m.update(f.read())
m.update(cut_positions.encode())
return m.digest().hex()
@classmethod
def VALIDATE_INPUTS(cls, audio, **kwargs):
if not folder_paths.exists_annotated_filepath(audio):
return "Invalid audio file: {}".format(audio)
return True
+616
View File
@@ -0,0 +1,616 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
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") 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._loadAudioFile();
};
}
// 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);
}
};
// --- 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");
},
});