From a570e4d571ccdd205f140ed294aa182c13d7bc2a Mon Sep 17 00:00:00 2001 From: skal Date: Wed, 28 Jan 2026 12:04:44 +0100 Subject: feat(editor): Add audio playback and redo functionality Enhances the spectrogram editor with audio playback capabilities and a redo button. - : Added 'Listen Original' and 'Listen Generated' buttons, and a 'Redo' button. - : Added styling for the new buttons. - : Implemented JavaScript IDCT, Hanning window, and for audio synthesis. - Connected playback buttons to audio functionality. - Fully implemented logic. --- tools/editor/script.js | 112 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) (limited to 'tools/editor/script.js') diff --git a/tools/editor/script.js b/tools/editor/script.js index 1d6ca18..0926ea4 100644 --- a/tools/editor/script.js +++ b/tools/editor/script.js @@ -16,6 +16,38 @@ 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)(); + +// --- Utility Functions for Audio Processing --- +// JavaScript equivalent of C++ idct_512 +function javascript_idct_512(input) { + const output = new Float32Array(dctSize); + const PI = Math.PI; + const N = dctSize; + + for (let n = 0; n < N; ++n) { + let sum = input[0] / 2.0; + for (let k = 1; k < N; ++k) { + sum += input[k] * Math.cos((PI / N) * k * (n + 0.5)); + } + output[n] = sum * (2.0 / N); + } + return output; +} + +// 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 + // --- File Handling --- const specFileInput = document.getElementById('specFileInput'); specFileInput.addEventListener('change', handleFileSelect); @@ -478,4 +510,82 @@ window.addEventListener('resize', () => { }); // Initial call to set button states -updateUndoRedoButtons(); \ No newline at end of file +updateUndoRedoButtons(); + +// --- Audio Playback --- +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 = 32000; // 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 + + const windowArray = hanningWindow(dctSize); // Generate Hanning window for each frame + + // 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); + const timeDomainFrame = javascript_idct_512(spectralFrame); + + // Apply Hanning window for smooth transitions + for (let i = 0; i < dctSize; i++) { + const globalIndex = frameIndex * dctSize + i; + if (globalIndex < totalAudioSamples) { + audioData[globalIndex] += timeDomainFrame[i] * windowArray[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)`); +} + +// --- Playback Button Event Listeners --- +const listenOriginalButton = document.getElementById('listenOriginalButton'); +const listenGeneratedButton = document.getElementById('listenGeneratedButton'); + +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."); + } +}); + +// --- Utility for sizeof(float) in JS context --- +// This is a workaround since typeof(float) is not directly available. +// Float32Array.BYTES_PER_ELEMENT is used in handleFileSelect. +function sizeof(type) { + if (type === 'float') { + return Float32Array.BYTES_PER_ELEMENT; + } + return 0; +} -- cgit v1.2.3