/ simulated_instruments / triangle
Sign in
simulated_instruments / triangle
triangle / webapp / src / components / plots / SpectrumPlot.tsx
108 lines · 3.6 KBCodeBlameHistory
96aec9cTriangle strike simulator: MATLAB model and interactive web appJeremy Magland 1// Magnitude spectrum of the rendered strike, log-frequency axis, with the
2// retained rod modes overlaid. A triangle's spectrum is a sparse comb of
3// inharmonic partials with almost nothing between them; the broadband floor
4// near the attack is the direct click standing in for the truncated modes.
6import { magnitudeSpectrum } from '../../lib/fft';
7import { useCanvas, drawFrame, PLOT_COLORS } from './canvasUtils';
9interface Props {
10 y: Float32Array;
11 sampleRate: number;
12 modeF: Float32Array;
13 modeFamily: Float32Array;
16const F_LO = 100;
17const F_HI = 24000;
18const DB_RANGE = 80;
20export default function SpectrumPlot({ y, sampleRate, modeF, modeFamily }: Props) {
21 const ref = useCanvas(
22 (ctx, w, h) => {
23 drawFrame(ctx, w, h);
24 const padL = 34;
25 const padB = 18;
26 const pw = w - padL - 6;
27 const ph = h - padB - 8;
28 const fx = (f: number) =>
29 padL + ((Math.log10(f) - Math.log10(F_LO)) / (Math.log10(F_HI) - Math.log10(F_LO))) * pw;
31 ctx.font = '10px system-ui';
32 ctx.textAlign = 'center';
33 for (const f of [100, 200, 500, 1000, 2000, 5000, 10000, 20000]) {
34 const x = fx(f);
35 ctx.strokeStyle = PLOT_COLORS.grid;
36 ctx.beginPath();
37 ctx.moveTo(x, 8);
38 ctx.lineTo(x, 8 + ph);
39 ctx.stroke();
40 ctx.fillStyle = PLOT_COLORS.axis;
41 ctx.fillText(f >= 1000 ? `${f / 1000}k` : `${f}`, x, h - 5);
42 }
43 ctx.textAlign = 'right';
44 for (let db = 0; db >= -DB_RANGE; db -= 20) {
45 const yy = 8 + (-db / DB_RANGE) * ph;
46 ctx.strokeStyle = PLOT_COLORS.grid;
47 ctx.beginPath();
48 ctx.moveTo(padL, yy);
49 ctx.lineTo(padL + pw, yy);
50 ctx.stroke();
51 ctx.fillStyle = PLOT_COLORS.axis;
52 ctx.fillText(`${db}`, padL - 4, yy + 3);
53 }
55 const { mag, size } = magnitudeSpectrum(y, 32768);
56 let peak = 0;
57 for (let i = 1; i < mag.length; i++) peak = Math.max(peak, mag[i]);
58 if (peak <= 0) return;
60 // overlays first so the trace draws on top
61 ctx.setLineDash([4, 3]);
62 for (let i = 0; i < modeF.length; i++) {
63 const f = modeF[i];
64 if (f < F_LO || f > F_HI) continue;
65 ctx.strokeStyle = modeFamily[i] === 0 ? 'rgba(77, 208, 225, 0.45)' : 'rgba(232, 163, 61, 0.45)';
66 ctx.beginPath();
67 ctx.moveTo(fx(f), 8);
68 ctx.lineTo(fx(f), 8 + ph);
69 ctx.stroke();
70 }
71 ctx.setLineDash([]);
73 ctx.strokeStyle = PLOT_COLORS.trace;
74 ctx.beginPath();
75 let started = false;
76 for (let px = 0; px <= pw; px++) {
77 const f = 10 ** (Math.log10(F_LO) + (px / pw) * (Math.log10(F_HI) - Math.log10(F_LO)));
78 const bin = Math.min(mag.length - 1, Math.round((f / sampleRate) * size));
79 const fNext =
80 10 ** (Math.log10(F_LO) + ((px + 1) / pw) * (Math.log10(F_HI) - Math.log10(F_LO)));
81 const binNext = Math.min(
82 mag.length - 1,
83 Math.max(bin + 1, Math.round((fNext / sampleRate) * size)),
84 );
85 let m = 0;
86 for (let b = bin; b < binNext; b++) m = Math.max(m, mag[b]);
87 const db = 20 * Math.log10(m / peak + 1e-12);
88 const yy = 8 + Math.min(ph, (-db / DB_RANGE) * ph);
89 if (!started) {
90 ctx.moveTo(padL + px, yy);
91 started = true;
92 } else {
93 ctx.lineTo(padL + px, yy);
94 }
95 }
96 ctx.stroke();
98 ctx.textAlign = 'left';
99 ctx.fillStyle = 'rgba(77, 208, 225, 0.9)';
100 ctx.fillText('in-plane modes', padL + 4, 18);
101 ctx.fillStyle = 'rgba(232, 163, 61, 0.9)';
102 ctx.fillText('out-of-plane modes', padL + 84, 18);
103 },
104 [y, sampleRate, modeF, modeFamily],
105 200,
106 );
107 return <canvas ref={ref} />;
↑↓ move↵openescclose