/ simulated_instruments / recorder
Sign in
simulated_instruments / recorder
recorder / webapp / src / components / plots / BreathPlot.tsx
92 lines · 3.1 KBBlameHistoryRaw
1// The jet inflow envelope over the whole gesture: the mean flow rises with
2// the breath envelope, the oscillation rides on it once the note locks, and
3// the attack chiff is visible as the ragged region before lock. Note onsets
4// are marked with the frequency each note locked to.
6import { useCanvas, drawFrame, PLOT_COLORS } from './canvasUtils';
8interface Props {
9 qPlot: Float32Array;
10 qPlotRate: number;
11 noteOn: Float32Array;
12 fPlay: Float32Array;
13 durationS: number;
16export default function BreathPlot({ qPlot, qPlotRate, noteOn, fPlay, durationS }: Props) {
17 const ref = useCanvas(
18 (ctx, w, h) => {
19 drawFrame(ctx, w, h);
20 const padL = 40;
21 const padB = 16;
22 const pw = w - padL - 8;
23 const ph = h - padB - 12;
24 const top = 10;
26 let qMax = 0;
27 for (let i = 0; i < qPlot.length; i++) qMax = Math.max(qMax, qPlot[i]);
28 if (qMax <= 0) qMax = 1;
30 const tx = (t: number) => padL + (t / durationS) * pw;
32 ctx.font = '10px system-ui';
33 ctx.textAlign = 'center';
34 const tStep = durationS > 4 ? 1.0 : durationS > 1.5 ? 0.5 : 0.1;
35 for (let t = 0; t <= durationS + 1e-9; t += tStep) {
36 const x = tx(t);
37 ctx.strokeStyle = PLOT_COLORS.grid;
38 ctx.beginPath();
39 ctx.moveTo(x, top);
40 ctx.lineTo(x, top + ph);
41 ctx.stroke();
42 ctx.fillStyle = PLOT_COLORS.axis;
43 ctx.fillText(`${t.toFixed(1)}s`, x, h - 4);
44 }
45 ctx.textAlign = 'right';
46 ctx.fillStyle = PLOT_COLORS.axis;
47 ctx.fillText(`${(qMax * 1000).toFixed(2)} L/s`, padL - 4, top + 4);
48 ctx.fillText('0', padL - 4, top + ph);
50 // per-pixel min/max envelope of the decimated jet flow
51 ctx.strokeStyle = PLOT_COLORS.trace;
52 ctx.lineWidth = 1;
53 for (let px = 0; px < pw; px++) {
54 const i0 = Math.floor((px / pw) * durationS * qPlotRate);
55 const i1 = Math.min(qPlot.length, Math.floor(((px + 1) / pw) * durationS * qPlotRate) + 1);
56 if (i0 >= qPlot.length) break;
57 let lo = Infinity;
58 let hi = -Infinity;
59 for (let i = i0; i < i1; i++) {
60 lo = Math.min(lo, qPlot[i]);
61 hi = Math.max(hi, qPlot[i]);
62 }
63 if (lo > hi) continue;
64 const y0 = top + ph - (hi / qMax) * ph * 0.92;
65 const y1 = top + ph - (lo / qMax) * ph * 0.92;
66 ctx.beginPath();
67 ctx.moveTo(padL + px + 0.5, y0);
68 ctx.lineTo(padL + px + 0.5, Math.max(y0 + 0.5, y1));
69 ctx.stroke();
70 }
72 // note onsets with locked frequency
73 ctx.textAlign = 'left';
74 for (let i = 0; i < noteOn.length; i++) {
75 const x = tx(noteOn[i]);
76 ctx.strokeStyle = 'rgba(232, 163, 61, 0.6)';
77 ctx.setLineDash([4, 3]);
78 ctx.beginPath();
79 ctx.moveTo(x, top);
80 ctx.lineTo(x, top + ph);
81 ctx.stroke();
82 ctx.setLineDash([]);
83 ctx.fillStyle = PLOT_COLORS.accent;
84 const f = fPlay[i];
85 ctx.fillText(f > 0 ? `${f.toFixed(0)} Hz` : 'did not speak', x + 3, top + 10);
86 }
87 },
88 [qPlot, qPlotRate, noteOn, fPlay, durationS],
89 150,
90 );
91 return <canvas ref={ref} />;
↑↓ move↵openescclose