import type { Chapter } from '../../ui/types.ts'
import type { Num, Params } from '../../engine/types.ts'
import driver from './driver.m?raw'
import script from './ks.m?raw'
import text from './text.md?raw'
import Listen, { type Clip } from '../../audio/Listen.tsx'
import { SeriesPlot, fmtCents, fmtErr, fmtMs } from '../common/panels.tsx'
import { method } from '../../plot/palette.ts'
import { FS, noteName } from '../shared.ts'
interface EquivOut {
Ns: number[]
tl: number[]
tw: number[]
diff: number[]
M: number
}
interface KeysOut {
notes: number[]
f: number[]
tunings: string[]
cents: Num[][]
t60: Num[][]
clipInt: Clip
clipLin: Clip
clipAp: Clip
clipExact: Clip
}
const TUNE = [
{ label: 'whole-sample loop', color: method.fd2, shape: 'circle' as const },
{ label: 'linear interpolation', color: method.alt, shape: 'square' as const },
{ label: 'allpass', color: method.waveguide, shape: 'diamond' as const },
]
function EquivPanels({ d }: { d: EquivOut }) {
const tip = (i: number) => [`N = ${d.Ns[i]}`, `leapfrog ${fmtMs(d.tl[i])}`, `delay loop ${fmtMs(d.tw[i])}`]
return (
<>
[`N = ${d.Ns[i]}`, fmtErr(d.diff[i])] }]}
legend={false}
/>
>
)
}
function KeysPanels({ d }: { d: KeysOut }) {
const tip = (k: number) => (i: number) => [
`${noteName(d.notes[i])}, ${d.f[i].toFixed(1)} Hz`,
`pitch ${fmtCents(d.cents[k][i])}`,
`T60 ${(d.t60[k][i] ?? NaN).toPrecision(3)} s`,
]
return (
<>
({ x: d.f, y: d.cents[k], color: t.color, shape: t.shape, label: t.label, width: 1.4, tip: tip(k) }))}
/>
({ x: d.f, y: d.t60[k], color: t.color, shape: t.shape, label: t.label, width: 1.4, tip: tip(k) }))}
/>
>
)
}
export const ch03: Chapter = {
id: 'waveguide',
nav: 'The digital waveguide',
part: 'Strings',
title: 'The digital waveguide',
blurb:
'Leapfrog at the magic time step without the grid: a delay loop that costs one operation per sample, and the tuning problem that comes with it.',
text,
labs: [
{
id: 'equiv',
title: 'The same sound, without the grid',
caption:
'The loop is seeded with the first 2N samples of the leapfrog recording and then left to itself. The two agree to rounding error for the rest of the second.',
driver,
script,
controls: [],
defaults: {},
derive: () => ({ mode: 'equiv', fs: FS, T: 1, beta: 0.2, Ns: [25, 50, 100, 200, 400, 800, 1600, 3200] }),
render: (d: EquivOut) => ,
summary: (d: EquivOut) => [
{ label: 'leapfrog at N = 3200', value: fmtMs(d.tl[d.tl.length - 1]) },
{ label: 'delay loop', value: fmtMs(d.tw[d.tw.length - 1]) },
{ label: 'largest difference', value: fmtErr(Math.max(...d.diff)) },
],
},
{
id: 'keys',
title: 'Across the keyboard',
caption:
'One note on every third key from C2 to C8, with each of three ways of making the loop the right length. Then listen to a scale played high up, where the differences are.',
driver,
script,
controls: [],
defaults: {},
derive: (_p: Params) => ({
mode: 'keys',
fs: FS,
beta: 0.2,
notes: Array.from({ length: 25 }, (_, i) => 36 + 3 * i),
Tn: 0.5,
scale: [84, 86, 88, 89, 91, 93, 95, 96, 98, 100, 101, 103, 105, 107, 108],
step: 0.22,
ring: 0.9,
}),
editorOpen: true,
render: (d: KeysOut) => ,
summary: (d: KeysOut) =>
TUNE.map((t, k) => ({
label: `${t.label}, worst`,
value: fmtCents(d.cents[k].reduce((m, v) => (Math.abs(v ?? 0) > Math.abs(m) ? (v as number) : m), 0)),
})),
},
],
}