import type { Chapter } from '../../ui/types.ts' import type { Params } from '../../engine/types.ts' import type { Num } from '../../engine/types.ts' import driver from './driver.m?raw' import script from './pluck.m?raw' import text from './text.md?raw' import Listen, { type Clip } from '../../audio/Listen.tsx' import { SeriesPlot, fmtDb, fmtErr } from '../common/panels.tsx' import { faded, ink, method } from '../../plot/palette.ts' import { FS, noteControl, noteHz } from '../shared.ts' interface PluckOut { fs: number f0: number nyq: number tf: number[] exact: number[] ts: number[] synth: number[] n: number[] cdb: number[] missing: number fr: number[] db: number[] clip: Clip reldiff: number } interface AliasOut { fs: number f0: number nyq: number measured: number predicted: number fr: number[] dbnaive: Num[] dbband: Num[] falias: number[] harm: number[] clipNaive: Clip clipBand: Clip clipDiff: Clip ratio: number } const WHERE = { kind: 'select' as const, key: 'where', label: 'listen to', options: [ { value: 'bridge', label: 'the force on the bridge (partials fall as 1/n)' }, { value: 'pickup', label: 'the displacement at a pickup (1/n²)' }, ], } const BETA = { kind: 'slider' as const, key: 'beta', label: 'pluck position β', min: 0.05, max: 0.5, step: 0.01 } const kHz = (v: number) => (v >= 1000 ? `${v / 1000}k` : String(v)) function PluckPanels({ d }: { d: PluckOut }) { return ( <> [`n = ${d.n[i]}, ${(d.n[i] * d.f0).toFixed(0)} Hz`, `${d.cdb[i].toFixed(1)} dB`], }, ]} legend={false} /> ) } function AliasPanels({ d }: { d: AliasOut }) { return ( <> ) } export const ch01: Chapter = { id: 'exact', nav: 'An exact string', part: 'Strings', title: 'Sound from an exact solution', blurb: "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.", text, labs: [ { id: 'pluck', title: 'A plucked string, from its modes', caption: '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.', driver, script, controls: [noteControl('m', 'note', [45, 52, 57, 64, 69]), BETA, WHERE], defaults: { m: 45, beta: 0.2, where: 'bridge' }, derive: (p: Params) => ({ mode: 'pluck', fs: FS, f0: noteHz(Number(p.m)), beta: p.beta, where: p.where, xp: 0.12, T: 2.5, T60: 3, fmax: 6000, }), editorOpen: false, render: (d: PluckOut) => , summary: (d: PluckOut) => [ { label: 'partials below Nyquist', value: String(d.nyq) }, { label: 'missing in the first 60', value: String(d.missing) }, { label: "modal sum vs d'Alembert, rms", value: fmtErr(d.reldiff) }, ], }, { id: 'alias', title: 'Sampling a signal that is not band-limited', caption: "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.", driver, script, controls: [noteControl('m', 'note', [45, 57, 69, 76, 81, 88, 93, 96, 100]), BETA, WHERE], defaults: { m: 88, beta: 0.2, where: 'bridge' }, derive: (p: Params) => ({ mode: 'alias', fs: FS, f0: noteHz(Number(p.m)), beta: p.beta, where: p.where, xp: 0.12, T: 1.5, T60: 1.2, }), render: (d: AliasOut) => , summary: (d: AliasOut) => [ { label: 'fs / f0', value: d.ratio.toFixed(3) }, { label: 'partials below Nyquist', value: String(d.nyq) }, { label: 'folded energy, measured', value: fmtDb(d.measured) }, { label: 'expected from the tail', value: fmtDb(d.predicted) }, ], }, ], }