File size: 3,744 Bytes
ffc05fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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
}