/ tutorials / wave-acoustics
Sign in
tutorials / wave-acoustics
wave-acoustics / src / chapters / ch01 / chapter.tsx
207 lines · 6.2 KBBlameHistoryRaw
1import type { Chapter } from '../../ui/types.ts'
2import type { Params } from '../../engine/types.ts'
3import type { Num } from '../../engine/types.ts'
4import driver from './driver.m?raw'
5import script from './pluck.m?raw'
6import text from './text.md?raw'
7import Listen, { type Clip } from '../../audio/Listen.tsx'
8import { SeriesPlot, fmtDb, fmtErr } from '../common/panels.tsx'
9import { faded, ink, method } from '../../plot/palette.ts'
10import { FS, noteControl, noteHz } from '../shared.ts'
12interface PluckOut {
13 fs: number
14 f0: number
15 nyq: number
16 tf: number[]
17 exact: number[]
18 ts: number[]
19 synth: number[]
20 n: number[]
21 cdb: number[]
22 missing: number
23 fr: number[]
24 db: number[]
25 clip: Clip
26 reldiff: number
29interface AliasOut {
30 fs: number
31 f0: number
32 nyq: number
33 measured: number
34 predicted: number
35 fr: number[]
36 dbnaive: Num[]
37 dbband: Num[]
38 falias: number[]
39 harm: number[]
40 clipNaive: Clip
41 clipBand: Clip
42 clipDiff: Clip
43 ratio: number
46const WHERE = {
47 kind: 'select' as const,
48 key: 'where',
49 label: 'listen to',
50 options: [
51 { value: 'bridge', label: 'the force on the bridge (partials fall as 1/n)' },
52 { value: 'pickup', label: 'the displacement at a pickup (1/n²)' },
53 ],
56const BETA = { kind: 'slider' as const, key: 'beta', label: 'pluck position β', min: 0.05, max: 0.5, step: 0.01 }
58const kHz = (v: number) => (v >= 1000 ? `${v / 1000}k` : String(v))
60function PluckPanels({ d }: { d: PluckOut }) {
61 return (
62 <>
63 <SeriesPlot
64 title="three periods of the signal: d'Alembert's exact solution (grey) and the band-limited modal sum at the audio sample instants"
65 height={200}
66 xLabel="ms"
67 series={[
68 { x: d.tf, y: d.exact, color: ink.reference, label: "d'Alembert, exact", width: 2 },
69 {
70 x: d.ts,
71 y: d.synth,
72 color: method.modal,
73 label: `modal sum, ${d.nyq} partials`,
74 width: 1.4,
75 },
76 ]}
77 />
78 <SeriesPlot
79 title="amplitude of each partial, relative to the largest; the gaps are the harmonics the pluck position does not excite"
80 height={170}
81 xLabel="partial n"
82 yLabel="dB"
83 yDomain={[-80, 3]}
84 series={[
85 {
86 x: d.n,
87 y: d.cdb,
88 color: method.modal,
89 stems: true,
90 base: -80,
91 shape: 'circle',
92 line: false,
93 label: 'exact modal amplitude',
94 tip: (i) => [`n = ${d.n[i]}, ${(d.n[i] * d.f0).toFixed(0)} Hz`, `${d.cdb[i].toFixed(1)} dB`],
95 },
96 ]}
97 legend={false}
98 />
99 <SeriesPlot
100 title="spectrum of the sound as played, first half second, with the listening loss"
101 height={170}
102 xLabel="Hz"
103 yLabel="dB"
104 yDomain={[-90, 3]}
105 formatX={kHz}
106 series={[{ x: d.fr, y: d.db, color: method.modal, width: 1.2 }]}
107 legend={false}
108 />
109 <Listen items={[{ label: 'the plucked string', clip: d.clip }]} note="start with the volume low" />
110 </>
111 )
114function AliasPanels({ d }: { d: AliasOut }) {
115 return (
116 <>
117 <SeriesPlot
118 title="spectrum of the first half second, up to the Nyquist frequency; faint lines mark the true harmonics"
119 height={250}
120 xLabel="Hz"
121 yLabel="dB"
122 yDomain={[-100, 3]}
123 formatX={kHz}
124 vlines={[{ x: d.harm, color: faded(ink.muted, 0.45) }]}
125 series={[
126 { x: d.fr, y: d.dbnaive, color: method.alt, label: "d'Alembert, sampled directly", width: 1.2 },
127 { x: d.fr, y: d.dbband, color: method.modal, label: 'modal sum below the Nyquist frequency', width: 1.4 },
128 ]}
129 />
130 <Listen
131 items={[
132 { label: 'sampled directly', clip: d.clipNaive },
133 { label: 'band-limited', clip: d.clipBand },
134 { label: 'their difference', clip: d.clipDiff },
135 ]}
136 note="all three at the same scale"
137 />
138 </>
139 )
142export const ch01: Chapter = {
143 id: 'exact',
144 nav: 'An exact string',
145 part: 'Strings',
146 title: 'Sound from an exact solution',
147 blurb:
148 "The plucked string solved twice, by d'Alembert and by Fourier, and the first thing that goes wrong: an exact solution that cannot be played as it stands.",
149 text,
150 labs: [
151 {
152 id: 'pluck',
153 title: 'A plucked string, from its modes',
154 caption:
155 'Move the pluck towards the middle and listen to the sound go hollow as the even harmonics disappear; switch to the pickup to hear what the 1/n² roll-off does.',
156 driver,
157 script,
158 controls: [noteControl('m', 'note', [45, 52, 57, 64, 69]), BETA, WHERE],
159 defaults: { m: 45, beta: 0.2, where: 'bridge' },
160 derive: (p: Params) => ({
161 mode: 'pluck',
162 fs: FS,
163 f0: noteHz(Number(p.m)),
164 beta: p.beta,
165 where: p.where,
166 xp: 0.12,
167 T: 2.5,
168 T60: 3,
169 fmax: 6000,
170 }),
171 editorOpen: false,
172 render: (d: PluckOut) => <PluckPanels d={d} />,
173 summary: (d: PluckOut) => [
174 { label: 'partials below Nyquist', value: String(d.nyq) },
175 { label: 'missing in the first 60', value: String(d.missing) },
176 { label: "modal sum vs d'Alembert, rms", value: fmtErr(d.reldiff) },
177 ],
178 },
179 {
180 id: 'alias',
181 title: 'Sampling a signal that is not band-limited',
182 caption:
183 "Raise the note and listen for tones that are not harmonics of it. They are the partials above 22.05 kHz, folded back into the audible band by sampling.",
184 driver,
185 script,
186 controls: [noteControl('m', 'note', [45, 57, 69, 76, 81, 88, 93, 96, 100]), BETA, WHERE],
187 defaults: { m: 88, beta: 0.2, where: 'bridge' },
188 derive: (p: Params) => ({
189 mode: 'alias',
190 fs: FS,
191 f0: noteHz(Number(p.m)),
192 beta: p.beta,
193 where: p.where,
194 xp: 0.12,
195 T: 1.5,
196 T60: 1.2,
197 }),
198 render: (d: AliasOut) => <AliasPanels d={d} />,
199 summary: (d: AliasOut) => [
200 { label: 'fs / f0', value: d.ratio.toFixed(3) },
201 { label: 'partials below Nyquist', value: String(d.nyq) },
202 { label: 'folded energy, measured', value: fmtDb(d.measured) },
203 { label: 'expected from the tail', value: fmtDb(d.predicted) },
204 ],
205 },
206 ],
↑↓ move↵openescclose