5623b11Recorder simulator: MATLAB jet-drive model and interactive web appJeremy Magland 1// Magnitude spectrum of the rendered note, log-frequency axis, with the
2// fitted bore resonances and the harmonics of the played fundamental
3// overlaid. A recorder's spectrum is a harmonic comb over a breath-noise
4// floor: the harmonics come from the jet nonlinearity, and only the first
5// one or two sit on bore resonances.
7import { magnitudeSpectrum } from '../../lib/fft';
8import { useCanvas, drawFrame, PLOT_COLORS } from './canvasUtils';
10interface Props {
11 y: Float32Array;
12 sampleRate: number;
13 modeF: Float32Array;
14 fPlay: Float32Array;
15}
17const F_LO = 100;
18const F_HI = 24000;
19const DB_RANGE = 80;
21export default function SpectrumPlot({ y, sampleRate, modeF, fPlay }: Props) {
22 const ref = useCanvas(
23 (ctx, w, h) => {
24 drawFrame(ctx, w, h);
25 const padL = 34;
26 const padB = 18;
27 const pw = w - padL - 6;
28 const ph = h - padB - 8;
29 const fx = (f: number) =>
30 padL + ((Math.log10(f) - Math.log10(F_LO)) / (Math.log10(F_HI) - Math.log10(F_LO))) * pw;
32 ctx.font = '10px system-ui';
33 ctx.textAlign = 'center';
34 for (const f of [100, 200, 500, 1000, 2000, 5000, 10000, 20000]) {
35 const x = fx(f);
36 ctx.strokeStyle = PLOT_COLORS.grid;
37 ctx.beginPath();
38 ctx.moveTo(x, 8);
39 ctx.lineTo(x, 8 + ph);
40 ctx.stroke();
41 ctx.fillStyle = PLOT_COLORS.axis;
42 ctx.fillText(f >= 1000 ? `${f / 1000}k` : `${f}`, x, h - 5);
43 }
44 ctx.textAlign = 'right';
45 for (let db = 0; db >= -DB_RANGE; db -= 20) {
46 const yy = 8 + (-db / DB_RANGE) * ph;
47 ctx.strokeStyle = PLOT_COLORS.grid;
48 ctx.beginPath();
49 ctx.moveTo(padL, yy);
50 ctx.lineTo(padL + pw, yy);
51 ctx.stroke();
52 ctx.fillStyle = PLOT_COLORS.axis;
53 ctx.fillText(`${db}`, padL - 4, yy + 3);
54 }
56 const { mag, size } = magnitudeSpectrum(y, 32768);
57 let peak = 0;
58 for (let i = 1; i < mag.length; i++) peak = Math.max(peak, mag[i]);
59 if (peak <= 0) return;
61 // overlays first so the trace draws on top
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.strokeStyle = 'rgba(232, 163, 61, 0.4)';
67 ctx.beginPath();
68 ctx.moveTo(fx(f), 8);
69 ctx.lineTo(fx(f), 8 + ph);
70 ctx.stroke();
71 }
72 if (fPlay.length > 0 && fPlay[0] > 0) {
73 for (let hnum = 1; hnum * fPlay[0] < F_HI; hnum++) {
74 const f = hnum * fPlay[0];
75 if (f < F_LO) continue;
76 ctx.strokeStyle = 'rgba(111, 191, 115, 0.35)';
77 ctx.beginPath();
78 ctx.moveTo(fx(f), 8);
79 ctx.lineTo(fx(f), 8 + ph);
80 ctx.stroke();
81 }
82 }
83 ctx.setLineDash([]);
85 ctx.strokeStyle = PLOT_COLORS.trace;
86 ctx.beginPath();
87 let started = false;
88 for (let px = 0; px <= pw; px++) {
89 const f = 10 ** (Math.log10(F_LO) + (px / pw) * (Math.log10(F_HI) - Math.log10(F_LO)));
90 const bin = Math.min(mag.length - 1, Math.round((f / sampleRate) * size));
91 const fNext =
92 10 ** (Math.log10(F_LO) + ((px + 1) / pw) * (Math.log10(F_HI) - Math.log10(F_LO)));
93 const binNext = Math.min(
94 mag.length - 1,
95 Math.max(bin + 1, Math.round((fNext / sampleRate) * size)),
96 );
97 let m = 0;
98 for (let b = bin; b < binNext; b++) m = Math.max(m, mag[b]);
99 const db = 20 * Math.log10(m / peak + 1e-12);
100 const yy = 8 + Math.min(ph, (-db / DB_RANGE) * ph);
101 if (!started) {
102 ctx.moveTo(padL + px, yy);
103 started = true;
104 } else {
105 ctx.lineTo(padL + px, yy);
106 }
107 }
108 ctx.stroke();
110 ctx.textAlign = 'left';
111 ctx.fillStyle = 'rgba(232, 163, 61, 0.9)';
112 ctx.fillText('bore resonances', padL + 4, 18);
113 ctx.fillStyle = 'rgba(111, 191, 115, 0.9)';
114 ctx.fillText('harmonics of the played note', padL + 94, 18);
115 },
116 [y, sampleRate, modeF, fPlay],
117 200,
118 );
119 return <canvas ref={ref} />;
120}