diff options
Diffstat (limited to 'tools/editor/script.js')
| -rw-r--r-- | tools/editor/script.js | 668 |
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 |
