/ tutorials / wave-acoustics
Sign in
tutorials / wave-acoustics
wave-acoustics / src / chapters / ch08 / chapter.tsx
206 lines · 6.9 KBCodeBlameHistory
d8098edAn interactive tutorial on the wave equation in acoustics: strings, drums and rooms, measured by earJeremy Magland 1import type { Chapter } from '../../ui/types.ts'
2import type { Params } from '../../engine/types.ts'
3import driver from './driver.m?raw'
4import script from './images.m?raw'
5import text from './text.md?raw'
6import Listen, { type Clip } from '../../audio/Listen.tsx'
7import { Row, SeriesPlot, fmtMs, fmtNum } from '../common/panels.tsx'
8import { faded, method } from '../../plot/palette.ts'
10interface RoomOut {
11 ms: number
12 rt: number
13 rtbroad: number
14 sabine: number
15 eyring: number
16 lattice: number
17 ts: number[]
18 sch: number[]
19 schB: number[]
20 tp: number[]
21 sp: number[]
22 tmax: number
23 te: number[]
24 he: number[]
25 direct: number
26 clipDry: Clip
27 clipWet: Clip
28 clipIr: Clip
29 alpha: number
30 V: number
31 S: number
34interface DecayOut {
35 alphas: number[]
36 rt: number[]
37 lattice: number[]
38 sabine: number[]
39 eyring: number[]
40 ms: number[]
43const ROOM = { L: [6, 4.5, 3], xs: [1.5, 1.2, 1.4], xr: [4.3, 3.1, 1.6] }
44const FORMULA = { sabine: method.fd2, eyring: method.waveguide, lattice: method.exact }
45const s = (v: number) => `${fmtNum(v, 3)} s`
47function RoomPanels({ d }: { d: RoomOut }) {
48 const line = (rt: number) => ({ x: [0, d.tmax * 1000], y: [0, (-60 * d.tmax) / rt] })
49 return (
50 <>
51 <SeriesPlot
52 title={`the first 60 ms of the impulse response: the direct sound at ${d.direct.toFixed(1)} ms, then the early reflections one by one`}
53 height={170}
54 xLabel="ms"
55 series={[{ x: d.te, y: d.he, color: method.boundary, width: 1 }]}
56 legend={false}
57 />
58 <SeriesPlot
59 title="Schroeder decay curves: the backward-integrated energy of the response, against three predictions"
60 height={260}
61 xLabel="ms"
62 yLabel="dB"
63 yDomain={[-60, 2]}
64 series={[
65 { ...line(d.sabine), color: FORMULA.sabine, dash: '6 4', width: 1.4, label: `Sabine, ${s(d.sabine)}` },
66 { ...line(d.eyring), color: FORMULA.eyring, dash: '6 4', width: 1.4, label: `Eyring, ${s(d.eyring)}` },
67 { x: d.tp, y: d.sp, color: FORMULA.lattice, width: 2.4, label: `the lattice, ${s(d.lattice)}` },
68 { x: d.ts, y: d.sch, color: faded(method.boundary, 0.4), width: 1.4, label: `measured, broadband, ${s(d.rtbroad)}` },
69 { x: d.ts, y: d.schB, color: method.boundary, width: 1.8, label: `measured, 0.7–5.7 kHz, ${s(d.rt)}` },
70 ]}
71 />
72 <Listen
73 items={[
74 { label: 'dry', clip: d.clipDry },
75 { label: 'in the room', clip: d.clipWet },
76 { label: 'the impulse response itself', clip: d.clipIr },
77 ]}
78 note="each normalised to its own peak"
79 />
80 </>
81 )
84function DecayPanels({ d }: { d: DecayOut }) {
85 const tip = (i: number) => [
86 `α = ${d.alphas[i]}`,
87 `measured ${s(d.rt[i])}`,
88 `lattice ${s(d.lattice[i])}`,
89 `Sabine ${s(d.sabine[i])}, Eyring ${s(d.eyring[i])}`,
90 `images ${fmtMs(d.ms[i])}`,
91 ]
92 const ratio = (v: number[]) => v.map((x, i) => x / d.lattice[i])
93 return (
94 <Row>
95 <SeriesPlot
96 title="reverberation time against the absorption coefficient"
97 height={250}
98 xLog
99 yLog
100 xTicks={d.alphas}
101 xLabel="absorption α"
102 yLabel="seconds"
103 series={[
104 { x: d.alphas, y: d.sabine, color: FORMULA.sabine, shape: 'circle', dash: '6 4', label: 'Sabine', tip },
105 { x: d.alphas, y: d.eyring, color: FORMULA.eyring, shape: 'diamond', dash: '6 4', label: 'Eyring', tip },
106 { x: d.alphas, y: d.lattice, color: FORMULA.lattice, shape: 'plus', label: 'the lattice', tip },
107 { x: d.alphas, y: d.rt, color: method.boundary, shape: 'cross', label: 'measured', tip },
108 ]}
109 />
110 <SeriesPlot
111 title="each as a fraction of the lattice's prediction"
112 height={250}
113 xLog
114 xTicks={d.alphas}
115 yDomain={[0.4, 1.6]}
116 xLabel="absorption α"
117 series={[
118 { x: d.alphas, y: ratio(d.sabine), color: FORMULA.sabine, shape: 'circle', dash: '6 4', label: 'Sabine', tip },
119 { x: d.alphas, y: ratio(d.eyring), color: FORMULA.eyring, shape: 'diamond', dash: '6 4', label: 'Eyring', tip },
120 { x: d.alphas, y: ratio(d.rt), color: method.boundary, shape: 'cross', label: 'measured', tip },
121 ]}
122 hlines={[{ y: 1, color: FORMULA.lattice, dash: '1 0' }]}
123 legend={false}
124 />
125 </Row>
126 )
129export const ch08: Chapter = {
130 id: 'images',
131 nav: 'A room, by images',
132 part: 'Rooms',
133 title: 'A room, by image sources',
134 blurb:
135 'The walls replaced by mirrors, the room by a lattice of images, and the two reverberation formulas every acoustician knows, measured against both.',
136 text,
137 labs: [
138 {
139 id: 'room',
140 title: 'A 6 × 4.5 × 3 m room',
141 caption:
142 'Every wall absorbs the same fraction of the energy that reaches it. Change it, and listen to the same phrase played in each room.',
143 driver,
144 script,
145 controls: [
146 {
147 kind: 'select',
148 key: 'alpha',
149 label: 'absorption α',
150 options: [
151 { value: 0.1, label: '0.1, hard walls' },
152 { value: 0.2, label: '0.2' },
153 { value: 0.35, label: '0.35' },
154 { value: 0.6, label: '0.6, heavily damped' },
155 ],
156 },
157 {
158 kind: 'select',
159 key: 'taps',
160 label: 'each arrival placed',
161 options: [
162 { value: 0, label: 'on the nearest sample' },
163 { value: 8, label: 'as an 8-tap windowed sinc' },
164 { value: 32, label: 'as a 32-tap windowed sinc' },
165 ],
166 },
167 ],
168 defaults: { alpha: 0.2, taps: 8 },
169 derive: (p: Params) => ({
170 mode: 'room',
171 ...ROOM,
172 fs: 22050,
173 alpha: p.alpha,
174 taps: p.taps,
175 tcap: 1.2,
176 notes: [60, 64, 67, 72, 76, 72, 67, 64, 60],
177 step: 0.24,
178 }),
179 editorOpen: true,
180 render: (d: RoomOut) => <RoomPanels d={d} />,
181 summary: (d: RoomOut) => [
182 { label: 'images', value: fmtMs(d.ms) },
183 { label: 'RT measured', value: s(d.rt) },
184 { label: 'lattice', value: s(d.lattice) },
185 { label: 'Sabine', value: s(d.sabine) },
186 { label: 'Eyring', value: s(d.eyring) },
187 ],
188 },
189 {
190 id: 'decay',
191 title: 'Reverberation time against absorption',
192 caption: 'Six rooms, from nearly bare to heavily damped. This lab takes a few seconds, so it waits for the run button.',
193 driver,
194 script,
195 controls: [],
196 defaults: {},
197 derive: () => ({ mode: 'decay', ...ROOM, fs: 16000, alphas: [0.05, 0.1, 0.2, 0.3, 0.5, 0.7], tcap: 1.5, taps: 8 }),
198 manual: true,
199 render: (d: DecayOut) => <DecayPanels d={d} />,
200 summary: (d: DecayOut) => {
201 const dev = Math.max(...d.rt.slice(0, 4).map((v, i) => Math.abs(v / d.lattice[i] - 1)))
202 return [{ label: 'measured vs lattice, α ≤ 0.3', value: `within ${(100 * dev).toFixed(0)} %` }]
203 },
204 },
205 ],
↑↓ move↵openescclose