diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-06 22:53:29 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-06 22:53:29 +0100 |
| commit | 0c98c830b382d66c420524ff395e12164a566dd8 (patch) | |
| tree | dc80376bd0f0a492b7a74037ae84a926af18bf15 /tools/editor/script.js | |
| parent | 64145080cddbc0fe9fec7159e9ffdedca48ae9be (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/script.js')
| -rw-r--r-- | tools/editor/script.js | 650 |
1 files changed, 0 insertions, 650 deletions
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)`); -} |
