7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 1import { useCallback, useMemo } from 'react'
2import { useCanvas } from '../hooks/useCanvas'
3import { AXIS, FONT, FRAME, formatTick, magma, niceTicks } from '../plot'
4import type { SpectrogramData } from '../types'
6type Props = {
7 spec: SpectrogramData
8 /** dB below the shared maximum shown as black */
9 dbRange: number
10 label: string
11 color: string
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 12 /** time range to outline, e.g. the waveform view */
13 highlight?: [number, number]
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 14}
16const M = { l: 48, r: 12, t: 8, b: 26 }
18/** Draws a dB spectrogram as an image with time and frequency axes. */
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 19export function SpectrogramPlot({ spec, dbRange, label, color, highlight }: Props) {
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 20 // Rasterize once per spectrogram, then scale to the canvas on every draw.
21 const image = useMemo(() => {
22 const { rows, cols, db } = spec
23 const off = document.createElement('canvas')
24 off.width = cols
25 off.height = rows
26 const ictx = off.getContext('2d')
27 if (!ictx) return null
28 const img = ictx.createImageData(cols, rows)
29 const d = img.data
30 for (let r = 0; r < rows; r++) {
31 const yImg = rows - 1 - r // row 0 (0 Hz) at the bottom
32 for (let c = 0; c < cols; c++) {
33 const v = db[r * cols + c]
34 const [R, G, B] = magma(1 + v / dbRange)
35 const o = 4 * (yImg * cols + c)
36 d[o] = R
37 d[o + 1] = G
38 d[o + 2] = B
39 d[o + 3] = 255
40 }
41 }
42 ictx.putImageData(img, 0, 0)
43 return off
44 }, [spec, dbRange])
46 const draw = useCallback(
47 (ctx: CanvasRenderingContext2D, w: number, h: number) => {
48 const pw = w - M.l - M.r
49 const ph = h - M.t - M.b
50 ctx.font = FONT
51 if (image) {
52 ctx.imageSmoothingEnabled = false
53 ctx.drawImage(image, M.l, M.t, pw, ph)
54 }
55 ctx.strokeStyle = FRAME
56 ctx.lineWidth = 1
57 ctx.strokeRect(M.l + 0.5, M.t + 0.5, pw, ph)
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 58 if (highlight) {
59 const x0 = M.l + (Math.max(0, highlight[0]) / spec.durationSec) * pw
60 const x1 = M.l + (Math.min(spec.durationSec, highlight[1]) / spec.durationSec) * pw
61 ctx.strokeStyle = 'rgba(255, 255, 255, 0.85)'
62 ctx.setLineDash([4, 3])
63 ctx.strokeRect(Math.round(x0) + 0.5, M.t + 0.5, Math.max(2, Math.round(x1 - x0)), ph)
64 ctx.setLineDash([])
65 }
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 66
67 ctx.fillStyle = AXIS
68 ctx.textAlign = 'right'
69 ctx.textBaseline = 'middle'
70 for (const v of niceTicks(0, spec.fmax, 4)) {
71 if (v > spec.fmax) continue
72 const y = M.t + ph - (v / spec.fmax) * ph
73 ctx.fillText(formatTick(v), M.l - 5, y)
74 }
75 ctx.textAlign = 'center'
76 ctx.textBaseline = 'top'
77 for (const v of niceTicks(0, spec.durationSec, 6)) {
78 if (v > spec.durationSec) continue
79 const x = M.l + (v / spec.durationSec) * pw
80 ctx.fillText(formatTick(v), x, M.t + ph + 4)
81 }
82 ctx.textBaseline = 'bottom'
83 ctx.fillText('time (s)', M.l + pw / 2, h - 1)
84 ctx.save()
85 ctx.translate(11, M.t + ph / 2)
86 ctx.rotate(-Math.PI / 2)
87 ctx.textBaseline = 'middle'
88 ctx.fillText('Hz', 0, 0)
89 ctx.restore()
91 ctx.fillStyle = color
92 ctx.font = 'bold 12px system-ui, sans-serif'
93 ctx.textAlign = 'left'
94 ctx.textBaseline = 'top'
95 ctx.fillText(label, M.l + 6, M.t + 5)
96 },
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 97 [image, spec, label, color, highlight],
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 98 )
99 const ref = useCanvas(draw)
100 return <canvas ref={ref} className="plot spectrogram-plot" />
101}