Files
diodiogod-TTS-Audio-Suite/web/audio_analyzer_core.js

831 lines
31 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { AudioAnalyzerUI } from "./audio_analyzer_ui.js";
import { AudioAnalyzerEvents } from "./audio_analyzer_events.js";
import { AudioAnalyzerVisualization } from "./audio_analyzer_visualization.js";
import { AudioAnalyzerNodeIntegration } from "./audio_analyzer_node_integration.js";
/**
* Core Audio Wave Analyzer Interface
* Main class that coordinates all audio wave analyzer functionality
*/
export class AudioAnalyzerInterface {
constructor(node) {
this.node = node;
this.canvas = null;
this.ctx = null;
this.waveformData = null;
this.selectedRegions = [];
this.zoomLevel = 1;
this.scrollOffset = 0;
this.isPlaying = false;
this.currentTime = 0;
this.audioElement = null;
this.playbackSpeed = 1;
this.isDragging = false;
this.dragStart = null;
this.dragEnd = null;
this.selectedStart = null;
this.selectedEnd = null;
this.selectedRegionIndices = []; // For tracking multiple regions selected for deletion
this.highlightedRegionIndex = -1; // For click-to-highlight feedback (persists until next click)
// Loop markers
this.loopStart = null;
this.loopEnd = null;
this.isLooping = false;
// Amplitude scaling
this.amplitudeScale = 0.4; // Default scaling factor (0.1 to 1.0)
this.maxAmplitudeRange = 1.0; // Maximum amplitude range (+/- 1.0)
// Color scheme for the interface
this.colors = {
background: '#1a1a1a',
waveform: '#4a9eff',
rms: '#ff6b6b',
grid: '#333333',
selection: 'rgba(255, 255, 0, 0.3)',
playhead: '#ff0000',
region: 'rgba(0, 255, 0, 0.2)',
regionSelected: 'rgba(255, 165, 0, 0.4)',
regionHovered: 'rgba(0, 255, 0, 0.4)',
loopMarker: '#ff00ff',
text: '#ffffff'
};
// Initialize modules
this.ui = new AudioAnalyzerUI(this);
this.events = new AudioAnalyzerEvents(this);
this.visualization = new AudioAnalyzerVisualization(this);
this.nodeIntegration = new AudioAnalyzerNodeIntegration(this);
this.setupInterface();
}
setupInterface() {
// Create the main interface using UI module
this.ui.createInterface();
// Setup event listeners
this.events.setupEventListeners();
// Setup canvas resize observer
this.ui.setupCanvasResize();
// Setup widget change listeners for bidirectional sync
this.setupWidgetListeners();
// Attempt to load cached data, otherwise show initial message
this.loadCachedData();
}
setupWidgetListeners() {
// Store the last known values for comparison
const manualRegionsWidget = this.node.widgets.find(w => w.name === 'manual_regions');
const labelsWidget = this.node.widgets.find(w => w.name === 'region_labels');
if (manualRegionsWidget) {
this.lastManualRegionsValue = manualRegionsWidget.value;
}
if (labelsWidget) {
this.lastLabelsValue = labelsWidget.value;
}
// Add canvas click listener to check for text changes
this.canvas.addEventListener('mousedown', () => {
this.checkAndSyncTextChanges();
});
// Also check when canvas gets focus (user clicks on interface)
this.canvas.addEventListener('focus', () => {
this.checkAndSyncTextChanges();
});
}
checkAndSyncTextChanges() {
const manualRegionsWidget = this.node.widgets.find(w => w.name === 'manual_regions');
const labelsWidget = this.node.widgets.find(w => w.name === 'region_labels');
let hasChanges = false;
// Check if manual_regions text changed
if (manualRegionsWidget && manualRegionsWidget.value !== this.lastManualRegionsValue) {
this.lastManualRegionsValue = manualRegionsWidget.value;
hasChanges = true;
}
// Check if labels text changed
if (labelsWidget && labelsWidget.value !== this.lastLabelsValue) {
this.lastLabelsValue = labelsWidget.value;
hasChanges = true;
}
// Only sync if there were actual changes
if (hasChanges && !manualRegionsWidget?._updating) {
this.parseManualRegionsFromText();
this.showMessage('Regions synced from text input');
}
}
async loadCachedData() {
// Attempts to load persisted analysis data from the last execution.
if (!this.node?.id || this.node.id < 0) {
this.visualization.redraw();
return;
}
const cacheUrl = `/output/audio_analyzer_cache_${this.node.id}.json?t=${Date.now()}`;
try {
const response = await fetch(cacheUrl);
if (!response.ok) {
throw new Error(`Cache file not found or server error: ${response.status}`);
}
const data = await response.json();
this.nodeIntegration.updateVisualization(data.visualization_data || data);
console.log(`🌊 Audio Wave Analyzer: Successfully loaded cached data for node ${this.node.id}`);
} catch (error) {
// console.log(`🌊 Audio Wave Analyzer: No cache found for node ${this.node.id}. Showing initial message. Details: ${error.message}`); // Debug: cache miss
this.visualization.redraw();
}
}
// Utility functions
pixelToTime(pixel) {
if (!this.waveformData) return 0;
const canvasWidth = this.getCanvasLogicalWidth();
const visibleDuration = this.waveformData.duration / this.zoomLevel;
const startTime = this.scrollOffset;
return startTime + (pixel / canvasWidth) * visibleDuration;
}
timeToPixel(time) {
if (!this.waveformData) return 0;
const canvasWidth = this.getCanvasLogicalWidth();
const visibleDuration = this.waveformData.duration / this.zoomLevel;
const startTime = this.scrollOffset;
return ((time - startTime) / visibleDuration) * canvasWidth;
}
// Coordinate transformation utilities for ComfyUI zoom handling
getCanvasCoordinates(clientX, clientY) {
if (!this.canvas) return { x: 0, y: 0 };
// Get the canvas bounding rect (this gives display size affected by ComfyUI zoom)
const rect = this.canvas.getBoundingClientRect();
// Calculate coordinates relative to canvas in display pixels
const displayX = clientX - rect.left;
const displayY = clientY - rect.top;
// Convert from display pixels to logical canvas pixels.
const logicalCanvasWidth = this.getCanvasLogicalWidth();
const logicalCanvasHeight = this.getCanvasLogicalHeight();
// Scale coordinates from display size to logical size
const x = (displayX / rect.width) * logicalCanvasWidth;
const y = (displayY / rect.height) * logicalCanvasHeight;
return { x, y };
}
getComfyUIZoomLevel() {
// Try to get ComfyUI's zoom level from various possible sources
try {
// Method 1: Check for app canvas zoom
if (window.app && window.app.canvas && typeof window.app.canvas.ds !== 'undefined') {
return window.app.canvas.ds.scale || 1;
}
// Method 2: Check for LiteGraph canvas zoom
if (window.LiteGraph && window.LiteGraph.LGraphCanvas && window.LiteGraph.LGraphCanvas.active_canvas) {
const canvas = window.LiteGraph.LGraphCanvas.active_canvas;
return canvas.ds?.scale || 1;
}
// Method 3: Check node graph zoom
if (this.node && this.node.graph && this.node.graph.canvas) {
return this.node.graph.canvas.ds?.scale || 1;
}
// Method 4: Check for ComfyUI main canvas element and its transform
const comfyCanvas = document.querySelector('.litegraph canvas');
if (comfyCanvas) {
const style = window.getComputedStyle(comfyCanvas);
const transform = style.transform;
if (transform && transform !== 'none') {
const match = transform.match(/scale\(([^)]+)\)/);
if (match) {
return parseFloat(match[1]) || 1;
}
}
}
return 1; // Default to no zoom if we can't detect it
} catch (error) {
console.warn('Failed to get ComfyUI zoom level:', error);
return 1;
}
}
getComfyUITransform() {
// Get the full transformation matrix including zoom and pan offset
try {
// Method 1: Check for app canvas transform
if (window.app && window.app.canvas && window.app.canvas.ds) {
const ds = window.app.canvas.ds;
return {
scale: ds.scale || 1,
offset: { x: ds.offset?.[0] || 0, y: ds.offset?.[1] || 0 }
};
}
// Method 2: Check for LiteGraph canvas transform
if (window.LiteGraph && window.LiteGraph.LGraphCanvas && window.LiteGraph.LGraphCanvas.active_canvas) {
const canvas = window.LiteGraph.LGraphCanvas.active_canvas;
const ds = canvas.ds;
if (ds) {
return {
scale: ds.scale || 1,
offset: { x: ds.offset?.[0] || 0, y: ds.offset?.[1] || 0 }
};
}
}
// Method 3: Check node graph transform
if (this.node && this.node.graph && this.node.graph.canvas && this.node.graph.canvas.ds) {
const ds = this.node.graph.canvas.ds;
return {
scale: ds.scale || 1,
offset: { x: ds.offset?.[0] || 0, y: ds.offset?.[1] || 0 }
};
}
return null; // No transform available
} catch (error) {
console.warn('Failed to get ComfyUI transform:', error);
return null;
}
}
getCanvasPixelScale() {
const dpr = window.devicePixelRatio || 1;
const zoom = Math.max(0.1, Math.min(4, this.getComfyUIZoomLevel() || 1));
return Math.max(1, Math.min(4, dpr * zoom));
}
getCanvasCssWidth() {
if (!this.canvas) return 0;
return this.canvas.clientWidth || this.canvas.offsetWidth || 0;
}
getCanvasCssHeight() {
if (!this.canvas) return 0;
return this.canvas.clientHeight || this.canvas.offsetHeight || 0;
}
getCanvasLogicalWidth() {
if (!this.canvas) return 0;
return this.getCanvasCssWidth() || (this.canvas.width / this.getCanvasPixelScale());
}
getCanvasLogicalHeight() {
if (!this.canvas) return 0;
return this.getCanvasCssHeight() || (this.canvas.height / this.getCanvasPixelScale());
}
getPrecisionMode() {
const precisionWidget = this.node.widgets?.find(w => w.name === 'precision_level');
return precisionWidget ? precisionWidget.value : 'miliseconds'; // Default to 'miliseconds'
}
formatTime(seconds) {
const mode = this.getPrecisionMode();
if (mode === 'Samples') {
if (!this.waveformData || !this.waveformData.sampleRate) {
return 'N/A Samples';
}
const sampleIndex = Math.floor(seconds * this.waveformData.sampleRate);
return `${sampleIndex.toLocaleString()} smp`;
}
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
if (mode === 'seconds') {
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
// Default to 'miliseconds'
const ms = Math.floor((seconds % 1) * 1000);
const msString = ms.toString().padStart(3, '0');
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}.${msString}`;
}
formatRegionValue(timeInSeconds) {
const mode = this.getPrecisionMode();
switch (mode) {
case 'Samples':
if (!this.waveformData || !this.waveformData.sampleRate) return '0';
return Math.floor(timeInSeconds * this.waveformData.sampleRate).toString();
case 'seconds':
return Math.round(timeInSeconds).toString(); // Use Math.round for whole seconds
case 'miliseconds':
default:
return timeInSeconds.toFixed(3);
}
}
// Canvas management
resizeCanvas(redraw = true) {
const cssWidth = this.getCanvasCssWidth();
const cssHeight = this.getCanvasCssHeight();
if (!cssWidth || !cssHeight) return false;
const pixelScale = this.getCanvasPixelScale();
const width = Math.max(1, Math.round(cssWidth * pixelScale));
const height = Math.max(1, Math.round(cssHeight * pixelScale));
const changed = this.canvas.width !== width || this.canvas.height !== height;
if (this.canvas.width !== width || this.canvas.height !== height) {
this.canvas.width = width;
this.canvas.height = height;
}
this.ctx.setTransform(pixelScale, 0, 0, pixelScale, 0, 0);
if (redraw) {
this.visualization.redraw();
}
return changed;
}
// Selection management
setSelection(startTime, endTime) {
this.selectedStart = startTime;
this.selectedEnd = endTime;
this.ui.updateSelectionDisplay();
this.visualization.redraw();
}
// Region management
addSelectedRegion() {
if (this.selectedStart !== null && this.selectedEnd !== null) {
const region = {
start: this.selectedStart,
end: this.selectedEnd,
label: `Region ${this.selectedRegions.length + 1}`,
id: Date.now()
};
this.selectedRegions.push(region);
// Sort regions chronologically by start time
this.selectedRegions.sort((a, b) => a.start - b.start);
// Renumber regions to be chronological
this.selectedRegions.forEach((region, index) => {
if (region.label.match(/^Region \d+$/)) {
region.label = `Region ${index + 1}`;
}
});
this.clearSelection();
this.visualization.redraw();
// Update manual regions in the node
this.updateManualRegions();
}
}
clearAllRegions() {
this.selectedRegions = [];
this.selectedRegionIndices = [];
this.highlightedRegionIndex = -1;
// Only clear manual regions, keep auto-detected regions
// (waveformData.regions only contains auto-detected regions after fix above)
this.visualization.redraw();
this.updateManualRegions();
}
deleteSelectedRegion() {
if (this.selectedRegionIndices.length > 0) {
// Delete multiple selected regions (orange) - sort indices in reverse order to avoid index shifting
const sortedIndices = [...this.selectedRegionIndices].sort((a, b) => b - a);
sortedIndices.forEach(index => {
if (index >= 0 && index < this.selectedRegions.length) {
this.selectedRegions.splice(index, 1);
}
});
this.selectedRegionIndices = [];
this.showMessage(`Deleted ${sortedIndices.length} selected regions`);
} else if (this.highlightedRegionIndex >= 0 && this.highlightedRegionIndex < this.selectedRegions.length) {
// Delete single hovered region (green)
this.selectedRegions.splice(this.highlightedRegionIndex, 1);
this.highlightedRegionIndex = -1;
this.showMessage('Deleted hovered region');
}
// Sort regions chronologically by start time (in case they weren't)
this.selectedRegions.sort((a, b) => a.start - b.start);
// Renumber remaining regions chronologically
this.selectedRegions.forEach((region, index) => {
if (region.label.match(/^Region \d+$/)) {
region.label = `Region ${index + 1}`;
}
});
this.visualization.redraw();
this.updateManualRegions();
}
selectRegionAtTime(time) {
// Find which region contains this time
for (let i = 0; i < this.selectedRegions.length; i++) {
const region = this.selectedRegions[i];
if (time >= region.start && time <= region.end) {
// Toggle selection: if already selected, deselect it; otherwise add it
const existingIndex = this.selectedRegionIndices.indexOf(i);
if (existingIndex >= 0) {
// Deselect: remove from array
this.selectedRegionIndices.splice(existingIndex, 1);
this.showMessage(`Region ${i + 1} deselected. ${this.selectedRegionIndices.length} regions selected.`);
} else {
// Select: add to array
this.selectedRegionIndices.push(i);
this.showMessage(`Region ${i + 1} selected. ${this.selectedRegionIndices.length} regions selected.`);
}
this.visualization.redraw();
return i;
}
}
// Clicked empty space - clear all selections
this.selectedRegionIndices = [];
this.visualization.redraw();
return -1;
}
getRegionAtTime(time) {
for (let i = 0; i < this.selectedRegions.length; i++) {
const region = this.selectedRegions[i];
if (time >= region.start && time <= region.end) {
return i;
}
}
return -1;
}
updateManualRegions() {
// Update the manual_regions widget with current selections (multiline format)
const manualRegionsWidget = this.node.widgets.find(w => w.name === 'manual_regions');
if (manualRegionsWidget) {
// Temporarily disable the change listener to prevent infinite loop
manualRegionsWidget._updating = true;
const regionsText = this.selectedRegions
.map(r => `${this.formatRegionValue(r.start)},${this.formatRegionValue(r.end)}`)
.join('\n'); // Use newline separator for multiline widget
manualRegionsWidget.value = regionsText;
manualRegionsWidget._updating = false;
}
// Update labels widget (multiline format)
const labelsWidget = this.node.widgets.find(w => w.name === 'region_labels');
if (labelsWidget) {
// Temporarily disable the change listener to prevent infinite loop
labelsWidget._updating = true;
const labelsText = this.selectedRegions
.map(r => r.label) // Labels don't need precision, but we keep the structure
.join('\n'); // Use newline separator for multiline widget
labelsWidget.value = labelsText;
labelsWidget._updating = false;
}
}
// Parse manual regions text back into selectedRegions array
parseManualRegionsFromText() {
const manualRegionsWidget = this.node.widgets.find(w => w.name === 'manual_regions');
const labelsWidget = this.node.widgets.find(w => w.name === 'region_labels');
if (!manualRegionsWidget || !manualRegionsWidget.value.trim()) {
this.selectedRegions = [];
this.visualization.redraw();
return;
}
const regionsText = manualRegionsWidget.value.trim();
const labelsText = labelsWidget ? labelsWidget.value.trim() : '';
const regionLines = regionsText.split('\n').filter(line => line.trim());
const labelLines = labelsText ? labelsText.split('\n').filter(line => line.trim()) : [];
const newRegions = [];
regionLines.forEach((line, index) => {
const trimmed = line.trim();
if (!trimmed) return;
// Parse start,end format
const parts = trimmed.split(',').map(p => p.trim());
if (parts.length === 2) {
const start = parseFloat(parts[0]);
const end = parseFloat(parts[1]);
if (!isNaN(start) && !isNaN(end) && start < end) {
const label = labelLines[index] || `Region ${index + 1}`;
newRegions.push({
start: start,
end: end,
label: label,
id: Date.now() + index // Unique ID
});
}
}
});
// Sort regions chronologically by start time
newRegions.sort((a, b) => a.start - b.start);
// Update labels to be chronological (Region 1, Region 2, etc.)
newRegions.forEach((region, index) => {
if (region.label.match(/^Region \d+$/)) {
region.label = `Region ${index + 1}`;
}
});
this.selectedRegions = newRegions;
this.visualization.redraw();
// Update the labels widget to reflect any changes
this.updateManualRegions();
}
// Loop marker management
setLoopFromSelection() {
if (this.selectedStart !== null && this.selectedEnd !== null) {
this.loopStart = this.selectedStart;
this.loopEnd = this.selectedEnd;
this.visualization.redraw();
this.showMessage(`Loop set: ${this.formatTime(this.loopStart)} - ${this.formatTime(this.loopEnd)}`);
} else {
this.showMessage('Please select a region first to set loop markers');
}
}
setLoopFromRegion(regionIndex) {
if (regionIndex >= 0 && regionIndex < this.selectedRegions.length) {
const region = this.selectedRegions[regionIndex];
this.loopStart = region.start;
this.loopEnd = region.end;
this.visualization.redraw();
this.showMessage(`Loop set from ${region.label}: ${this.formatTime(this.loopStart)} - ${this.formatTime(this.loopEnd)}`);
}
}
clearLoopMarkers() {
this.loopStart = null;
this.loopEnd = null;
this.isLooping = false;
this.visualization.redraw();
this.showMessage('Loop markers cleared');
}
toggleLooping() {
this.isLooping = !this.isLooping;
this.showMessage(this.isLooping ? 'Looping enabled' : 'Looping disabled');
}
// Audio playback controls
togglePlayback() {
if (this.isPlaying) {
this.pausePlayback();
} else {
this.startPlayback();
}
}
startPlayback() {
if (!this.audioElement) return;
// If looping is enabled and we have loop markers, start from loop start
if (this.isLooping && this.loopStart !== null) {
this.currentTime = this.loopStart;
}
this.audioElement.currentTime = this.currentTime;
this.audioElement.play();
this.isPlaying = true;
this.ui.playButton.textContent = '⏸️ Pause';
// Update playhead position
this.updatePlayhead();
}
pausePlayback() {
if (this.audioElement) {
this.audioElement.pause();
}
this.isPlaying = false;
this.stopPlayheadAnimation(); // Stop animation loop
this.ui.playButton.textContent = '▶️ Play';
}
stopPlayback() {
if (this.audioElement) {
this.audioElement.pause();
this.audioElement.currentTime = 0;
}
this.isPlaying = false;
this.currentTime = 0;
this.stopPlayheadAnimation(); // Stop animation loop
this.visualization.stopAnimation(); // Stop visualization animation loop
this.ui.playButton.textContent = '▶️ Play';
this.ui.updateTimeDisplay();
this.visualization.redraw();
}
updatePlayhead() {
// Double-check both isPlaying and audio element state
if (!this.isPlaying) {
return;
}
if (this.playbackSpeed < 0) {
// Backwards movement: manual time control, silent audio
const deltaTime = Math.abs(this.playbackSpeed) / 60; // Approximate frame rate adjustment
this.currentTime -= deltaTime;
// Clamp to audio bounds and stop at start (don't loop)
if (this.currentTime < 0) {
this.currentTime = 0;
// Don't stop playing - just clamp position
}
// Handle looping in reverse
if (this.isLooping && this.loopStart !== null && this.currentTime <= this.loopStart) {
this.currentTime = this.loopEnd || (this.waveformData ? this.waveformData.duration : 0);
}
} else {
// Normal forward playback
if (this.audioElement) {
if (this.audioElement.ended) {
// Audio ended but we're still "playing" - clamp to end position
this.currentTime = this.waveformData ? this.waveformData.duration : this.audioElement.duration;
// Don't stop playing - just clamp position
} else if (!this.audioElement.paused) {
this.currentTime = this.audioElement.currentTime;
// Check for loop end
if (this.isLooping && this.loopEnd !== null && this.currentTime >= this.loopEnd) {
this.currentTime = this.loopStart || 0;
this.audioElement.currentTime = this.currentTime;
}
}
}
}
this.ui.updateTimeDisplay();
this.visualization.redraw();
// Continue animation if still playing
if (this.isPlaying) {
this.playheadAnimationId = requestAnimationFrame(() => this.updatePlayhead());
}
}
stopPlayheadAnimation() {
if (this.playheadAnimationId) {
cancelAnimationFrame(this.playheadAnimationId);
this.playheadAnimationId = null;
}
}
// Zoom controls
zoomIn() {
this.zoomLevel = Math.min(this.zoomLevel * 2, 100);
this.visualization.redraw();
}
zoomOut() {
this.zoomLevel = Math.max(this.zoomLevel / 2, 0.1);
this.visualization.redraw();
}
resetZoom() {
this.zoomLevel = 1;
this.scrollOffset = 0;
this.amplitudeScale = 0.4; // Reset amplitude scale to default
// Reset speed to 1x
this.playbackSpeed = 1;
if (this.ui.speedSlider) {
this.ui.speedSlider.value = '1';
}
if (this.ui.speedValue) {
this.ui.speedValue.textContent = '1.00x';
}
if (this.audioElement) {
this.audioElement.playbackRate = 1;
if (this.audioElement.volume === 0) {
this.audioElement.volume = 1; // Restore volume if muted from backwards mode
}
}
this.visualization.redraw();
}
// Speed control
setPlaybackSpeed(speed) {
const wasBackwards = this.playbackSpeed < 0;
const isNowBackwards = speed < 0;
if (isNowBackwards) {
// Negative speed: pause audio, backwards playhead movement
this.playbackSpeed = speed;
if (this.audioElement && this.isPlaying) {
this.audioElement.pause(); // Pause audio completely
}
this.showMessage(`Backwards mode: ${Math.abs(speed)}x (silent)`);
} else {
// Positive speed: normal audio playback
this.playbackSpeed = speed;
if (this.audioElement) {
// Always sync position and resume when switching to forward
if (wasBackwards && !isNowBackwards && this.isPlaying) {
this.audioElement.currentTime = this.currentTime; // Sync audio to visual playhead
this.audioElement.playbackRate = speed;
// Force resume playback from current position
this.audioElement.play().catch(e => {
console.log('Audio play failed:', e);
});
} else {
this.audioElement.playbackRate = speed;
}
}
this.showMessage(`Playback speed set to ${speed}x`);
}
}
// Export functionality
exportTiming() {
if (this.selectedRegions.length === 0) {
alert('No regions selected. Please select timing regions first.');
return;
}
const timingData = this.selectedRegions
.map(r => `${this.formatRegionValue(r.start)},${this.formatRegionValue(r.end)}`)
.join('\n');
// Copy to clipboard
navigator.clipboard.writeText(timingData).then(() => {
alert('Timing data copied to clipboard!');
}).catch(() => {
// Fallback: show in alert
alert(`Timing data:\n${timingData}`);
});
}
// Show message
showMessage(message, level = 'info') {
this.ui.showMessage(message, level);
}
// Update visualization with new data
updateVisualization(data) {
this.nodeIntegration.updateVisualization(data);
}
// Handle audio file selection
onAudioFileSelected(filePath) {
this.nodeIntegration.onAudioFileSelected(filePath);
}
// Handle parameter changes
onParametersChanged() {
this.nodeIntegration.onParametersChanged();
}
// Handle audio connection
onAudioConnected() {
this.nodeIntegration.onAudioConnected();
}
// Clear current selection (but don't clear region selection)
clearSelection() {
this.selectedStart = null;
this.selectedEnd = null;
this.dragStart = null;
this.dragEnd = null;
this.isDragging = false;
this.ui.updateSelectionDisplay();
this.visualization.redraw();
}
}