Download frontend/src/hooks/useSpectrogram.js from mg643/chirp: direct link, hf CLI and curl.
- Browser
- Download file 3.74 kB
-
https://huggingface.co/spaces/mg643/chirp/resolve/main/frontend/src/hooks/useSpectrogram.js
- Command line
-
hf download hf://spaces/mg643/chirp/frontend/src/hooks/useSpectrogram.js
-
curl -L -o useSpectrogram.js https://huggingface.co/spaces/mg643/chirp/resolve/main/frontend/src/hooks/useSpectrogram.js
3.74 kB
| import { useState, useCallback } from 'react' | |
| /** | |
| * Hook to generate a spectrogram canvas from an audio File or Blob. | |
| * Uses the Web Audio API — no external dependencies. | |
| * | |
| * @returns {{ spectrogramUrl, generateSpectrogram, isGenerating }} | |
| */ | |
| export function useSpectrogram() { | |
| const [spectrogramUrl, setSpectrogramUrl] = useState(null) | |
| const [isGenerating, setIsGenerating] = useState(false) | |
| const generateSpectrogram = useCallback(async (audioFile) => { | |
| if (!audioFile) return | |
| setIsGenerating(true) | |
| setSpectrogramUrl(null) | |
| try { | |
| const arrayBuffer = await audioFile.arrayBuffer() | |
| const audioCtx = new (window.AudioContext || window.webkitAudioContext)() | |
| const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer) | |
| const channelData = audioBuffer.getChannelData(0) | |
| const sampleRate = audioBuffer.sampleRate | |
| // FFT parameters | |
| const fftSize = 1024 | |
| const hopSize = 256 | |
| const numFrames = Math.floor((channelData.length - fftSize) / hopSize) | |
| const numFreqBins = fftSize / 2 | |
| // Canvas dimensions | |
| const width = Math.min(numFrames, 900) | |
| const height = 220 | |
| const canvas = document.createElement('canvas') | |
| canvas.width = width | |
| canvas.height = height | |
| const ctx = canvas.getContext('2d') | |
| // Draw background | |
| ctx.fillStyle = '#2a1f2a' | |
| ctx.fillRect(0, 0, width, height) | |
| // Hanning window | |
| const window_ = new Float32Array(fftSize) | |
| for (let i = 0; i < fftSize; i++) { | |
| window_[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / (fftSize - 1))) | |
| } | |
| // Compute magnitude spectrum for each frame | |
| const frameStep = Math.floor(numFrames / width) | |
| for (let x = 0; x < width; x++) { | |
| const frameIdx = x * frameStep | |
| const sampleIdx = frameIdx * hopSize | |
| // Extract windowed frame | |
| const frame = new Float32Array(fftSize) | |
| for (let i = 0; i < fftSize; i++) { | |
| const s = sampleIdx + i < channelData.length ? channelData[sampleIdx + i] : 0 | |
| frame[i] = s * window_[i] | |
| } | |
| // Simple DFT magnitude (fast enough for visualization) | |
| const magnitudes = new Float32Array(numFreqBins) | |
| for (let k = 0; k < numFreqBins; k++) { | |
| let re = 0, im = 0 | |
| for (let n = 0; n < fftSize; n++) { | |
| const angle = (2 * Math.PI * k * n) / fftSize | |
| re += frame[n] * Math.cos(angle) | |
| im -= frame[n] * Math.sin(angle) | |
| } | |
| magnitudes[k] = Math.sqrt(re * re + im * im) | |
| } | |
| // Draw column — map freq bins to height, mel-scale approximation | |
| for (let y = 0; y < height; y++) { | |
| const freqFrac = (height - y) / height | |
| // Mel-like: more resolution at low frequencies | |
| const binIdx = Math.floor(Math.pow(freqFrac, 1.8) * numFreqBins) | |
| const mag = magnitudes[Math.min(binIdx, numFreqBins - 1)] | |
| const db = 20 * Math.log10(mag + 1e-6) | |
| const norm = Math.max(0, Math.min(1, (db + 80) / 80)) | |
| // Mauve → gold color ramp | |
| const r = Math.floor(lerp(42, 210, norm)) | |
| const g = Math.floor(lerp(31, 168, norm)) | |
| const b = Math.floor(lerp(42, 76, norm)) | |
| ctx.fillStyle = `rgb(${r},${g},${b})` | |
| ctx.fillRect(x, y, 1, 1) | |
| } | |
| } | |
| await audioCtx.close() | |
| setSpectrogramUrl(canvas.toDataURL('image/png')) | |
| } catch (err) { | |
| console.error('Spectrogram generation failed:', err) | |
| } finally { | |
| setIsGenerating(false) | |
| } | |
| }, []) | |
| return { spectrogramUrl, generateSpectrogram, isGenerating } | |
| } | |
| function lerp(a, b, t) { | |
| return a + (b - a) * t | |
| } | |