summaryrefslogtreecommitdiff
path: root/tools/editor
diff options
context:
space:
mode:
Diffstat (limited to 'tools/editor')
-rw-r--r--tools/editor/script.js561
1 files changed, 397 insertions, 164 deletions
diff --git a/tools/editor/script.js b/tools/editor/script.js
index 0c5200b..dcb5478 100644
--- a/tools/editor/script.js
+++ b/tools/editor/script.js
@@ -507,322 +507,555 @@ function updateUndoRedoButtons() {
redoButton.disabled = redoStack.length === 0;
}
-// --- Utility to map canvas coords to spectrogram bins/frames (LOG SCALE) ---
+const hanningWindowArray = hanningWindow(dctSize); // Pre-calculate window
+// --- Utility to map canvas coords to spectrogram bins/frames (LOG SCALE) ---
// Maps a linear frequency bin index to its corresponding frequency in Hz
-
function binIndexToFreq(binIndex) {
-
return (binIndex / dctSize) * MAX_FREQ;
-
}
-
-
// Maps a frequency in Hz to its corresponding linear bin index
-
function freqToBinIndex(freq) {
-
return Math.floor((freq / MAX_FREQ) * dctSize);
-
}
-
-
// Maps a frequency (Hz) to its corresponding log-scaled bin index
-
function freqToBinIndexLog(freq) {
-
if (freq < MIN_FREQ) freq = MIN_FREQ; // Clamp minimum frequency
-
const logMin = Math.log(MIN_FREQ);
-
const logMax = Math.log(MAX_FREQ);
-
const logFreq = Math.log(freq);
-
const normalizedLog = (logFreq - logMin) / (logMax - logMin);
-
return Math.floor(normalizedLog * dctSize);
-
}
-
-
// Maps a log-scaled bin index to its corresponding frequency in Hz
-
function binIndexToFreqLog(binIndex) {
-
const normalizedLog = binIndex / dctSize;
-
const logMin = Math.log(MIN_FREQ);
-
const logMax = Math.log(MAX_FREQ);
-
const logFreq = normalizedLog * (logMax - logMin) + logMin;
-
return Math.exp(logFreq);
-
}
-
-
// Converts a frequency (Hz) to a Y-coordinate on the canvas (log scale)
-
function freqToCanvasYLog(freq, canvasHeight) {
-
if (freq < MIN_FREQ) freq = MIN_FREQ; // Clamp minimum frequency
-
const logMin = Math.log(MIN_FREQ);
-
const logMax = Math.log(MAX_FREQ);
-
const logFreq = Math.log(freq);
-
const normalizedLog = (logFreq - logMin) / (logMax - logMin);
-
return canvasHeight * (1 - normalizedLog); // Y-axis is inverted
-
}
-
-
// Converts a Y-coordinate on the canvas to a frequency (Hz) (log scale)
-
function canvasYToFreqLog(canvasY, canvasHeight) {
-
const normalizedLog = 1 - (canvasY / canvasHeight);
-
const logMin = Math.log(MIN_FREQ);
-
const logMax = Math.log(MAX_FREQ);
-
const logFreq = normalizedLog * (logMax - logMin) + logMin;
-
return Math.exp(logFreq);
-
}
-
-
// Converts canvas Y-coordinate to log-scaled bin index
-
function canvasYToBinIndexLog(canvasY, specData) {
-
const freq = canvasYToFreqLog(canvasY, canvas.height);
-
return freqToBinIndex(freq); // Use linear bin index from calculated log freq
-
}
-
-
// Converts log-scaled bin index to canvas Y-coordinate
-
function binIndexToCanvasYLog(binIndex, specData) {
-
const freq = binIndexToFreq(binIndex);
-
return freqToCanvasYLog(freq, canvas.height);
-
}
-
-
// Helper to get frequency delta from canvas delta (for ellipse radius in freq)
-
function canvasDeltaYToFreqDeltaLog(canvasDeltaY, canvasHeight) {
-
// This is an approximation as delta in log scale is not linear
-
// For small deltas around a center, it can be approximated
-
const centerCanvasY = canvasHeight / 2;
-
const freqAtCenter = canvasYToFreqLog(centerCanvasY, canvasHeight);
-
const freqAtCenterPlusDelta = canvasYToFreqLog(centerCanvasY - canvasDeltaY, canvasHeight);
-
return Math.abs(freqAtCenterPlusDelta - freqAtCenter);
-
}
-
-
// Initial setup for canvas size (can be updated on window resize)
-
window.addEventListener('resize', () => {
-
if (originalSpecData) {
-
canvas.width = window.innerWidth * 0.7;
-
canvas.height = 400; // Fixed height
-
redrawCanvas();
-
}
-
});
-
-
// Initial call to set button states
-
updateUndoRedoButtons();
-
-
-// --- Audio Playback ---
-
-let currentAudioSource = null; // To stop currently playing audio
-
-
-
-async function playSpectrogramData(specData) {
-
- if (!specData || !specData.data || specData.header.num_frames === 0) {
-
- alert("No spectrogram data to play.");
-
- return;
-
+// --- Utility for sizeof(float) in JS context ---
+// This is a workaround since typeof(float) is not directly available.
+// Float32Array.BYTES_PER_ELEMENT is used in handleFileSelect.
+function sizeof(type) {
+ if (type === 'float') {
+ return Float32Array.BYTES_PER_ELEMENT;
}
+ return 0;
+}
+// --- File Handling ---
+const specFileInput = document.getElementById('specFileInput');
+specFileInput.addEventListener('change', handleFileSelect);
- if (currentAudioSource) {
-
- currentAudioSource.stop();
-
- currentAudioSource.disconnect();
-
- currentAudioSource = null;
-
+async function handleFileSelect(event) {
+ const file = event.target.files[0];
+ if (!file) {
+ return;
}
+ try {
+ const buffer = await file.arrayBuffer();
+ const dataView = new DataView(buffer);
+ // Parse SPEC header
+ const header = {
+ magic: String.fromCharCode(...new Uint8Array(buffer.slice(0, 4))),
+ version: dataView.getInt32(4, true),
+ dct_size: dataView.getInt32(8, true),
+ num_frames: dataView.getInt32(12, true)
+ };
- const sampleRate = SAMPLE_RATE; // Fixed sample rate
+ if (header.magic !== "SPEC" || header.version !== 1) {
+ console.error("Invalid SPEC file format.");
+ alert("Invalid SPEC file format. Please load a valid .spec file.");
+ return;
+ }
- const numFrames = specData.header.num_frames;
+ dctSize = header.dct_size;
+ const dataStart = 16;
+ const numBytes = header.num_frames * header.dct_size * Float32Array.BYTES_PER_ELEMENT;
+ const spectralDataFloat = new Float32Array(buffer, dataStart, header.num_frames * header.dct_size);
- const totalAudioSamples = numFrames * dctSize; // Total samples in time domain
+ originalSpecData = { header: header, data: new Float32Array(spectralDataFloat) }; // Store pristine copy
+ currentSpecData = { header: header, data: new Float32Array(spectralDataFloat) }; // Editable copy
+ shapes = []; // Clear shapes on new file load
+ undoStack = []; // Clear undo history
+ redoStack = []; // Clear redo history
+ console.log("Loaded SPEC file:", header);
+ redrawCanvas(); // Redraw with new data
- const audioBuffer = audioContext.createBuffer(1, totalAudioSamples, sampleRate);
+ } catch (error) {
+ console.error("Error loading SPEC file:", error);
+ alert("Failed to load SPEC file. Check console for details.");
+ }
+}
- const audioData = audioBuffer.getChannelData(0); // Mono channel
+// --- Spectrogram Visualization ---
+const canvas = document.getElementById('spectrogramCanvas');
+const ctx = canvas.getContext('2d');
+// Add canvas event listeners
+canvas.addEventListener('mousedown', handleMouseDown);
+canvas.addEventListener('mousemove', handleMouseMove);
+canvas.addEventListener('mouseup', handleMouseUp);
+canvas.addEventListener('mouseout', handleMouseUp); // Treat mouse out as mouse up
+// Function to get a color based on intensity (0 to 1)
+function getColorForIntensity(intensity) {
+ // Example: Blue to white/yellow gradient
+ const h = (1 - intensity) * 240; // Hue from blue (240) to red (0), inverse for intensity
+ const s = 100; // Saturation
+ const l = intensity * 50 + 50; // Lightness from 50 to 100
+ return `hsl(${h}, ${s}%, ${l}%)`;
+}
- const windowArray = hanningWindow(dctSize); // Generate Hanning window for each frame
+function drawSpectrogram(specData) {
+ const width = canvas.width;
+ const height = canvas.height;
+ ctx.clearRect(0, 0, width, height);
+ ctx.fillStyle = '#ffffff';
+ ctx.fillRect(0, 0, width, height);
+ if (!specData || !specData.data || specData.header.num_frames === 0 || specData.data.length === 0) {
+ console.warn("No spectrogram data or invalid header/data to draw.");
+ return;
+ }
- // Convert spectrogram frames (frequency domain) to audio samples (time domain)
+ const numFrames = specData.header.num_frames;
+ const frameWidth = width / numFrames; // Width of each time frame
+ // Draw each frame's spectral data with log frequency scale
for (let frameIndex = 0; frameIndex < numFrames; frameIndex++) {
+ const frameDataStart = frameIndex * dctSize;
+ const xPos = frameIndex * frameWidth;
- const spectralFrame = specData.data.slice(frameIndex * dctSize, (frameIndex + 1) * dctSize);
-
- const timeDomainFrame = javascript_idct_512(spectralFrame);
-
-
-
- // Apply Hanning window for smooth transitions
-
- for (let i = 0; i < dctSize; i++) {
-
- const globalIndex = frameIndex * dctSize + i;
-
- if (globalIndex < totalAudioSamples) {
-
- audioData[globalIndex] += timeDomainFrame[i] * windowArray[i];
-
- }
+ // To draw with log scale, we iterate over canvas y-coordinates
+ // and map them back to frequency bins
+ for (let y = 0; y < height; y++) {
+ const binIndex = canvasYToBinIndexLog(y, specData);
+ if (binIndex < 0 || binIndex >= dctSize) continue; // Out of bounds
+ const value = specData.data[frameDataStart + binIndex];
+ const intensity = Math.min(1, Math.abs(value) / 1.0); // Assuming values are normalized to [-1, 1]
+
+ ctx.fillStyle = getColorForIntensity(intensity);
+ ctx.fillRect(xPos, height - y - 1, frameWidth, 1); // Draw a 1-pixel height line for each y
}
-
}
+ // Draw active shapes on top (previews for current drawing tool)
+ shapes.forEach(shape => {
+ drawShape(shape);
+ });
+}
+function drawShape(shape) {
+ // This draws the final, persistent shape. Preview is drawn in handleMouseMove.
+ ctx.strokeStyle = shape.color || 'red';
+ ctx.lineWidth = shape.width || 2;
+
+ switch (shape.type) {
+ case 'line':
+ ctx.beginPath();
+ ctx.moveTo(shape.x1, shape.y1);
+ ctx.lineTo(shape.x2, shape.y2);
+ ctx.stroke();
+ break;
+ case 'ellipse':
+ ctx.beginPath();
+ ctx.ellipse(shape.cx, shape.cy, shape.rx, shape.ry, 0, 0, 2 * Math.PI);
+ ctx.stroke();
+ break;
+ case 'noise_rect': // Noise is visualized as a rectangle
+ ctx.fillStyle = 'rgba(0, 0, 255, 0.2)';
+ ctx.fillRect(shape.x, shape.y, shape.width, shape.height);
+ ctx.strokeStyle = 'blue';
+ ctx.strokeRect(shape.x, shape.y, shape.width, shape.height);
+ break;
+ }
+}
- currentAudioSource = audioContext.createBufferSource();
-
- currentAudioSource.buffer = audioBuffer;
-
- currentAudioSource.connect(audioContext.destination);
-
- currentAudioSource.start();
+// --- Mouse Event Handlers ---
+function getMousePos(event) {
+ const rect = canvas.getBoundingClientRect();
+ return {
+ x: event.clientX - rect.left,
+ y: event.clientY - rect.top
+ };
+}
+function handleMouseDown(event) {
+ if (!activeTool || !currentSpecData) return;
+ isDrawing = true;
+ const pos = getMousePos(event);
+ startX = pos.x;
+ startY = pos.y;
+}
+function handleMouseMove(event) {
+ if (!isDrawing || !activeTool) return;
+ const pos = getMousePos(event);
+
+ redrawCanvas(); // Clear and redraw persistent state
- console.log(`Playing audio (Sample Rate: ${sampleRate}, Duration: ${audioBuffer.duration.toFixed(2)}s)`);
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)'; // Preview color
+ ctx.lineWidth = 1;
+ ctx.setLineDash([5, 5]); // Dashed line for preview
+ switch (activeTool) {
+ case 'line':
+ ctx.beginPath();
+ ctx.moveTo(startX, startY);
+ ctx.lineTo(pos.x, pos.y);
+ ctx.stroke();
+ break;
+ case 'ellipse':
+ // Draw preview ellipse based on start and current pos (bounding box)
+ const rx = Math.abs(pos.x - startX) / 2;
+ const ry = Math.abs(pos.y - startY) / 2;
+ const cx = startX + (pos.x - startX) / 2;
+ const cy = startY + (pos.y - startY) / 2;
+ if (rx > 0 && ry > 0) {
+ ctx.beginPath();
+ ctx.ellipse(cx, cy, rx, ry, 0, 0, 2 * Math.PI);
+ ctx.stroke();
+ }
+ break;
+ case 'noise':
+ // Draw preview rectangle for noise area
+ const rectX = Math.min(startX, pos.x);
+ const rectY = Math.min(startY, pos.y);
+ const rectW = Math.abs(pos.x - startX);
+ const rectH = Math.abs(pos.y - startY);
+ ctx.strokeRect(rectX, rectY, rectW, rectH);
+ break;
+ }
+ ctx.setLineDash([]); // Reset line dash
}
+function handleMouseUp(event) {
+ if (!isDrawing || !activeTool || !currentSpecData) return;
+ isDrawing = false;
+ const endPos = getMousePos(event);
+ let newShape = null;
-// --- Playback Button Event Listeners ---
+ switch (activeTool) {
+ case 'line': {
+ const startCoords = canvasToSpectrogramCoords(startX, startY, currentSpecData);
+ const endCoords = canvasToSpectrogramCoords(endPos.x, endPos.y, currentSpecData);
+ newShape = {
+ type: 'line',
+ x1: startX, y1: startY,
+ x2: endPos.x, y2: endPos.y,
+ frame1: startCoords.frame, bin1: startCoords.bin,
+ frame2: endCoords.frame, bin2: endCoords.bin,
+ amplitude: 0.5, // Default amplitude
+ width: 2, // Default width (in canvas pixels for drawing, bins for spec)
+ color: 'red',
+ };
+ break;
+ }
+ case 'ellipse': {
+ const rx = Math.abs(endPos.x - startX) / 2;
+ const ry = Math.abs(endPos.y - startY) / 2;
+ const cx = startX + (endPos.x - startX) / 2;
+ const cy = startY + (endPos.y - startY) / 2;
-listenOriginalButton.addEventListener('click', () => {
+ const centerCoords = canvasToSpectrogramCoords(cx, cy, currentSpecData);
+ // Map canvas radii to frequency/time spans. Log scale aware.
+ const radiusXFrames = Math.floor((rx / canvas.width) * currentSpecData.header.num_frames);
+ const radiusYFreq = canvasDeltaYToFreqDeltaLog(ry, canvas.height); // Delta in Hz
+ const centerFreq = canvasYToFreqLog(cy, canvas.height); // Center in Hz
+ const binCRadius = freqToBinIndexLog(centerFreq * Math.pow(2, (radiusY / canvas.height) * Math.log2(MAX_FREQ / MIN_FREQ)));
+ const binCMin = freqToBinIndexLog(centerFreq / Math.pow(2, (radiusY / canvas.height) * Math.log2(MAX_FREQ / MIN_FREQ)));
- if (originalSpecData) {
+ newShape = {
+ type: 'ellipse',
+ cx: cx, cy: cy,
+ rx: rx, ry: ry,
+ frameC: centerCoords.frame, binC: centerCoords.bin,
+ radiusFrames: radiusXFrames,
+ minBin: binCMin, maxBin: binCRadius,
+ amplitude: 0.5,
+ color: 'green',
+ };
+ break;
+ }
+ case 'noise': {
+ const rectX = Math.min(startX, endPos.x);
+ const rectY = Math.min(startY, endPos.y);
+ const rectW = Math.abs(endPos.x - startX);
+ const rectH = Math.abs(endPos.y - startY);
- playSpectrogramData(originalSpecData);
+ const startCoords = canvasToSpectrogramCoords(rectX, rectY, currentSpecData);
+ const endCoords = canvasToSpectrogramCoords(rectX + rectW, rectY + rectH, currentSpecData);
+ newShape = {
+ type: 'noise_rect',
+ x: rectX, y: rectY,
+ width: rectW, height: rectH,
+ frame1: startCoords.frame, bin1: startCoords.bin,
+ frame2: endCoords.frame, bin2: endCoords.bin,
+ amplitude: 0.3, // Default noise amplitude
+ density: 0.5, // Default noise density
+ color: 'blue',
+ };
+ break;
+ }
}
- else {
-
- alert("No original SPEC data loaded.");
+ if (newShape) {
+ // Capture the state *before* applying the new shape for undo
+ const previousDataSnapshot = new Float32Array(currentSpecData.data); // Copy of actual data
+ const previousShapesSnapshot = shapes.map(s => ({ ...s })); // Deep copy shapes array
+ applyShapeToSpectrogram(newShape, currentSpecData); // Modify currentSpecData directly
+ shapes.push(newShape);
+ addAction({
+ type: 'add_shape',
+ shape: newShape,
+ undo: () => {
+ // To undo, restore previous shapes and previous data
+ shapes = previousShapesSnapshot;
+ currentSpecData.data = previousDataSnapshot;
+ },
+ redo: () => {
+ // To redo, add the shape back and apply it to current data
+ shapes.push(newShape);
+ applyShapeToSpectrogram(newShape, currentSpecData);
+ }
+ });
}
+ redrawCanvas(); // Final redraw after action
+ updateUndoRedoButtons();
+}
-});
+// --- Spectrogram Data Manipulation ---
+function applyShapeToSpectrogram(shape, targetSpecData) {
+ if (!targetSpecData || !targetSpecData.data || targetSpecData.header.num_frames === 0) return;
+ const numFrames = targetSpecData.header.num_frames;
+ switch (shape.type) {
+ case 'line':
+ // Bresenham's-like line drawing to apply to spectrogram data
+ let x0 = shape.frame1, y0 = shape.bin1;
+ let x1 = shape.frame2, y1 = shape.bin2;
-listenGeneratedButton.addEventListener('click', () => {
+ const dx = Math.abs(x1 - x0);
+ const dy = Math.abs(y1 - y0);
+ const sx = (x0 < x1) ? 1 : -1;
+ const sy = (y0 < y1) ? 1 : -1;
+ let err = dx - dy;
- if (currentSpecData) {
+ let currentX = x0;
+ let currentY = y0;
- // Ensure currentSpecData reflects all shapes before playing
+ while (true) {
+ if (currentX >= 0 && currentX < numFrames && currentY >= 0 && currentY < dctSize) {
+ // Apply amplitude and width
+ for (let b = -shape.width; b <= shape.width; b++) {
+ const binToAffect = currentY + b;
+ if (binToAffect >= 0 && binToAffect < dctSize) {
+ targetSpecData.data[currentX * dctSize + binToAffect] += shape.amplitude;
+ // Clamp value
+ targetSpecData.data[currentX * dctSize + binToAffect] = Math.max(-1, Math.min(1, targetSpecData.data[currentX * dctSize + binToAffect]));
+ }
+ }
+ }
- redrawCanvas(); // This updates currentSpecData based on shapes
+ if (currentX === x1 && currentY === y1) break;
+ const e2 = 2 * err;
+ if (e2 > -dy) { err -= dy; currentX += sx; }
+ if (e2 < dx) { err += dx; currentY += sy; }
+ }
+ break;
+ case 'ellipse':
+ // Apply ellipse to spectrogram data (log frequency aware)
+ const centerFrame = shape.frameC;
+ const centerBin = shape.binC;
+ const radiusFrames = shape.radiusFrames;
+ const minBin = shape.minBin;
+ const maxBin = shape.maxBin;
- playSpectrogramData(currentSpecData);
+ for (let f = centerFrame - radiusFrames; f <= centerFrame + radiusFrames; f++) {
+ if (f < 0 || f >= numFrames) continue;
+ for (let b = minBin; b <= maxBin; b++) {
+ if (b < 0 || b >= dctSize) continue;
- }
+ // Check if (f, b) is within the ellipse
+ const normX = (f - centerFrame) / radiusFrames;
+ // Calculate relative frequency based on log scale
+ const currentFreq = binIndexToFreqLog(b);
+ const centerFreq = binIndexToFreqLog(centerBin);
+ const minFreq = binIndexToFreqLog(minBin);
+ const maxFreq = binIndexToFreqLog(maxBin);
- else {
+ const logNormY = (Math.log(currentFreq) - Math.log(centerFreq)) / (Math.log(maxFreq) - Math.log(minFreq));
- alert("No generated SPEC data to play.");
+ if (normX * normX + logNormY * logNormY <= 1) {
+ targetSpecData.data[f * dctSize + b] += shape.amplitude;
+ targetSpecData.data[f * dctSize + b] = Math.max(-1, Math.min(1, targetSpecData.data[f * dctSize + b]));
+ }
+ }
+ }
+ break;
+ case 'noise_rect':
+ // Apply noise to a rectangular region
+ const frameStart = Math.max(0, Math.min(numFrames - 1, shape.frame1));
+ const frameEnd = Math.max(0, Math.min(numFrames - 1, shape.frame2));
+ const binStart = Math.max(0, Math.min(dctSize - 1, shape.bin1));
+ const binEnd = Math.max(0, Math.min(dctSize - 1, shape.bin2));
+ for (let f = frameStart; f <= frameEnd; f++) {
+ for (let b = binStart; b <= binEnd; b++) {
+ if (Math.random() < shape.density) {
+ targetSpecData.data[f * dctSize + b] += (Math.random() * 2 - 1) * shape.amplitude; // Random value between -amp and +amp
+ // Clamp value
+ targetSpecData.data[f * dctSize + b] = Math.max(-1, Math.min(1, targetSpecData.data[f * dctSize + b]));
+ }
+ }
+ }
+ break;
}
+}
-});
-
+// --- Tool Interactions (Button Clicks) ---
+const lineToolButton = document.getElementById('lineTool');
+const ellipseToolButton = document.getElementById('ellipseTool');
+const noiseToolButton = document.getElementById('noiseTool');
+const undoButton = document.getElementById('undoButton');
+const redoButton = document.getElementById('redoButton');
+const listenOriginalButton = document.getElementById('listenOriginalButton');
+const listenGeneratedButton = document.getElementById('listenGeneratedButton');
+lineToolButton.addEventListener('click', () => { activeTool = 'line'; console.log('Line tool selected'); });
+ellipseToolButton.addEventListener('click', () => { activeTool = 'ellipse'; console.log('Ellipse tool selected'); });
+noiseToolButton.addEventListener('click', () => { activeTool = 'noise'; console.log('Noise tool selected'); });
-// --- Utility for sizeof(float) in JS context ---
+// --- Undo/Redo Logic ---
+function addAction(action) {
+ undoStack.push(action);
+ if (undoStack.length > MAX_HISTORY_SIZE) {
+ undoStack.shift();
+ }
+ redoStack = [];
+ updateUndoRedoButtons();
+}
-// This is a workaround since typeof(float) is not directly available.
+function handleUndo() {
+ if (undoStack.length === 0) {
+ console.log('Undo stack is empty.');
+ return;
+ }
+ const actionToUndo = undoStack.pop();
+ actionToUndo.undo();
+ redoStack.push(actionToUndo);
+ redrawCanvas();
+ updateUndoRedoButtons();
+}
-// Float32Array.BYTES_PER_ELEMENT is used in handleFileSelect.
+function handleRedo() {
+ if (redoStack.length === 0) {
+ console.log('Redo stack is empty.');
+ return;
+ }
-function sizeof(type) {
+ const actionToRedo = redoStack.pop();
+ actionToRedo.redo();
+ undoStack.push(actionToRedo);
+ redrawCanvas();
+ updateUndoRedoButtons();
+}
- if (type === 'float') {
+function redrawCanvas() {
+ console.log('Redrawing canvas...');
+ if (!originalSpecData) {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = '#ffffff';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ return;
+ }
- return Float32Array.BYTES_PER_ELEMENT;
+ // Start with a fresh copy of the original data
+ currentSpecData.data = new Float32Array(originalSpecData.data);
- }
+ // Replay all shapes from the `shapes` array to `currentSpecData`
+ shapes.forEach(shape => {
+ applyShapeToSpectrogram(shape, currentSpecData);
+ });
- return 0;
+ drawSpectrogram(currentSpecData);
+}
+function updateUndoRedoButtons() {
+ undoButton.disabled = undoStack.length === 0;
+ redoButton.disabled = redoStack.length === 0;
}
+