nordic-skating / mistral-medium-35-littlejs / files / src / ice / acoustics.ts
Branch mistral-medium-35-littlejs · default branch · all branches
acoustics.ts
174b4a1a 2026-10-09 Pavel Safronov | 1 | /** |
| 2 | * Why thin ice sings, as numbers. | |
| 3 | * | |
| 4 | * A sheet of ice floating on water carries bending ("flexural") waves, and they are | |
| 5 | * dispersive: the higher the frequency, the faster it travels. Two things follow, and | |
| 6 | * they are the two sounds of skating on black ice. | |
| 7 | * | |
| 8 | * - A crack or the strike of a skate is every frequency at once. Some way off, the high | |
| 9 | * ones arrive first and the low ones after: a falling chirp, the "pew" of a laser. | |
| 10 | * - Slow bending waves cannot radiate into the air at all. Sound leaves the ice only at | |
| 11 | * frequencies whose bending wave travels at least as fast as sound does in air, and | |
| 12 | * most strongly right at the frequency where the two speeds match, where the wave in | |
| 13 | * the ice and the wave in the air keep step. That frequency is the note the ice sings, | |
| 14 | * and it rises as the ice thins: the pitch is a thickness gauge. | |
| 15 | * | |
| 16 | * The model and its dispersion relation are from Romeyn, Hanssen, Ruud and Johansen, | |
| 17 | * "Sea ice thickness from air-coupled flexural waves", The Cryosphere 15, 2939–2955 | |
| 18 | * (2021), https://doi.org/10.5194/tc-15-2939-2021 — who measured this very lake from | |
| 19 | * the film this app is modelled on: a note of about 725 Hz, from ice about 4.3 cm thick. | |
| 20 | */ | |
| 21 | ||
| 22 | /** Young's modulus of freshwater ice, Pa. */ | |
| 23 | export const YOUNGS_MODULUS = 9e9 | |
| 24 | export const POISSON_RATIO = 0.33 | |
| 25 | /** kg/m³ */ | |
| 26 | export const ICE_DENSITY = 917 | |
| 27 | export const WATER_DENSITY = 1000 | |
| 28 | /** Speed of sound in air at -5 °C, m/s. */ | |
| 29 | export const SOUND_SPEED_AIR = 328 | |
| 30 | const GRAVITY = 9.81 | |
| 31 | ||
| 32 | /** How hard a sheet of ice `h` metres thick is to bend, N·m. */ | |
| 33 | export function flexuralRigidity(h: number): number { | |
| 34 | return (YOUNGS_MODULUS * h ** 3) / (12 * (1 - POISSON_RATIO ** 2)) | |
| 35 | } | |
| 36 | ||
| 37 | /** | |
| 38 | * Angular frequency, rad/s, of the bending wave of wavenumber `k` rad/m in ice `h` metres | |
| 39 | * thick on deep water. The water matters: it has to be moved out of the way, which | |
| 40 | * weighs the ice down and slows the wave. | |
| 41 | */ | |
| 42 | export function angularFrequency(k: number, h: number): number { | |
| 43 | const stiffness = (flexuralRigidity(h) * k ** 5) / WATER_DENSITY + GRAVITY * k | |
| 44 | return Math.sqrt(stiffness / (k * h * (ICE_DENSITY / WATER_DENSITY) + 1)) | |
| 45 | } | |
| 46 | ||
| 47 | /** Speed of a single crest of that wave, m/s. */ | |
| 48 | export function phaseSpeed(k: number, h: number): number { | |
| 49 | return angularFrequency(k, h) / k | |
| 50 | } | |
| 51 | ||
| 52 | /** Speed at which that wave carries a sound across the ice, m/s. */ | |
| 53 | export function groupSpeed(k: number, h: number): number { | |
| 54 | const dk = k * 1e-4 | |
| 55 | return (angularFrequency(k + dk, h) - angularFrequency(k - dk, h)) / (2 * dk) | |
| 56 | } | |
| 57 | ||
| 58 | /** Find where an increasing function crosses zero between `lo` and `hi`. */ | |
| 59 | function solve(f: (x: number) => number, lo: number, hi: number): number { | |
| 60 | for (let i = 0; i < 80; i++) { | |
| 61 | const mid = (lo + hi) / 2 | |
| 62 | if (f(mid) < 0) lo = mid | |
| 63 | else hi = mid | |
| 64 | } | |
| 65 | return (lo + hi) / 2 | |
| 66 | } | |
| 67 | ||
| 68 | /** Wavenumber, rad/m, of the bending wave of frequency `f` Hz in ice `h` metres thick. */ | |
| 69 | export function wavenumber(f: number, h: number): number { | |
| 70 | const omega = 2 * Math.PI * f | |
| 71 | return solve((k) => angularFrequency(k, h) - omega, 1e-3, 1e4) | |
| 72 | } | |
| 73 | ||
| 74 | /** | |
| 75 | * The note the ice sings, Hz: the frequency whose bending wave moves at the speed of | |
| 76 | * sound in air. | |
| 77 | */ | |
| 78 | export function singingFrequency(h: number): number { | |
| 79 | // The wave gets faster as it gets shorter, so the match is where the speed crosses. | |
| 80 | const k = solve((k) => phaseSpeed(k, h) - SOUND_SPEED_AIR, 0.5, 1e4) | |
| 81 | return (SOUND_SPEED_AIR * k) / (2 * Math.PI) | |
| 82 | } | |
| 83 | ||
| 84 | /** The other way round: how thick, in metres, is ice that sings at `f` Hz. */ | |
| 85 | export function thicknessFromFrequency(f: number): number { | |
| 86 | // Thicker ice sings lower. | |
| 87 | return solve((h) => f - singingFrequency(h), 1e-3, 2) | |
| 88 | } | |
| 89 | ||
| 90 | /** Seconds a sound of `f` Hz takes to cross `distance` metres of ice `h` metres thick. */ | |
| 91 | export function travelTime(f: number, h: number, distance: number): number { | |
| 92 | return distance / groupSpeed(wavenumber(f, h), h) | |
| 93 | } | |
| 94 | ||
| 95 | /** | |
| 96 | * The chirp heard `distance` metres from a crack in ice `h` metres thick: the frequency | |
| 97 | * arriving at each moment, at `points` evenly spaced moments from the arrival of `from` | |
| 98 | * Hz until the arrival of the ice's own note, which comes last and is where a chirp ends. | |
| 99 | */ | |
| 100 | export function chirp(h: number, distance: number, from = 6000, points = 64): { duration: number; frequencies: Float32Array } { | |
| 101 | // Just above the note itself, where the fall has all but levelled out. | |
| 102 | const lowest = singingFrequency(h) * 1.02 | |
| 103 | const first = travelTime(from, h, distance) | |
| 104 | const duration = travelTime(lowest, h, distance) - first | |
| 105 | const frequencies = new Float32Array(points) | |
| 106 | for (let i = 0; i < points; i++) { | |
| 107 | const t = first + (duration * i) / (points - 1) | |
| 108 | // Higher frequencies arrive sooner, so the arrival time falls as the frequency rises. | |
| 109 | frequencies[i] = solve((f) => t - travelTime(f, h, distance), lowest, from) | |
| 110 | } | |
| 111 | return { duration, frequencies } | |
| 112 | } |