/ tutorials / wave-acoustics
Sign in
tutorials / wave-acoustics
wave-acoustics / CLAUDE.md
10.5 KBPreviewCodeBlameHistoryRaw

CLAUDE.md#

Notes for anyone, human or agent, working in this repository. Read README.md first for what the tutorial contains.

The shape of the thing#

A Vite + React app that embeds numbl as a library (numbl/browser, createNumblSession), in the same pattern as the sibling laplace-dirichlet and particle-filters tutorials: no numbl-project.json, and the deploy is a plain vite build. The UI, engine, plotting layer and test scripts were taken from particle-filters and changed as little as possible; what is new is src/audio/, the chapters, and the library.

src/
  App.tsx              sidebar, hash routing, chapter switching
  audio/Listen.tsx     the play buttons: one AudioContext, one clip at a time
  ui/                  Lab (one runnable section), ChapterView, Controls, Markdown
  engine/              the numbl session wrapper
  plot/                a small hand-rolled SVG layer, plus a canvas field plot
  figures/             static diagrams, keyed in figures/index.ts
  matlab/              the shared wv_*.m library, shown on its own page
  chapters/
    intro/             front matter, no labs
    common/panels.tsx  SeriesPlot, NumTable, Row and the number formatters
    membrane/          driver.m and panels.tsx shared by Chapters 4 and 5
    chNN/              text.md, the editable .m script, chapter.tsx, and a
                       driver.m where the chapter has its own

A chapter is prose in one text.md with <!--lab:id--> and <!--fig:id--> markers. A lab is a driver (fixed, hidden), an editable script (the method), declarative controls, and a render function. main.m is driver.m + "\n" + the script, so the script's functions become local functions of the driver. Each driver's header comment states the contract its script must meet, and every driver computes the exact answer itself: an edited script cannot flatter itself.

driver chapters the script must define
ch01/driver.m 1 y = pluck(t, f0, beta, where, xp, fmax, T60)
ch02/driver.m 2 [y, U] = leapfrog(u, uold, lam, M, bc, jr, K)
ch03/driver.m 3 y = ks(x0, D, tuning, M)
membrane/driver.m 4, 5 [y, U] = drum2d(u, uold, lam, M, scheme, ir, jr, K)
ch06/driver.m 6 [C, E, W, Nn, S, in] = diskstencil(N, method)
ch07/driver.m 7 U = openbox(u0, uold, lam, M, method, W, keep)
ch08/driver.m 8 h = images(L, xs, xr, beta, fs, tmax, taps)
ch09/driver.m 9 y = room3d(n, lam, M, is, ir, src)

Sound#

wv_audio.m packs a clip as 16-bit integers scaled by a factor the driver chooses. Every clip in one comparison is given the same scale, so that relative loudness on the page is relative size in the computation; that is a rule, and the Listen component's note says so to the reader. It also fades each end by 4 ms, and raises any clip below 8 kHz to a whole multiple of its rate by zero-padding its spectrum, because browsers are only required to play 8 kHz and up and a coarse simulation's own rate can be lower (Chapter 9's is 3.1 kHz).

Simulations produce sound at their own rate, 1/dt, which is only 44.1 kHz where the chapter makes the time step one sample on purpose (Chapters 2 and 3). The player hands the rate to Web Audio, which resamples.

Listen.tsx keeps one AudioContext for the page, created on the first click because browsers refuse audio before a user gesture, and stops whatever is playing before starting anything else. An unstable run (Chapter 2, N = 201) is silenced from the moment it exceeds fifty times the exact signal, and every clip is clamped, so nothing can reach the speakers at full scale by accident.

Measuring frequency#

Every frequency in the tutorial comes from wv_peakfreq: a sin⁸ taper, the peak of a zero-padded FFT inside a band, and Newton's method on |X(f)|² with exact derivatives. On a clean partial it is good to about 1e-10 relative, so a measured pitch error is the method's and not the estimator's. Wherever a projection onto an exact eigenvector is available (the sine vectors in Chapters 2, 4 and 5), the driver measures that projection, which is a single sinusoid. Where it is not (Chapter 6), it projects onto the continuum mode; the taper's sidelobes are what keep the neighbouring modes out.

The drivers never use a dispersion relation to measure anything. The relations appear only as predictions, and the tests check that measurement and prediction agree to 1e-7 cents.

Working with numbl#

This tutorial was developed against numbl 0.4.21. Several of the things below were found by profiling it, and the scripts are written around them.

Things that silently put a loop on the interpreter#

All of these made a loop run five to twenty times slower, with no error. Check with NUMBL_JIT_LOG=1, which prints every decline with its reason.

Three subscripts are slow; one is fast#

A three-dimensional stencil written as U(i,j,c) ran at about 1e7 updates per second; the same loop on a flat vector with neighbour offsets q +- 1, q +- sy, q +- sz ran at about 5e7. room3d.m is written the flat way. The two-dimensional scripts still use two subscripts, which run at about 3e7 and were fast enough; converting them is a possible speed-up that has not been measured.

Scalar loops beat vectorised slices here, as in laplace-dirichlet#

Every stencil is a written-out loop. The one-dimensional leapfrog step was six times faster as a loop than as a slice expression.

jsonencode flattens, and refuses complex values#

As in the sibling tutorials, anything matrix-shaped goes through wv_rows and any per-method vector through wv_vec. NaN and Inf cross as null, and the TypeScript side uses Num = number | null.

The browser is slower than the CLI, by up to two times#

The time per node per step in Chapter 9 is about 38 ns in the CLI and about 70 in Chrome. Nothing in the prose depends on the absolute numbers except Chapter 9's extrapolation, whose text quotes the CLI figure and says that the lab prints its own.

Measuring time honestly#

Timings are tic/toc inside the MATLAB layer around the method's call and nothing else; computing the exact answer, measuring frequencies and encoding sound are not charged. Where a timing is compared across methods (Chapter 5), the driver keeps the fastest of several runs while they are cheap, with the rule rep < 20 && tot < 0.3 && (rep < 3 || tot < 0.05). Timings are never asserted as values in the tests, only as rough ratios or orderings, and the prose quotes them as "about".

Testing#

npm run test:matlab     # ~90 s: the .m layer through the numbl CLI
npm run build && npm run test:browser [--shots] [--only <name>]
npm run figures         # tmp/figs/, one PNG per static figure

Every assertion in test:matlab is a number stated in the prose, checked to the precision it is quoted to. If one fails, either the code or the text is wrong, and the text is not allowed to win by default. node scripts/matlab-test.mjs --only ch5 runs one chapter.

test:browser opens every chapter, presses run on the labs that wait to be asked, and checks that nothing throws and that the figures contain data. It cannot judge a picture or a sound. --shots writes full-page screenshots into tmp/; npm run figures writes each static figure at the size it is drawn, which is the only way to catch a label that collides with something.

Static figures#

src/figures/ holds diagrams drawn as inline SVG against a 716-unit viewBox, using plot/palette.ts for colour so that a series means the same thing in a diagram as in a plot. Every curve in them that looks computed is computed, from the same closed forms the chapters use (d'Alembert's solution, the dispersion relations). Text is placed by hand; look at tmp/figs/ after changing one.

Plotting#

src/plot/ is the sibling tutorials' SVG layer. palette.ts assigns the eight validated hexes to methods (fd2, fd4, modal, waveguide, boundary, exact, alt) and adds familyColor, shades of one hue for one method at several settings of a parameter. Two chapters borrow a slot on purpose and say so where they do: Chapter 5 gives fourth order in space only the waveguide's amber, and Chapter 7 gives the damping layer fd4's orange. chapters/common/panels.tsx holds SeriesPlot, which draws any list of series with markers, stems, guides, reference slopes, end labels and a hover read-out.

Deployment#

.mochi/workflows/deploy.yml runs on every push to main: npm ci, npm run build, then upload-pages-artifact and deploy-pages, which the vault substitutes with its own implementations. actions/cache is not implemented in the vault's CI, so setup-node must not be given cache: npm. A vault site is served sandboxed, in an opaque origin, so localStorage and friends throw; the app uses none. Web Audio is expected to work there after a click, and test:site checks that it does.

Things deliberately not done#

↑↓ move↵openescclose