| import { useState, useCallback } from 'react' |
|
|
| |
| |
| |
| |
| |
| |
| 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 |
|
|
| |
| const fftSize = 1024 |
| const hopSize = 256 |
| const numFrames = Math.floor((channelData.length - fftSize) / hopSize) |
| const numFreqBins = fftSize / 2 |
|
|
| |
| 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') |
|
|
| |
| ctx.fillStyle = '#2a1f2a' |
| ctx.fillRect(0, 0, width, height) |
|
|
| |
| 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))) |
| } |
|
|
| |
| const frameStep = Math.floor(numFrames / width) |
|
|
| for (let x = 0; x < width; x++) { |
| const frameIdx = x * frameStep |
| const sampleIdx = frameIdx * hopSize |
|
|
| |
| 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] |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| for (let y = 0; y < height; y++) { |
| const freqFrac = (height - y) / height |
| |
| 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)) |
|
|
| |
| 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 |
| } |
|
|