fuzzyworld

nordic-skating / mistral-medium-35-littlejs / files / src / ice / acoustics.ts

Branch mistral-medium-35-littlejs · default branch · all branches

acoustics.ts

4.8 KB · history · blame

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. */
23export const YOUNGS_MODULUS = 9e9
24export const POISSON_RATIO = 0.33
25/** kg/m³ */
26export const ICE_DENSITY = 917
27export const WATER_DENSITY = 1000
28/** Speed of sound in air at -5 °C, m/s. */
29export const SOUND_SPEED_AIR = 328
30const GRAVITY = 9.81
31
32/** How hard a sheet of ice `h` metres thick is to bend, N·m. */
33export 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 */
42export 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. */
48export 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. */
53export 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`. */
59function 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. */
69export 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 */
78export 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. */
85export 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. */
91export 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 */
100export 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}