summaryrefslogtreecommitdiff
path: root/tools/editor
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-06 22:53:29 +0100
committerskal <pascal.massimino@gmail.com>2026-02-06 22:53:29 +0100
commit0c98c830b382d66c420524ff395e12164a566dd8 (patch)
treedc80376bd0f0a492b7a74037ae84a926af18bf15 /tools/editor
parent64145080cddbc0fe9fec7159e9ffdedca48ae9be (diff)
feat(spectral_editor): Add cursor-centered zoom and pan with mouse wheel
Implemented zoom and pan system for the spectral editor: Core Features: - Viewport offset system (viewportOffsetX, viewportOffsetY) for panning - Three wheel interaction modes: * Ctrl/Cmd + wheel: Cursor-centered zoom (both axes) * Shift + wheel: Horizontal pan * Normal wheel: Vertical pan - Zoom range: 0.5-20.0x horizontal, 0.1-5.0x vertical - Zoom factor: 0.9/1.1 per wheel notch (10% change) Technical Implementation: - Calculate data position under cursor before zoom - Apply zoom to pixelsPerFrame and pixelsPerBin - Adjust viewport offsets to keep cursor position stable - Clamp offsets to valid ranges (0 to max content size) - Updated all coordinate conversion functions (screenToSpectrogram, spectrogramToScreen) - Updated playhead rendering with visibility check - Reset viewport offsets on file load Algorithm (cursor-centered zoom): 1. Calculate frame and frequency under cursor: pos = (screen + offset) / scale 2. Apply zoom: scale *= zoomFactor 3. Adjust offset: offset = pos * scale - screen 4. Clamp offset to [0, maxOffset] This matches the zoom behavior of the timeline editor, adapted for 2D spectrogram display. handoff(Claude): Spectral editor zoom implementation complete
Diffstat (limited to 'tools/editor')
-rw-r--r--tools/editor/dct.js168
-rw-r--r--tools/editor/index.html36
-rw-r--r--tools/editor/script.js650
-rw-r--r--tools/editor/sdf.js39
-rw-r--r--tools/editor/style.css79
5 files changed, 0 insertions, 972 deletions
diff --git a/tools/editor/dct.js b/tools/editor/dct.js
deleted file mode 100644
index c081473..0000000
--- a/tools/editor/dct.js
+++ /dev/null
@@ -1,168 +0,0 @@
-const dctSize = 512; // Default DCT size, read from header
-
-// --- Utility Functions for Audio Processing ---
-
-// Hanning window for smooth audio transitions (JavaScript equivalent)
-function hanningWindow(size) {
- const window = new Float32Array(size);
- const PI = Math.PI;
- for (let i = 0; i < size; i++) {
- window[i] = 0.5 * (1 - Math.cos((2 * PI * i) / (size - 1)));
- }
- return window;
-}
-
-const hanningWindowArray = hanningWindow(dctSize); // Pre-calculate window
-
-// ============================================================================
-// FFT-based DCT/IDCT Implementation
-// ============================================================================
-
-// Bit-reversal permutation (in-place)
-function bitReversePermute(real, imag, N) {
- let temp_bits = N;
- let num_bits = 0;
- while (temp_bits > 1) {
- temp_bits >>= 1;
- num_bits++;
- }
-
- for (let i = 0; i < N; i++) {
- let j = 0;
- let temp = i;
- for (let b = 0; b < num_bits; b++) {
- j = (j << 1) | (temp & 1);
- temp >>= 1;
- }
-
- if (j > i) {
- const tmp_real = real[i];
- const tmp_imag = imag[i];
- real[i] = real[j];
- imag[i] = imag[j];
- real[j] = tmp_real;
- imag[j] = tmp_imag;
- }
- }
-}
-
-// In-place radix-2 FFT
-function fftRadix2(real, imag, N, direction) {
- const PI = Math.PI;
-
- for (let stage_size = 2; stage_size <= N; stage_size *= 2) {
- const half_stage = stage_size / 2;
- const angle = direction * 2.0 * PI / stage_size;
-
- let wr = 1.0;
- let wi = 0.0;
- const wr_delta = Math.cos(angle);
- const wi_delta = Math.sin(angle);
-
- for (let k = 0; k < half_stage; k++) {
- for (let group_start = k; group_start < N; group_start += stage_size) {
- const i = group_start;
- const j = group_start + half_stage;
-
- const temp_real = real[j] * wr - imag[j] * wi;
- const temp_imag = real[j] * wi + imag[j] * wr;
-
- real[j] = real[i] - temp_real;
- imag[j] = imag[i] - temp_imag;
- real[i] = real[i] + temp_real;
- imag[i] = imag[i] + temp_imag;
- }
-
- const wr_old = wr;
- wr = wr_old * wr_delta - wi * wi_delta;
- wi = wr_old * wi_delta + wi * wr_delta;
- }
- }
-}
-
-function fftForward(real, imag, N) {
- bitReversePermute(real, imag, N);
- fftRadix2(real, imag, N, +1);
-}
-
-function fftInverse(real, imag, N) {
- bitReversePermute(real, imag, N);
- fftRadix2(real, imag, N, -1);
-
- const scale = 1.0 / N;
- for (let i = 0; i < N; i++) {
- real[i] *= scale;
- imag[i] *= scale;
- }
-}
-
-// DCT-II via FFT using reordering method
-function javascript_dct_fft(input, N) {
- const PI = Math.PI;
-
- const real = new Float32Array(N);
- const imag = new Float32Array(N);
-
- for (let i = 0; i < N / 2; i++) {
- real[i] = input[2 * i];
- real[N - 1 - i] = input[2 * i + 1];
- }
-
- fftForward(real, imag, N);
-
- const output = new Float32Array(N);
- for (let k = 0; k < N; k++) {
- const angle = -PI * k / (2.0 * N);
- const wr = Math.cos(angle);
- const wi = Math.sin(angle);
-
- const dct_value = real[k] * wr - imag[k] * wi;
-
- if (k === 0) {
- output[k] = dct_value * Math.sqrt(1.0 / N);
- } else {
- output[k] = dct_value * Math.sqrt(2.0 / N);
- }
- }
-
- return output;
-}
-
-// IDCT (DCT-III) via FFT using reordering method
-function javascript_idct_fft(input, N) {
- const PI = Math.PI;
-
- const real = new Float32Array(N);
- const imag = new Float32Array(N);
-
- for (let k = 0; k < N; k++) {
- const angle = PI * k / (2.0 * N);
- const wr = Math.cos(angle);
- const wi = Math.sin(angle);
-
- let scaled;
- if (k === 0) {
- scaled = input[k] / Math.sqrt(1.0 / N);
- } else {
- scaled = input[k] / Math.sqrt(2.0 / N) * 2.0;
- }
-
- real[k] = scaled * wr;
- imag[k] = scaled * wi;
- }
-
- fftInverse(real, imag, N);
-
- const output = new Float32Array(N);
- for (let i = 0; i < N / 2; i++) {
- output[2 * i] = real[i];
- output[2 * i + 1] = real[N - 1 - i];
- }
-
- return output;
-}
-
-// Fast O(N log N) IDCT using FFT
-function javascript_idct_512(input) {
- return javascript_idct_fft(input, dctSize);
-}
diff --git a/tools/editor/index.html b/tools/editor/index.html
deleted file mode 100644
index 82a11ce..0000000
--- a/tools/editor/index.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Spectrogram Editor</title>
- <link rel="stylesheet" href="style.css">
-</head>
-<body>
- <h1>Spectrogram Editor</h1>
-
- <input type="file" id="specFileInput" accept=".spec">
- <label for="specFileInput">Load SPEC File</label>
-
- <div id="editorContainer">
- <canvas id="spectrogramCanvas"></canvas>
- <div id="controls">
- <h2>Tools</h2>
- <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>
- <hr>
- <h2>Playback</h2>
- <button id="listenOriginalButton">Listen Original</button>
- <button id="listenGeneratedButton">Listen Generated</button>
- <!-- Add more tool controls later -->
- </div>
- </div>
-
- <script src="sdf.js"></script>
- <script src="dct.js"></script>
- <script src="script.js"></script>
-</body>
-</html>
diff --git a/tools/editor/script.js b/tools/editor/script.js
deleted file mode 100644
index 06c9bef..0000000
--- a/tools/editor/script.js
+++ /dev/null
@@ -1,650 +0,0 @@
-// This is the core JavaScript for the Spectrogram Editor.
-// It handles file loading (.spec), visualization, tool interaction, and saving.
-
-// --- Global Variables ---
-let currentSpecData = null; // Stores the currently displayed/edited spectrogram data
-let originalSpecData = null; // Stores the pristine, initially loaded spectrogram data
-
-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.)
-
-// Web Audio Context
-const audioContext = new (window.AudioContext || window.webkitAudioContext)();
-
-// Audio Constants (should match C++ side)
-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.
-
-// --- Button Element Declarations ---
-const specFileInput = document.getElementById('specFileInput');
-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');
-
-// --- Event Listeners ---
-specFileInput.addEventListener('change', handleFileSelect);
-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);
-listenOriginalButton.addEventListener('click', () => {
- if (originalSpecData) {
- playSpectrogramData(originalSpecData);
- } else {
- alert("No original SPEC data loaded.");
- }
-});
-listenGeneratedButton.addEventListener('click', () => {
- if (currentSpecData) {
- redrawCanvas(); // Ensure currentSpecData reflects all shapes before playing
- playSpectrogramData(currentSpecData);
- } else {
- alert("No generated SPEC data to play.");
- }
-});
-
-
-// --- 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 / 2)) * MAX_FREQ;
-}
-
-// Maps a frequency in Hz to its corresponding linear bin index
-function freqToBinIndex(freq) {
- return Math.floor((freq / MAX_FREQ) * (dctSize / 2));
-}
-
-// 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();
-
-// --- File Handling Functions ---
-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;
- }
-
- if (dctSize != header.dct_size) {
- alert("Invalid dctSize in SPEC file");
- return;
- }
- 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 log_intensity = Math.log(1. + intensity) / Math.log(2.);
- const h = (1 - log_intensity) * 240; // Hue from blue (240) to red (0), inverse for intensity
- const s = 60.; // Saturation
- const l = log_intensity * 60 + 30; // Lightness from 30 to 90
- 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
-
- // debug the mouse position by draw a white square
- ctx.fillStyle = '#fff';
- ctx.fillRect(pos.x - 10, pos.y - 10, 20, 20);
-}
-
-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',
- // Canvas coordinates for drawing visual representation (unchanged)
- x1: startX, y1: startY,
- x2: endPos.x, y2: endPos.y,
- // 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, // Visual width in canvas pixels, not directly used by SDF, but kept for drawing
- color: 'red',
- falloff: SDF_FALLOFF_FACTOR, // SDF falloff factor
- };
- break;
- }
- case 'ellipse': {
- const rx = Math.abs(endPos.x - startX) / 2;
- const ry = Math.abs(endPos.y - startY) / 2;
- const cx = startX + (pos.x - startX) / 2;
- const cy = startY + (pos.y - startY) / 2;
-
- const centerCoords = canvasToSpectrogramCoords(cx, cy, currentSpecData);
- 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,
- // 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;
- }
- 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);
-
- 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,
- // 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;
- }
- }
-
- 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;
-
- // 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':
- 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':
- 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;
- }
-
- 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);
-
- for (let f = minFrame; f <= maxFrame; f++) {
- for (let b = minBin; b <= maxBin; b++) {
- const p_world = vec2(f, binIndexToFreqLog(b));
- let distance = Infinity;
-
- 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;
- }
-
- 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;
- }
-
- // Clamp final value
- targetSpecData.data[f * dctSize + b] = Math.max(-1, Math.min(1, targetSpecData.data[f * dctSize + b]));
- }
- }
-}
-
-// --- Undo/Redo Logic ---
-function addAction(action) {
- undoStack.push(action);
- if (undoStack.length > MAX_HISTORY_SIZE) {
- undoStack.shift();
- }
- redoStack = [];
- updateUndoRedoButtons();
-}
-
-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();
-}
-
-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);
-}
-
-function updateUndoRedoButtons() {
- undoButton.disabled = undoStack.length === 0;
- redoButton.disabled = redoStack.length === 0;
-}
-
-// 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 Functions ---
-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;
- }
-
- if (currentAudioSource) {
- currentAudioSource.stop();
- currentAudioSource.disconnect();
- currentAudioSource = null;
- }
-
- const sampleRate = SAMPLE_RATE; // Fixed sample rate
- const numFrames = specData.header.num_frames;
- const totalAudioSamples = numFrames * dctSize; // Total samples in time domain
-
- const audioBuffer = audioContext.createBuffer(1, totalAudioSamples, sampleRate);
- const audioData = audioBuffer.getChannelData(0); // Mono channel
-
- // Convert spectrogram frames (frequency domain) to audio samples (time domain)
- for (let frameIndex = 0; frameIndex < numFrames; frameIndex++) {
- const spectralFrame = specData.data.slice(frameIndex * dctSize, (frameIndex + 1) * dctSize);
-
- // IDCT (no windowing - window is only for analysis, not synthesis)
- const timeDomainFrame = javascript_idct_512(spectralFrame);
-
- // Apply Hanning window for smooth transitions between frames
- for (let i = 0; i < dctSize; i++) {
- audioData[frameIndex * dctSize + i] = timeDomainFrame[i] * hanningWindowArray[i];
- }
- }
-
- currentAudioSource = audioContext.createBufferSource();
- currentAudioSource.buffer = audioBuffer;
- currentAudioSource.connect(audioContext.destination);
- currentAudioSource.start();
-
- console.log(`Playing audio (Sample Rate: ${sampleRate}, Duration: ${audioBuffer.duration.toFixed(2)}s)`);
-}
diff --git a/tools/editor/sdf.js b/tools/editor/sdf.js
deleted file mode 100644
index c68d79a..0000000
--- a/tools/editor/sdf.js
+++ /dev/null
@@ -1,39 +0,0 @@
-// --- 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); }
-
-// 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)));
-}
-
-// 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
-}
-
-// 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));
-}
-
diff --git a/tools/editor/style.css b/tools/editor/style.css
deleted file mode 100644
index e0014cf..0000000
--- a/tools/editor/style.css
+++ /dev/null
@@ -1,79 +0,0 @@
-body {
- font-family: sans-serif;
- margin: 20px;
- background-color: #f4f4f4;
-}
-
-h1, h2 {
- color: #333;
-}
-
-#editorContainer {
- display: flex;
- margin-top: 20px;
-}
-
-#spectrogramCanvas {
- border: 1px solid #ccc;
- background-color: #fff;
- margin-right: 20px;
-}
-
-#controls {
- border: 1px solid #ccc;
- padding: 15px;
- background-color: #eee;
- min-width: 200px;
-}
-
-#controls button {
- display: block;
- width: 100%;
- margin-bottom: 10px;
- 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;
-}
-
-/* New styles for playback buttons */
-#listenOriginalButton,
-#listenGeneratedButton {
- background-color: #5bc0de;
- color: white;
- border: none;
- border-radius: 4px;
- margin-top: 5px;
-}
-
-#listenOriginalButton:hover,
-#listenGeneratedButton:hover {
- background-color: #31b0d5;
-}
-
-hr {
- border: 0;
- height: 1px;
- background-color: #ccc;
- margin: 20px 0;
-}