import type { Chapter } from '../../ui/types.ts' import type { Params } from '../../engine/types.ts' import driver from './driver.m?raw' import script from './images.m?raw' import text from './text.md?raw' import Listen, { type Clip } from '../../audio/Listen.tsx' import { Row, SeriesPlot, fmtMs, fmtNum } from '../common/panels.tsx' import { faded, method } from '../../plot/palette.ts' interface RoomOut { ms: number rt: number rtbroad: number sabine: number eyring: number lattice: number ts: number[] sch: number[] schB: number[] tp: number[] sp: number[] tmax: number te: number[] he: number[] direct: number clipDry: Clip clipWet: Clip clipIr: Clip alpha: number V: number S: number } interface DecayOut { alphas: number[] rt: number[] lattice: number[] sabine: number[] eyring: number[] ms: number[] } const ROOM = { L: [6, 4.5, 3], xs: [1.5, 1.2, 1.4], xr: [4.3, 3.1, 1.6] } const FORMULA = { sabine: method.fd2, eyring: method.waveguide, lattice: method.exact } const s = (v: number) => `${fmtNum(v, 3)} s` function RoomPanels({ d }: { d: RoomOut }) { const line = (rt: number) => ({ x: [0, d.tmax * 1000], y: [0, (-60 * d.tmax) / rt] }) return ( <> ) } function DecayPanels({ d }: { d: DecayOut }) { const tip = (i: number) => [ `α = ${d.alphas[i]}`, `measured ${s(d.rt[i])}`, `lattice ${s(d.lattice[i])}`, `Sabine ${s(d.sabine[i])}, Eyring ${s(d.eyring[i])}`, `images ${fmtMs(d.ms[i])}`, ] const ratio = (v: number[]) => v.map((x, i) => x / d.lattice[i]) return ( ) } export const ch08: Chapter = { id: 'images', nav: 'A room, by images', part: 'Rooms', title: 'A room, by image sources', blurb: 'The walls replaced by mirrors, the room by a lattice of images, and the two reverberation formulas every acoustician knows, measured against both.', text, labs: [ { id: 'room', title: 'A 6 × 4.5 × 3 m room', caption: 'Every wall absorbs the same fraction of the energy that reaches it. Change it, and listen to the same phrase played in each room.', driver, script, controls: [ { kind: 'select', key: 'alpha', label: 'absorption α', options: [ { value: 0.1, label: '0.1, hard walls' }, { value: 0.2, label: '0.2' }, { value: 0.35, label: '0.35' }, { value: 0.6, label: '0.6, heavily damped' }, ], }, { kind: 'select', key: 'taps', label: 'each arrival placed', options: [ { value: 0, label: 'on the nearest sample' }, { value: 8, label: 'as an 8-tap windowed sinc' }, { value: 32, label: 'as a 32-tap windowed sinc' }, ], }, ], defaults: { alpha: 0.2, taps: 8 }, derive: (p: Params) => ({ mode: 'room', ...ROOM, fs: 22050, alpha: p.alpha, taps: p.taps, tcap: 1.2, notes: [60, 64, 67, 72, 76, 72, 67, 64, 60], step: 0.24, }), editorOpen: true, render: (d: RoomOut) => , summary: (d: RoomOut) => [ { label: 'images', value: fmtMs(d.ms) }, { label: 'RT measured', value: s(d.rt) }, { label: 'lattice', value: s(d.lattice) }, { label: 'Sabine', value: s(d.sabine) }, { label: 'Eyring', value: s(d.eyring) }, ], }, { id: 'decay', title: 'Reverberation time against absorption', caption: 'Six rooms, from nearly bare to heavily damped. This lab takes a few seconds, so it waits for the run button.', driver, script, controls: [], defaults: {}, derive: () => ({ mode: 'decay', ...ROOM, fs: 16000, alphas: [0.05, 0.1, 0.2, 0.3, 0.5, 0.7], tcap: 1.5, taps: 8 }), manual: true, render: (d: DecayOut) => , summary: (d: DecayOut) => { const dev = Math.max(...d.rt.slice(0, 4).map((v, i) => Math.abs(v / d.lattice[i] - 1))) return [{ label: 'measured vs lattice, α ≤ 0.3', value: `within ${(100 * dev).toFixed(0)} %` }] }, }, ], }