diff options
| author | skal <pascal.massimino@gmail.com> | 2026-01-28 12:07:26 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-01-28 12:07:26 +0100 |
| commit | 1742f917ef417b9c5edb549a5883a96a6018d3d7 (patch) | |
| tree | e7cd3217a8510c9236cdb199b0191562b144c20a | |
| parent | a570e4d571ccdd205f140ed294aa182c13d7bc2a (diff) | |
feat(editor): Implement logarithmic frequency scale
Enhances spectrogram visualization by mapping frequency bins to a logarithmic Y-axis, providing better perceptual uniformity.
- : Renders frequency data using a logarithmic scale.
- Coordinate mapping utilities (, ): Updated to support logarithmic frequency mapping.
- : Shape creation now uses the logarithmic mapping for accurate placement in frequency space.
| -rw-r--r-- | tools/editor/script.js | 169 |
1 files changed, 123 insertions, 46 deletions
diff --git a/tools/editor/script.js b/tools/editor/script.js index 0926ea4..c764803 100644 --- a/tools/editor/script.js +++ b/tools/editor/script.js @@ -19,6 +19,11 @@ 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 + // --- Utility Functions for Audio Processing --- // JavaScript equivalent of C++ idct_512 function javascript_idct_512(input) { @@ -130,27 +135,24 @@ function drawSpectrogram(specData) { } const numFrames = specData.header.num_frames; - const binHeight = height / dctSize; // Height of each frequency bin const frameWidth = width / numFrames; // Width of each time frame - // 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 - - // Draw each frame's spectral data + // 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; - for (let binIndex = 0; binIndex < dctSize; binIndex++) { + // 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) / maxAbsValue); // Normalized intensity + 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 - (binIndex * binHeight) - binHeight, frameWidth, binHeight); + ctx.fillRect(xPos, height - y - 1, frameWidth, 1); // Draw a 1-pixel height line for each y } } @@ -274,15 +276,22 @@ 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 radiusYBins = Math.floor((ry / canvas.height) * dctSize); + const radiusYFreq = canvasDeltaYToFreqDeltaLog(ry, canvas.height); // Delta in Hz + const centerFreq = canvasYToFreqLog(cy, canvas.height); // Center in Hz + const binCRadius = freqToBinIndexLog(centerFreq + radiusYFreq / 2); // Max bin + const binC = freqToBinIndexLog(centerFreq); // Center bin + const binCMin = freqToBinIndexLog(centerFreq - radiusYFreq / 2); // Min bin newShape = { type: 'ellipse', cx: cx, cy: cy, rx: rx, ry: ry, frameC: centerCoords.frame, binC: centerCoords.bin, - radiusFrames: radiusXFrames, radiusBins: radiusYBins, + radiusFrames: radiusXFrames, + // Store as freq range for easy application to spec data + minBin: binCMin, maxBin: binCRadius, amplitude: 0.5, color: 'green', }; @@ -313,8 +322,8 @@ function handleMouseUp(event) { 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 + 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); @@ -322,13 +331,12 @@ function handleMouseUp(event) { type: 'add_shape', shape: newShape, undo: () => { - // Revert shapes array - shapes = previousShapesSnapshot; - // Revert currentSpecData.data to the snapshot before this action + // To undo, restore previous shapes and previous data + shapes = previousShapesSnapshot; currentSpecData.data = previousDataSnapshot; }, redo: () => { - // Re-apply the shape and update currentSpecData.data + // To redo, add the shape back and apply it to current data shapes.push(newShape); applyShapeToSpectrogram(newShape, currentSpecData); } @@ -338,6 +346,7 @@ function handleMouseUp(event) { updateUndoRedoButtons(); } +// --- Spectrogram Data Manipulation --- function applyShapeToSpectrogram(shape, targetSpecData) { if (!targetSpecData || !targetSpecData.data || targetSpecData.header.num_frames === 0) return; @@ -360,7 +369,6 @@ function applyShapeToSpectrogram(shape, targetSpecData) { 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; @@ -379,9 +387,29 @@ function applyShapeToSpectrogram(shape, targetSpecData) { } break; case 'ellipse': - // Apply ellipse to spectrogram data - // TODO: Implement ellipse algorithm - console.log("Applying ellipse to spectrogram data (TODO)."); + // 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; + const normY = (binIndexToFreqLog(b) - binIndexToFreqLog(centerBin)) / + (binIndexToFreqLog(maxBin) - binIndexToFreqLog(centerBin)); // Normalized freq diff + + if (normX * normX + normY * normY <= 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 @@ -408,14 +436,34 @@ 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 +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'); }); undoButton.addEventListener('click', handleUndo); -redoButton.addEventListener('click', handleRedo); // Redo button listener +redoButton.addEventListener('click', handleRedo); + +listenOriginalButton.addEventListener('click', () => { + if (originalSpecData) { + playSpectrogramData(originalSpecData); + } else { + alert("No original SPEC data loaded."); + } +}); + +listenGeneratedButton.addEventListener('click', () => { + if (currentSpecData) { + // Ensure currentSpecData reflects all shapes before playing + redrawCanvas(); // This updates currentSpecData based on shapes + playSpectrogramData(currentSpecData); + } else { + alert("No generated SPEC data to play."); + } +}); // --- Undo/Redo Logic --- function addAction(action) { @@ -433,7 +481,7 @@ function handleUndo() { return; } const actionToUndo = undoStack.pop(); - actionToUndo.undo(); // Execute the inverse operation stored in the action + actionToUndo.undo(); redoStack.push(actionToUndo); redrawCanvas(); updateUndoRedoButtons(); @@ -446,7 +494,7 @@ function handleRedo() { } const actionToRedo = redoStack.pop(); - actionToRedo.redo(); // Re-apply the action + actionToRedo.redo(); undoStack.push(actionToRedo); redrawCanvas(); updateUndoRedoButtons(); @@ -474,30 +522,59 @@ function redrawCanvas() { function updateUndoRedoButtons() { undoButton.disabled = undoStack.length === 0; - redoButton.disabled = redoStack.length === 0; // Update redo button state + redoButton.disabled = redoStack.length === 0; } -// --- 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; +// --- Utility to map canvas coords to spectrogram bins/frames (LOG SCALE) --- +// Maps a linear frequency bin index to its corresponding frequency in Hz +function binIndexToFreq(binIndex) { + return (binIndex / dctSize) * MAX_FREQ; +} - const frameIndex = Math.floor((canvasX / canvasWidth) * numFrames); - const binIndex = Math.floor(((canvasHeight - canvasY) / canvasHeight) * dctSize); +// Maps a frequency in Hz to its corresponding linear bin index +function freqToBinIndex(freq) { + return Math.floor((freq / MAX_FREQ) * dctSize); +} - return { frame: frameIndex, bin: binIndex }; +// 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 } -function spectrogramToCanvasCoords(frameIndex, binIndex, specData) { - const canvasWidth = canvas.width; - const canvasHeight = canvas.height; - const numFrames = specData.header.num_frames; +// 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); +} - const canvasX = (frameIndex / numFrames) * canvasWidth; - const canvasY = canvasHeight - ((binIndex / dctSize) * canvasHeight); +// 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 +} - return { x: canvasX, y: canvasY }; +// 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) @@ -527,7 +604,7 @@ async function playSpectrogramData(specData) { currentAudioSource = null; } - const sampleRate = 32000; // Fixed sample rate + const sampleRate = SAMPLE_RATE; // Fixed sample rate const numFrames = specData.header.num_frames; const totalAudioSamples = numFrames * dctSize; // Total samples in time domain @@ -588,4 +665,4 @@ function sizeof(type) { return Float32Array.BYTES_PER_ELEMENT; } return 0; -} +}
\ No newline at end of file |
