0e92d35Pizzicato standup bass: coupled string-body modal model, numbl MATLAB reference, and interactive web appJeremy Magland 1// The bridge admittance the string is loaded by: the modal sum over the body
2// table, |Y(om)| = |sum_j i om a_j^2 / (om_j^2 - om^2 + 2 i zeta_j om_j om)|,
3// plus the resistive residual floor. This is the wolf-hunting plot: the
4// fundamentals of the gesture are overlaid, and a fundamental sitting on a
5// tall peak is a note the body will pull, split, and eat quickly.
7import { midiHz } from '../../params';
8import { useCanvas, drawFrame, PLOT_COLORS } from './canvasUtils';
10interface Props {
11 bodyF: Float32Array;
12 bodyZeta: Float32Array;
13 bodyA: Float32Array;
14 bodyMono: Float32Array;
15 noteMidi: Float32Array;
16 tuningTrim: number;
17 residualY: number;
18}
20const F_LO = 30;
21const F_HI = 2000;
22const Y_LO = 1e-4;
23const Y_HI = 1e-1;
25export default function AdmittancePlot({
26 bodyF, bodyZeta, bodyA, bodyMono, noteMidi, tuningTrim, residualY,
27}: Props) {
28 const ref = useCanvas(
29 (ctx, w, h) => {
30 drawFrame(ctx, w, h);
31 const padL = 44;
32 const padB = 18;
33 const pw = w - padL - 8;
34 const ph = h - padB - 10;
35 const top = 10;
37 const fx = (f: number) =>
38 padL + ((Math.log10(f) - Math.log10(F_LO)) / (Math.log10(F_HI) - Math.log10(F_LO))) * pw;
39 const yy = (v: number) =>
40 top + ph - ((Math.log10(v) - Math.log10(Y_LO)) / (Math.log10(Y_HI) - Math.log10(Y_LO))) * ph;
42 ctx.font = '10px system-ui';
43 ctx.textAlign = 'center';
44 for (const f of [30, 50, 100, 200, 500, 1000, 2000]) {
45 const x = fx(f);
46 ctx.strokeStyle = PLOT_COLORS.grid;
47 ctx.beginPath();
48 ctx.moveTo(x, top);
49 ctx.lineTo(x, top + ph);
50 ctx.stroke();
51 ctx.fillStyle = PLOT_COLORS.axis;
52 ctx.fillText(f >= 1000 ? `${f / 1000}k` : `${f}`, x, h - 5);
53 }
54 ctx.textAlign = 'right';
55 for (let e = -4; e <= -1; e++) {
56 const v = 10 ** e;
57 const y = yy(v);
58 ctx.strokeStyle = PLOT_COLORS.grid;
59 ctx.beginPath();
60 ctx.moveTo(padL, y);
61 ctx.lineTo(padL + pw, y);
62 ctx.stroke();
63 ctx.fillStyle = PLOT_COLORS.axis;
64 ctx.fillText(`1e${e}`, padL - 4, y + 3);
65 }
67 // fundamentals of the gesture (deduplicated)
68 const seen = new Set<number>();
69 ctx.setLineDash([4, 3]);
70 for (let i = 0; i < noteMidi.length; i++) {
71 const m = Math.round(noteMidi[i]);
72 if (seen.has(m)) continue;
73 seen.add(m);
74 const f = midiHz(m) * tuningTrim;
75 if (f < F_LO || f > F_HI) continue;
76 ctx.strokeStyle = 'rgba(111, 191, 115, 0.55)';
77 ctx.beginPath();
78 ctx.moveTo(fx(f), top);
79 ctx.lineTo(fx(f), top + ph);
80 ctx.stroke();
81 }
82 ctx.setLineDash([]);
84 // |Y| trace
85 ctx.strokeStyle = PLOT_COLORS.accent;
86 ctx.beginPath();
87 for (let px = 0; px <= pw; px++) {
88 const f = 10 ** (Math.log10(F_LO) + (px / pw) * (Math.log10(F_HI) - Math.log10(F_LO)));
89 const om = 2 * Math.PI * f;
90 let re = residualY;
91 let im = 0;
92 for (let j = 0; j < bodyF.length; j++) {
93 const omj = 2 * Math.PI * bodyF[j];
94 const a2 = bodyA[j] * bodyA[j];
95 const dr = omj * omj - om * om;
96 const di = 2 * bodyZeta[j] * omj * om;
97 // i om a^2 / (dr + i di)
98 const den = dr * dr + di * di;
99 re += (om * a2 * di) / den;
100 im += (om * a2 * dr) / den;
101 }
102 const mag = Math.max(Y_LO, Math.min(Y_HI, Math.hypot(re, im)));
103 const x = padL + px;
104 const y = yy(mag);
105 if (px === 0) ctx.moveTo(x, y);
106 else ctx.lineTo(x, y);
107 }
108 ctx.stroke();
110 // label the peaks: A0 and the first corpus mode
111 ctx.fillStyle = PLOT_COLORS.axis;
112 ctx.textAlign = 'left';
113 if (bodyF.length > 0 && bodyMono[0] > 0) {
114 ctx.fillText('A0', fx(bodyF[0]) + 3, top + 12);
115 }
116 ctx.fillStyle = PLOT_COLORS.accent;
117 ctx.fillText('|Y| at the bridge (s/kg)', padL + 6, top + 2);
118 ctx.fillStyle = 'rgba(111, 191, 115, 0.9)';
119 ctx.fillText('· note fundamentals', padL + 130, top + 2);
120 },
121 [bodyF, bodyZeta, bodyA, bodyMono, noteMidi, tuningTrim, residualY],
122 190,
123 );
124 return <canvas ref={ref} />;
125}