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) },
],
},
],
}