File size: 2,636 Bytes
e6a1f55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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<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);

    // 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 (
      <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>
  );
}