summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tools/editor/index.html2
-rw-r--r--tools/editor/script.js415
-rw-r--r--tools/editor/style.css22
3 files changed, 379 insertions, 60 deletions
diff --git a/tools/editor/index.html b/tools/editor/index.html
index 73356bd..914ac93 100644
--- a/tools/editor/index.html
+++ b/tools/editor/index.html
@@ -19,6 +19,8 @@
<button id="lineTool">Line</button>
<button id="ellipseTool">Ellipse</button>
<button id="noiseTool">Noise</button>
+ <button id="undoButton">Undo</button>
+ <button id="redoButton">Redo</button>
<!-- Add more tool controls later -->
</div>
</div>
diff --git a/tools/editor/script.js b/tools/editor/script.js
index 737a9e6..1d6ca18 100644
--- a/tools/editor/script.js
+++ b/tools/editor/script.js
@@ -2,13 +2,20 @@
// It handles file loading (.spec), visualization, tool interaction, and saving.
// --- Global Variables ---
-let currentSpecData = null; // Stores the parsed spectrogram data
+let currentSpecData = null; // Stores the currently displayed/edited spectrogram data
+let originalSpecData = null; // Stores the pristine, initially loaded spectrogram data
let dctSize = 512; // Default DCT size, read from header
let undoStack = [];
let redoStack = [];
const MAX_HISTORY_SIZE = 50;
+let activeTool = null; // 'line', 'ellipse', 'noise', etc.
+let isDrawing = false;
+let startX, startY; // For tracking mouse down position
+
+let shapes = []; // Array to store all drawn shapes (lines, ellipses, etc.)
+
// --- File Handling ---
const specFileInput = document.getElementById('specFileInput');
specFileInput.addEventListener('change', handleFileSelect);
@@ -42,13 +49,15 @@ async function handleFileSelect(event) {
const numBytes = header.num_frames * header.dct_size * Float32Array.BYTES_PER_ELEMENT;
const spectralDataFloat = new Float32Array(buffer, dataStart, header.num_frames * header.dct_size);
- currentSpecData = {
- header: header,
- data: spectralDataFloat
- };
+ 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);
- drawSpectrogram(currentSpecData);
+ redrawCanvas(); // Redraw with new data
} catch (error) {
console.error("Error loading SPEC file:", error);
@@ -60,72 +69,329 @@ async function handleFileSelect(event) {
const canvas = document.getElementById('spectrogramCanvas');
const ctx = canvas.getContext('2d');
-function drawSpectrogram(specData) {
- if (!specData || !specData.data) {
- console.warn("No spectrogram data to draw.");
- return;
- }
+// 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 width = canvas.width = window.innerWidth * 0.7; // Example dimensions
- const height = canvas.height = 400; // Example dimensions
+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;
+ }
+
const numFrames = specData.header.num_frames;
- const binsPerFrame = specData.data.length / numFrames;
+ const binHeight = height / dctSize; // Height of each frequency bin
+ const frameWidth = width / numFrames; // Width of each time frame
- if (numFrames === 0 || binsPerFrame === 0) {
- console.warn("Spectrogram has no frames or invalid data.");
- return;
+ // Find max value for normalization (for better visualization)
+ let maxAbsValue = 0;
+ for (let i = 0; i < specData.data.length; i++) {
+ maxAbsValue = Math.max(maxAbsValue, Math.abs(specData.data[i]));
}
+ if (maxAbsValue === 0) maxAbsValue = 1; // Avoid division by zero
- // Simplified visualization: draw a few lines to represent frames
- const frameWidth = width / numFrames;
- ctx.strokeStyle = '#000000';
- ctx.lineWidth = 1;
- ctx.beginPath();
+ // Draw each frame's spectral data
+ for (let frameIndex = 0; frameIndex < numFrames; frameIndex++) {
+ const frameDataStart = frameIndex * dctSize;
+ const xPos = frameIndex * frameWidth;
+
+ for (let binIndex = 0; binIndex < dctSize; binIndex++) {
+ const value = specData.data[frameDataStart + binIndex];
+ const intensity = Math.min(1, Math.abs(value) / maxAbsValue); // Normalized intensity
+
+ ctx.fillStyle = getColorForIntensity(intensity);
+ ctx.fillRect(xPos, height - (binIndex * binHeight) - binHeight, frameWidth, binHeight);
+ }
+ }
+
+ // 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;
- // Draw a simplified representation of the first frame
- const frameIndex = 0;
- const frameDataStart = frameIndex * dctSize;
- const maxVal = 1.0; // Assume normalization or known range for now
+ 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;
+ }
+}
+
+// --- 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
+
+ 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);
- for (let i = 0; i < dctSize; ++i) {
- const value = specData.data[frameDataStart + i];
- const x = (i / dctSize) * width;
- const y = height - (Math.abs(value) / maxVal) * height * 0.5; // Simplified scaling
+ let newShape = null;
- if (i === 0) {
- ctx.moveTo(x, y);
- } else {
- ctx.lineTo(x, y);
+ 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;
+
+ const centerCoords = canvasToSpectrogramCoords(cx, cy, currentSpecData);
+ const radiusXFrames = Math.floor((rx / canvas.width) * currentSpecData.header.num_frames);
+ const radiusYBins = Math.floor((ry / canvas.height) * dctSize);
+
+ newShape = {
+ type: 'ellipse',
+ cx: cx, cy: cy,
+ rx: rx, ry: ry,
+ frameC: centerCoords.frame, binC: centerCoords.bin,
+ radiusFrames: radiusXFrames, radiusBins: radiusYBins,
+ 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);
+
+ 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;
+ }
+ }
+
+ if (newShape) {
+ // Capture the state *before* applying the new shape for undo
+ const previousDataSnapshot = new Float32Array(currentSpecData.data);
+ const previousShapesSnapshot = shapes.map(s => ({ ...s })); // Deep copy shapes
+
+ applyShapeToSpectrogram(newShape, currentSpecData); // Modify currentSpecData directly
+ shapes.push(newShape);
+ addAction({
+ type: 'add_shape',
+ shape: newShape,
+ undo: () => {
+ // Revert shapes array
+ shapes = previousShapesSnapshot;
+ // Revert currentSpecData.data to the snapshot before this action
+ currentSpecData.data = previousDataSnapshot;
+ },
+ redo: () => {
+ // Re-apply the shape and update currentSpecData.data
+ shapes.push(newShape);
+ applyShapeToSpectrogram(newShape, currentSpecData);
+ }
+ });
}
- ctx.stroke();
+ redrawCanvas(); // Final redraw after action
+ updateUndoRedoButtons();
}
-// --- Tool Interactions ---
+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;
+
+ 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;
+
+ let currentX = x0;
+ let currentY = y0;
+
+ while (true) {
+ if (currentX >= 0 && currentX < numFrames && currentY >= 0 && currentY < dctSize) {
+ const index = currentX * dctSize + currentY;
+ // 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]));
+ }
+ }
+ }
+
+ 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
+ // TODO: Implement ellipse algorithm
+ console.log("Applying ellipse to spectrogram data (TODO).");
+ 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'); // New redo button
-lineToolButton.addEventListener('click', () => console.log('Line tool selected'));
-ellipseToolButton.addEventListener('click', () => console.log('Ellipse tool selected'));
-noiseToolButton.addEventListener('click', () => console.log('Noise tool selected'));
+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'); });
undoButton.addEventListener('click', handleUndo);
+redoButton.addEventListener('click', handleRedo); // Redo button listener
// --- Undo/Redo Logic ---
function addAction(action) {
undoStack.push(action);
- // Limit history size
if (undoStack.length > MAX_HISTORY_SIZE) {
- undoStack.shift(); // Remove oldest action
+ undoStack.shift();
}
- redoStack = []; // Clear redo stack on new action
+ redoStack = [];
updateUndoRedoButtons();
}
@@ -134,11 +400,10 @@ function handleUndo() {
console.log('Undo stack is empty.');
return;
}
-
const actionToUndo = undoStack.pop();
- actionToUndo.undo(); // Execute the inverse operation
+ actionToUndo.undo(); // Execute the inverse operation stored in the action
redoStack.push(actionToUndo);
- redrawCanvas(); // Redraw canvas to reflect the undo operation
+ redrawCanvas();
updateUndoRedoButtons();
}
@@ -151,36 +416,66 @@ function handleRedo() {
const actionToRedo = redoStack.pop();
actionToRedo.redo(); // Re-apply the action
undoStack.push(actionToRedo);
- redrawCanvas(); // Redraw canvas to reflect the redo operation
+ redrawCanvas();
updateUndoRedoButtons();
}
function redrawCanvas() {
- // This function needs to be implemented to redraw the entire canvas state
- // based on the current undoStack. For now, it's a placeholder.
console.log('Redrawing canvas...');
- if (currentSpecData) {
- drawSpectrogram(currentSpecData);
- } else {
- // Clear canvas if no data is loaded
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- ctx.fillStyle = '#ffffff';
- ctx.fillRect(0, 0, canvas.width, canvas.height);
+ if (!originalSpecData) {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = '#ffffff';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ return;
}
+
+ // 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);
+ });
+
+ drawSpectrogram(currentSpecData);
}
function updateUndoRedoButtons() {
- // Enable/disable buttons based on stack emptiness
undoButton.disabled = undoStack.length === 0;
- // redoButton.disabled = redoStack.length === 0; // If redo button exists
+ redoButton.disabled = redoStack.length === 0; // Update redo button state
+}
+
+// --- Utility to map canvas coords to spectrogram bins/frames ---
+function canvasToSpectrogramCoords(canvasX, canvasY, specData) {
+ const canvasWidth = canvas.width;
+ const canvasHeight = canvas.height;
+ const numFrames = specData.header.num_frames;
+
+ const frameIndex = Math.floor((canvasX / canvasWidth) * numFrames);
+ const binIndex = Math.floor(((canvasHeight - canvasY) / canvasHeight) * dctSize);
+
+ return { frame: frameIndex, bin: binIndex };
+}
+
+function spectrogramToCanvasCoords(frameIndex, binIndex, specData) {
+ const canvasWidth = canvas.width;
+ const canvasHeight = canvas.height;
+ const numFrames = specData.header.num_frames;
+
+ const canvasX = (frameIndex / numFrames) * canvasWidth;
+ const canvasY = canvasHeight - ((binIndex / dctSize) * canvasHeight);
+
+ return { x: canvasX, y: canvasY };
}
// Initial setup for canvas size (can be updated on window resize)
window.addEventListener('resize', () => {
- if (currentSpecData) {
- drawSpectrogram(currentSpecData);
+ if (originalSpecData) {
+ canvas.width = window.innerWidth * 0.7;
+ canvas.height = 400; // Fixed height
+ redrawCanvas();
}
});
-// Initial call to set button states and potentially draw initial state if any is loaded
+// Initial call to set button states
updateUndoRedoButtons(); \ No newline at end of file
diff --git a/tools/editor/style.css b/tools/editor/style.css
index f610969..c02eb4a 100644
--- a/tools/editor/style.css
+++ b/tools/editor/style.css
@@ -33,3 +33,25 @@ h1, h2 {
padding: 10px;
cursor: pointer;
}
+
+#undoButton {
+ background-color: #d9534f;
+ color: white;
+ border: none;
+ border-radius: 4px;
+}
+
+#undoButton:hover {
+ background-color: #c9302c;
+}
+
+#redoButton {
+ background-color: #5cb85c;
+ color: white;
+ border: none;
+ border-radius: 4px;
+}
+
+#redoButton:hover {
+ background-color: #4cae4c;
+}