// Time-frequency view of the render, cropped to the band the bass actually // occupies; per-partial decay is visible as horizontal streaks, and a choked // walking-bass note as a streak that ends where the next note lands. import { stft } from '../../lib/fft'; import { useCanvas, drawFrame, PLOT_COLORS, colormap } from './canvasUtils'; interface Props { y: Float32Array; sampleRate: number; } const F_TOP = 4000; // display band, Hz export default function SpectrogramPlot({ y, sampleRate }: Props) { const ref = useCanvas( (ctx, w, h) => { drawFrame(ctx, w, h); const padL = 34; const padB = 16; const pw = w - padL - 6; const ph = h - padB - 6; const win = 2048; // 23 Hz bins at 48 kHz: enough to separate the low partials const { frames, bins, db, floorDb } = stft(y, win, 512); const nyq = sampleRate / 2; const binsShown = Math.min(bins, Math.ceil((F_TOP / nyq) * bins)); const img = ctx.createImageData(frames, binsShown); for (let f = 0; f < frames; f++) { for (let b = 0; b < binsShown; b++) { const v = db[f * bins + b]; const t = Math.min(1, Math.max(0, (v - floorDb) / -floorDb)); const [r, g, bl] = colormap(t); // flip vertically: low frequency at the bottom const row = binsShown - 1 - b; const idx = (row * frames + f) * 4; img.data[idx] = r; img.data[idx + 1] = g; img.data[idx + 2] = bl; img.data[idx + 3] = 255; } } const off = new OffscreenCanvas(frames, binsShown); const octx = off.getContext('2d')!; octx.putImageData(img, 0, 0); ctx.imageSmoothingEnabled = true; ctx.drawImage(off, padL, 6, pw, ph); // axes ctx.fillStyle = PLOT_COLORS.axis; ctx.font = '10px system-ui'; ctx.textAlign = 'right'; const fTop = (binsShown / bins) * nyq; for (let kf = 0; kf <= fTop; kf += 1000) { const yy = 6 + ph - (kf / fTop) * ph; ctx.fillText(kf >= 1000 ? `${kf / 1000}k` : `${kf}`, padL - 4, yy + 3); } ctx.textAlign = 'center'; const durS = y.length / sampleRate; const tStep = durS > 8 ? 2.0 : durS > 4 ? 1.0 : durS > 1.5 ? 0.5 : 0.1; for (let t = 0; t <= durS + 1e-9; t += tStep) { const x = padL + (t / durS) * pw; ctx.fillText(`${t.toFixed(1)}s`, x, h - 4); } }, [y, sampleRate], 200, ); return ; }