1// The passive resonator of the first note's fingering: |Y(f)| of the series
2// loop (window mass + bore input impedance) on a log-log axis, with the
3// fitted modal centre frequencies overlaid and the frequency the jet
4// actually locked to marked. The playable notes are the peaks; where the
5// played line sits relative to the first peak is the intonation.
7import { useCanvas, drawFrame, PLOT_COLORS } from './canvasUtils';
9interface Props {
10 gridF: Float32Array;
11 gridY: Float32Array;
12 modeF: Float32Array;
13 fPlay: number;
14}
16const DB_RANGE = 60;
18export default function AdmittancePlot({ gridF, gridY, modeF, fPlay }: Props) {
19 const ref = useCanvas(
20 (ctx, w, h) => {
21 drawFrame(ctx, w, h);
22 if (gridF.length < 2) return;
23 const padL = 34;
24 const padB = 18;
25 const pw = w - padL - 6;
26 const ph = h - padB - 8;
27 const fLo = Math.max(80, gridF[0]);
28 const fHi = gridF[gridF.length - 1];
29 const fx = (f: number) =>
30 padL + ((Math.log10(f) - Math.log10(fLo)) / (Math.log10(fHi) - Math.log10(fLo))) * pw;
32 ctx.font = '10px system-ui';
33 ctx.textAlign = 'center';
34 for (const f of [100, 200, 500, 1000, 2000, 5000, 10000]) {
35 if (f < fLo || f > fHi) continue;
36 const x = fx(f);
37 ctx.strokeStyle = PLOT_COLORS.grid;
38 ctx.beginPath();
39 ctx.moveTo(x, 8);
40 ctx.lineTo(x, 8 + ph);
41 ctx.stroke();
42 ctx.fillStyle = PLOT_COLORS.axis;
43 ctx.fillText(f >= 1000 ? `${f / 1000}k` : `${f}`, x, h - 5);
44 }
46 let yPk = 0;
47 for (let i = 0; i < gridY.length; i++) yPk = Math.max(yPk, gridY[i]);
48 if (yPk <= 0) return;
50 ctx.textAlign = 'right';
51 for (let db = 0; db >= -DB_RANGE; db -= 20) {
52 const yy = 8 + (-db / DB_RANGE) * ph;
53 ctx.strokeStyle = PLOT_COLORS.grid;
54 ctx.beginPath();
55 ctx.moveTo(padL, yy);
56 ctx.lineTo(padL + pw, yy);
57 ctx.stroke();
58 ctx.fillStyle = PLOT_COLORS.axis;
59 ctx.fillText(`${db}`, padL - 4, yy + 3);
60 }
62 // fitted modes, then the played frequency, then the trace on top
63 ctx.setLineDash([4, 3]);
64 for (let i = 0; i < modeF.length; i++) {
65 const f = modeF[i];
66 if (f < fLo || f > fHi) continue;
67 ctx.strokeStyle = 'rgba(232, 163, 61, 0.45)';
68 ctx.beginPath();
69 ctx.moveTo(fx(f), 8);
70 ctx.lineTo(fx(f), 8 + ph);
71 ctx.stroke();
72 }
73 ctx.setLineDash([]);
74 if (fPlay > fLo && fPlay < fHi) {
75 ctx.strokeStyle = 'rgba(111, 191, 115, 0.9)';
76 ctx.lineWidth = 1.5;
77 ctx.beginPath();
78 ctx.moveTo(fx(fPlay), 8);
79 ctx.lineTo(fx(fPlay), 8 + ph);
80 ctx.stroke();
81 ctx.lineWidth = 1;
82 }
84 ctx.strokeStyle = PLOT_COLORS.trace;
85 ctx.beginPath();
86 let started = false;
87 for (let i = 0; i < gridF.length; i++) {
88 if (gridF[i] < fLo) continue;
89 const x = fx(gridF[i]);
90 const db = 20 * Math.log10(gridY[i] / yPk + 1e-12);
91 const yy = 8 + Math.min(ph, (-db / DB_RANGE) * ph);
92 if (!started) {
93 ctx.moveTo(x, yy);
94 started = true;
95 } else {
96 ctx.lineTo(x, yy);
97 }
98 }
99 ctx.stroke();
101 ctx.textAlign = 'left';
102 ctx.fillStyle = 'rgba(232, 163, 61, 0.9)';
103 ctx.fillText('fitted modes', padL + 4, 18);
104 ctx.fillStyle = 'rgba(111, 191, 115, 0.9)';
105 ctx.fillText('played', padL + 74, 18);
106 },
107 [gridF, gridY, modeF, fPlay],
108 200,
109 );
110 return <canvas ref={ref} />;
111}