/ simulated_instruments / bass
Sign in
simulated_instruments / bass
bass / webapp / src / components / plots / ModeMap.tsx
101 lines · 3.3 KBBlameHistoryRaw
1// The coupled modes of the first note: frequency against ring time
2// T60 = 6.91 / (zeta om), colored by body-energy fraction. String-dominated
3// modes form the long-ringing partial ladder; body-dominated modes sit low
4// and damped; a mixed pair straddling one frequency is the wolf signature.
6import { useCanvas, drawFrame, PLOT_COLORS } from './canvasUtils';
8interface Props {
9 modeF: Float32Array;
10 modeZeta: Float32Array;
11 modeBodyFrac: Float32Array;
14const F_LO = 30;
15const F_HI = 8000;
17function mixColor(t: number): string {
18 const r = Math.round(77 + t * (232 - 77));
19 const g = Math.round(208 + t * (163 - 208));
20 const b = Math.round(225 + t * (61 - 225));
21 return `rgb(${r}, ${g}, ${b})`;
24export default function ModeMap({ modeF, modeZeta, modeBodyFrac }: Props) {
25 const ref = useCanvas(
26 (ctx, w, h) => {
27 drawFrame(ctx, w, h);
28 const padL = 40;
29 const padB = 20;
30 const pw = w - padL - 10;
31 const ph = h - padB - 14;
32 const top = 12;
33 const n = modeF.length;
34 if (n === 0) return;
36 const t60 = new Float64Array(n);
37 let tMin = Infinity;
38 let tMax = 0;
39 for (let i = 0; i < n; i++) {
40 t60[i] = 6.91 / (Math.max(1e-9, modeZeta[i]) * 2 * Math.PI * modeF[i]);
41 tMin = Math.min(tMin, t60[i]);
42 tMax = Math.max(tMax, t60[i]);
43 }
44 tMin = Math.max(1e-3, tMin / 1.5);
45 tMax = tMax * 1.5;
47 const fx = (f: number) =>
48 padL + ((Math.log10(f) - Math.log10(F_LO)) / (Math.log10(F_HI) - Math.log10(F_LO))) * pw;
49 const ty = (t: number) =>
50 top + ph - ((Math.log10(t) - Math.log10(tMin)) / (Math.log10(tMax) - Math.log10(tMin))) * ph;
52 ctx.font = '10px system-ui';
53 ctx.textAlign = 'center';
54 for (const f of [30, 50, 100, 200, 500, 1000, 2000, 5000]) {
55 const x = fx(f);
56 ctx.strokeStyle = PLOT_COLORS.grid;
57 ctx.beginPath();
58 ctx.moveTo(x, top);
59 ctx.lineTo(x, top + ph);
60 ctx.stroke();
61 ctx.fillStyle = PLOT_COLORS.axis;
62 ctx.fillText(f >= 1000 ? `${f / 1000}k` : `${f}`, x, h - 7);
63 }
64 ctx.textAlign = 'right';
65 for (let e = Math.ceil(Math.log10(tMin)); e <= Math.floor(Math.log10(tMax)); e++) {
66 const t = 10 ** e;
67 const y = ty(t);
68 ctx.strokeStyle = PLOT_COLORS.grid;
69 ctx.beginPath();
70 ctx.moveTo(padL, y);
71 ctx.lineTo(padL + pw, y);
72 ctx.stroke();
73 ctx.fillStyle = PLOT_COLORS.axis;
74 ctx.fillText(t >= 1 ? `${t}s` : `${t * 1000}ms`, padL - 4, y + 3);
75 }
77 for (let i = 0; i < n; i++) {
78 const x = fx(Math.min(F_HI, Math.max(F_LO, modeF[i])));
79 const y = ty(t60[i]);
80 const bf = Math.min(1, Math.max(0, modeBodyFrac[i]));
81 ctx.fillStyle = mixColor(bf);
82 ctx.globalAlpha = 0.85;
83 ctx.beginPath();
84 ctx.arc(x, y, 2.5 + 2.5 * bf, 0, 2 * Math.PI);
85 ctx.fill();
86 ctx.globalAlpha = 1;
87 }
89 ctx.textAlign = 'left';
90 ctx.fillStyle = mixColor(0);
91 ctx.fillText('string-dominated', padL + 6, top + 2);
92 ctx.fillStyle = mixColor(1);
93 ctx.fillText('body-dominated', padL + 96, top + 2);
94 ctx.fillStyle = PLOT_COLORS.axis;
95 ctx.fillText(`· ${n} coupled modes of the first note · ring time T60`, padL + 182, top + 2);
96 },
97 [modeF, modeZeta, modeBodyFrac],
98 210,
99 );
100 return <canvas ref={ref} />;
↑↓ move↵openescclose