import { useEffect, useRef } from "react"; interface Props { /** mel spectrogram values normalised to [0,1], shape [n_mels][T]. */ data: number[][]; height?: number; } /** Canvas mel-spectrogram with a cyan→amber→red colour ramp. */ export default function SpectrogramViewer({ data, height = 160 }: Props) { const ref = useRef(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); // Colour ramp: cyber (cool) → amber → danger (hot) const ramp = (v: number): [number, number, number] => { const x = Math.max(0, Math.min(1, v)); // Three-stop interpolation const stops: [number, [number, number, number]][] = [ [0.0, [10, 18, 32]], // near-black bg [0.35, [0, 110, 145]], // dim cyan [0.6, [0, 212, 255]], // cyan [0.8, [245, 158, 11]], // amber [1.0, [255, 61, 90]], // danger red ]; 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]; }; // mel rows are low → high frequency; canvas y=0 is top so flip vertically. 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 (
spectrogram will appear after analysis
); } return (
); }