| import { useEffect, useRef } from "react"; |
|
|
| interface Props { |
| |
| data: number[][]; |
| height?: number; |
| } |
|
|
| |
| export default function SpectrogramViewer({ data, height = 160 }: Props) { |
| const ref = useRef<HTMLCanvasElement | null>(null); |
|
|
| useEffect(() => { |
| const cv = ref.current; |
| if (!cv) return; |
| if (!data.length) return; |
| const T = data[0]?.length ?? 0; |
| const F = data.length; |
| if (T === 0) return; |
|
|
| cv.width = T; |
| cv.height = F; |
| const ctx = cv.getContext("2d"); |
| if (!ctx) return; |
| const img = ctx.createImageData(T, F); |
|
|
| |
| const ramp = (v: number): [number, number, number] => { |
| const x = Math.max(0, Math.min(1, v)); |
| |
| const stops: [number, [number, number, number]][] = [ |
| [0.0, [10, 18, 32]], |
| [0.35, [0, 110, 145]], |
| [0.6, [0, 212, 255]], |
| [0.8, [245, 158, 11]], |
| [1.0, [255, 61, 90]], |
| ]; |
| for (let i = 0; i < stops.length - 1; i += 1) { |
| const [a, ca] = stops[i]; |
| const [b, cb] = stops[i + 1]; |
| if (x >= a && x <= b) { |
| const t = (x - a) / Math.max(1e-9, b - a); |
| return [ |
| ca[0] + t * (cb[0] - ca[0]), |
| ca[1] + t * (cb[1] - ca[1]), |
| ca[2] + t * (cb[2] - ca[2]), |
| ]; |
| } |
| } |
| return stops[stops.length - 1][1]; |
| }; |
|
|
| |
| for (let f = 0; f < F; f += 1) { |
| const row = data[F - 1 - f]; |
| for (let t = 0; t < T; t += 1) { |
| const v = row[t] ?? 0; |
| const [r, g, b] = ramp(v); |
| const idx = (f * T + t) * 4; |
| img.data[idx] = r | 0; |
| img.data[idx + 1] = g | 0; |
| img.data[idx + 2] = b | 0; |
| img.data[idx + 3] = 255; |
| } |
| } |
| ctx.putImageData(img, 0, 0); |
| }, [data]); |
|
|
| if (!data.length) { |
| return ( |
| <div className="panel-alt flex items-center justify-center font-mono text-xs text-ink-dim" style={{ height }}> |
| spectrogram will appear after analysis |
| </div> |
| ); |
| } |
|
|
| return ( |
| <div className="panel-alt overflow-hidden" style={{ height }}> |
| <canvas |
| ref={ref} |
| className="block h-full w-full" |
| style={{ imageRendering: "pixelated" }} |
| aria-label="mel spectrogram" |
| /> |
| </div> |
| ); |
| } |
|
|