0e92d35Pizzicato standup bass: coupled string-body modal model, numbl MATLAB reference, and interactive web appJeremy Magland 1// The gesture as a piano roll: each note at its onset and pitch, colored by
2// the string it is fingered on, with the marker height proportional to the
3// pluck force. For a single note this is one mark; for a walking line the
4// jitter and force spread are what keep it from sounding like a sequencer.
6import { midiName } from '../../params';
7import { useCanvas, drawFrame, PLOT_COLORS } from './canvasUtils';
9interface Props {
10 noteT: Float32Array;
11 noteMidi: Float32Array;
12 noteStr: Float32Array;
13 noteForce: Float32Array;
14 durationS: number;
15}
17const STRING_COLORS = ['#4dd0e1', '#6fbf73', '#e8a33d', '#e57373']; // G D A E
18const STRING_NAMES = ['G', 'D', 'A', 'E'];
20export default function NotesPlot({ noteT, noteMidi, noteStr, noteForce, durationS }: Props) {
21 const ref = useCanvas(
22 (ctx, w, h) => {
23 drawFrame(ctx, w, h);
24 const padL = 40;
25 const padB = 16;
26 const pw = w - padL - 8;
27 const ph = h - padB - 14;
28 const top = 12;
29 const n = noteT.length;
30 if (n === 0) return;
32 let mLo = Infinity, mHi = -Infinity, fMax = 0;
33 for (let i = 0; i < n; i++) {
34 mLo = Math.min(mLo, noteMidi[i]);
35 mHi = Math.max(mHi, noteMidi[i]);
36 fMax = Math.max(fMax, noteForce[i]);
37 }
38 mLo -= 2;
39 mHi += 2;
40 if (fMax <= 0) fMax = 1;
42 const tx = (t: number) => padL + (t / Math.max(1e-6, durationS)) * pw;
43 const my = (m: number) => top + ph - ((m - mLo) / (mHi - mLo)) * ph;
45 ctx.font = '10px system-ui';
46 ctx.textAlign = 'center';
47 const tStep = durationS > 8 ? 2.0 : durationS > 4 ? 1.0 : durationS > 1.5 ? 0.5 : 0.1;
48 for (let t = 0; t <= durationS + 1e-9; t += tStep) {
49 const x = tx(t);
50 ctx.strokeStyle = PLOT_COLORS.grid;
51 ctx.beginPath();
52 ctx.moveTo(x, top);
53 ctx.lineTo(x, top + ph);
54 ctx.stroke();
55 ctx.fillStyle = PLOT_COLORS.axis;
56 ctx.fillText(`${t.toFixed(1)}s`, x, h - 4);
57 }
58 // pitch gridlines on the octave Cs and the open strings
59 ctx.textAlign = 'right';
60 for (const m of [28, 33, 36, 38, 43, 48]) {
61 if (m < mLo || m > mHi) continue;
62 const yy = my(m);
63 ctx.strokeStyle = PLOT_COLORS.grid;
64 ctx.beginPath();
65 ctx.moveTo(padL, yy);
66 ctx.lineTo(padL + pw, yy);
67 ctx.stroke();
68 ctx.fillStyle = PLOT_COLORS.axis;
69 ctx.fillText(midiName(m), padL - 4, yy + 3);
70 }
72 // note marks: a bar from onset toward the next onset, sized by force
73 for (let i = 0; i < n; i++) {
74 const x0 = tx(noteT[i]);
75 const x1 = i + 1 < n ? tx(noteT[i + 1]) - 2 : x0 + Math.min(40, pw * 0.06);
76 const yy = my(noteMidi[i]);
77 const s = Math.round(noteStr[i]) - 1;
78 const th = 2 + 4 * (noteForce[i] / fMax);
79 const bw = Math.max(4, x1 - x0);
80 ctx.fillStyle = STRING_COLORS[Math.min(3, Math.max(0, s))];
81 ctx.globalAlpha = 0.9;
82 ctx.fillRect(x0, yy - th / 2, bw, th);
83 ctx.globalAlpha = 1;
84 if (n <= 16) {
85 ctx.fillStyle = PLOT_COLORS.axis;
86 ctx.textAlign = 'center';
87 ctx.fillText(midiName(noteMidi[i]), x0 + bw / 2, yy - 7);
88 }
89 }
91 ctx.textAlign = 'left';
92 let lx = padL + 6;
93 for (let s = 0; s < 4; s++) {
94 ctx.fillStyle = STRING_COLORS[s];
95 ctx.fillText(`${STRING_NAMES[s]} string`, lx, top + 2);
96 lx += 52;
97 }
98 ctx.fillStyle = PLOT_COLORS.axis;
99 ctx.fillText('· bar height = pluck force', lx + 4, top + 2);
100 },
101 [noteT, noteMidi, noteStr, noteForce, durationS],
102 160,
103 );
104 return <canvas ref={ref} />;
105}