aa03146Maraca shake simulator: MATLAB model and interactive web appmagland 1// Magnitude spectrum of the rendered shake, log-frequency axis, with the
2// retained shell modes and the breathing mode overlaid. Unlike a struck bar,
3// the spectrum here is broadband: thousands of short Hertz contacts fill the
4// space between the modes, and the modal peaks sit on top of that floor.
6import { magnitudeSpectrum } from '../../lib/fft';
7import { useCanvas, drawFrame, PLOT_COLORS } from './canvasUtils';
9interface Props {
10 y: Float32Array;
11 sampleRate: number;
12 modeF: Float32Array;
13 breathingHz: number;
14}
16const F_LO = 100;
17const F_HI = 24000;
18const DB_RANGE = 80;
20export default function SpectrumPlot({ y, sampleRate, modeF, breathingHz }: 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.strokeStyle = PLOT_COLORS.accent;
62 ctx.setLineDash([4, 3]);
63 for (let i = 0; i < modeF.length; i++) {
64 const f = modeF[i];
65 if (f < F_LO || f > F_HI) continue;
66 ctx.beginPath();
67 ctx.moveTo(fx(f), 8);
68 ctx.lineTo(fx(f), 8 + ph);
69 ctx.stroke();
70 }
71 if (breathingHz >= F_LO && breathingHz <= F_HI) {
72 ctx.strokeStyle = PLOT_COLORS.good;
73 ctx.beginPath();
74 ctx.moveTo(fx(breathingHz), 8);
75 ctx.lineTo(fx(breathingHz), 8 + ph);
76 ctx.stroke();
77 ctx.fillStyle = PLOT_COLORS.good;
78 ctx.textAlign = 'left';
79 ctx.fillText(`breathing ${breathingHz.toFixed(0)}`, fx(breathingHz) + 3, 30);
80 }
81 ctx.setLineDash([]);
83 ctx.strokeStyle = PLOT_COLORS.trace;
84 ctx.beginPath();
85 let started = false;
86 for (let px = 0; px <= pw; px++) {
87 const f = 10 ** (Math.log10(F_LO) + (px / pw) * (Math.log10(F_HI) - Math.log10(F_LO)));
88 const bin = Math.min(mag.length - 1, Math.round((f / sampleRate) * size));
89 const fNext =
90 10 ** (Math.log10(F_LO) + ((px + 1) / pw) * (Math.log10(F_HI) - Math.log10(F_LO)));
91 const binNext = Math.min(
92 mag.length - 1,
93 Math.max(bin + 1, Math.round((fNext / sampleRate) * size)),
94 );
95 let m = 0;
96 for (let b = bin; b < binNext; b++) m = Math.max(m, mag[b]);
97 const db = 20 * Math.log10(m / peak + 1e-12);
98 const yy = 8 + Math.min(ph, (-db / DB_RANGE) * ph);
99 if (!started) {
100 ctx.moveTo(padL + px, yy);
101 started = true;
102 } else {
103 ctx.lineTo(padL + px, yy);
104 }
105 }
106 ctx.stroke();
108 ctx.fillStyle = PLOT_COLORS.accent;
109 ctx.textAlign = 'left';
110 ctx.fillText('shell modes', padL + 4, 18);
111 },
112 [y, sampleRate, modeF, breathingHz],
113 200,
114 );
115 return <canvas ref={ref} />;
116}