import type { Chapter } from '../../ui/types.ts' import type { Num, Params } from '../../engine/types.ts' import driver from './driver.m?raw' import script from './disk.m?raw' import text from './text.md?raw' import Listen, { type Clip } from '../../audio/Listen.tsx' import { NumTable, Row, SeriesPlot, fmtCents, fmtMs, fmtNum } from '../common/panels.tsx' import { faded, ink, method } from '../../plot/palette.ts' interface HearOut { f: number[] adb: number[] m: number[] k: number[] ratio: number[] fr: number[] db: number[] clipRound: Clip clipSquare: Clip tm: number[] tk: number[] tratio: number[] } interface GridOut { Ns: number[] methods: string[] modes: number[][] cents: Num[][] worst: Num[][] lam: Num[][] ms: Num[][] orders: Num[][] } const RIM: Record = { staircase: { label: 'staircase rim', color: method.fd2, shape: 'circle' }, sw: { label: 'Shortley–Weller rim', color: method.boundary, shape: 'cross' }, } function HearPanels({ d }: { d: HearOut }) { const harm = Array.from({ length: 27 }, (_, i) => i + 1) return ( <> [`mode (m, k) = (${d.m[i]}, ${d.k[i]})`, `${d.f[i].toFixed(1)} Hz, ratio ${d.ratio[i].toFixed(3)}`, `${d.adb[i].toFixed(1)} dB`], }, ]} legend={false} /> [m, d.tk[i], d.tratio[i].toFixed(4)])} /> ) } function GridPanels({ d }: { d: GridOut }) { const nq = d.modes.length const rim = d.methods.map((m) => RIM[m]) const tip = (s: number) => (j: number) => [ `N = ${d.Ns[j]}, λ = ${fmtNum(d.lam[s][j], 3)}, ${fmtMs(d.ms[s][j])}`, ...d.modes.map((m, q) => `mode (${m[0]},${m[1]}): ${fmtCents(d.cents[s * nq + q][j])}`), ] return ( <> ({ x: d.Ns, y: d.worst[s], color: r.color, shape: r.shape, label: r.label, tip: tip(s) }))} /> ({ x: d.Ns, y: d.lam[s], color: r.color, shape: r.shape, label: r.label, tip: tip(s) }))} legend={false} /> ({ x: d.ms[s], y: d.worst[s], color: r.color, shape: r.shape, label: r.label, tip: tip(s) }))} legend={false} /> [`${m} error`, `${m} order`, `${m} λ`])]} rows={d.Ns.map((N, j) => [ N, ...d.methods.flatMap((_, s) => [fmtNum(d.worst[s][j], 3), fmtNum(d.orders[s][j], 2), fmtNum(d.lam[s][j], 3)]), ])} /> ) } export const ch06: Chapter = { id: 'round', nav: 'A round drum', part: 'Drums', title: 'A round drum on a square grid', blurb: 'Bessel modes and the sound of an inharmonic spectrum, and what a curved rim costs a grid that cannot follow it.', text, labs: [ { id: 'hear', title: 'The partials of a round membrane', caption: 'Strike at the centre and only the circularly symmetric modes sound; move outwards and the others join in. Neither set lines up with the harmonic series.', driver, script, controls: [ { kind: 'select', key: 'r0', label: 'strike at radius', options: [ { value: 0, label: 'the centre' }, { value: 0.4, label: '0.4 of the way out' }, { value: 0.8, label: '0.8, near the rim' }, ], }, ], defaults: { r0: 0.4 }, derive: (p: Params) => ({ mode: 'hear', fs: 22050, f01: 150, T: 1.5, width: 0.03, mmax: 40, kmax: 14, fmax: 4000, r0: p.r0, rr: 0.55, thr: 2.1, tau: 0.5, }), render: (d: HearOut) => , summary: (d: HearOut) => [ { label: 'partials below 4 kHz', value: String(d.f.length) }, { label: 'second partial', value: `${d.ratio[1]?.toFixed(3)} × f₀₁` }, ], }, { id: 'grid', title: 'Two rims, measured', caption: 'Modes (0,1) and (1,1), started exactly and measured from the simulation. This lab runs for several seconds, so it waits for the run button.', driver, script, controls: [], defaults: {}, derive: () => ({ mode: 'grid', Ns: [16, 24, 32, 40, 48, 64], methods: ['staircase', 'sw'], modes: [0, 1, 1, 1], periods: 10, }), manual: true, editorOpen: true, render: (d: GridOut) => , summary: (d: GridOut) => d.methods.map((m, s) => ({ label: `${RIM[m].label}, N = ${d.Ns[d.Ns.length - 1]}`, value: fmtCents(d.worst[s][d.Ns.length - 1]) })), }, ], }