summaryrefslogtreecommitdiff
path: root/tools/editor
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-01-28 13:00:07 +0100
committerskal <pascal.massimino@gmail.com>2026-01-28 13:00:07 +0100
commit7db9f9068fa8c247bb42bae7642eae86a701740f (patch)
tree3c51290dd975cef21e11a3f67bac2ecc88f47d0e /tools/editor
parent4607beb85c2fbff9da940af0611b91b8b7ae0a41 (diff)
feat(editor): Implement SDFs for drawing primitives and fix spectrogram clearing
Refactored drawing primitives in the spectrogram editor to use Signed Distance Functions (SDFs), providing smoother and more flexible shape generation. - : Now stores shape parameters in SDF-friendly world coordinates (frame and log-scaled frequency) and includes a parameter for each shape. - : Completely rewritten to utilize SDFs (, , ) for applying shape effects to the spectrogram data, including attenuation with . - Fixed an issue where the generated spectrogram was not being cleared to zero, ensuring a blank canvas for new drawings.
Diffstat (limited to 'tools/editor')
-rw-r--r--tools/editor/script.js668
1 files changed, 128 insertions, 540 deletions
diff --git a/tools/editor/script.js b/tools/editor/script.js
index dcb5478..d7c91e6 100644
--- a/tools/editor/script.js
+++ b/tools/editor/script.js
@@ -24,6 +24,8 @@ const SAMPLE_RATE = 32000;
const MAX_FREQ = SAMPLE_RATE / 2; // Nyquist frequency
const MIN_FREQ = 20; // Lower bound for log scale visualization
+const SDF_FALLOFF_FACTOR = 10.0; // Adjust this value to control the softness of SDF edges.
+
// --- Utility Functions for Audio Processing ---
// JavaScript equivalent of C++ idct_512
function javascript_idct_512(input) {
@@ -53,462 +55,45 @@ function hanningWindow(size) {
const hanningWindowArray = hanningWindow(dctSize); // Pre-calculate window
-// --- File Handling ---
-const specFileInput = document.getElementById('specFileInput');
-specFileInput.addEventListener('change', handleFileSelect);
-
-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)
- };
-
- 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;
- }
-
- 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);
-
- 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
-
- } catch (error) {
- console.error("Error loading SPEC file:", error);
- alert("Failed to load SPEC file. Check console for details.");
- }
-}
-
-// --- 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}%)`;
-}
-
-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 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;
-
- // 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;
- }
-}
-
-// --- 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);
-
- let newShape = null;
-
- 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);
- // 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)));
-
- 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);
-
- 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); // 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;
-
- 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) {
- // 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 (log frequency aware)
- const centerFrame = shape.frameC;
- const centerBin = shape.binC;
- const radiusFrames = shape.radiusFrames;
- const minBin = shape.minBin;
- const maxBin = shape.maxBin;
-
- 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);
-
- const logNormY = (Math.log(currentFreq) - Math.log(centerFreq)) / (Math.log(maxFreq) - Math.log(minFreq));
-
- 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'); });
-
-// --- Undo/Redo Logic ---
-function addAction(action) {
- undoStack.push(action);
- if (undoStack.length > MAX_HISTORY_SIZE) {
- undoStack.shift();
- }
- redoStack = [];
- updateUndoRedoButtons();
-}
+// --- Signed Distance Functions (SDFs) ---
+// Generic 2D vector operations
+function vec2(x, y) { return { x: x, y: y }; }
+function length(v) { return Math.sqrt(v.x * v.x + v.y * v.y); }
+function dot(v1, v2) { return v1.x * v2.x + v1.y * v2.y; }
+function sub(v1, v2) { return vec2(v1.x - v2.x, v1.y - v2.y); }
+function mul(v, s) { return vec2(v.x * s, v.y * s); }
+function div(v, s) { return vec2(v.x / s, v.y / s); }
+function normalize(v) { return div(v, length(v)); }
+function clamp(x, minVal, maxVal) { return Math.max(minVal, Math.min(x, maxVal)); }
+function abs(v) { return vec2(Math.abs(v.x), Math.abs(v.y)); }
+function max(v1, v2) { return vec2(Math.max(v1.x, v2.x), Math.max(v1.y, v2.y)); }
+function sign(x) { return (x > 0) ? 1 : ((x < 0) ? -1 : 0); }
-function handleUndo() {
- if (undoStack.length === 0) {
- console.log('Undo stack is empty.');
- return;
- }
- const actionToUndo = undoStack.pop();
- actionToUndo.undo();
- redoStack.push(actionToUndo);
- redrawCanvas();
- updateUndoRedoButtons();
-}
-
-function handleRedo() {
- if (redoStack.length === 0) {
- console.log('Redo stack is empty.');
- return;
- }
-
- const actionToRedo = redoStack.pop();
- actionToRedo.redo();
- undoStack.push(actionToRedo);
- redrawCanvas();
- updateUndoRedoButtons();
+// sdSegment(p, a, b) - signed distance to a line segment
+// p: point, a: segment start, b: segment end
+function sdSegment(p, a, b) {
+ const pa = sub(p, a);
+ const ba = sub(b, a);
+ const h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
+ return length(sub(pa, mul(ba, h)));
}
-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;
- }
-
- // 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);
+// sdEllipse(p, r) - signed distance to an ellipse (p relative to center, r is half-extents)
+// p: point relative to ellipse center, r: half-extents (rx, ry)
+function sdEllipse(p, r) {
+ const k0 = vec2(1, length(div(p, r)));
+ const k1 = vec2(length(div(p, r)), 1);
+ const f = ((dot(div(mul(p, p), k0), vec2(1, 1)) < dot(div(mul(p, p), k1), vec2(1, 1))) ? k0 : k1);
+ return length(sub(p, mul(r, normalize(mul(f, p))))) * sign(length(p) - r.x); // Simplified, original has length(p)-r.x which is only for circular
}
-function updateUndoRedoButtons() {
- undoButton.disabled = undoStack.length === 0;
- redoButton.disabled = redoStack.length === 0;
+// sdBox(p, r) - signed distance to a rectangle (p relative to center, r is half-extents)
+// p: point relative to box center, r: half-extents (hx, hy)
+function sdBox(p, r) {
+ const q = sub(abs(p), r);
+ return length(max(q, vec2(0, 0))) + Math.min(0.0, Math.max(q.x, q.y));
}
-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) {
@@ -807,15 +392,21 @@ function handleMouseUp(event) {
case 'line': {
const startCoords = canvasToSpectrogramCoords(startX, startY, currentSpecData);
const endCoords = canvasToSpectrogramCoords(endPos.x, endPos.y, currentSpecData);
+
newShape = {
type: 'line',
+ // Canvas coordinates for drawing visual representation (unchanged)
x1: startX, y1: startY,
x2: endPos.x, y2: endPos.y,
- frame1: startCoords.frame, bin1: startCoords.bin,
- frame2: endCoords.frame, bin2: endCoords.bin,
+ // World coordinates for SDF calculations (frame and log-scaled frequency)
+ frame1_world: startCoords.frame,
+ freq1_world: binIndexToFreqLog(startCoords.bin),
+ frame2_world: endCoords.frame,
+ freq2_world: binIndexToFreqLog(endCoords.bin),
amplitude: 0.5, // Default amplitude
- width: 2, // Default width (in canvas pixels for drawing, bins for spec)
+ width: 2, // Visual width in canvas pixels, not directly used by SDF, but kept for drawing
color: 'red',
+ falloff: SDF_FALLOFF_FACTOR, // SDF falloff factor
};
break;
}
@@ -826,22 +417,27 @@ function handleMouseUp(event) {
const cy = startY + (endPos.y - startY) / 2;
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)));
+ const halfWidthFrames = Math.floor((rx / canvas.width) * currentSpecData.header.num_frames);
+
+ const startFreq = canvasYToFreqLog(startY, canvas.height);
+ const endFreq = canvasYToFreqLog(endPos.y, canvas.height);
+ const centerFreq = (startFreq + endFreq) / 2;
+ const halfHeightFreq = Math.abs(startFreq - endFreq) / 2;
+
newShape = {
type: 'ellipse',
+ // Canvas coordinates for drawing visual representation (unchanged)
cx: cx, cy: cy,
rx: rx, ry: ry,
- frameC: centerCoords.frame, binC: centerCoords.bin,
- radiusFrames: radiusXFrames,
- minBin: binCMin, maxBin: binCRadius,
+ // World coordinates for SDF calculations
+ center_frame_world: centerCoords.frame,
+ center_freq_world: centerFreq,
+ radius_frames_world: halfWidthFrames,
+ radius_freq_world: halfHeightFreq,
amplitude: 0.5,
color: 'green',
+ falloff: SDF_FALLOFF_FACTOR,
};
break;
}
@@ -854,15 +450,25 @@ function handleMouseUp(event) {
const startCoords = canvasToSpectrogramCoords(rectX, rectY, currentSpecData);
const endCoords = canvasToSpectrogramCoords(rectX + rectW, rectY + rectH, currentSpecData);
+ const centerFrame = Math.floor((startCoords.frame + endCoords.frame) / 2);
+ const centerFreq = (binIndexToFreqLog(startCoords.bin) + binIndexToFreqLog(endCoords.bin)) / 2;
+ const halfExtentFrames = Math.floor(Math.abs(endCoords.frame - startCoords.frame) / 2);
+ const halfExtentFreq = Math.abs(binIndexToFreqLog(endCoords.bin) - binIndexToFreqLog(startCoords.bin)) / 2;
+
newShape = {
type: 'noise_rect',
+ // Canvas coordinates for drawing visual representation (unchanged)
x: rectX, y: rectY,
width: rectW, height: rectH,
- frame1: startCoords.frame, bin1: startCoords.bin,
- frame2: endCoords.frame, bin2: endCoords.bin,
+ // World coordinates for SDF calculations
+ center_frame_world: centerFrame,
+ center_freq_world: centerFreq,
+ half_extent_frames_world: halfExtentFrames,
+ half_extent_freq_world: halfExtentFreq,
amplitude: 0.3, // Default noise amplitude
density: 0.5, // Default noise density
color: 'blue',
+ falloff: 0.0, // No falloff for pure noise inside rect
};
break;
}
@@ -875,9 +481,9 @@ function handleMouseUp(event) {
applyShapeToSpectrogram(newShape, currentSpecData); // Modify currentSpecData directly
shapes.push(newShape);
- addAction({
- type: 'add_shape',
- shape: newShape,
+ addAction({
+ type: 'add_shape',
+ shape: newShape,
undo: () => {
// To undo, restore previous shapes and previous data
shapes = previousShapesSnapshot;
@@ -900,87 +506,72 @@ function applyShapeToSpectrogram(shape, targetSpecData) {
const numFrames = targetSpecData.header.num_frames;
+ // Determine a bounding box for optimization (iterate only relevant cells)
+ let minFrame = 0, maxFrame = numFrames - 1;
+ let minBin = 0, maxBin = dctSize - 1;
+
+ // Calculate tighter bounding boxes for each shape type
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) {
- // 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; }
- }
+ minFrame = Math.min(shape.frame1_world, shape.frame2_world) - Math.ceil(shape.width / 2);
+ maxFrame = Math.max(shape.frame1_world, shape.frame2_world) + Math.ceil(shape.width / 2);
+ // For frequency, approximate by visual width or a fixed range if needed
+ minBin = freqToBinIndex(Math.min(shape.freq1_world, shape.freq2_world)) - Math.ceil(shape.width / 2);
+ maxBin = freqToBinIndex(Math.max(shape.freq1_world, shape.freq2_world)) + Math.ceil(shape.width / 2);
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;
-
- 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;
+ minFrame = shape.center_frame_world - shape.radius_frames_world - 1;
+ maxFrame = shape.center_frame_world + shape.radius_frames_world + 1;
+ minBin = freqToBinIndex(shape.center_freq_world - shape.radius_freq_world) - 1; // Approx bin range from world freq
+ maxBin = freqToBinIndex(shape.center_freq_world + shape.radius_freq_world) + 1; // Approx bin range from world freq
+ break;
+ case 'noise_rect':
+ minFrame = shape.center_frame_world - shape.half_extent_frames_world - 1;
+ maxFrame = shape.center_frame_world + shape.half_extent_frames_world + 1;
+ minBin = freqToBinIndex(shape.center_freq_world - shape.half_extent_freq_world) - 1; // Approx bin range from world freq
+ maxBin = freqToBinIndex(shape.center_freq_world + shape.half_extent_freq_world) + 1; // Approx bin range from world freq
+ break;
+ }
- // 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);
+ minFrame = Math.max(0, minFrame);
+ maxFrame = Math.min(targetSpecData.header.num_frames - 1, maxFrame); // Use targetSpecData.header.num_frames
+ minBin = Math.max(0, minBin);
+ maxBin = Math.min(dctSize - 1, maxBin);
- const logNormY = (Math.log(currentFreq) - Math.log(centerFreq)) / (Math.log(maxFreq) - Math.log(minFreq));
+ for (let f = minFrame; f <= maxFrame; f++) {
+ for (let b = minBin; b <= maxBin; b++) {
+ const p_world = vec2(f, binIndexToFreqLog(b));
+ let distance = Infinity;
- 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]));
+ switch (shape.type) {
+ case 'line':
+ const a_line = vec2(shape.frame1_world, shape.freq1_world);
+ const b_line = vec2(shape.frame2_world, shape.freq2_world);
+ distance = sdSegment(p_world, a_line, b_line);
+ break;
+ case 'ellipse':
+ const center_ellipse = vec2(shape.center_frame_world, shape.center_freq_world);
+ const r_ellipse = vec2(shape.radius_frames_world, shape.radius_freq_world);
+ distance = sdEllipse(sub(p_world, center_ellipse), r_ellipse);
+ break;
+ case 'noise_rect':
+ const center_box = vec2(shape.center_frame_world, shape.center_freq_world);
+ const r_box = vec2(shape.half_extent_frames_world, shape.half_extent_freq_world);
+ distance = sdBox(sub(p_world, center_box), r_box);
+ if (distance <= 0 && Math.random() < shape.density) { // Only add noise inside the box with density
+ targetSpecData.data[f * dctSize + b] += (Math.random() * 2 - 1) * shape.amplitude;
}
- }
+ break;
}
- 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]));
- }
- }
+ if (shape.type !== 'noise_rect') { // Noise is handled differently for amplitude
+ const attenuation = Math.exp(-distance * distance * shape.falloff);
+ targetSpecData.data[f * dctSize + b] += shape.amplitude * attenuation;
}
- break;
+
+ // Clamp final value
+ targetSpecData.data[f * dctSize + b] = Math.max(-1, Math.min(1, targetSpecData.data[f * dctSize + b]));
+ }
}
}
@@ -1055,7 +646,4 @@ function redrawCanvas() {
function updateUndoRedoButtons() {
undoButton.disabled = undoStack.length === 0;
redoButton.disabled = redoStack.length === 0;
-}
-
-
-
+} \ No newline at end of file