bell — a physically-modeled bell #
A reusable bell voice whose sound comes from a real finite-element model of a vibrating shell, so its material parameters (Young's modulus, density, Poisson ratio, wall thickness, damping) and geometry genuinely shape the timbre — not a table of hand-typed partial ratios. The same model drives a 3-D visualization of the bell shell deforming by mode.
Zero dependencies beyond libm (C) and canvas/ffmpeg (the JS viz).
pop/bell/
c/bell.c, c/bell.h the engine + public API
c/build.sh cc -O3 -std=c11 -Wall -Wextra -o bell bell.c -lm
c/run-c.mjs pop entry point: render mp3 (+ optional viz)
c/compare.mjs C-vs-JS modal-render parity harness
bin/viz.mjs 3-D deforming-shell mp4 renderer
materials.json material presets (mirror of the C table)
geometries.json geometry presets (mirror of the C table)
The physics #
A bell is a surface of revolution, so expanding the displacement field in a
Fourier series in the angular coordinate θ — u(s,θ) = Σ_m u_m(s)·cos(mθ) —
decouples the full 3-D shell into one 1-D meridian problem per circumferential
order m. For each m we assemble small stiffness K and mass M matrices
along the meridian (conical-frustum thin-shell elements, 4 DOF/node: meridional
u, circumferential v, normal w, meridional rotation β; selective reduced
integration on the transverse shear avoids locking) and solve the generalized
symmetric eigenproblem
K φ = ω² M φ
with a hand-written Cholesky + cyclic-Jacobi eigensolver (no LAPACK). The
eigenvalues are the modal frequencies; the eigenvectors are the meridian mode
shapes. The bell tone is the rim-flexural family m ≥ 2 — the hum, prime,
tierce, quint and nominal are all m=2/m=3 modes with differing numbers of
nodal circles. (m=0 breathing and m=1 whole-body sway are excluded: a rim
strike barely excites them and they are not part of the tone.)
- Damping comes from the material loss factor η: amplitude decay
δ = π·f·η, soτ = 1/δ. Constant η ⇒ higher partials decay faster — exactly the bell-like behavior. - Strike at the mouth rim sets each mode's initial amplitude from its participation (its normal shape sampled at the strike point).
- Pitch is set by
bell_retune(), a uniform geometric scale that multiplies every modal frequency by the same factor so the strike note lands on the requested pitch while the inharmonic ratio set is preserved.
Material parameters behave correctly by construction: K ∝ E, M ∝ ρ, so pitch
scales as √(E/ρ); thickness raises pitch; a larger bell lowers it as ~1/size².
Validation (./bell --selftest) #
The eigensolver is gated against analytic limits before anything trusts it:
- Jacobi on a known symmetric matrix.
- Generalized eig on a known
(K, M)pair. - Free-free Euler-Bernoulli beam vs
(βL)²√(EI/ρA L⁴)— validates meridional bending (matches to ~1e-6). - Cylinder → analytic in-plane ring flexural series
f_m = (1/2π)·(m(m²−1)/√(m²+1))·(h/a²)·√(E/12ρ)form=2,3,4(with ν=0) — validates the hoop physics (0.0 % error).
compare.mjs separately confirms the C render equals a JS reimplementation of
the same mode table to ~1e-8 (with the strike transient + normalization off).
CLI #
./build.sh
./bell --note A4 --material bronze --geometry church --dur 8 --out bell.wav
./bell ... --modes bell-modes.json # export geometry + modes + shapes (viz)
./bell ... --print-modes # print the partial table
./bell --selftest
Flags: --note (name like C#5 or a bare Hz), --material, --geometry,
--dur, --vel, --sr, --maxm, --nostrike, --nonorm (last two for parity).
Pop pipeline #
node run-c.mjs --note A4 --material glass --geometry church \
--out bell.mp3 --master bell --viz bell.mp4
Materials: bronze brass steel aluminum silver glass gold
Geometries: church handbell tubular bowl glass
Visualization #
node bin/viz.mjs --modes bell-modes.json --audio bell.wav --out bell.mp4 \
[--dur 9] [--fps 30] [--portrait]
A dependency-free software 3-D pipeline projects the deforming surface of
revolution (the displacement is the live sum of excited mode shapes as the sound
decays), with a HUD of material params, strike note and a per-partial spectrum
hued by circumferential order. Frames are BGRA → ffmpeg via
pop/lib/preview-shared.mjs (spawnFFmpegEncode), audio muxed in.
Public API (bell.h) #
bell_geometry_preset(&g, "church");
bell_material_preset(&mat, "bronze");
bell_solve_modes(&g, &mat, /*max_m*/8, /*max_modes*/32, &modes);
bell_retune(&modes, 440.0);
bell_render(&modes, /*vel*/0.9, /*sr*/48000, /*dur*/8, L, R, nsamp);
bell_export_modes_json(&g, &mat, &modes, "modes.json");
Reuse across the monorepo + AC OS (follow-up) #
bell.c/bell.h are self-contained with the same shape as
fedac/native/src/gm_synth.c, which is compiled into the AC OS kernel
(fedac/native/Makefile) and symlinked into menuband
(slab/menuband/Sources/CGMSynth/). To make the bell live everywhere:
- Add
bell.cto thefedac/nativeMakefile SRCS; callbell_solve_modesonce per voice config andbell_render-style modal playback in the audio mix (modes can be precomputed at note-on; the eigensolve is sub-millisecond per bell but is best cached). - Symlink
bell.c/bell.hinto aslab/menuband/Sources/CBell/target. - Optionally have
gm_synth.c's bell programs source their ratios from a solvedBellModesinstead of the hand-tabulatedgm_chromperc_programs.
Notes / honest limits #
The element is a faceted-conical thin-shell (Kirchhoff/Mindlin) reduced model.
It reproduces the analytic ring and beam limits exactly and gives a genuinely
inharmonic, material-driven bell spectrum, but it is not a research-grade shell
solver: the absolute ratio set of a specific historic bell is the product of
centuries of profile tuning. Edit the build_profile control points (or the
geometry presets) to chase a particular bell's hum/prime/tierce/nominal.